Laravel: Queued Notifications for a Deleted User or Eloquent Model
Tags: php, laravel, redis
Published: 2019-04-27 09:19:29

---
title: Laravel: Queued Notifications for a Deleted User or Eloquent Model
published: true
description: Don't let the async nature of queues fool ya!
tags: PHP, Laravel, Redis
cover_image: https://johnalcher.me/storage/header_images/DYNu3V8P0jNmpFA6Wl7h6Pq1uXzelSMWas8GdN6J.jpeg
canonical_url: https://johnalcher.me/articles/laravel-notifications-for-a-deleted-user-or-eloquent-model
---

<small><a href="https://www.pexels.com/@freestocks">Cover by freestocks from Pexels</a></small>

## Contents

- [Contents](#contents)
- [Introduction](#introduction)
- [Our Problem At Hand](#our-problem-at-hand)
- [The Obvious Symptom](#the-obvious-symptom)
- [Solution](#solution)
- [Don't Forget The Tests!](#dont-forget-the-tests)
- [How about Regular Eloquent Models?](#how-about-regular-eloquent-models)
    - [As an array](#1-pass-an-array-representation-of-the-model)
    - [Only relevant fields](#2-pass-only-the-relevant-model-fields)
- [Conclusion](#conclusion)


## Introduction

Out of the box, [Laravel](https://laravel.com/) provides strong facilities for utilizing [queues](https://laravel.com/docs/queues) and/or [Redis](https://laravel.com/docs/redis) to support your queued jobs, mails, or any other expensive (time or resource-wise) operations. But the asynchronous nature of queues might bite you in your back side when dealing with objects wherein their current state is tricky to visualize once the queued action is executing. In this article, we'll take a look at two instances where this may be the case.

## Our Problem at Hand

Our friends at ACME Airconditioning has a shopping cart system wherein customers can buy their desired AC units online, provided they have an account in the system. Now, a feature request had come in where the specs is described (in [GWT](https://www.martinfowler.com/bliki/GivenWhenThen.html)) as follows:

```
Feature: Farewell Email Promotion for a Deleted Customer
    Scenario: Customer deletes their account
        Given I have my email verified

        When I delete my customer account
     
        Then I will receive an email containing ACME's farewell promo
```

Now how do we implement this feature? It seems straightforward: send the user an email  before deleting their account:

```php
// CustomerController.php

public function destroy(Customer $customer)
{
    $customer->notify(new FarewellPromotion());
    $customer->delete();

    // ...
}

// FarewellPromotion.php

class FarewellPromotion extends Notification implements ShouldQueue
{
    use Queueable;

    public function toMail($notifiable)
    {
        return (new MailMessage)
            ->line('We are sad to see you go.')
            ->line('How about a 2-for-1 special?')
            ->action(
                'Buy Now,
                '/farewell-promo'
            );
    }
}
```

But to our surprise, `$customer` never receives the email! Why? Let's examine it further.

## The Obvious Symptom

Hmmm, take a second look at the program flow. It should become apparent when you consider that `$customer->notify(new FarewellPromotion());`  pushes the notification _into a queue_, thus notifying the user **asynchronously**. In a nutshell, the account deletion happens **before** the notification is sent. This causes the notification to fail because the recipient that it is intended for is already deleted (or more precisely, `null`).

![](https://i.imgur.com/aKb4RlS.png)

## Solution

You can solve this by **not** queuing the notification (unacceptable for UX) or by storing the customer's email address and sending the mail directly (slightly better, but discards the abstraction of the notifications). But luckily, Laravel supports [On-Demand Notifications](https://laravel.com/docs/notifications#on-demand-notifications) that allows notification to be sent without a `Notifiable` instance (in our case, the customer). 

```php
// CustomerController.php

public function destroy(Customer $customer)
{
    $customer->delete();

    // Doesn't matter if it's before or after deletion since 
    // the Customer instance still has its properties 
    // up until the request context expires.
     Notification::route('mail', $customer->email)
            ->notify(new FarewellPromotion());

    // ...
}
```

## Don't Forget The Tests!

Once you changed the implementation from a simple notification into an on-demand one, you'd have to change any test cases that account for this feature. A testcase might look something like this:

```php
/** @test */
public function customers_that_deletes_their_account_is_notified_of_the_farewell_promo()
{
    Notification::fake();

    $customer = factory(Customer::class)->create();
    
    $this->delete('/customers/' . $customer->id);

    Notification::assertSentTo(
        $customer,
        FarewellPromotion::class
    );
}
```

This testcase would now fail because `$customer` is already `null` once the assertion is made. Instead, we should use the `AnonymousNotifiable` class to proxy for our customer, as is the case when using [`Notification::route`](https://laravel.com/docs/5.8/notifications#on-demand-notifications)

```php
use Illuminate\Notifications\AnonymousNotifiable;

Notification::assertSentTo(
    new AnonymousNotifiable,
    FarewellPromotion::class
);
```

## How about Regular Eloquent Models?

That covers how to notify a deleted `Customer` model that uses the `Notifiable` trait. But what if we need a deleted model for a queued notification? Consider the following:

```
Feature: Recommend another Product when a Customer's "liked" Product is Deleted
    Scenario: A Product is Deleted
        Given I have my email verified
            And I have "liked" a product

        When that product is deleted
     
        Then I will receive a notification that recommends me another product.
```

Now obviously, blindly passing the `Product` model into a queued notification will result with the same async problem that we encountered before:

```php
// ProductsController.php

public function delete(Product $product)
{
    $product->delete();
    
    $product->likers->notify(new SimilarProductRecommendation($product));
}

// SimilarProductRecommendation.php
class SimilarProductRecommendation extends Notification implements ShouldQueue
{
    use Queueable;

    private $product;

    public function __construct(Product $product)
    {
        $this->product = $product;
    }

    public function toArray($notifiable)
    {
        return [
            'message' => $this->product->name . ' has been deleted. Try this other product....' 
        ];
    }
}
```

There is no clear cut solution about it, because unlike `Customer`, `Product` is not `Notifiable` and we won't be able to leverage the  `Notification::route()` method. When I'm faced with this problem, I use one of the following solutions:

### 1. Pass an array representation of the model 

```php
// ProductsController.php
public function delete()
{    
    $product->delete();
    
    $product->likers->notify(new SimilarProductRecommendation($product->toArray()));
}

// SimilarProductRecommendation.php
class SimilarProductRecommendation extends Notification implements ShouldQueue
{
    use Queueable;

    private $product;

    public function __construct(array $product)
    {
        $this->product = $product;
    }

    public function toArray($notifiable)
    {
        return [
            'message' => $this->product['name'] . ' has been deleted. Try this other product....' 
        ];
    }
}
```

### 2. Pass only the relevant model fields

```php
// ProductsController.php
public function delete()
{    
    $product->delete();
    
    $product->likers->notify(new SimilarProductRecommendation($product->name));
}

// SimilarProductRecommendation.php
class SimilarProductRecommendation extends Notification implements ShouldQueue
{
    use Queueable;

    private $productName;

    public function __construct(string $productName)
    {
        $this->productName = $productName;
    }

    public function toArray($notifiable)
    {
        return [
            'message' => $this->productName . ' has been deleted. Try this other product....' 
        ];
    }
}
```

Both of these solutions bypass the unpredictable state of the deleted model by passing in separate values _instead of a reference_ to the model itself.

## Conclusion

In this article, we've covered the trickiness that queued notifications might present when handling deleted entities. We've also covered how to notify a deleted `User` (or any model that implements the `Notifiable` trait, to be precise) and testing the said action. Lastly, we took a look at passing deleted Eloquent model references to a queued notifications, and discussed a couple of solutions on how to "access" these deleted models.