From 3e47385fbea753f957e548e43d85775a376bba43 Mon Sep 17 00:00:00 2001 From: Derek MacDonald Date: Mon, 8 Jun 2020 11:27:19 -0400 Subject: [PATCH] Fix withoutEvents() not registering boot() listeners (#33149) If an Eloquent model class is instantiated for the first time inside a withoutEvents() Closure, any model boot() callbacks registering custom event listeners will be skipped. Instead of removing the dispatcher, replaced it with a null pattern implementation. Registration method calls still go through to the concrete dispatcher however fired event dispatch() calls become noop. --- .../Database/Eloquent/Concerns/HasEvents.php | 5 +- src/Illuminate/Events/NullDispatcher.php | 139 ++++++++++++++++++ .../EloquentModelWithoutEventsTest.php | 52 +++++++ 3 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 src/Illuminate/Events/NullDispatcher.php create mode 100644 tests/Integration/Database/EloquentModelWithoutEventsTest.php diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php b/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php index 96ed62334d49..0dc54308f395 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Eloquent\Concerns; use Illuminate\Contracts\Events\Dispatcher; +use Illuminate\Events\NullDispatcher; use Illuminate\Support\Arr; use InvalidArgumentException; @@ -399,7 +400,9 @@ public static function withoutEvents(callable $callback) { $dispatcher = static::getEventDispatcher(); - static::unsetEventDispatcher(); + if ($dispatcher) { + static::setEventDispatcher(new NullDispatcher($dispatcher)); + } try { return $callback(); diff --git a/src/Illuminate/Events/NullDispatcher.php b/src/Illuminate/Events/NullDispatcher.php new file mode 100644 index 000000000000..a27b88cf1f61 --- /dev/null +++ b/src/Illuminate/Events/NullDispatcher.php @@ -0,0 +1,139 @@ +dispatcher = $dispatcher; + } + + /** + * Don't fire an event. + * + * @param string|object $event + * @param mixed $payload + * @param bool $halt + * @return void + */ + public function dispatch($event, $payload = [], $halt = false) + { + } + + /** + * Don't register an event and payload to be fired later. + * + * @param string $event + * @param array $payload + * @return void + */ + public function push($event, $payload = []) + { + } + + /** + * Don't dispatch an event. + * + * @param string|object $event + * @param mixed $payload + * @return array|null + */ + public function until($event, $payload = []) + { + } + + /** + * Register an event listener with the dispatcher. + * + * @param string|array $events + * @param \Closure|string $listener + * @return void + */ + public function listen($events, $listener) + { + return $this->dispatcher->listen($events, $listener); + } + + /** + * Determine if a given event has listeners. + * + * @param string $eventName + * @return bool + */ + public function hasListeners($eventName) + { + return $this->dispatcher->hasListeners($eventName); + } + + /** + * Register an event subscriber with the dispatcher. + * + * @param object|string $subscriber + * @return void + */ + public function subscribe($subscriber) + { + return $this->dispatcher->subscribe($subscriber); + } + + /** + * Flush a set of pushed events. + * + * @param string $event + * @return void + */ + public function flush($event) + { + return $this->dispatcher->flush($event); + } + + /** + * Remove a set of listeners from the dispatcher. + * + * @param string $event + * @return void + */ + public function forget($event) + { + return $this->dispatcher->forget($event); + } + + /** + * Forget all of the queued listeners. + * + * @return void + */ + public function forgetPushed() + { + return $this->dispatcher->forgetPushed(); + } + + /** + * Dynamically pass method calls to the underlying dispatcher. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + return $this->forwardCallTo($this->dispatcher, $method, $parameters); + } +} diff --git a/tests/Integration/Database/EloquentModelWithoutEventsTest.php b/tests/Integration/Database/EloquentModelWithoutEventsTest.php new file mode 100644 index 000000000000..7a15415f2bb8 --- /dev/null +++ b/tests/Integration/Database/EloquentModelWithoutEventsTest.php @@ -0,0 +1,52 @@ +increments('id'); + $table->text('project')->nullable(); + }); + } + + public function testWithoutEventsRegistersBootedListenersForLater() + { + $model = AutoFilledModel::withoutEvents(function () { + return AutoFilledModel::create(); + }); + + $this->assertNull($model->project); + + $model->save(); + + $this->assertEquals('Laravel', $model->project); + } +} + +class AutoFilledModel extends Model +{ + public $table = 'auto_filled_models'; + public $timestamps = false; + protected $guarded = ['id']; + + public static function boot() + { + parent::boot(); + + static::saving(function ($model) { + $model->project = 'Laravel'; + }); + } +}