# Event Pattern

## Overview

The Event Pattern enables loose coupling between components by allowing objects to publish events that other objects can subscribe to and react upon. This is essential for cross-domain communication in a DDD architecture.

## Benefits

- **Decoupling**: Publishers don't need to know about subscribers
- **Extensibility**: Add new behaviors without modifying existing code
- **Async Processing**: Events can be queued for background processing
- **Audit Trail**: Events provide a natural history of system changes

## Implementation Structure

```
app/Domains/{Domain}/
├── Events/
│   └── {EventName}.php
└── Listeners/
    └── {ListenerName}.php
```

## Event Definition

**Location**: `app/Domains/Catalog/Products/Events/ProductCreated.php`

```php
<?php

namespace App\Domains\Catalog\Products\Events;

use App\Domains\Catalog\Products\Models\Product;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class ProductCreated
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * Create a new event instance
     */
    public function __construct(
        public Product $product
    ) {}
}
```

## Event with Additional Data

```php
<?php

namespace App\Domains\Catalog\Products\Events;

use App\Domains\Catalog\Products\Models\Product;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class ProductUpdated
{
    use Dispatchable, SerializesModels;

    public function __construct(
        public Product $product,
        public array $changedAttributes,
        public int $updatedBy
    ) {}

    /**
     * Check if a specific attribute was changed
     */
    public function wasChanged(string $attribute): bool
    {
        return in_array($attribute, $this->changedAttributes);
    }
}
```

## Broadcastable Event

For real-time updates via WebSocket:

```php
<?php

namespace App\Domains\Maintenance\Events;

use App\Domains\Maintenance\SubDomains\Visits\Models\Visit;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class MaintenanceVisitUpdated implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(
        public Visit $visit
    ) {}

    /**
     * Get the channels the event should broadcast on
     */
    public function broadcastOn(): array
    {
        return [
            new PrivateChannel('company.' . $this->visit->company_id),
            new PrivateChannel('technician.' . $this->visit->technician_id),
        ];
    }

    /**
     * Get the data to broadcast
     */
    public function broadcastWith(): array
    {
        return [
            'visit_id' => $this->visit->id,
            'status' => $this->visit->status,
            'updated_at' => $this->visit->updated_at->toISOString(),
        ];
    }

    /**
     * The event's broadcast name
     */
    public function broadcastAs(): string
    {
        return 'visit.updated';
    }
}
```

## Event Listener

**Location**: `app/Domains/Catalog/Products/Listeners/LogProductCreation.php`

```php
<?php

namespace App\Domains\Catalog\Products\Listeners;

use App\Domains\Catalog\Products\Events\ProductCreated;
use Illuminate\Support\Facades\Log;

class LogProductCreation
{
    /**
     * Handle the event
     */
    public function handle(ProductCreated $event): void
    {
        Log::info('Product created', [
            'product_id' => $event->product->id,
            'name' => $event->product->name,
            'sku' => $event->product->sku,
            'created_by' => auth()->id(),
        ]);
    }
}
```

## Queued Event Listener

For heavy processing:

```php
<?php

namespace App\Domains\Catalog\Products\Listeners;

use App\Domains\Catalog\Products\Events\ProductCreated;
use App\Domains\Catalog\Products\Services\ProductIndexingService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class IndexProductForSearch implements ShouldQueue
{
    use InteractsWithQueue;

    /**
     * The queue connection to use
     */
    public string $connection = 'redis';

    /**
     * The queue to use
     */
    public string $queue = 'indexing';

    /**
     * Number of times to retry
     */
    public int $tries = 3;

    public function __construct(
        private ProductIndexingService $indexingService
    ) {}

    /**
     * Handle the event
     */
    public function handle(ProductCreated $event): void
    {
        $this->indexingService->index($event->product);
    }

    /**
     * Handle a job failure
     */
    public function failed(ProductCreated $event, \Throwable $exception): void
    {
        Log::error('Failed to index product', [
            'product_id' => $event->product->id,
            'error' => $exception->getMessage(),
        ]);
    }
}
```

## Cross-Domain Event Listener

Listening to events from another domain:

```php
<?php

namespace App\Domains\Accounting\Listeners;

use App\Domains\Procurement\Events\PurchaseOrderApproved;
use App\Domains\Accounting\Services\PayableService;
use Illuminate\Contracts\Queue\ShouldQueue;

class CreatePayableFromPurchaseOrder implements ShouldQueue
{
    public function __construct(
        private PayableService $payableService
    ) {}

    /**
     * Handle the event from Procurement domain
     */
    public function handle(PurchaseOrderApproved $event): void
    {
        $this->payableService->createFromPurchaseOrder(
            $event->purchaseOrder
        );
    }
}
```

## Event Registration

**Location**: `app/Providers/EventServiceProvider.php`

```php
<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event to listener mappings
     */
    protected $listen = [
        // Catalog domain events
        \App\Domains\Catalog\Products\Events\ProductCreated::class => [
            \App\Domains\Catalog\Products\Listeners\LogProductCreation::class,
            \App\Domains\Catalog\Products\Listeners\IndexProductForSearch::class,
            \App\Domains\Catalog\Products\Listeners\NotifyInventoryTeam::class,
        ],

        \App\Domains\Catalog\Products\Events\ProductUpdated::class => [
            \App\Domains\Catalog\Products\Listeners\UpdateSearchIndex::class,
            \App\Domains\Catalog\Products\Listeners\SyncWithExternalSystems::class,
        ],

        // Procurement domain events
        \App\Domains\Procurement\Events\PurchaseOrderApproved::class => [
            \App\Domains\Accounting\Listeners\CreatePayableFromPurchaseOrder::class,
            \App\Domains\Supply\Listeners\ScheduleDelivery::class,
        ],

        // Maintenance domain events
        \App\Domains\Maintenance\Events\MaintenanceRequestCreated::class => [
            \App\Domains\Maintenance\Listeners\NotifyMaintenanceTeam::class,
            \App\Domains\Maintenance\Listeners\ScheduleVisit::class,
        ],
    ];

    /**
     * Event subscribers
     */
    protected $subscribe = [
        \App\Domains\Core\Subscribers\AuditLogSubscriber::class,
    ];
}
```

