# Event Sourcing Projections

Event Projections build **denormalized read models** from event streams for optimized query performance.

## Architecture

```
EventStore → ProjectionManager → Projection → Read Model (Database/Cache)
                    ↓
            ProjectionRepository (State Tracking)
                    ↓
            Queue (Async Execution)
```

## Core Components

### 1. ProjectionInterface
Contract for all projections:
```php
interface ProjectionInterface
{
    public function getName(): string;
    public function project(AggregateEnvelope $envelope): void;
    public function subscribedTo(): array; // Event class names
    public function reset(): void;
    public function getState(): ProjectionState;
}
```

### 2. ProjectionState (Value Object)
Tracks projection execution:
- `projectionName` - Unique identifier
- `lastEventVersion` - Last processed event version
- `status` - IDLE | RUNNING | ERROR | REBUILDING
- `lastUpdatedAt` - Timestamp
- `errorMessage` - Error details if failed

### 3. ProjectionManager
Orchestrates projection execution:
- `runProjection(name)` - Run single projection sync
- `runProjectionAsync(name)` - Run via queue
- `runAllProjections()` - Run all projections
- `rebuildProjection(name)` - Reset and rebuild from scratch
- `getProjectionStatus(name)` - Get current state

### 4. ProjectionRepository
Persists projection state:
- `CacheProjectionRepository` - Cache-based (default)
- Stores state in framework's `SmartCache`

## Usage

### Create a Projection

```php
use App\Framework\EventSourcing\Projections\ProjectionInterface;
use App\Framework\EventSourcing\Projections\ProjectionState;
use App\Framework\EventSourcing\AggregateEnvelope;

final class UserListProjection implements ProjectionInterface
{
    private ProjectionState $state;

    public function __construct(
        private readonly ConnectionInterface $connection
    ) {
        $this->state = ProjectionState::initial('user_list');
    }

    public function getName(): string
    {
        return 'user_list';
    }

    public function subscribedTo(): array
    {
        return [
            UserCreated::class,
            UserUpdated::class,
            UserDeleted::class
        ];
    }

    public function project(AggregateEnvelope $envelope): void
    {
        $event = $envelope->event;

        match ($event::class) {
            UserCreated::class => $this->handleUserCreated($event, $envelope),
            UserUpdated::class => $this->handleUserUpdated($event, $envelope),
            UserDeleted::class => $this->handleUserDeleted($event, $envelope),
            default => null
        };
    }

    public function reset(): void
    {
        $this->connection->exec('DELETE FROM user_list_projection');
        $this->state = ProjectionState::initial($this->getName());
    }

    public function getState(): ProjectionState
    {
        return $this->state;
    }

    private function handleUserCreated(UserCreated $event, AggregateEnvelope $envelope): void
    {
        $this->connection->insert('user_list_projection', [
            'user_id' => $event->userId->value,
            'email' => $event->email->value,
            'name' => $event->name,
            'created_at' => $envelope->recordedAt->format('Y-m-d H:i:s'),
            'event_version' => $envelope->version
        ]);
    }

    private function handleUserUpdated(UserUpdated $event, AggregateEnvelope $envelope): void
    {
        $this->connection->update('user_list_projection', [
            'email' => $event->email->value,
            'name' => $event->name,
            'updated_at' => $envelope->recordedAt->format('Y-m-d H:i:s')
        ], ['user_id' => $event->userId->value]);
    }

    private function handleUserDeleted(UserDeleted $event, AggregateEnvelope $envelope): void
    {
        $this->connection->delete('user_list_projection', [
            'user_id' => $event->userId->value
        ]);
    }
}
```

### Register Projection

```php
// In ProjectionInitializer
private function discoverProjections(): array
{
    return [
        $this->container->get(UserListProjection::class),
        $this->container->get(OrderSummaryProjection::class)
    ];
}
```

### Run Projections

**CLI Commands**:
```bash
# Run single projection
php console.php projection:run UserListProjection

# Run all projections
php console.php projection:run-all

# Rebuild projection (reset + run)
php console.php projection:rebuild UserListProjection

# Check projection status
php console.php projection:status
php console.php projection:status UserListProjection
```

**Programmatic**:
```php
// Synchronous
$state = $projectionManager->runProjection('user_list');

// Asynchronous (via Queue)
$projectionManager->runProjectionAsync('user_list');

// Rebuild from scratch
$state = $projectionManager->rebuildProjection('user_list');

// Get status
$state = $projectionManager->getProjectionStatus('user_list');
```

### Query Read Models

```php
// Query the denormalized projection table directly
$users = $connection->query(
    'SELECT * FROM user_list_projection WHERE name LIKE ?',
    ["%John%"]
);

// Or add query methods to projection
class UserListProjection implements ProjectionInterface
{
    public function findByName(string $name): array
    {
        return $this->connection->query(
            'SELECT * FROM user_list_projection WHERE name LIKE ?',
            ["%{$name}%"]
        );
    }

    public function getUserCount(): int
    {
        $result = $this->connection->query(
            'SELECT COUNT(*) as count FROM user_list_projection'
        );

        return (int) $result[0]['count'];
    }
}
```

