Closed scorgn closed 4 years ago
Do you want to submit a PR for review?
If @ShawnCorrigan does not want to submit a PR I'm happy to give it a shot
I added static::boot() in the beginning of the
withoutEvents()
method before copying the dispatcher, and everything worked. I recommend this as a fix.
The full Model
boot lifecycle, including events 'booting' and 'booted' would have to be run.
Laravel 7.x added new booting()
and booted()
event hooks.
Model::boot()
ahead of time in withoutEvents()
Model::withoutEvents()
method doesn't have an instance of the model however the bootIfNotBooted()
method requires one to dispatch all boot events.Model::withoutEvents()
temporarily removes the event dispatcher for all Eloquent Model
-extending classes.withoutEvents()
would need to instantiate and pre-boot every Eloquent Model
referenced in that Closure
. e.g.,
Comment::creating(function ($comment) {
$comment->uuid = Str::uuid()->toString();
});
$commenter = User::withoutEvents(function () {
$user = factory(User::class)->create();
factory(Comment::class, 4)->create(['user_id' => $user->id]);
factory(Avatar::class)->create(['user_id' => $user->id]);
return $user;
});
User
, Comment
, and Avatar
would all need the boot lifecycle. withoutEvents()
can't know ahead of time that those models will be referenced in the Closure
.
The workaround I can see is introducing a new Model
property (similar to Model::$booted
) that tracks which classes were booted without the event dispatcher.
protected static $bootedWithoutEvents = [];
// ....
protected function bootIfNotBooted()
{
if (! isset(static::$booted[static::class])) {
// ...
if (! isset(static::$dispatcher)) {
static::$bootedWithoutEvents[static::class] = true;
}
}
}
Then withoutEvents()
could "unboot" models once it completes. ... but the problem with that approach is userland can register boot behavior that has absolutely nothing to do with event dispatchers. So this solution would allow boot hooks to be called twice: once without an event dispatcher, another with the dispatcher.
Model::registerModelEvent()
calls until after withoutEvents()
Another approach could be to cache Model::registerModelEvent()
callbacks when registered within withoutEvents()
. They could be restored at the end of withoutEvents()
after static::setEventDispatcher($dispatcher)
is called.
EDIT: Here is an example implementation where
Model@registerModelEvent()
is intercepted. It requires introducing two new properties toIlluminate\Database\Eloquent\Model
, which isn't ideal.
The most straight-forward solution might be to add a callout alert in the docs that note to keep custom event listeners you must new
up model classes before calling withoutEvents()
. https://laravel.com/docs/7.x currently doesn't have any documentation about how the withoutEvents()
method behaves.
The workaround I can see is introducing a new
Model
property (similar toModel::$booted
) that tracks which classes were booted without the event dispatcher.
Love this approach personally, but as you mention it does add more API surface to a class that's already often criticized for being big.
The most straight-forward solution might be to add a callout alert in the docs that note to keep custom event listeners you must
new
up model classes before callingwithoutEvents()
. https://laravel.com/docs/7.x currently doesn't have any documentation about how thewithoutEvents()
method behaves.
Documenting the current behaviour is probably a good thing but it might also invite more people to use this functionality, and withoutEvents
is something a developer should hesitate to reach for in my opinion.
E: thanks for the clarifying research by the way!
Implementation attempt 2: https://github.com/laravel/framework/compare/6.x...derekmd:without-events-registers-listeners
This is lightweight on the Eloquent Model
class and no object properties are added. A null pattern is used on the dispatcher to do nothing when a fired event is attempted. By proxying the concrete Illuminate\Events\Dispatcher
, listener registrations can still be done when inside Model::withoutEvents()
.
The NullDispatcher
code is kind of gnarly because PHP interfaces don't allow falling back to a proxy __call()
. You must explicitly define each method in the contract.
@derekmd looks good to me. Can you send that in? Thanks!
Description:
In one of the services I am building, all of the models use a trait that use the
boot()
method to add a UUID onto the model oncreating()
. Without this UUID, the model would not be able to be inserted into the database. In my seeders, I have disabled event listeners. As expecting, thiscreating()
observer did not fire, but in seeders it was fine because I just added it in there.The issue came up after the seeder was ran. It looked like if I use
withoutEvents(...)
in the seeder, then even model observer events outside of the callable argument do not become triggered.I dug deep and wide to figure out why this was happening, and I finally figured it out.
The first thing the
withoutEvents(...)
method will do is copy the dispatcher on the model. Since this is ran before the model class is booted, thecreated
model event listener which is generated in theboot
method of the model is not in the copied dispatcher. Inside the callable, it is booting the method and attempting to observe thecreated
event. After all is said and done, the dispatcher (which doesn't have thecreated
observer) is put back onto the model. Subsequent usages of the model do not trigger theboot
method and the model will now never have thecreated
observer defined there.Steps To Reproduce:
boot()
method that sets a model observer (self::creating(function ($model) { ... }
)Model::withoutEvents(function() { ... })
callableSuggestion for fixing
I added
static::boot()
in the beginning of thewithoutEvents()
method before copying the dispatcher, and everything worked. I recommend this as a fix.