0

I'm currently working with Laravel's Queued Event Listeners on an Eloquent model's saving event. On my event class, I have set up a public property that contains my model which I can then access inside the $event object within my listener via $event->myModel.

The handle() method on my listener compares old values from the new one so I removed Illuminate\Queue\SerializesModels on my event class. This works fine and I'm now able to compare old values from the new one.

The issue is with testing. I am making a test that checks if the old and new value that I'm getting inside my listener's handle() method is different. However, the $event->myModel->getOriginal() now returns an empty array. This only happens when testing.

Is this a bug?

I tried setting a public $originalModelProps on my event class however, it returns empty when the handle() method on my listener is called.

I'm not using Illuminate\Queue\SerializesModels or any trait/class/interface named SerializesModels on my code.

Events/Models/MyModel/MyModelSavedEvent.php

<?php

namespace App\Events\Models\MyModel;

use App\Models\MyModel;
use Illuminate\Queue\SerializesModels;

class MyModelSavedEvent
{
    /**
     *
     * @var \App\Models\MyModel
     */
    public $myModel;

    public $originalAccessLevelProps;

    /**
     * Create a new event instance.
     * 
     * @param \App\Models\MyModel
     *
     * @return void
     */
    public function __construct(MyModel $myModel)
    {
        $this->myModel = $myModel;
        $this->originalAccessLevelProps = $this->myModel->getOriginal('level');
    }
}

Listeners/Models/MyModel/MyModelSavedEventListener.php

<?php

namespace App\Listeners\Models\MyModel;

use Log;
use App\Models\MyModel;
use Illuminate\Contracts\Queue\ShouldQueue;
use App\Events\Models\MyModel\MyModelSavedEvent;

class MyModelSavedEventListener implements ShouldQueue
{
    /**
     * The queue connection that should handle the job.
     *
     * @var string
     */
    public $connection = 'sqs';

    /**
     * The number of times the job may be attempted.
     *
     * @var int
     */
    public $tries = 3;

    /**
     * The number of seconds the job can run before timing out.
     *
     * @var int
     */
    public $timeout = 60;

    public function __construct()
    {
        // ..
    }

    /**
     * This job should only be queued if the level of myModel
     * has been updated.
     *
     * @param  \App\Events\Models\MyModel\MyModelSavedEvent $event
     * 
     * @return bool
     */
    public function shouldQueue(MyModelSavedEvent $event)
    {
        $prevLevel = $event->myModel->getOriginal('level');
        $currLevel = $event->myModel->level;

        return $prevLevel !== $currLevel;
    }

    public function handle(MyModelSavedEvent $event): void
    {
        $prevLevel = $event->myModel->getOriginal('level');
        $currLevel = $event->myModel->level;

        Log::info([
            'Running handle!',
            $event->myModel->getOriginal(), // This returns an empty array
            $event->originalAccessLevelProps, // This is null
            $event->myModel->id, // This returns a string
            $prevLevel, // This returns null
            $currLevel // This contains the latest value
        ]);

        // ..
    }
}

tests/app/Listeners/Models/MyModel/MyModelSavedEventListener.php

<?php

public function testSample()
{
    Queue::fake();

    $myModel = MyModel::where(...)->first();

    Queue::assertNothingPushed();

    // This will trigger the event
    $myModel->level = 'new level';
    $myModel->save();

    Queue::assertPushedOn('sqs', MyModelSavedEventListener::class);
}

I was expecting that similar to how it works outside of the test, I'll be able to compare old and new values.

Edit: The event is triggered via MyModel's $dispatchesEvents property:

/**
 * The event map for the model.
 *
 * @var array
 */
protected $dispatchesEvents = [
    'saved' => \App\Events\Models\MyModel\MyModelSavedEvent::class,
];

Update: Found out that my model is indeed not being serialized. Its just that getOriginal() is still returning an empty array when called within the listener's handle() method

Dreadnaut
  • 107
  • 1
  • 10
  • How is the event being triggered? – miken32 Aug 26 '19 at 17:03
  • Note that the event listener used in your test class has the same name as the test class itself `MyModelSavedEventListener` – Salim Djerbouh Aug 26 '19 at 17:14
  • Hi @miken32, I updated the question. The event is being triggered via MyModel's $dispatchesEvents property – Dreadnaut Aug 27 '19 at 01:29
  • @CaddyDZ, i might have mixed the class names but only because I had to rename them for a bit of privacy :) Got no issues with the names, everything's firing fine, just the serialization. – Dreadnaut Aug 27 '19 at 01:30

1 Answers1

0

SerializesModel is used to save space by serializing the model ID and some meta information when storing the job in the queue.

When you pass a model instance to a job that does not use SerializesModel the instance is still serialized with PHP's serialize() and restored when the listener gets called.

So maybe try to pass your properties of interest directly to MyModelSavedEvent::dispatch()

Dreadnaut
  • 107
  • 1
  • 10
Olivenbaum
  • 971
  • 1
  • 6
  • 17
  • Hi @timw, how should I do this via queued event listeners? The model is firing the event manually via MyModel's $dispatchesEvents property – Dreadnaut Aug 27 '19 at 01:32
  • @Dreadnaut you can fire another event from the models listener and queue only the subsequent event. Then you can pass any properties you like. I also had some problems with queued model listeners. For example `MyModelCompareOriginal::dispatch($old, $original)`. – Olivenbaum Aug 27 '19 at 12:29