## Event Subscriber

For handling multiple events from same domain:

```php
<?php

namespace App\Domains\Core\Subscribers;

use App\Domains\Catalog\Products\Events\ProductCreated;
use App\Domains\Catalog\Products\Events\ProductUpdated;
use App\Domains\Catalog\Products\Events\ProductDeleted;
use Illuminate\Events\Dispatcher;

class AuditLogSubscriber
{
    /**
     * Handle product created events
     */
    public function handleProductCreated(ProductCreated $event): void
    {
        activity()
            ->performedOn($event->product)
            ->log('created');
    }

    /**
     * Handle product updated events
     */
    public function handleProductUpdated(ProductUpdated $event): void
    {
        activity()
            ->performedOn($event->product)
            ->withProperties(['changes' => $event->changedAttributes])
            ->log('updated');
    }

    /**
     * Handle product deleted events
     */
    public function handleProductDeleted(ProductDeleted $event): void
    {
        activity()
            ->performedOn($event->product)
            ->log('deleted');
    }

    /**
     * Register the listeners for the subscriber
     */
    public function subscribe(Dispatcher $events): array
    {
        return [
            ProductCreated::class => 'handleProductCreated',
            ProductUpdated::class => 'handleProductUpdated',
            ProductDeleted::class => 'handleProductDeleted',
        ];
    }
}
```

## Dispatching Events

From a Service:

```php
<?php

namespace App\Domains\Catalog\Products\Services;

use App\Domains\Catalog\Products\DTOs\ProductDTO;
use App\Domains\Catalog\Products\Events\ProductCreated;
use App\Domains\Catalog\Products\Events\ProductUpdated;
use App\Domains\Catalog\Products\Models\Product;
use Illuminate\Support\Facades\DB;

class ProductService
{
    public function create(ProductDTO $dto): Product
    {
        return DB::transaction(function () use ($dto) {
            $product = Product::create($dto->toArray());

            // Dispatch event after successful creation
            event(new ProductCreated($product));

            return $product;
        });
    }

    public function update(Product $product, ProductDTO $dto): Product
    {
        return DB::transaction(function () use ($product, $dto) {
            $changedAttributes = [];

            foreach ($dto->toArray() as $key => $value) {
                if ($product->$key !== $value) {
                    $changedAttributes[] = $key;
                }
            }

            $product->update($dto->toArray());

            // Only dispatch if something changed
            if (!empty($changedAttributes)) {
                event(new ProductUpdated(
                    $product->fresh(),
                    $changedAttributes,
                    auth()->id()
                ));
            }

            return $product;
        });
    }
}
```

## Events in the Project

| Domain | Event | Purpose |
|--------|-------|---------|
| Catalog | `ProductCreated` | New product added |
| Catalog | `ProductUpdated` | Product modified |
| Procurement | `PurchaseOrderApproved` | Order approved |
| Procurement | `PurchaseOrderCreated` | New order created |
| Maintenance | `MaintenanceRequestCreated` | New maintenance request |
| Maintenance | `VisitScheduled` | Visit scheduled |
| Maintenance | `VisitCompleted` | Visit finished |
| Supply | `DeliveryDispatched` | Delivery started |
| Management | `EmployeeHired` | New employee |
| Management | `LeaveApproved` | Leave request approved |

## Testing Events

```php
<?php

namespace Tests\Unit\Events;

use App\Domains\Catalog\Products\Events\ProductCreated;
use App\Domains\Catalog\Products\Listeners\LogProductCreation;
use App\Domains\Catalog\Products\Models\Product;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;

class ProductEventsTest extends TestCase
{
    public function test_product_created_event_is_dispatched(): void
    {
        Event::fake([ProductCreated::class]);

        $product = Product::factory()->create();

        event(new ProductCreated($product));

        Event::assertDispatched(ProductCreated::class, function ($event) use ($product) {
            return $event->product->id === $product->id;
        });
    }

    public function test_listener_handles_product_created(): void
    {
        $product = Product::factory()->create();
        $event = new ProductCreated($product);

        $listener = new LogProductCreation();

        // Should not throw
        $listener->handle($event);

        $this->assertTrue(true);
    }

    public function test_queued_listener_is_pushed_to_queue(): void
    {
        Event::fake([ProductCreated::class]);
        Queue::fake();

        $product = Product::factory()->create();
        event(new ProductCreated($product));

        // Assert the listener job was pushed
        Queue::assertPushed(IndexProductForSearch::class);
    }
}
```

## Best Practices

### Do

- Use events for cross-domain communication
- Keep event payloads minimal
- Use queued listeners for heavy processing
- Implement `ShouldBroadcast` for real-time updates
- Use event subscribers for related event handling

### Don't

- Put business logic in listeners
- Create circular event dependencies
- Dispatch events inside transactions that might rollback
- Use events for synchronous, critical operations
- Skip error handling in listeners

## Related Patterns

- [Observer Pattern](OBSERVER_PATTERN.md) - Model-specific events
- [Service Pattern](SERVICE_PATTERN.md) - Services dispatch domain events
