Criando Feed de Casos de Usos Usando Notifications com Channel Personalizado de Banco de Dados

Editar no GitHub

Crie um feed simples com notifications do Laravel 📌

Quando uma notificação deve ser lançada por conta de um caso de uso específico no fluxo do negócio, avisando ao usuário sobre determinada ação/atividade, então, nesse caso é útil ter uma tabela pra poder "listar" e manipular essa notificações pra serem visíveis posteriormente pelo usuário (feed de atividades dos casos de usos), além do mais, o próprio usuário pode também fazer determinada ação que acione uma notificação do lado da aplicação para "avisar"/responder ao gatilho de sua ação.

Esse feed de notificações nada mais é do que uma lista de atividades dos casos de usos dos fluxos da aplicação.

Para que isso possa acontecer, primeiro é necessário criar a tabela para que os dados da notificação possam ser persistidos para que posteriormente possam ser manipulados, isso é, possam ser recuperados, marcados como lidos, ou até mesmo fazer outra ação com determinada notificação.

Criando tabela para manipular as notificações

<?php

use App\Enums\UserNotificationType;
use App\Enums\UserNotificationStatus;
use Illuminate\Support\Facades\Schema;
use App\Enums\UserNotificationCausedBy;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('user_notifications', function (Blueprint $table) {
            $table->id();
            $table->unsignedInteger('user_id');
            $table->nullableMorphs('subject', 'subject');
            $table->enum('caused_by', UserNotificationCausedBy::values());
            $table->enum('type', UserNotificationType::values())->index();
            $table->enum('status', UserNotificationStatus::values())->default(UserNotificationStatus::SUCCESS->value);
            $table->string('title')->nullable();
            $table->string('message');

            $table->timestamp('read_at')->index()->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('user_notifications');
    }
};

A coluna de subject contêm o recurso em que está sendo manipulado, por exemplo, quando o usuário envia determinado documento para análise da plataforma, o subject nesse caso seria o registro do documento em que está sendo enviado.

A coluna de caused_by é quando uma notificação é causada pelo usuário, de acordo com algum caso de uso, ou quando é causada pelo sistema, como se fosse a resposta de determinada ação do usuário.

A coluna de type é pra ser possível filtrar e manipular determinado tipo de notificação.

A coluna de status é pra saber se a notificação é uma notificação de success ou failed.

O enum de UserNotificationCausedBy tem o seguinte conteúdo:

<?php

namespace App\Enums;

enum UserNotificationCausedBy: string
{
    case USER = 'user';
    case SYSTEM = 'system';

    public static function values(): array
    {
        return array_column(self::cases(), 'value');
    }
}

O enum de UserNotificationType tem o seguinte conteúdo:

<?php

namespace App\Enums;

enum UserNotificationType: string
{
    case PAYMENT_DONE = 'payment_done';
    case ORDER_IN_PROCESS = 'order_in_process';
    // e more ...

    public static function values(): array
    {
        return array_column(self::cases(), 'value');
    }
}

O enum de UserNotificationStatus tem o seguinte conteúdo:

<?php

namespace App\Enums;

enum UserNotificationStatus: string
{
    case SUCCESS = 'success';
    case FAILED = 'failed';

    public static function values(): array
    {
        return array_column(self::cases(), 'value');
    }
}

Criando Model de UserNotification

<?php

namespace App\Support\Notifications;

