Avoid Eloquent model event from being triggered multiple/ infinite times using Laravel - laravel

I have the following code:
AppServiceProvider:
User::saved(function (User $user) {
return $user->modifiedEvent();
});
User::deleted(function (User $user)
return $user->modifiedEvent();
});
In my User model I have:
public function modifiedEvent()
{
$stage = $this->isDirty('terms_and_conditions')
&& $this->terms_and_conditions !== null ? 'completed' : 'incomplete';
$this->stage_terms_and_conditions = $stage;
return $this->save();
}
The issue I have is that the modifiedEvent also modifies the model, which means we get an infinite loop!
Whats the best way to make sure that this event is not triggered multiple times due caused by the event it self.

I will try to use the 'saving' event so there is no need to call $this->save() at the end.

Related

Event of model not fire when use in Query Scopes

I am using laravel 9, PHP 8.1.
I noticed the following problem:
// App\Model\Store
class Store extends Model
{
....
public function scopeUpdateStore($query, $id) {
Store::find($id)->update(['name' => 'foo']);
return $query;
}
}
Then if I use :
Store::updateStore($id)->first();
Event updating and updated will not fire.
But when I use in controller:
// StoreController
Store::find($id)->update(['name'])
Event updating and updated will fire.
My problem is that it is not clear why there is this difference, and is there a way to make the event fireable in the Query Scope?
Thank you very much.

Function return doesnt call else condition in Laravel 5.8

I have this big function that is calling a lot of other functions who that make API requests
to BlackBoard, my problem is, in this main function, I create my user and sign him to the current course, but actually we faced this situation: that same user is already registered in BlackBoard, so I made an update function, to that specific case. But that main function it's all in a try catch block, so every time that I call the method:
$userSalvo = $bd->createUser($user)
and the user already exist in BlackBoard database, he immediatly stop my function and call the catch block, to obviously get the exception, but I need if that user already exist, he comeback from this request and go to the next method:
$userSalvo = $bd->updateUser($user, $userId);
I already tried to put this in a if statement, but It doesnt work as it should:
if($userSalvo = $bd->createUser($user))
{
dd('create');
$user->id = $userSalvo['id'];
}else
{
dd('update');
$userSalvo = $bd->updateUser($user, $userId);
$user->id = $userSalvo['id'];
}
Anyone has some ideia of how can I do this?
This is the API method that I'm calling to update/create:
public function updateUser($user, $userId)
{
$user->dataSourceId = $this->dataSourceId;
return $this->bd->patch("/users/{$userId}", json_decode(json_encode($user), true));
}
public function createUser($user)
{
$user->dataSourceId = $this->dataSourceId;
return $this->bd->post("/users", json_decode(json_encode($user), true));
}
You should then use the try-catch block if you do not want to refactor your code. What's happening by the sounds of it is your if condition dies as 'createUser' is called.
Rather try this to replace your entire if statement.
try{
$userSalvo = $bd->createUser($user)
}catch(Exception $e){
$userSalvo = $bd->updateUser($user, $userId);
}
This is assuming that $user and $userId is defined.

Laravel Backpack - getting current record from crud controller

In my crud controller I am trying to get the name of the person who is currently being edited.
so
http://192.168.10.10/admin/people/93/edit
In the people crud controller
public function setup() {
dd(\App\Models\People::get()->first()->name)
}
This returns the first person not the person currently being edited.
How do I return the current person (with an id of 93 in this example)
Ok, So since you use backpack look into CrudController to see how the method looks:
public function edit($id)
{
$this->crud->hasAccessOrFail('update');
$this->data['entry'] = $this->crud->getEntry($id);
$this->data['crud'] = $this->crud;
$this->data['fields'] = $this->crud->getUpdateFields($id);
$this->data['id'] = $id;
return view('crud::edit', $this->data);
}
So now you can overwrite the edit function and change whatever you want. You can even create a custom edit page if you so wish.
Setup on the other hand is usually used to add things like
$this->crud->addClause(...);
Or you can even get the entire constructor and put it in the setup method because setup call looks like this:
public function __construct()
{
// call the setup function inside this closure to also have the request there
// this way, developers can use things stored in session (auth variables, etc)
$this->middleware(function ($request, $next) {
$this->setup();
return $next($request);
});
}
So you could do something like \Auth::user()->id;
Also it's normal to work like this. If you only use pure laravel you will only have access to the current id in the routes that you set accordingly.
Rahman said about find($id) method. If you want to abort 404 exception just use method findOrFail($id). In my opinion it's better way, because find($id)->name can throw
"Trying to get property of non-object error ..."
findOrFail($id) first fetch user with specified ID. If doesn't exists just throw 404, not 500.
The best answer is:
public function edit($id)
{
return \App\Models\People::findOrFail($id);
}
Good luck.
you need person against id, try below
public function setup($id) {
dd(\App\Models\People::find($id)->name);
}

Prevent Certain CRUD Operations on Laravel Eloquent Models

