# Observer Pattern

## Overview

The Observer Pattern allows objects to notify other objects about changes in their state. In Laravel, this is implemented through Model Observers that respond to Eloquent model lifecycle events.

## Benefits

- **Decoupling**: Separates event handling from model logic
- **Reusability**: Same observer logic for multiple events
- **Maintainability**: Centralized side-effect handling
- **Testability**: Observers can be mocked or disabled in tests

## Model Lifecycle Events

Laravel provides the following model events:

| Event | When Triggered |
|-------|---------------|
| `retrieved` | After model is retrieved from database |
| `creating` | Before model is inserted |
| `created` | After model is inserted |
| `updating` | Before model is updated |
| `updated` | After model is updated |
| `saving` | Before model is saved (insert or update) |
| `saved` | After model is saved (insert or update) |
| `deleting` | Before model is deleted |
| `deleted` | After model is deleted |
| `restoring` | Before soft-deleted model is restored |
| `restored` | After soft-deleted model is restored |
| `forceDeleting` | Before model is force deleted |
| `forceDeleted` | After model is force deleted |

## Implementation Structure

```
app/Domains/{Domain}/
└── Observers/
    └── {Model}Observer.php
```

## Attendance Observer

**Location**: `app/Domains/Management/Attendance/Observers/AttendanceObserver.php`

```php
<?php

namespace App\Domains\Management\Attendance\Observers;

use App\Domains\Management\Attendance\Models\Attendance;
use App\Domains\Management\Attendance\Models\EmployeeBreak;
use App\Domains\Management\Shift\Models\ShiftTime;
use Carbon\Carbon;
use Illuminate\Validation\ValidationException;

class AttendanceObserver
{
    /**
     * Handle the "saving" event (before insert or update)
     * Validates check-in and check-out times
     */
    public function saving(Attendance $attendance): void
    {
        if ($attendance->check_in && $attendance->check_out) {
            $startTime = Carbon::parse($attendance->check_in);
            $endTime = Carbon::parse($attendance->check_out);

            if ($endTime < $startTime) {
                throw ValidationException::withMessages([
                    'check_out' => __('Check Out Time is Invalid.'),
                ]);
            }
        }
    }

    /**
     * Handle the "creating" event
     * Calculates expected work seconds based on shift
     */
    public function creating(Attendance $attendance): void
    {
        if (!$attendance->shift_id) {
            return;
        }

        $currentDayOfWeek = Carbon::now()->dayOfWeek;

        $shiftTime = ShiftTime::where('shift_id', $attendance->shift_id)
            ->where('day_of_week', $currentDayOfWeek)
            ->first();

        if (!$shiftTime) {
            $attendance->expected_work_seconds = 0;
            return;
        }

        $startTime = Carbon::parse($shiftTime->start_time);
        $endTime = Carbon::parse($shiftTime->end_time);

        $attendance->expected_work_seconds = $endTime->diffInSeconds($startTime);
    }

    /**
     * Handle the "updated" event
     * Closes open breaks when checking out
     */
    public function updated(Attendance $attendance): void
    {
        if ($attendance->wasChanged('check_out') && $attendance->check_out !== null) {
            EmployeeBreak::where('attendance_id', $attendance->id)
                ->whereNull('end_time')
                ->update(['end_time' => $attendance->check_out]);
        }
    }

    /**
     * Handle the "deleted" event
     * Cleanup related breaks
     */
    public function deleted(Attendance $attendance): void
    {
        $attendance->breaks()->delete();
    }
}
```

## Leave Observer

**Location**: `app/Domains/Management/Leave/Observers/LeaveObserver.php`

