How to detect update event in model in Laravel 8 - laravel

Good day to all
The situation is as follows
In the controller, in the update method, I try to update the object
There is an image in the fields of this object
Wrote a trait to process this field and load an image
In the model itself, I called the update method, which just determines the event of updating the object
The problem lies in the following image in the specified directory is loaded and the entry itself in the database does not change
Here is my code
Controller
Model
Trait
There is extra code in the model
public function update(Request $request, MainHeader $mainHeader): RedirectResponse
{
$mainHeader->update([
'language_id' => $request->language_id,
'brandLogoImage' => $request->file('brandLogoImage'),
'homeTitle' => $request->homeTitle,
'ourProjectsTitle' => $request->ourProjectsTitle,
'contactTitle' => $request->contactTitle,
'feedbackTitle' => $request->feedbackTitle,
]);
return redirect()->route('admin.header.index')->with('success', 'Данные успешно обновлены');
}
public function setBrandLogoImageAttribute($value): string
{
return $this->uploadImage('brandLogoImage', $value);
}
public function update(array $attributes = [], array $options = [])
{
$this->uploadImage('brandLogoImage', $attributes['brandLogoImage']);
$this->setBrandLogoImageAttribute($attributes['brandLogoImage']);
return parent::update($attributes, $options); // TODO: Change the autogenerated stub
}
protected function uploadImage(string $attr, $value): string
{
$uploadDir = public_path('uploads/');
$imageDir = public_path('uploads/image/');
if (!file_exists($uploadDir)){
mkdir($uploadDir);
}
if (!file_exists($imageDir)){
mkdir($imageDir);
}
if (!file_exists(public_path("uploads/image/$this->table/"))){
mkdir(public_path("uploads/image/$this->table/"));
}
$imageName = Str::random(12) . '.png';
Image::make($value)->save(public_path("uploads/image/$this->table/$imageName") , 100);
return $this->attributes[$attr] = (string) "uploads/image/$this->table/$imageName";
}

if you call the update methode in your model then you are overriding the default update() of the model class , its not listening to the event it simply runs your code before parent:: , so you need to make sure that the changes you are making does not get overwitten by the parent call .
regarding your question on how to detect update , if you want to perform anything before update than i advise you to use eloquent events or use observers , Observers listen to various events regarding your model like updating or updated .. but i think if its only for updating event than you should use event using closure
for example :
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* The "booted" method of the model.
*
* #return void
*/
protected static function booted()
{
static::updating(function ($user) {
// do what you want
});
}
}
If your pupose

Related

Laravel/Livewire: Use withTrashed() on model route binding on to show deleted records

In the list I display the latest topic, including those that is deleted.
function latest()
{
return Topic::withTrashed()->latest();
}
For displaying a single topic I have a Livewire component with that topic passed into it.
class ShowTopic extends Component
{
public $topic;
public function mount(Topic $topic)
{
$this->topic = $topic;
}
public function render()
{
return view('livewire.show-topic', [
'topic' => $this->topic,
]);
}
}
But when I go to a single topic that is deleted, it doesn't show. How can I use withTrashed() on model route bindings to show deleted records with my Livewire component?
You can overwrite the resolveRouteBinding() method on your Eloquent model, and conditionally remove the SoftDeletingScope global scope.
Here I'm using a policy for that model to check if I can delete the model - and if the user can delete it, they can also see it. You could implement any logic you want, or remove the global scope for all requests if that is more suitable for your application.
use Illuminate\Database\Eloquent\SoftDeletingScope;
class Topic extends Model {
// ...
/**
* Retrieve the model for a bound value.
*
* #param mixed $value
* #param string|null $field
* #return \Illuminate\Database\Eloquent\Model|null
*/
public function resolveRouteBinding($value, $field = null)
{
// If no field was given, use the primary key
if ($field === null) {
$field = $this->getKey();
}
// Apply where clause
$query = $this->where($field, $value);
// Conditionally remove the softdelete scope to allow seeing soft-deleted records
if (Auth::check() && Auth::user()->can('delete', $this)) {
$query->withoutGlobalScope(SoftDeletingScope::class);
}
// Find the first record, or abort
return $query->firstOrFail();
}
}

Eloquent model event `updating` isn't firing in laravel 7