use App\Models\User;
use App\Enums\UserNotificationType;
use App\Enums\UserNotificationStatus;
use App\Enums\UserNotificationCausedBy;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class UserNotification extends Model
{
    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 'user_notifications';

    /**
     * The attributes that are mass assignable.
     *
     * @var array<string>
     */
    protected $fillable = [
        'user_id',
        'subject_type',
        'subject_id',
        'caused_by',
        'type',
        'status',
        'title',
        'message',
        'read_at',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, mixed>
     */
    protected $casts = [
        'caused_by' => UserNotificationCausedBy::class,
        'type' => UserNotificationType::class,
        'status' => UserNotificationStatus::class,
        'read_at' => 'datetime',
    ];

    /**
     * The relationships that should always be loaded.
     *
     * @var array
     */
    protected $with = ['user'];

    /**
     * The accessors to append to the model's array form.
     *
     * @var array
     */
    protected $appends = ['humanize_created_at'];

    public function getHumanizeCreatedAtAttribute(): string
    {
        return $this->created_at->diffForHumans();
    }

    /**
     * Get the notifiable entity that the notification belongs to.
     *
     * @return \Illuminate\Database\Eloquent\Relations\MorphTo
     */
    public function subject(): MorphTo
    {
        return $this->morphTo();
    }

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class, 'user_id');
    }

    /**
     * Mark the notification as read.
     *
     * @return void
     */
    public function markAsRead(): void
    {
        if (is_null($this->read_at)):
            $this->forceFill(['read_at' => $this->freshTimestamp()])->save();
        endif;
    }

    /**
     * Marks many notifications in the `$ids` parameter as read.
     *
     * @param array<int> $ids
     *
     * @return int
     */
    public function markManyAsRead(array $ids): int
    {
        $updatedRow = $this->distinct()
                           ->whereKey($ids)
                           ->update([
                               'read_at' => $this->freshTimestamp()
                           ]);

        return $updatedRow;
    }

    /**
     * Mark the notification as unread.
     *
     * @return void
     */
    public function markAsUnread(): void
    {
        if (! is_null($this->read_at)):
            $this->forceFill(['read_at' => null])->save();
        endif;
    }

    /**
     * Determine if a notification has been read.
     *
     * @return bool
     */
    public function isRead(): bool
    {
        return $this->read_at !== null;
    }

    /**
     * Determine if a notification has not been read.
     *
     * @return bool
     */
    public function isUnread(): bool
    {
        return $this->read_at === null;
    }

    /**
     * Scope a query to only include read notifications.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeRead(Builder $query)
    {
        return $query->whereNotNull('read_at');
    }

    /**
     * Scope a query to only include unread notifications.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeUnread(Builder $query)
    {
        return $query->whereNull('read_at');
    }
}

Trait para relacionar ao subject

Para relacionar o subject com as notificações, ou seja, para recuperar determinadas notificações em que determinada Model está nas colunas morph de subject_type e subject_id utilize a trait na model acima:

<?php

namespace App\Support\Notifications\Traits;

use App\Support\Notifications\UserNotification;

trait UserNotifiable
{
    public function userNotifications()
    {
        return $this->morphMany(UserNotification::class, 'subject')->latest();
    }

    /**
     * Get the user's read notifications.
     *
     * @return \Illuminate\Database\Query\Builder
     */
    public function readUserNotifications()
    {
        return $this->userNotifications()->read();
    }

    /**
     * Get the user's unread notifications.
     *
     * @return \Illuminate\Database\Query\Builder
     */
    public function unreadUserNotifications()
    {
        return $this->userNotifications()->unread();
    }
}

Criando driver/channel de bancos de dados

Para enviar uma notificação e salvar ela na tabela acima, é necessário customizar, criar um canal personalizado no Laravel e usá-lo na notificação em que será disparada.

O nome do canal em que estamos manipulando vai se chamar UserUseCaseChannel, já que é sobre um canal de notificações de casos de uso do usuário.

O arquivo de UserUseCaseChannel.php tem o seguinte conteúdo:

<?php

namespace App\Support\Notifications;

use App\Models\User;

class UserUseCaseChannel
{
    /**
     * Send the given notification.
     *
     * @param \App\Models\User $notifiable
     * @param \App\Support\Notifications\UserUseCaseNotification $notification
     *
     * @return void
     */
    public function send(User $notifiable, UserUseCaseNotification $notification): void
    {
        $notification->toUserUseCase($notifiable);
    }
}