```php
<?php

namespace App\Domains\Management\Leave\Observers;

use App\Domains\Management\Leave\Models\Leave;
use App\Domains\Management\Leave\Services\LeaveBalanceService;
use App\Notifications\LeaveStatusNotification;

class LeaveObserver
{
    public function __construct(
        private LeaveBalanceService $balanceService
    ) {}

    /**
     * Handle the "creating" event
     * Validate leave balance before creation
     */
    public function creating(Leave $leave): void
    {
        $availableBalance = $this->balanceService->getAvailable(
            $leave->employee_id,
            $leave->leave_type_id
        );

        if ($leave->days_requested > $availableBalance) {
            throw new \Exception(__('Insufficient leave balance'));
        }
    }

    /**
     * Handle the "created" event
     * Send notification and deduct balance
     */
    public function created(Leave $leave): void
    {
        // Notify supervisors
        $leave->employee->supervisor?->notify(
            new LeaveStatusNotification($leave, 'pending')
        );

        // Reserve balance
        $this->balanceService->reserve(
            $leave->employee_id,
            $leave->leave_type_id,
            $leave->days_requested
        );
    }

    /**
     * Handle the "updated" event
     * Process status changes
     */
    public function updated(Leave $leave): void
    {
        if (!$leave->wasChanged('status')) {
            return;
        }

        $newStatus = $leave->status;
        $oldStatus = $leave->getOriginal('status');

        switch ($newStatus) {
            case 'approved':
                $this->handleApproval($leave);
                break;
            case 'rejected':
                $this->handleRejection($leave);
                break;
            case 'cancelled':
                $this->handleCancellation($leave);
                break;
        }

        // Notify employee of status change
        $leave->employee->notify(
            new LeaveStatusNotification($leave, $newStatus)
        );
    }

    private function handleApproval(Leave $leave): void
    {
        $this->balanceService->deduct(
            $leave->employee_id,
            $leave->leave_type_id,
            $leave->days_requested
        );
    }

    private function handleRejection(Leave $leave): void
    {
        $this->balanceService->release(
            $leave->employee_id,
            $leave->leave_type_id,
            $leave->days_requested
        );
    }

    private function handleCancellation(Leave $leave): void
    {
        if ($leave->getOriginal('status') === 'approved') {
            $this->balanceService->restore(
                $leave->employee_id,
                $leave->leave_type_id,
                $leave->days_requested
            );
        } else {
            $this->balanceService->release(
                $leave->employee_id,
                $leave->leave_type_id,
                $leave->days_requested
            );
        }
    }
}
```

## Equipment Observer

**Location**: `app/Domains/Construction/Equipment/Observers/EquipmentObserver.php`

```php
<?php

namespace App\Domains\Construction\Equipment\Observers;

use App\Domains\Construction\Equipment\Models\Equipment;
use Illuminate\Support\Facades\Log;

class EquipmentObserver
{
    /**
     * Handle the "creating" event
     * Generate equipment code if not provided
     */
    public function creating(Equipment $equipment): void
    {
        if (!$equipment->code) {
            $equipment->code = $this->generateEquipmentCode($equipment);
        }
    }

    /**
     * Handle the "updating" event
     * Track status changes
     */
    public function updating(Equipment $equipment): void
    {
        if ($equipment->isDirty('status')) {
            $equipment->previous_status = $equipment->getOriginal('status');
            $equipment->status_changed_at = now();
        }
    }

    /**
     * Handle the "updated" event
     * Log significant changes
     */
    public function updated(Equipment $equipment): void
    {
        if ($equipment->wasChanged('status')) {
            Log::info('Equipment status changed', [
                'equipment_id' => $equipment->id,
                'old_status' => $equipment->getOriginal('status'),
                'new_status' => $equipment->status,
                'changed_by' => auth()->id(),
            ]);

            // Notify maintenance team if equipment needs maintenance
            if ($equipment->status === 'maintenance_required') {
                $this->notifyMaintenanceTeam($equipment);
            }
        }
    }

    /**
     * Handle the "deleting" event
     * Prevent deletion if equipment is assigned
     */
    public function deleting(Equipment $equipment): bool
    {
        if ($equipment->assignments()->active()->exists()) {
            throw new \Exception(__('Cannot delete equipment with active assignments'));
        }

        return true;
    }

    private function generateEquipmentCode(Equipment $equipment): string
    {
        $prefix = strtoupper(substr($equipment->type, 0, 3));
        $count = Equipment::where('type', $equipment->type)->count() + 1;

        return sprintf('%s-%05d', $prefix, $count);
    }

    private function notifyMaintenanceTeam(Equipment $equipment): void
    {
        // Notification logic
    }
}
```

## Visit Observer (Maintenance Domain)

**Location**: `app/Domains/Maintenance/SubDomains/Visits/Observers/VisitObserver.php`