Is there an easy way to prevent certain CRUD operations from being performed on an Eloquent model?
How I'm doing it now (from memory, I think I'm missing an argument to be compatible with Eloquent's save(), but that's not important):
<?php
class Foo extends Eloquent {
public function save()
{
// Prevent Foo from being updated.
if (!empty($this->id)) {
throw new \Exception('Update functionality is not allowed.');
}
parent::save();
}
}
In this case, these models should not be allowed to be updated under any circumstance, and I want my app to explode should something try to update them. Is there a cleaner way to do this without overriding Eloquent's save() method?
In addition to #AlanStorm's answer, here's a comprehensive info:
You can setup global listener for all the models:
Event::listen('eloquent.saving: *', function ($model) {
return false;
});
Or for given model:
Event::listen('eloquent.saving: User', function ($user) {
return false;
});
// or
User::saving(function ($user) {
return false;
});
// If it's not global, but for single model, then I would place it in boot():
// SomeModel
public static function boot()
{
parent::boot();
static::saving(function ($someModel) {
return false;
});
}
For read-only model you need just one saving event listener returning false, then all: Model::create, $model->save(), $model->update() will be rejected.
Here's the list of all Eloquent events: booting, booted, creating, created, saving, saved, updating, updated, deleting, deleted and also restoring and restored provided by SoftDeletingTrait.
Eloquent's event system allows you to cancel a write operation by
Listening for the creating, updating, saving, or deleting events
Returning false from your event callback.
For example, to prevent people from creating new model objects, something like this
Foo::creating(function($foo)
{
return false; //no one gets to create something
});
in your app/start/global.php file would do the job.

Trouble with multiple model observers in Laravel

I'm stuck on a weird issue. It feels like in Laravel, you're not allowed to have multiple model observers listening to the same event. In my case:
Parent Model
class MyParent extends Eloquent {
private static function boot()
{
parent::boot();
$called_class = get_called_class();
$called_class::creating(function($model) {
doSomethingInParent();
return true;
}
}
}
Child Model
class MyChild extends myParent {
private static function boot()
{
parent::boot();
MyChild::creating(function($model) {
doSomethingInChild();
return true;
}
}
}
In the above example, if I do:
$instance = MyChild::create();
... the line doSomethingInChild() will not fire. doSomethingInParent(), does.
If I move parent::boot() within the child after MyChild::creating(), however, it does work. (I didn't confirm whether doSomethingInParent() fires, but I'm presuming it doesn't)
Can Laravel have multiple events registered to Model::creating()?
This one is tricky. Short version: Remove your return values from you handlers and both events will fire. Long version follows.
First, I'm going to assume you meant to type MyParent (not myParent), that you meant your boot methods to be protected, and not private, and that you included a final ) in your create method calls. Otherwise your code doesn't run. :)
However, the problem you describe is real. The reason for it is certain Eloquent events are considered "halting" events. That is, for some events, if any non-null value is returned from the event handlers (be it a closure or PHP callback), the event will stop propagating. You can see this in the dispatcher
#File: vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php
public function fire($event, $payload = array(), $halt = false)
{
}
See that third parameter $halt? Later on, while the dispatcher is calling event listeners
#File: vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php
foreach ($this->getListeners($event) as $listener)
{
$response = call_user_func_array($listener, $payload);
// If a response is returned from the listener and event halting is enabled
// we will just return this response, and not call the rest of the event
// listeners. Otherwise we will add the response on the response list.
if ( ! is_null($response) && $halt)
{
array_pop($this->firing);
return $response;
}
//...
If halt is true and the callback returned anything that's not null (true, false, a sclaer value, an array, an object), the fire method short circuits with a return $response, and the events stop propagating. This is above and beyond that standard "return false to stop event propagation". Some events have halting built in.
So, which Model events halt? If you look at the definition of fireModelEvent in the base eloquent model class (Laravel aliases this as Eloquent)
#File: vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
protected function fireModelEvent($event, $halt = true)
{
//...
}
You can see a model's events default to halting. So, if we look through the model for firing events, we see the events that do halt are
#File: vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
$this->fireModelEvent('deleting')
$this->fireModelEvent('saving')
$this->fireModelEvent('updating')
$this->fireModelEvent('creating')
and events that don't halt are
#File: vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
$this->fireModelEvent('booting', false);
$this->fireModelEvent('booted', false);
$this->fireModelEvent('deleted', false);
$this->fireModelEvent('saved', false);
$this->fireModelEvent('updated', false);
$this->fireModelEvent('created', false);
As you can see, creating is a halting event, which is why returning any value, even true, halted the event and your second listener didn't fire. Halting events are typically used when the Model class wants to do something with the return value from an event. Specifically for creating
#File: vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
protected function performInsert(Builder $query)
{
if ($this->fireModelEvent('creating') === false) return false;
//...
if you return false, (not null) from your callback, Laravel will actually skip performing the INSERT. Again, this is different behavior from the standard stop event propagation by returning false. In the case of these four model events, returning false will also cancel the action they're listening for.
Remove the return values (or return null) and you'll be good to go.

Resources