I have an Laravel application for Properties, let's say somewhere in my code I do:
$property = new Property();
$property->city = "New York";
...
$property->save();
Then I have Event Listener that listens for specific event:
$events->listen(
'eloquent.saved: Properties\\Models\\Property',
'Google\Listeners\SetGeoLocationInfo@fire'
);
And finally in SetGeoLocationInfo.php
I have
public function fire($event)
{
$property = $event;
...
//get GPS data from Google Maps
$property->latitude = $googleMapsObject->latitude;
$property->longitude = $googleMapsObject->longitude;
$property->save();
}
And when I save model in goes to infinite recursion, because of save()
evoked in the handler.
How I can change my code to make it fill location data just one time after saving and avoid recursion?
I cannot use flushEventListeners()
because in this case other listeners stop working (e.g. property photo attaching).
In this case probably better would be using saving
method. But be aware that during saving
you should not use save
method any more, so your fire
method should look like this:
public function fire($event)
{
$property = $event;
...
//get GPS data from Google Maps
$property->latitude = $googleMapsObject->latitude;
$property->longitude = $googleMapsObject->longitude;
}
Other solution would be adding condition to to set and save GPS location only if it's not set yet:
if (empty($property->latitude) || empty($property->longitude)) {
$property->latitude = $googleMapsObject->latitude;
$property->longitude = $googleMapsObject->longitude;
$property->save();
}
Your Property save method (you must define property constants for it):
public function save($mode = Property::SAVE_DEFAULT)
{
switch ($mode) {
case Property::SAVE_FOO:
// something for foo
break;
case Property::SAVE_BAR:
// something for bar
break;
default:
parent::save();
break;
}
}
Call it:
public function fire($event)
{
$property = $event;
...
//get GPS data from Google Maps
$property->latitude = $googleMapsObject->latitude;
$property->longitude = $googleMapsObject->longitude;
$property->save(Property::SAVE_FOO);
}
or
$property->save(); // as default
What good? All conditions are in one place (in save method).
You can user forget()
to unset an event listener.
Event::listen('a', function(){
Event::forget('a');
echo 'update a ';
event("b");
});
Event::listen('b', function(){
Event::forget('b');
echo 'update b ';
event("a");
});
event("a"); // update a update b
The model event keys are named as "eloquent.{$event}: {$name}
" eg "eloquent.updated: Foo
"
I hit the same. In Laravel 5.6 you can simply override the finishSave() function in your model:
protected function finishSave(array $options)
{
// this condition allow to control whenever fire the vent or not
if (!isset($options['nosavedevent']) or empty($options['nosavedevent'])) {
$this->fireModelEvent('saved', false);
}
if ($this->isDirty() && ($options['touch'] ?? true)) {
$this->touchOwners();
}
$this->syncOriginal();
}
Then you can use it like this:
public function fire($event)
{
$property = $event;
...
//get GPS data from Google Maps
$property->latitude = $googleMapsObject->latitude;
$property->longitude = $googleMapsObject->longitude;
$property->save(['nosavedevent' => true]);
}
Inside you're fire
method, you could have it call $property->syncOriginal()
before applying new attributes and saving.
Model classes have a concept of 'dirty' attributes vs. 'original' ones, as a way of knowing which values have already been pushed to the DB and which ones are still slated for an upcoming save. Typically, Laravel doesn't sync these together until after the Observers have fired. And strictly-speaking, it's an ant-pattern to have your Listener act like it's aware of the context in which it's fired; being that you're triggering it off the saved
action and can therefore feel confident that the data has already reached the DB. But since you are, the problem is simply that the Model just doesn't realize that yet. Any subsequent update will confuse the Observer into thinking the original values that got you there are brand new. So by explicitly calling the syncOriginal()
method yourself before applying any other changes, you should be able to avoid the recursion.