```php
<?php

namespace App\Domains\Maintenance\SubDomains\Visits\Observers;

use App\Domains\Maintenance\SubDomains\Visits\Models\Visit;
use App\Events\VisitScheduled;
use App\Events\VisitCompleted;

class VisitObserver
{
    /**
     * Handle the "creating" event
     * Set default values
     */
    public function creating(Visit $visit): void
    {
        $visit->status = $visit->status ?? 'scheduled';
        $visit->created_by = auth()->id();
    }

    /**
     * Handle the "created" event
     * Dispatch scheduled event
     */
    public function created(Visit $visit): void
    {
        event(new VisitScheduled($visit));
    }

    /**
     * Handle the "updated" event
     * Handle status transitions
     */
    public function updated(Visit $visit): void
    {
        if ($visit->wasChanged('status')) {
            $this->handleStatusChange($visit);
        }
    }

    private function handleStatusChange(Visit $visit): void
    {
        $status = $visit->status;

        match ($status) {
            'completed' => $this->handleCompletion($visit),
            'cancelled' => $this->handleCancellation($visit),
            'in_progress' => $this->handleStarted($visit),
            default => null,
        };
    }

    private function handleCompletion(Visit $visit): void
    {
        $visit->update([
            'completed_at' => now(),
            'completed_by' => auth()->id(),
        ]);

        event(new VisitCompleted($visit));
    }

    private function handleCancellation(Visit $visit): void
    {
        $visit->update([
            'cancelled_at' => now(),
            'cancelled_by' => auth()->id(),
        ]);
    }

    private function handleStarted(Visit $visit): void
    {
        $visit->update([
            'started_at' => now(),
        ]);
    }
}
```

## Registering Observers

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

```php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Register observers
        \App\Domains\Management\Attendance\Models\Attendance::observe(
            \App\Domains\Management\Attendance\Observers\AttendanceObserver::class
        );

        \App\Domains\Management\Leave\Models\Leave::observe(
            \App\Domains\Management\Leave\Observers\LeaveObserver::class
        );

        \App\Domains\Construction\Equipment\Models\Equipment::observe(
            \App\Domains\Construction\Equipment\Observers\EquipmentObserver::class
        );

        \App\Domains\Maintenance\SubDomains\Visits\Models\Visit::observe(
            \App\Domains\Maintenance\SubDomains\Visits\Observers\VisitObserver::class
        );
    }
}
```

## Alternative: Model Boot Method

For simple cases, use the model's `boot` method:

```php
<?php

namespace App\Domains\Catalog\Products\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected static function boot()
    {
        parent::boot();

        static::creating(function ($product) {
            $product->sku = $product->sku ?? static::generateSku();
        });

        static::deleting(function ($product) {
            if ($product->orderItems()->exists()) {
                throw new \Exception('Cannot delete product with orders');
            }
        });
    }

    private static function generateSku(): string
    {
        return 'PRD-' . strtoupper(uniqid());
    }
}
```

## Observers in the Project

| Model | Observer | Events Handled |
|-------|----------|----------------|
| `Attendance` | `AttendanceObserver` | saving, creating, updated |
| `Leave` | `LeaveObserver` | creating, created, updated |
| `Equipment` | `EquipmentObserver` | creating, updating, updated, deleting |
| `Visit` | `VisitObserver` | creating, created, updated |
| `User` | `UserObserver` | created, updated |
| `Project` | `ProjectObserver` | created, updated, deleted |

## Disabling Observers in Tests

```php
<?php

namespace Tests\Feature;

use App\Domains\Management\Attendance\Models\Attendance;
use Tests\TestCase;

class AttendanceTest extends TestCase
{
    public function test_create_attendance_without_observer(): void
    {
        // Disable all observers for this model
        Attendance::unsetEventDispatcher();

        $attendance = Attendance::create([
            'employee_id' => 1,
            'check_in' => now(),
        ]);

        // Re-enable observers
        Attendance::setEventDispatcher(app('events'));

        $this->assertDatabaseHas('attendances', [
            'id' => $attendance->id,
        ]);
    }
}
```

## Best Practices

### Do

- Keep observers focused on side effects
- Use dependency injection for external services
- Log important state changes
- Handle exceptions gracefully
- Use database transactions for multi-step operations

### Don't

- Put complex business logic in observers
- Create circular dependencies between observers
- Perform heavy operations synchronously
- Ignore the return value of `deleting` events
- Skip null checks for nullable relationships

## Related Patterns

- [Event Pattern](EVENT_PATTERN.md) - For cross-domain communication
- [Service Pattern](SERVICE_PATTERN.md) - Observers often call services