I'm trying to catch the model event updating in laravel 7 but it doesn't fire.
This is the place where the model get's changed:
public function update(Request $request, Model $model) {
$model->update($request->input());
return new Resource($model);
}
I also tried this to update the values:
public function update(Request $request, Model $model) {
$model->attribute1 = $request->get('value1');
$model->attribute2 = $request->get('value2');
$model->attribute3 = $request->get('value3');
$model->save();
return new Resource($model);
}
And here I'm trying to catch the event within the bill model:
protected static function boot() {
static::updating(function ($model) {
// code
});
}
What am I doing wrong?
You have to call parent::boot() at the start of your boot method:
protected static function boot() {
parent::boot();
static::updating(function ($model) {
// code
});
}
Laravel 7 added a booted method to make it easier:
Adding booting / booted methods to Model
Currently, users who extend the boot method to add event listeners on
model events must remember to call parent::boot() at the start of
their method (or after). This is often forgotten and causes confusion
for the user. By adding these simple place-holder extension points we
can point users towards these methods instead which do not require
them to call any parent methods at all.
From the docs:
Instead of using custom event classes, you may register Closures that
execute when various model events are fired. Typically, you should
register these Closures in the booted method of your model:
<?php
namespace App;
use App\Scopes\AgeScope;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* The "booted" method of the model.
*
* #return void
*/
protected static function booted()
{
static::created(function ($user) {
//
});
}
}

lararvel uuid as primary key

