I have a load of Models. Users can comment on some of the model records. For example, a user can comment on a Notice model record, or a user can comment on a Calendar Model record.
I've created a trait called Commentable. This contains all the methods needed to retrieve comments based on a model, to add / delete comments, create the comment create form and so on.
Whenever I want a model to be commentable, all I need to do is use that trait within the model.
Because that comment is a polymorphic relationship with the model record, I can't do an onCascade = delete migration.
Whenever the parent model is deleted (such as a notice or calendar item), then I want all associated records to be deleted as well, but I'd prefer not to have to rely on the developer writing the deleteRelatedComments() method call into an overridden delete function.
I thought I would create a service provider that listened for any model deletion event, check to see if that model was commentable, and delete any associated comments, but the event doesn't fire correctly.
This is my service provider code:
Model::deleting(function (Model $record) {
if(in_array('App\Libraries\Traits\Commentable', class_uses($record))) {
$record->deleteRelatedComments();
}
return true;
});
As you can see, all it does is check the deleted model to see if it uses the Commentable trait. If it does, it calls the deleteRelatedComments() method.
My Question
Is there any way to automatically delete related polymorphic related content on the deletion of it's parent record:
If I delete a Notice record, is there any way to delete any associated polymorphic Comment records?
I have previously created a service provider to listen for any Model deletes using Model Events as specified at http://laravel.com/docs/5.1/eloquent#events
However, this doesn't work if you're trying to listen to the Model class instead of a specific class such as Notice.
Instead, when you delete each model, a eloquent.deleting event is fired. I can then manually create a listener within the EventServiceProvider to listen for any model being deleted, and perform my checks accordingly, but without having the rely on the user overloading the delete() method and manually deleting the polymorphic relations.
Answer
If you want to delete polymorphic content automatically when you delete a model, make sure you either implement an interface or use a trait in the model.
For example, I want to delete all polymorphic comments when I delete the parent model record (If I delete a Notice record delete all polymorphic comments).
I created a Commentable trait which I use in any model (but you can just as easily use an empty interface on your model).
Then, in your EventServiceProvider.php alter the boot() method accordingly:
public function boot(DispatcherContract $events)
{
parent::boot($events);
/**
* If you use a trait in your model:
*/
$events->listen('eloquent.deleting*', function ($record) {
if (in_array('App\Libraries\Traits\Commentable', class_uses($record))) {
$record->deleteRelatedComments();
}
});
/**
* Or if you use an interface:
*/
$events->listen('eloquent.deleting*', function ($record) {
if ($record instanceof SomeInterface) {
$record->deleteSomePolymorphicRelation();
{
});
}
Use case:
Commentable Trait
namespace App\Libraries\Traits;
trait Commentable
{
public function comments()
{
return $this->morphMany(Comment::class, 'content');
}
public function deleteRelatedComments()
{
$this->comments()->delete();
}
}
Commentable Model
class Notice extends Model
{
use Commentable;
/** ... **/
/**
* NOTE: You do not need to overload the delete() method
*/
}
EventServiceProvider.php
public function boot(DispatcherContract $events)
{
parent::boot($events);
$events->listen('eloquent.deleting*', function ($record) {
if (in_array('App\Libraries\Traits\Commentable', class_uses($record))) {
$record->deleteRelatedComments();
}
});
}
Now, the developer creating a new content type that wants it to be commentable doesn't need to worry about creating the relations, managing the relation methods, or even deleting the polymorphic contents by overloading delete(). To make a model entirely commentable, they just need to use the Commentable trait and the framework handles everything.
This method can be used to automatically delete any polymorphic record, as long as the parent model implements some interface or uses a related trait.
Related
I have a model that needs to delete images when the model is deleted. The model I will be calling is Seminar.php. This "Seminar" model has got instructors in the model called SeminarInstructor.php. Suppose I called Seminar::find(1)->delete(); to delete the seminar with id = 1-- I would also need this delete off any instructors connected to the model as below:
public function seminarInstructor()
{
return $this->hasMany(SeminarInstructor::class, 'seminar_id', 'id');
}
I have enabled cascading, so when I call delete, laravel/mysql should automatically resolve it. However, I have a method called public function deleteCloudImage1() in SeminarInstructor.php which handles the deletion logic for the image existing in my table as a url. I could manually call this method before I delete a seminar model, but I want to know if there is any convenient method to handle this in eloquent/laravel. I looked up the code a bit and there seems to be a callable attribute called onDelete in the implementation. Anyone know how to use this, or any other alternative.
I have 2 models: User and Role.
A user can have many roles.
A role can have many users.
I have a custom pivot model between these 2 models. This custom pivot model only exists because it uses a trait that listens/logs for events such as created, updated, deleted.
Let's say I have a role called moderator. When I attach() (or detach()) that role to 5 users, it does successfully fire 5 created (or deleted) events for the pivot table.
$roleModerator->users()->attach($anArrayOfFiveUsersHere);
So far so good.
My problem is the following: when I delete the moderator role itself, it does delete all pivot rows associated to the role, but it does not fire any deleted event for each deleted pivot rows.
Expected behavior: I want Laravel to fire deleted events for each rows it deletes in the pivot table when I ask it to delete the role.
Environment: PHP 7.3 / Laravel 6
One weird thing I noticed, if I add this to my Role model :
public static function boot()
{
parent::boot();
static::deleting(function (self $model)
{
//$model->users()->detach(); // <-- this fails firing deleted events.
//MyCustomPivot::query()->where('role_id', $model->id)->get()->each->delete(); // <-- this fails firing deleted events.
$model->users()->sync([]); // <--- this works!
});
}
sync([]) will work great and fire as many deleted events as it deletes pivot rows.
but detach(), although it accomplishes the same thing, won't fire any deleted event. Why is that? They are both from InteractisWithPivotTable.php and sync() does even call detach() itself!
Not 100% sure it's applicable to your situation, but according to this issue on Github, you need to do some setup in your models.
First, make sure you have a primary key column in your table and defined on your pivot model.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
class MyCustomPivot extends Pivot
{
public $primaryKey = "id";
public $incrementing = true;
}
Second, make sure you include your custom pivot model in your relationships.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
public function users()
{
return $this->belongsToMany(User::class)
->using(MyCustomPivot::class)
->withPivot('id');
}
}
class User extends Model
{
public function roles()
{
return $this->belongsToMany(Role::class)
->using(MyCustomPivot::class)
->withPivot('id');
}
}
Laravel 5.8 is supposed to dispatch the syncing, attaching and detaching events (https://laravel.com/docs/5.8/releases search for Intermediate Table / Pivot Model Events section).
UPDATE: the release notes have been update after posting this question (more info: https://github.com/laravel/framework/issues/28050 - https://github.com/laravel/docs/pull/5096).
I tried it out but the following code throws the exception:
Call to undefined method App\ProjectUser::syncing()
NOTE: since Laravel 5.8 is supposed to dispatch the syncing event I don't want to use an external package.
class User extends Model
{
public function projects()
{
return $this->belongsToMany(\App\Project::class)->using(\App\ProjectUser::class);
}
}
class Project extends Model
{
public function users()
{
return $this->belongsToMany(\App\User::class)->using(\App\ProjectUser::class);
}
}
class ProjectUser extends Pivot
{
public static function boot()
{
parent::boot();
static::syncing(function ($item) {
dd('syncing event has been fired!');
});
}
}
// web.php
$project = \App\Project::first();
$project->users()->sync([1,2]);
I tried to move the boot method from ProjectUser to User and Project but I get the same exception.
On Laravel 5.8, when you are using the methods sync, attach or detach is going to be fired the appropriate model events (creating, updating, saving, ...) for the called action. Note that using sync, attach or detach is not going to fire any event like syncing, attaching or detaching.
More specifically, the sequence of events fired for each element passed to the sync method are:
saving
creating
created
saved
The sequence of events fired for each element passed to the attach method are:
saving
creating
created
saved
The sequence of events fired for each element passed to the detach method are:
deleting
deleted
So if you want to observe the syncing operation you actually have to observe the saving (or saved) event from the pivot model (in this case ProjectUser):
class ProjectUser extends Pivot
{
public static function boot()
{
parent::boot();
static::saving(function ($item) {
// this will die and dump on the first element passed to ->sync()
dd($item);
});
}
}
A working example https://github.com/danielefavi/laravel-issue-example
More info on this issue https://github.com/laravel/framework/issues/28050
The release notes were misleading and they have been changed https://github.com/laravel/docs/pull/5096.
If detach method called without ids (for detach all relations), events are not firing
https://github.com/laravel/framework/pull/27571#issuecomment-493451259
i tried many different way for the solve this need, but it is impossible without use external package or override many method.
I choose chelout/laravel-relationship-events package.
It's look clean and understable. And use with trait.
I want to execute my custom code after or before every $model->save() in Yii2.
I want to perform this globally like using components, etc.
I want to create a user activity log to store how many times a user insert or update any rows in database table, so for this I want to run some code when ever data inserted or update in tables.
Any help or suggestion will appreciated.
As #patryk mentioned ActiveRecord has beforeSave and afterSave methods.
I use something like the following to store a created date for new records (and updated date when existing records are updated). The code in the example is, of course, trivial but it allows you to use any arbitrary code you need, see the layout and how to split code for 'new' records and existing.
This overridden method can be added to any model class which extends ActiveRecord to allow the parent beforeSave to be called correctly also.
/**
* #inheritdoc
*/
public function beforeSave($insert)
{
if ($insert) {
// This is a new instance of modelClass, run your 'insert' code here.
$this->created_date = time();
}
// Anything else will be run any time a model is saved.
$this->updated_date = time();
return parent::beforeSave($insert);
}
edited to add:
if the code to be run is the same for each model you could create a trait and use the trait in each model to allow you to change the behaviour in one place. Or create a custom ActiveRecord class to override the beforeSave method for each subclass.
Create new class(MyActiveRecord) which extends \yii\db\ActiveRecord
Use extends MyActiveRecord to all your project models
Ex:
class MyActiveRecord extends \yii\db\ActiveRecord
{
public function afterSave($insert, $changedAttributes){
//This will called after every model saved
return parent::beforeSave($insert,$changedAttributes);
}
}
In your project other models
class Customer extends app\models\MyActiveRecord
{
}
Yii2 ActiveRecord class has beforeSave and afterSave methods. https://github.com/yiisoft/yii2/blob/master/framework/db/BaseActiveRecord.php#L926
But maybe it would be better to do such operation on database triggers?
I'm trying to detect whether a particular HasOne() relation has changed on one of my models before saving. I tried to hook into a model observer and use the getDirty() method, but this seems to only return standard model attributes, not relations:
// Observers
public static function boot() {
parent::boot();
Mission::updating(function($mission) use ($missionMailQueuer) {
$attrs = $mission->getDirty();
});
}
Is there any way to detect a relationship change too?