skip to Main Content

I’m working in a Laravel 10 application. My application has queued event listeners. My project is using Laravel Horizon to process queued jobs and events, I’ve got a notification feature that applies a global model observer to all models that then dispatches my ProcessModelObserver job when it’s create, updated, deleted etc.

The problem I’ve got, in some areas of my application it looks like the job is processed before the model is saved and thus throws an error:

IlluminateDatabaseEloquentModelNotFoundException: No query results for model

For my circumstances, I’d rather catch this somehow so that it doesn’t show up as a failed job in Horizon, but where I’ve added this catch already appears to not be catching the error.

What am I missing to achieve this, here’s my files:

EventServiceProvider boot method

**
 * Register any events for your application.
 */
public function boot(): void
{
    $modelsToObserve = [
        AppModelsAffiliate::class,
        AppModelsAffiliateCampaign::class,
        AppModelsAffiliateProduct::class,
        AppModelsAffiliateSplit::class,
        AppModelsAnalytic::class,
        AppModelsApiRequestLog::class,
        AppModelsApplication::class,
        AppModelsBuyer::class,
        AppModelsBuyerTier::class,
        AppModelsBuyerTierOption::class,
        AppModelsCountry::class,
        AppModelsPingtree::class,
        AppModelsPingtreeGroup::class,
        AppModelsProduct::class,
        AppModelsSetting::class,
    ];

    foreach ($modelsToObserve as $model) {
        try {
            $model::observe(GlobalModelObserver::class);
        } catch (IlluminateDatabaseEloquentModelNotFoundException $e) {
            Log::info("ModelNotFoundException"); <-- not catching anything?
        }
    }
}

GlobalModelObserver.php file

None of the ModelNotFoundException are being caught here either.

<?php

namespace AppObservers;

use AppContractsListensForChangesContract;
use IlluminateSupportArr;
use IlluminateSupportFacadesLog;
use IlluminateDatabaseEloquentModelNotFoundException;
use AppJobsProcessModelObserver;
use CarbonCarbon;
use Exception;

class GlobalModelObserver
{
    /**
     * Handle the User "created" event.
     */
    public function created($model): void
    {
        if (! $model) return;

        try {
            ProcessModelObserver::dispatch($model, $model->id, 'created', Carbon::now());
        } catch (IlluminateDatabaseEloquentModelNotFoundException $e) {
            Log::info("ModelNotFoundException");
        } catch (Exception $e) {
            // ...
        }
    }

    /**
     * Handle the User "updated" event.
     */
    public function updated($model): void
    {
        if (! $model) return;

        try {
            $changes = Arr::except(
                $model->getDirty(),
                $model->excludeFromNotificationComparison()
            );

            // only dispatch if there are differences
            if (count($changes) > 0) {
                ProcessModelObserver::dispatch($model, $model->id, 'updated', Carbon::now(), [
                    'model' => [
                        'comparison' => [
                            'original' => collect($model->getOriginal())->toArray(),
                            'current' => collect($model->getAttributes())->toArray(),
                        ],
                        'changes_since_last_update' => collect($changes)->toArray(),
                    ]
                ]);
            }

        } catch (IlluminateDatabaseEloquentModelNotFoundException $e) {
            Log::info("ModelNotFoundException");
        } catch (Exception $e) {
            // ...
        }
    }

    /**
     * Handle the User "deleted" event.
     */
    public function deleted($model): void
    {
        if (! $model) return;

        try {
            ProcessModelObserver::dispatch($model, $model->id, 'deleted', Carbon::now());
        } catch (IlluminateDatabaseEloquentModelNotFoundException $e) {
            Log::info("ModelNotFoundException");
        } catch (Exception $e) {
            // ...
        }
    }

    /**
     * Handle the User "force deleted" event.
     */
    public function forceDeleted($model): void
    {
        if (! $model) return;

        try {
            ProcessModelObserver::dispatch($model, $model->id, 'deleted', Carbon::now());
        } catch (IlluminateDatabaseEloquentModelNotFoundException $e) {
            Log::info("ModelNotFoundException");
        } catch (Exception $e) {
            // ...
        }
    }
}

ProcessModelObserver job

<?php

namespace AppJobs;

use IlluminateBusQueueable;
use IlluminateContractsQueueShouldBeUnique;
use IlluminateContractsQueueShouldQueue;
use IlluminateFoundationBusDispatchable;
use IlluminateQueueInteractsWithQueue;
use IlluminateQueueSerializesModels;
use AppNotificationsModelsModelStored;
use AppNotificationsModelsModelUpdated;
use AppNotificationsModelsModelDestroyed;
use IlluminateSupportFacadesLog;
use IlluminateSupportFacadesCache;
use IlluminateSupportStr;
use AppModelsUser;
use AppModelsCompany;
use AppModelsCompanyEntry;
use CarbonCarbon;
use Exception;

