Skip to content

Commit

Permalink
Fix withoutEvents() not registering boot() listeners (#33149)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
derekmd committed Jun 8, 2020
1 parent 765c26b commit 3e47385
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 1 deletion.
5 changes: 4 additions & 1 deletion src/Illuminate/Database/Eloquent/Concerns/HasEvents.php
Expand Up @@ -3,6 +3,7 @@
namespace Illuminate\Database\Eloquent\Concerns;

use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Events\NullDispatcher;
use Illuminate\Support\Arr;
use InvalidArgumentException;

Expand Down Expand Up @@ -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();
Expand Down
139 changes: 139 additions & 0 deletions src/Illuminate/Events/NullDispatcher.php
@@ -0,0 +1,139 @@
<?php

namespace Illuminate\Events;

use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Support\Traits\ForwardsCalls;

class NullDispatcher implements DispatcherContract
{
use ForwardsCalls;

/**
* The underlying event dispatcher instance.
*/
protected $dispatcher;

/**
* Create a new event dispatcher instance that does not fire.
*
* @param \Illuminate\Contracts\Events\Dispatcher $dispatcher
* @return void
*/
public function __construct(DispatcherContract $dispatcher)
{
$this->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);
}
}
52 changes: 52 additions & 0 deletions tests/Integration/Database/EloquentModelWithoutEventsTest.php
@@ -0,0 +1,52 @@
<?php

namespace Illuminate\Tests\Integration\Database;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

/**
* @group integration
*/
class EloquentModelWithoutEventsTest extends DatabaseTestCase
{
protected function setUp(): void
{
parent::setUp();

Schema::create('auto_filled_models', function (Blueprint $table) {
$table->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';
});
}
}

0 comments on commit 3e47385

Please sign in to comment.