I'm trying to set an uuid as primary key in a Laravel Model. I've done it setting a boot method in my model as stablished here so I don't have to manually create it everytime I want to create and save the model. I have a controller that just creates the model and saves it in database.
It is saved correctly in database but when controller returns the value of the id is always returned with 0. How can I make it to actually return the value that it is creating in database?
Model
class UserPersona extends Model
{
protected $guarded = [];
protected $casts = [
'id' => 'string'
];
/**
* Setup model event hooks
*/
public static function boot()
{
parent::boot();
self::creating(function ($model) {
$uuid = Uuid::uuid4();
$model->id = $uuid->toString();
});
}
}
Controller
class UserPersonaController extends Controller
{
public function new(Request $request)
{
return UserPersona::create();
}
}
You need to change the keyType to string and incrementing to false. Since it's not incrementing.
public $incrementing = false;
protected $keyType = 'string';
Additionally I have an trait which I simply add to those models which have UUID keys. Which is pretty flexible. This comes originally from https://garrettstjohn.com/articles/using-uuid-laravel-eloquent-orm/ and I added some small adjustments to it for issues which I have discovered while using it intensively.
use Illuminate\Database\Eloquent\Model;
use Ramsey\Uuid\Uuid;
/**
* Class Uuid.
* Manages the usage of creating UUID values for primary keys. Drop into your models as
* per normal to use this functionality. Works right out of the box.
* Taken from: http://garrettstjohn.com/entry/using-uuids-laravel-eloquent-orm/
*/
trait UuidForKey
{
/**
* The "booting" method of the model.
*/
public static function bootUuidForKey()
{
static::retrieved(function (Model $model) {
$model->incrementing = false; // this is used after instance is loaded from DB
});
static::creating(function (Model $model) {
$model->incrementing = false; // this is used for new instances
if (empty($model->{$model->getKeyName()})) { // if it's not empty, then we want to use a specific id
$model->{$model->getKeyName()} = (string)Uuid::uuid4();
}
});
}
public function initializeUuidForKey()
{
$this->keyType = 'string';
}
}
Hope this helps.
Accepted answer not worked for me on Laravel 9, but this way worked perfect, you can try it:
1- Create new Trait Class in project path app/Traits/IdAsUuidTrait.php (if you not found Traits folder create it, this is full code of this Class:
<?php
namespace App\Traits;
use Illuminate\Support\Str;
trait IdAsUuidTrait
{
public function initializeIdAsUuidTrait(): void
{
$this->keyType = 'string';
$this->id = Str::orderedUuid()->toString();
}
}
2- In any model you want to make id as UUID just call trait like this:
use App\Traits\IdAsUuidTrait;
class YourModelName extends Model
{
use IdAsUuidTrait;
...
That is it, now try to create, select, update any row in database by this model...

Laravel Mystery - Two Similar Item Types Producing 2 Different Query Strings in Same Use Case

Ok, this is weird... You ready?
I have an item type on my site, lets call it SomeItem
It can have tags associated with it via a one-to-many relationship.
The sorts of queries that Laravel builds when dealing with tags for SomeItem are like this, for instance in response to route api/someitem/10:
select `tags`.*, `someitem_tag`.`someitem_id` as `pivot_someitem_id`, `someitem_tag`.`tag_id` as `pivot_tag_id` from `tags` inner join `someitem_tag` on `tags`.`id` = `someitem_tag`.`tag_id` where `someitem_tag`.`someitem_id` in (10)
When I create a second Item with identical settings - let's call it AnotherItems - it treats the database query for extracting tags in a different manner, using a different syntax in the queries. Extremely weird.
(and yes, I have an s at the end of the model name...)
For instance, this route api/anotheritems/1
produces this error:
Base table or view not found: 1146 Table 'mysite.tag_anotheritems' doesn't exist (SQL: select `tags`.*, `tag_anotheritems`.`anotheritems_id` as `pivot_anotheritems_id`, `tag_anotheritems`.`tag_id` as `pivot_tag_id` from `tags` inner join `tag_anotheritems` on `tags`.`id` = `tag_anotheritems`.`tag_id` where `tag_anotheritems`.`anotheritems_id` in (1))
See what is happening? Of course I am getting this error - in the database this tag table for AnotherItems is created as anotheritems_tag. That is analogous to SomeItem.
How on earth can Laravel be using syntax someitem_tag for one item but tag_anotheritems for another item??? WTF?
First let me show you how SomeItem is set up.
Here is the database structure related to Tags:
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateSomeItemTagTable extends Migration
{
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
Schema::create('someitem_tag', function (Blueprint $table) {
$table->integer('tag_id')->unsigned();
$table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
$table->integer('someitem_id')->unsigned();
$table->foreign('someitem_id')->references('id')->on('someitems')->onDelete('cascade');
$table->primary(array('tag_id', 'someitem_id'));
});
}
/**
* Reverse the migrations.
*
* #return void
*/
public function down()
{
Schema::drop('someitem_tag');
}
}
There is a Tags model/class that has this:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
protected $fillable = ['name'];
protected $hidden = [];
public $timestamps = false;
public function someitems()
{
return $this->belongsToMany(SomeItem::class);
}
}
And here is some relevant lines for SomeItem model/class:
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use App\Presenters\Presentable;
use Illuminate\Notifications\Notifiable;
use Auth;
class Exercise extends Model
implements Presentable
{
use Traits\SerializesUniversalDate;
use Traits\Presents;
use Notifiable;
protected $presenter = 'App\Presenters\SomeItemPresenter';
protected $fillable = ['title', etc];
protected $hidden = [];
public function parentitem()
{
return $this->belongsTo(ParentItem::class);
}
public function tags()
{
return $this->belongsToMany(Tag::class);
}
/**
* Update lesson tag array.
*
* #param array \App\Tag $tags
* #return void
*/
public function updateTags($tagsArray)
{
foreach ($tagsArray as &$value)
{
$tag = Tag::where('name', $value['name'])->first();
if (is_null($tag))
{
$tag = new Tag([
'name' => $value['name']
]);
$tag->save();
}
if (!$this->tags->contains($tag->id))
{
$this->tags()->attach($tag->id);
}
}
foreach($this->tags as &$existingTag)
{
if (!self::arrayContains($tagsArray, 'name', $existingTag->name))
{
$this->tags()->detach($existingTag->id);
}
}
$this->load('tags');
}
private static function arrayContains($array, $key, $value)
{
foreach ($array as $item)
{
if($item[$key] == $value) return true;
}
return false;
}
}
And here is some relevant code for SomeItem API controller:
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Input;
class SomeItemController extends Controller
{
public function index(Request $request)
{
$query = \App\SomeItem::query();
return $query->get()->load('parentitem')->load('tags');
}
//show item for editing
public function show($id)
{
$someitem = \App\SomeItem::find($id);
$someitem->load('parentitem')->load('tags');
$someitem->attachKindToFiles();
return $someitem;
}
//store new entry to db
public function store()
{
$someitem = \App\SomeItem::create(Input::all());
isset(Input::all()['tags']) ? $someitem->updateTags(Input::all()['tags']) : '';
return $someitem;
}
//update/save
public function update($id)
{
$someitem = \App\SomeItem::find($id);
$someitem->update(Input::all());
$someitem->updateTags(Input::all()['tags']);
$someitem->load('tags');
return $someitem;
}
There is also a SomeItem presenter and composer but they don't do anything with tags.
With AnotherItems, I literally I duplicated everything from SomeItem and just changed names as needed.
So in the Tag model there is
public function anotheritems()
{
return $this->belongsToMany(AnotherItems::class);
}
In AnotherItems model there is this, for instance
public function tags()
{
return $this->belongsToMany(Tag::class);
}
In the AnotherItems API controller there is this, for instance (which is for route api/anotheritems/1):
public function index(Request $request)
{
$query = \App\AnotherItems::query();
if ($request->has('id')) {
$query->where('id', $request['id']);
}
return $query->get()->load('parentitem')->load('tags');
}
So, this is a total mystery. I have been trying to figure this out for 2 days now. And I continue asking myself
How on earth can Laravel be using syntax someitem_tag for one item but tag_anotheritems for another item???
I upgraded from laravel 5.2 to 5.3 and it is after the upgrade that I added this AnotherItems. But I can't figure out how that could possibly alter things in terms of these database queries.
I have tried a ton of artisan commands for clearing everything imaginable, but somewhere in the framework it wants to handle SomeItem and AnotherItems differently when building these join queries to extract/save tags.
Thoughts?
thanks,
Brian
Decided to step through code in debugger. Seems things are breaking down in Str.php in various snake related function, and I also noticed a snakeCache call, whatever the heck that is. Not sure why such a strange methodology to determine table names... Also in these functions there is some pluralizing related checks, so maybe this is related to me using an s at the end of my item name. Pretty messed up stuff if an s at the end of a model name can cause two different logic branches...

Trying to hook into Model 'updating' event with a trait

I'm trying to provide a way to track when a user makes a change to a model for a notes section in my application. E.g. John goes and modifies 2 fields, a note would be created saying John has changed title from 'My title 1' to 'My title 2' and content from 'Lipsum' to 'Lipsum2'.
Here is a trait I created:
<?php
namespace App\Traits;
use Illuminate\Database\Eloquent\Model;
trait TrackChanges
{
public $changes;
public static function bootChangesTrait()
{
static::updating(function($model)
{
$this->changes = [];
foreach($model->getDirty() as $key => $value)
{
$original = $model->getOriginal($key);
$this->changes[$key] = [
'old' => $original,
'new' => $value,
];
}
});
}
}
And I am using that trait successfully on my model. However, I'm not sure how to capture the contents of the changes, or if they are even working correctly.
In my controller I have:
$site = Site::findOrFail($id);
// Catch and cast the is_active checkbox if it's been unselected
if ( ! $request->exists('is_active') )
{
$request->request->add([ 'is_active' => 0 ]);
}
// // Get rid of the CSRF field & method
$data = $request->except([ '_token', '_method' ]);
$site->update($data);
I tried dd($site->changes) before and after $site->update($data); but it just returns null.
What am I doing wrong?
You need to change your boot method in your trait to bootTrackChanges(). To boot traits you need to follow the naming pattern of boot{TraitName} for your boot method. Then you need to change your $this calls in your trait to $model so the change get saved to the model so your trait should look like this:
<?php
namespace App\Traits;
use Illuminate\Database\Eloquent\Model;
trait TrackChanges
{
public $changes;
public static function bootTrackChanges()
{
static::updating(function($model)
{
$changes = [];
foreach($model->getDirty() as $key => $value)
{
$original = $model->getOriginal($key);
$changes[$key] = [
'old' => $original,
'new' => $value,
];
}
$model->changes = $changes;
});
}
}
Another thing to note is if you have defined a boot method in your model make sure you call the parent boot method as well or else your trait's boot methods will not be called and your listener will not be registered.. I have spent hours and hours on this one before due to forgetting to call the parent method. In your model defining a boot method is not required but if you did call the parent like:
class MyModel extends Model
{
use TrackChanges;
protected static function boot()
{
// Your boot logic here
parent::boot();
}
}

Resources