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
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.
Did you try to use Throwlable ? Like this code bellow: