I am currently writing a class that caches model data for a select field.
Now obviously, if any model that is affecting this select field gets inserted, updated or deleted, the cache must be refreshed.
To handle this, I'd like to use the model events of Yii2. For example, if EVENT_AFTER_INSERT is triggered in the model Album, I want to execute the code to refresh the cache of the album select data.
Now I could do this the classical way and add an event to the model Album like this:
class Album extends ActiveRecord {
public function init(){
$this->on(self::EVENT_AFTER_INSERT, [$this, 'refresh_cache']);
$this->on(self::EVENT_AFTER_UPDATE, [$this, 'refresh_cache']);
$this->on(self::EVENT_AFTER_DELETE, [$this, 'refresh_cache']);
}
// ...
}
That would work, yes. Problem is, I'd need to include this code in any model I'd like to create a select field from at any point of development. It's not such a big deal, but you can easily forget it while coding and if the behavior needs to change at some point, you need to update a whole bunch of models.
Now here is my question: Is there any possibility to add events to a model from another component? My idea would be to create a component, that knows about all used select data caches and adds the necessary model events accordingly. any idea how to achieve this or something similar?
you just need create a behaviour and attach it to your various models. see the basic guide and speciffically the Behavior::events() use case
so i went ahead and wrote an example
class RefreshCacheBehavior extends \yii\base\Behavior
{
public function events() {
return [
\yii\db\ActiveRecord::EVENT_AFTER_INSERT => 'refreshCache',
\yii\db\ActiveRecord::EVENT_AFTER_UPDATE => 'refreshCache',
\yii\db\ActiveRecord::EVENT_AFTER_DELETE => 'refreshCache',
];
}
/**
* event handler
* #param \yii\base\Event $event
*/
public function refreshCache($event) {
// model that triggered the event will be $this->owner
// do things with Yii::$app->cache
}
}
class Album extends ActiveRecord {
public function behaviors() {
return [
['class' => RefreshCacheBehavior::className()],
];
}
// ...
}
Is there any possibility to add events to a model from another component?
Yes! You can use class level event handlers. The line of code below shows how to do that.
Event::on(ActiveRecord::className(), ActiveRecord::EVENT_AFTER_INSERT, function ($event) {
Yii::debug(get_class($event->sender) . ' is inserted');
});
You can use same code in your init method and bind it to your class method instead of that closure.
I would create a class implementing BootstrapInterface and add it to config. Then I would handle those class level events there!
Do yourself a favour and read about events in the Guide as well as the API Documentation
on() is a public method, so you can always attach event to already instantiated object. This may be useful if you're using some kind of factory do build your objects:
public function createModel($id) {
$model = Album::findOne($id);
if ($model === null) {
// some magic
}
$model->on(Album::EVENT_AFTER_INSERT, [$this, 'refresh_cache']);
$model->on(Album::EVENT_AFTER_UPDATE, [$this, 'refresh_cache']);
$model->on(Album::EVENT_AFTER_DELETE, [$this, 'refresh_cache']);
return $model;
}
Related
I have a model in laravel and I want to do something after the first time which an object of my model is created. the simplest way is to add a static boot method inside my model's class like the code below:
class modelName extends Model
{
public static function boot()
{
parent::boot();
self::created(function ($model) {
//the model created for the first time and saved
//do something
//code here
});
}
}
so far so good! the problem is: the ONLY parameter that created method accepts is the model object itself(according to the documentation) :
Each of these methods receives the model as their only argument.
https://laravel.com/docs/5.5/eloquent#events
I need more arguments to work with after model creation. how can I do that?
Or is there any other way to do something while it's guaranteed that the model has been created?
laravel version is 5.5.
You're close. What I would probably do would be to dispatch an event right after you actually create the model in your controller. Something like this.
class WhateverController
{
public function create()
{
$model = Whatever::create($request->all());
$anotherModel = Another::findOrFail($request->another_id);
if (!$model) {
// The model was not created.
return response()->json(null, 500);
}
event(new WhateverEvent($model, $anotherModel));
}
}
I solved the issue using static property in eloquent model class:
class modelName extends Model
{
public static $extraArguments;
public function __construct(array $attributes = [],$data = [])
{
parent::__construct($attributes);
self::$extraArguments = $data ;
public static function boot()
{
parent::boot();
self::created(function ($model) {
//the model created for the first time and saved
//do something
//code here
self::$extraArguments; // is available in here
});
}
}
It works! but I don't know if it may cause any other misbehavior in the application.
Using laravel events is also a better and cleaner way to do that in SOME cases.but the problem with event solution is you can't know if the model has been created for sure and it's time to call the event or it's still in creating status ( and not created status).
In a Laravel 5.5 project, I have a Person class and a Student class. The Student class extends the Person class. I have a bunch of stuff that needs to happen when a new person is created and a bunch of stuff that needs to happen when a new student (who is also a person of course) is created.
My classes look something like this...
class Person extends Model {
protected $dispatchesEvents = [
'created' => PersonJoins::class
];}
.
class Student extends Person {
protected $dispatchesEvents = [
'created' => StudentIsCreated::class,
];}
When a new Student instance is created, the StudentIsCreated event fires but the PersonJoins event does not.
A workaround is to change the 'created' in one of the models to 'saved' and then both events are triggered. From that, it seems obvious what is happening. The 'created' element in the $dispatchesEvents array on the Person model is being overwritten by the same on the Student model. Even just typing that, it seems the solution should be obvious but I just can't see it.
So, my question is this... How do I have an event triggered by 'created' on two models, one of which extends the other?
Thank you.
David.
EDIT:
After reading #hdifen answer. My Student model now looks like this...
class Student extends Person
{
protected static function boot()
{
parent::boot();
static::created(function($student) {
\Event::Fire('StudentCreated', $student);
});
}
}
and in App\Events\StudentCreated.php I have...
class StudentCreated
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*
* #return void
*/
public function __construct($student)
{
echo ("\r\nStudentCreated event has fired");
$this->student = $student;
}
/**
* Get the channels the event should broadcast on.
*
* #return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('Student-Is-Created-Channel');
}
}
But the event doesn't seem to be fired. Am I doing something wrong?
Yes you are correct the student model is overwriting the parent variable $dispatchesEvents.
I would recommend not using $dispatchedEvent as events are mainly used if you are doing something more complex that requires other parts of your code to react when they are fired.
The simple solution is to just manually hook into the created event in your model, in your case you want to create a grades model when a student is created?
protected static function boot()
{
parent::boot();
static::created(function($model) {
$grade = new Grade;
$model->grades->save($grade);
// Or if you really want to use an event something like
Event::fire('student.created', $student);
});
}
This post might give you an idea:
Laravel Model Events - I'm a bit confused about where they're meant to go.
I would like to refactor some event so I've created an event subscriber class.
class UserEventListener
{
public function onUserLogin($event, $remember) {
$event->user->last_login_at = Carbon::now();
$event->user->save();
}
public function onUserCreating($event) {
$event->user->token = str_random(30);
}
public function subscribe($events)
{
$events->listen(
'auth.login',
'App\Listeners\UserEventListener#onUserLogin'
);
$events->listen(
'user.creating',
'App\Listeners\UserEventListener#onUserCreating'
);
}
}
I register the listener as follows:
protected $subscribe = [
'App\Listeners\UserEventListener',
];
I added the following to the boot method of the user model as follows:
public static function boot()
{
parent::boot();
static::creating(function ($user) {
Event::fire('user.creating', $user);
});
}
But when I try the login I get following error:
Indirect modification of overloaded property App\User::$user has no effect
Whats wrong with the onUserLogin signature? I thought you can access user using $event->user...
If you want to use event subscribers, you'll need to listen on the events that Eloquent models fire at different stages of their lifecycle.
If you have a look at fireModelEvent method of Eloquent model, you'll see that the event names that are fired are built the following way:
$event = "eloquent.{$event}: ".get_class($this);
where $this is the model object and $event is the event name (creating, created, saving, saved, etc.). This event is fired with a single argument, being the model object.
Another option would be to use model observers - I prefer that to event subscribers as it makes listening on different lifecycle events easier - you can find an example here: http://laravel.com/docs/4.2/eloquent#model-observers
Regarding auth.login, when this event is fired, 2 parameters are passed along - user that logged in and the remember flag. Therefore, you'll need to define a listener that takes 2 arguments - first will be the user, second will be the remember flag.
I am trying to check in the constructor of a model if the currently authenticated user is allowed to access the given model, but I am finding that $this from the constructor's context is empty. Where are the attributes assigned to a model in Laravel and how should I go about calling a method once all of the attributes have been loaded?
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
var_dump($this); // empty model
$this->checkAccessible();
}
Cheers in advance
As stated in the other answers & comments, there are better ways to achieve the aims of the question (at least in modern Laravel). I would refer in this case to the Authorization chapter of the documentation that goes through both gates and policies.
However, to answer the specific question of how to call a method once a models attributes have been loaded - you can listen for the Eloquent retrieved event. The simplest way to do this within a class is using a closure within the class booted() method.
protected static function booted()
{
static::retrieved(function ($model) {
$model->yourMethod() //called once all attributes are loaded
});
}
You can also listen for these events in the normal way, using listeners. See the documentation for Eloquent events.
you can use controller filter to check whether user logged in or not and than you call any model function.
public function __construct(array $attributes = []){
$this->beforeFilter('auth', array('except' => 'login')); //login route
if(Auth::user()){
$user_id = Auth::user()->user_id;
$model = new Model($attributes);
//$model = User::find($user_id);
}
}
Binding Attributes to Model from constructor
Model.php
public function __construct(array $attributes = array())
{
$this->setRawAttributes($attributes, true);
parent::__construct($attributes);
}
As it was mentioned by Rory, the retrieved event is responsible for that.
Also, it could be formed in a much cleaner and OOP way with Event/Listener approach, especially if you need to write a lot of code or have few handlers.
As it described here, you can just create an event for the Model like
protected $dispatchesEvents = [
'retrieved' => UserLoaded::class,
];
You need to create this class, eloquent event accepts the model by default:
class UserLoaded
{
protected User $user;
public function __construct(User $user)
{
$this->user = $user;
}
}
Then here is described how to declare listener for this event. It should be somewhere in the EventListenerProvider like this:
protected $listen = [
UserLoaded::class => [
UserLoadedListener::class
],
];
The listener should just implement method handle() (check article) like:
public function handle(UserLoaded $event)
{
// your code
}
Another possibility is to register model Observer, as it´s described here
I have a laravel project, and I need to make some calculations immediately after I save a model and attach some data to it.
Is there any event that is triggered in laravel after calling attach (or detach/sync)?
No, there are no relation events in Eloquent. But you can easily do it yourself (Given for example Ticket belongsToMany Component relation):
// Ticket model
use App\Events\Relations\Attached;
use App\Events\Relations\Detached;
use App\Events\Relations\Syncing;
// ...
public function syncComponents($ids, $detaching = true)
{
static::$dispatcher->fire(new Syncing($this, $ids, $detaching));
$result = $this->components()->sync($ids, $detaching);
if ($detached = $result['detached'])
{
static::$dispatcher->fire(new Detached($this, $detached));
}
if ($attached = $result['attached'])
{
static::$dispatcher->fire(new Attached($this, $attached));
}
}
event object as simple as this:
<?php namespace App\Events\Relations;
use Illuminate\Database\Eloquent\Model;
class Attached {
protected $parent;
protected $related;
public function __construct(Model $parent, array $related)
{
$this->parent = $parent;
$this->related = $related;
}
public function getParent()
{
return $this->parent;
}
public function getRelated()
{
return $this->related;
}
}
then a basic listener as a sensible example:
// eg. AppServiceProvider::boot()
$this->app['events']->listen('App\Events\Relations\Detached', function ($event) {
echo PHP_EOL.'detached: '.join(',',$event->getRelated());
});
$this->app['events']->listen('App\Events\Relations\Attached', function ($event) {
echo PHP_EOL.'attached: '.join(',',$event->getRelated());
});
and usage:
$ php artisan tinker
>>> $t = Ticket::find(1);
=> <App\Models\Ticket>
>>> $t->syncComponents([1,3]);
detached: 4
attached: 1,3
=> null
Of course you could do it without creating Event objects, but this way is more convenient, flexible and simply better.
Steps to solve your problem:
Create custom BelongsToMany relation
In BelongsToMany custom relation override attach, detach, sync and updateExistingPivot methods
In overriden method dispatch desired events.
Override belongsToMany() method in Model and return your custom relation not default relation
and that's it. I created package that already doing that: https://github.com/fico7489/laravel-pivot
Laravel 5.8 now fires events on ->attach()
Check out: https://laravel.com/docs/5.8/releases
And search for: Intermediate Table / Pivot Model Events
https://laracasts.com/discuss/channels/eloquent/eloquent-attach-which-event-is-fired?page=1
Update:
From Laravel 5.8 Pivot Model Events are dispatched like normal model.
https://laravel.com/docs/5.8/releases#laravel-5.8
You just need to add using(PivotModel::class) to your relation and events will work on the PivotModel.
Attach($id) will dispatch Created and Creating
Detach($id) will dispatch Deleting and Deleted,
Sync($ids) will dispatch the needed events too [Created,Creating,Deleting,Deleted]
Only dispatch() with out id doesn't dispatch any event until now.