# build-side-effects — Laravel-native side effects (Jobs · Notifications · Events · Observers)

> **Builds:** the framework-native building blocks a service reaches for when an action has a **side effect** —
> send mail/SMS, notify, run slow/external work async, decouple, or react to a model lifecycle event.
> **Why this file exists:** the rules demand "Laravel-first" (events/jobs/notifications/observers where they fit),
> but the base ships **only** `SendNotificationJob` + `UserNotification` and **no** `Events/`, `Listeners/`,
> `Observers/` dirs — so there was nothing to mirror. These are the base-exact skeletons. Mirror them; don't drift.
> **Consumed by:** the sprint think-first plan (`sprint-template.md`) names which of these an action uses;
> the service **delegates** to them (services still own orchestration).

**When to reach for which (`match` the need):**
- **Job (queued)** → the work is **slow or external** (mail/SMS, exports, 3rd-party API, image processing) and
  the request shouldn't wait. Anything that can fail/retry independently.
- **Notification** → tell a user something, across channels (database + mail [+ sms]). Usually dispatched **from** a Job.
- **Event + Listener** → **decouple** one action from its N reactions ("order placed" → notify + log + award points),
  or let unrelated features hook in without the service knowing about them.
- **Observer** → react to a **model's own lifecycle** (`creating`/`created`/`updating`/`deleted`/`restored`) —
  set derived columns, cascade, audit. Prefer over scattering the same logic across every call site.

> Not speculatively — only when the action actually has that side effect. A pure CRUD write needs none of these.

---

## 1) Queued Job — mirror `app/Jobs/SendNotificationJob.php`

Slow/async work. `implements ShouldQueue` so it runs on the queue, not in the request.

```php
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class <Name>Job implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(public <Model> $model) {}   // pass IDs/models — SerializesModels re-loads them

    public function handle(): void
    {
        // the slow/external work
    }
}
```
Dispatch from the service: `<Name>Job::dispatch($model);` (or `->onQueue('...')`, `->delay(...)`). Base example:
`SendNotificationJob` takes a `Collection $users` + `array $notifyData` and calls `Notification::send(...)` in `handle()`.

> **Queue worker required.** Queued jobs only run if a worker is running (`php artisan queue:work`) and
> `QUEUE_CONNECTION` is set (`database`/`redis` — `sync` runs inline, defeating the point). Record the prod
> worker/scheduler requirement in the project's environment notes. `database` queue also needs the jobs table.

---

## 2) Notification — mirror `app/Notifications/UserNotification.php`

Multi-channel, **locale-aware**, `implements ShouldQueue`. `via()` picks channels; `toArray()` = the DB payload.

```php
<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;

class <Name>Notification extends Notification implements ShouldQueue
{
    use Queueable;

    public function __construct(protected array $data) {}

    public function via($notifiable): array
    {
        $channels = ['database'];
        if (($this->data['notification_type'] ?? null) === 'mail') {
            $channels[] = 'mail';
        }
        return $channels;
    }

    public function toMail($notifiable): MailMessage
    {
        $message = $this->data['message'][app()->getLocale()] ?? $this->data['message']['en'] ?? '';
        return (new MailMessage)->subject(__('<lang key>'))->line($message);
    }

    public function toArray($notifiable): array
    {
        return $this->data;   // stored in notifications table
    }
}
```
Send with `Notification::send($users, new <Name>Notification($data))` (usually inside a Job). The notifiable model
must `use Notifiable`. Bilingual messages ride as `['message' => ['ar' => ..., 'en' => ...]]` (base convention).

---

## 3) Event + Listener — decouple an action from its reactions

Base has none — create `app/Events/` + `app/Listeners/`. Laravel **auto-discovers** a listener that type-hints the
event in `handle()` (no manual registration). Make the listener `ShouldQueue` to run async.

```php
// app/Events/<Thing>Happened.php
namespace App\Events;

use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class <Thing>Happened
{
    use Dispatchable, SerializesModels;

    public function __construct(public <Model> $model) {}
}
```
```php
// app/Listeners/<Do>OnThingHappened.php
namespace App\Listeners;

use App\Events\<Thing>Happened;
use Illuminate\Contracts\Queue\ShouldQueue;   // optional: run on the queue

class <Do>OnThingHappened implements ShouldQueue
{
    public function handle(<Thing>Happened $event): void
    {
        // one reaction (notify / log / award / dispatch a Job)
    }
}
```
Fire it from the service: `<Thing>Happened::dispatch($model);`. Add more listeners later without touching the
service — that's the point. One listener = one reaction (keep them small and single-purpose).

---

## 4) Observer — react to a model's lifecycle

Derived columns, cascades, auditing tied to `creating/created/updating/updated/deleting/deleted/restored`.
Register with the `#[ObservedBy]` attribute on the model (Laravel 11/12).

```php
// app/Observers/<Model>Observer.php
namespace App\Observers;

use App\Models\<Model>;

class <Model>Observer
{
    public function creating(<Model> $model): void
    {
        // e.g. set a generated code / slug BEFORE insert
    }

    public function deleted(<Model> $model): void
    {
        // e.g. cascade / cleanup
    }
}
```
```php
// app/Models/<Model>.php
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use App\Observers\<Model>Observer;

#[ObservedBy(<Model>Observer::class)]
class <Model> extends Model { /* ... */ }
```

> **Observer vs mutator/cast:** a single derived field from the row's own attributes → a **cast/accessor/mutator**
> (simpler, see [`build-database.md`](build-database.md)). Cross-record effects, external calls, or several
> lifecycle hooks → an **Observer**. Don't put an Observer where a cast will do.

---

## 5) Verify (definition of done)
1. The Job/Listener is `ShouldQueue` where the work is slow/external, and a worker actually processes it
   (`QUEUE_CONNECTION` ≠ `sync` in prod; the jobs table exists for the `database` driver).
2. Notifications land in the `notifications` table (database channel) and mail renders localized (ar+en).
3. Events fire from the service and each listener is auto-discovered and runs (one reaction per listener).
4. Observers are registered via `#[ObservedBy]` and fire on the intended lifecycle hooks; no logic duplicated at call sites.
5. The **service still orchestrates** — these are how it delegates, not a replacement for the service layer.

## References (single source — do not duplicate here)
- Model casts/accessors/mutators (the simpler alternative to an Observer) → [`build-database.md`](build-database.md)
- Where a service sits + thin controllers → [`build-api.md`](build-api.md)
- Base examples to mirror → `app/Jobs/SendNotificationJob.php`, `app/Notifications/UserNotification.php`