## Integration with Framework

### Queue Integration
Projections can run asynchronously via the framework's Queue system:

```php
// ProjectionManager dispatches RunProjectionJob
$projectionManager->runProjectionAsync('user_list');

// Job is processed by queue worker
class RunProjectionJob
{
    public function handle(ProjectionManager $manager): array
    {
        return $manager->runProjection($this->projectionName);
    }
}
```

### Scheduler Integration
Schedule regular projection updates:

```php
use App\Framework\Scheduler\Services\SchedulerService;
use App\Framework\Scheduler\Schedules\IntervalSchedule;
use App\Framework\Core\ValueObjects\Duration;

$scheduler->schedule(
    'update-projections',
    IntervalSchedule::every(Duration::fromMinutes(5)),
    fn() => $projectionManager->runAllProjections()
);
```

### Cache Integration
Projection state stored in `SmartCache`:

```php
// CacheProjectionRepository uses framework's Cache
$cacheKey = CacheKey::fromString("projection:state:user_list");
$cacheItem = CacheItem::forSetting(
    key: $cacheKey,
    value: $state,
    ttl: Duration::fromDays(30)
);
```

## Best Practices

### 1. Projection Design
- **One Purpose**: Each projection serves one specific read use case
- **Denormalized**: Include all data needed for queries (no joins)
- **Idempotent**: Handle events multiple times safely
- **Fast Queries**: Optimize table structure for query patterns

### 2. Event Handling
- **Subscribe Selectively**: Only subscribe to relevant events
- **Version Tracking**: Store `event_version` for debugging
- **Error Handling**: Log errors but don't stop projection

### 3. Read Model Schema
```sql
-- Example projection table
CREATE TABLE user_list_projection (
    user_id VARCHAR(36) PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    name VARCHAR(255) NOT NULL,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NULL,
    event_version INT NOT NULL,
    INDEX idx_email (email),
    INDEX idx_name (name)
);
```

### 4. Performance
- **Batch Processing**: Process multiple events in transaction
- **Async Execution**: Use queue for non-critical projections
- **Incremental Updates**: Track last processed version
- **Index Optimization**: Add indexes for query patterns

## Examples

### Simple Projection (List View)
```php
// User list for admin panel
UserListProjection::class
→ Subscribes to: UserCreated, UserUpdated, UserDeleted
→ Read Model: user_list_projection table
→ Queries: findAll(), findByName(), getUserCount()
```

### Aggregated Projection (Analytics)
```php
// Order statistics for dashboard
OrderSummaryProjection::class
→ Subscribes to: OrderCreated, OrderPaid, OrderShipped
→ Read Model: order_summary_projection + daily_order_stats
→ Queries: getTodaysOrders(), getOrderStatsByStatus(), getRevenueByDay()
```

### Multi-Aggregate Projection
```php
// Customer order history (User + Orders)
CustomerOrderHistoryProjection::class
→ Subscribes to: UserCreated, OrderCreated, OrderStatusChanged
→ Read Model: customer_order_history (denormalized)
→ Queries: getCustomerOrders(), getCustomerStats()
```

## Monitoring

### Check Projection Health
```bash
# Get all statuses
php console.php projection:status

# Output:
# ✓ user_list
#   Status: idle
#   Last Event Version: 1523
#   Last Updated: 2025-10-05 15:30:42
#
# ✗ order_summary
#   Status: error
#   Last Event Version: 842
#   Last Updated: 2025-10-05 15:28:15
#   Error: Table 'order_summary_projection' doesn't exist
```

### Metrics to Track
- Last event version processed
- Projection lag (current event - last processed)
- Error count and types
- Rebuild frequency
- Query performance on read model

## Troubleshooting

### Projection Not Updating
1. Check if events are being appended to EventStore
2. Verify projection subscribes to correct event classes
3. Check projection state: `projection:status`
4. Look for errors in projection state

### Projection Lag
1. Events processed slower than created
2. Solutions:
   - Run projection more frequently (scheduler)
   - Use async queue processing
   - Optimize event handlers
   - Add database indexes

### Rebuild Required
Rebuild when:
- Projection logic changed
- Read model schema changed
- Projection corrupted/incomplete
- Testing/debugging

```bash
php console.php projection:rebuild user_list
```

## Future Enhancements

### Planned Features
- [ ] Automatic projection discovery via Discovery system
- [ ] Projection versioning for schema evolution
- [ ] Multi-tenant projection isolation
- [ ] Projection composition (projecting from projections)
- [ ] Event replay with filters (time range, aggregate type)
- [ ] Projection snapshots for large datasets
- [ ] Real-time projection updates via WebSocket

### EventStore Enhancement Needed
Current limitation: `EventStore` interface lacks:
- `loadStreamSince(int $version)` - Load events since version
- `loadStreamBetween(int $from, int $to)` - Load event range
- `loadAllEvents()` - Load all events across aggregates

These would enable efficient projection updates.