class ProcessModelObserver implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

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

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

    /**
     * The model
     */
    public $model;

    /**
     * The model id
     */
    public $id;

    /**
     * The model event
     */
    public $event;

    /**
     * The datetime it happened
     */
    public $datetime;

    /**
     * The datetime it happened
     */
    public $metadata;

    /**
     * Create a new job instance.
     */
    public function __construct($model, int $id, string $event, Carbon $datetime, array $metadata = [])
    {
        $this->onQueue('observers');
        $this->model = $model;
        $this->id = $id;
        $this->event = $event;
        $this->datetime = $datetime;
        $this->metadata = $metadata;
    }

    /**
     * Calculate the number of seconds to wait before retrying the job.
     */
    public function backoff(): array
    {
        return [1, 3];
    }

    /**
     * Get the tags that should be assigned to the job.
     *
     * @return array<int, string>
     */
    public function tags(): array
    {
        return ['processModelObserver'];
    }

    /**
     * Return the model
     */
    public function getModel()
    {
        $base = class_basename($this->model);

        $model = "App\Models\$base";

        return $model::find($this->id);
    }

    /**
     * Determine whether the user has notification channels to send to
     */
    public function userHasNotificationChannels(User $user, string $type): bool
    {
        $channels = $user->notificationPreferences()
                         ->where('type', $type)
                         ->with('notificationChannels')
                         ->get()
                         ->pluck('notificationChannels')
                         ->flatten()
                         ->pluck('name')
                         ->unique()
                         ->toArray();

        return !empty($channels) ? true : false;
    }

    /**
     * Return the model title
     */
    public function getModelTitle(): string
    {
        $title = class_basename($this->model);

        try {
            $title = $this->model->notificationDynamicTitle();
        } catch (Exception $e) {
            // ...
        }

        return $title;
    }

    /**
     * Notify the user
     */
    public function scheduleNotificationToSend(User $user): void
    {
        $base = class_basename($this->model);

        $title = $this->getModelTitle();

        $timestamp = Carbon::parse($user->next_notifiable_datetime)->toDateTimeString();
        $sendAt = Carbon::createFromFormat('Y-m-d H:i:s', $timestamp, $user->timezone);
        $sendAt->setTimezone('UTC');

        if ($sendAt->isPast()) {
            $sendAt = Carbon::now();
        }

        switch ($this->event) {
            case 'created':

                if (! $this->userHasNotificationChannels($user, Str::of($base.'_store')->snake())) {
                    return;
                }

                $user->notifyAt(
                    new ModelStored(
                        Str::of($base.'_store')->snake(),
                        "$title has been created",
                        "Model has been created",
                        $this->metadata
                    ), $sendAt
                );
                break;

            case 'updated':

                if (! $this->userHasNotificationChannels($user, Str::of($base.'_update')->snake())) {
                    return;
                }

                $user->notifyAt(
                    new ModelUpdated(
                        Str::of($base.'_update')->snake(),
                        "$title has been updated",
                        "Model has been updated",
                        $this->metadata
                    ), $sendAt
                );
                break;

            case 'deleted':

                if (! $this->userHasNotificationChannels($user, Str::of($base.'_destroy')->snake())) {
                    return;
                }

                $user->notifyAt(
                    new ModelDestroyed(
                        Str::of($base.'_destroy')->snake(),
                        "$title has been deleted",
                        "Model has been deleted",
                        $this->metadata
                    ), $sendAt
                );
                break;
        }
    }

    /**
     * Get company users
     */
    public function getCompanyUsers(int $companyId)
    {
        return Cache::tags([
            'company_entries'
        ])->remember('process_model_observer_company_users', 10, function () use ($companyId) {
            return CompanyEntry::where('company_id', $companyId)
                ->pluck('user_id')
                ->unique()
                ->map(function ($userId) {
                    return User::find($userId);
                })
                ->filter();
        });
    }

    /**
     * Execute the job.
     */
    public function handle(): void
    {
        $base = class_basename($this->model);
        $model = $this->getModel();

        if (! $model) {
            $this->fail("Model $base with id $this->id can't be found.");
            return;
        }

        if (! isset($model->company_id)) {
            return;
        }

        $users = $this->getCompanyUsers($model->company_id);

        if (empty($users)) {
            return;
        }

        foreach ($users as $user) {
            if (! empty($user->next_notifiable_datetime)) {
                $this->scheduleNotificationToSend($user);
            }
        }
    }
}

What am I missing again, I’d rather not show thousands of failed jobs for my circumstances and instead catch it.

2

Answers


  1. You have a lot going on in there, but it could be on your deleted observer job where you are still querying the model after its deleted and there it no actual issue when the event are fired, and only when it tries to deserialize the data from your job where your model is gone.

    You can just handle the exception in your actual job handle,

    e.i.

    public function handle(): void {
    
        try {
            .
            .
            // your original handle code
            .
            .
        }  catch (IlluminateDatabaseEloquentModelNotFoundException $exception) {     
            $data = [
                'model' => $this->model,
                'id'    => $this->id,
                'event' => $this->event,
                'datetime' => $this->datetime,
                'metadata' => $this->metadata,
            ]
            // Log it so you can see what the hell is going on
            Log::info( json_encode($data) );
    
            // Tell queue everything is alright.
            return;
    
        } catch (Throwable $th) {
            // Fail the job for other errors.
            throw $th;
        }    
        
    }
    
    Login or Signup to reply.
  2. Did you try to use Throwlable ? Like this code bellow:

    try {
         // Your code
    } catch (Throwlable $th) {
         Log::info($th->getMessage);
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search