O canal acima, usa o método de toUserUseCase que basicamente tem toda lógica de salvar os dados da notificação no banco de dados.

Para favorecer o principio do DRY, vamos criar uma classe abstrata de UserUseCaseNotification que terá o método de toUserUseCase além de alguns métodos obrigatórios que as classes de notificações terão de implementar.

<?php

namespace App\Support\Notifications;

use App\Models\User;
use App\Enums\UserNotificationType;
use App\Enums\UserNotificationStatus;
use App\Enums\UserNotificationCausedBy;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notification;

abstract class UserUseCaseNotification extends Notification
{
    protected readonly UserNotificationCausedBy $causedBy;

    protected ?Model $subject = null;

    public function __construct()
    {
        $this->causedBy = UserNotificationCausedBy::USER;
    }

    abstract public function type(): UserNotificationType;
    abstract public function status(): UserNotificationStatus;

    /**
     * Prepare the subject.
     *
     * @param \Illuminate\Database\Eloquent\Model $subject
     *
     * @return static
     */
    public function withSubject(Model $subject): static
    {
        $this->subject = $subject;

        return $this;
    }

    /**
     * Prepare the notification title.
     *
     * @return string|null
     */
    public function title(): ?string
    {
        return null;
    }

    /**
     * Prepare the notification message.
     *
     * @return string
     */
    public function message(): string
    {
        return 'default';
    }

    /**
     * Get the notification's delivery channels.
     *
     * @param mixed $notifiable
     *
     * @return array|string
     */
    public function via($notifiable): array|string
    {
        return UserUseCaseChannel::class;
    }

    /**
     * Get the "use case / activity" representation of the notification.
     *
     * @param \App\Models\User $notifiable
     *
     * @return \App\Models\UserNotification
     */
    public function toUserUseCase(User $notifiable): UserNotification
    {
        $userNotification = app(UserNotification::class);

        if (! is_null($this->subject)):
            $userNotification->subject()->associate($this->subject);
        endif;

        $userNotification->user_id = auth()->id() ?? $notifiable->getKey();
        $userNotification->caused_by = $this->causedBy;
        $userNotification->type = $this->type();
        $userNotification->status = $this->status();
        $userNotification->title = $this->title();
        $userNotification->message = $this->message();
        $userNotification->read_at = null;

        $userNotification->save();

        return $userNotification;
    }
}

A classe acima deverá ser utilizada apenas como uma classe "parent" pois se trata de uma classe abstrata com métodos abstratos. Principalmente os métodos de type e status que devem ter nas classes de notificações.

Criando feed de notificação - Inserindo no banco de dados

Para criar uma notificação de "atividade" ou "caso de uso" do negócio, é necessário criar uma classe de Notification que estende a classe abstrata de UserUseCaseNotification.

Vamos criar uma classe de notificação pra quando o usuário tem o pagamento feito/processado com sucesso. O nome da classe vai se chamar PaymentDone com o seguinte código:

<?php

namespace App\Notifications;

use App\Enums\UserNotificationType;
use App\Enums\UserNotificationStatus;
use App\Support\Notifications\UserUseCaseNotification;

class PaymentDone extends UserUseCaseNotification
{
    public function type(): UserNotificationType
    {
        return UserNotificationType::PAYMENT_DONE;
    }

    public function status(): UserNotificationStatus
    {
        return UserNotificationStatus::SUCCESS;
    }

    /**
     * Prepare the notification title.
     *
     * @return string
     */
    public function title(): string
    {
        return __('messages.payment done.title');
    }

    /**
     * Prepare the notification message.
     *
     * @return string
     */
    public function message(): string
    {
        return __('messages.payment done.message');
    }
}

E a notificação acima pode ser lançada como da seguinte forma:

use App\Notifications\InvoicePaid;

$user->notify(new  PaymentDone());

Após isso, verá que existe um registro na tabela de user_notifications.