Files
michaelschiemer/docs/examples/poll-system-usage.md

15 KiB

Poll System Usage Examples

Das Poll-System ermöglicht es, beliebige Methoden als pollable zu markieren und automatisch in konfigurierbaren Intervallen auszuführen.

Zwei Ansätze: Attribute vs. Closures

Das Framework bietet zwei komplementäre Ansätze für Polling:

  1. Attribute-basiert (#[Poll]): Für langlebige, wiederverwendbare Poll-Methoden in Klassen
  2. Closure-basiert (PollableClosure): Für Template-spezifische, dynamische Polls

Attribute-Based Polling

1. Methode mit #[Poll] Attribut markieren

use App\Framework\LiveComponents\Attributes\Poll;

final readonly class NotificationComponent
{
    public function __construct(
        private NotificationService $notificationService
    ) {}

    /**
     * Poll for new notifications every second
     */
    #[Poll(interval: 1000)]
    public function checkNotifications(): array
    {
        return [
            'count' => $this->notificationService->getUnreadCount(),
            'latest' => $this->notificationService->getLatest(5)
        ];
    }
}

2. Poll manuell ausführen

use App\Framework\LiveComponents\Polling\PollExecutor;

$executor = $container->get(PollExecutor::class);

// Execute specific poll
$result = $executor->executePoll(
    NotificationComponent::class,
    'checkNotifications'
);

if ($result->isSuccess()) {
    $data = $result->data;
    echo "Unread notifications: {$data['count']}\n";
}

Poll mit Event Dispatch

use App\Framework\LiveComponents\Attributes\Poll;

final readonly class ServerHealthComponent
{
    #[Poll(
        interval: 5000,           // Every 5 seconds
        event: 'server.health.checked'  // Dispatch event
    )]
    public function checkHealth(): array
    {
        return [
            'cpu' => $this->metrics->getCpuUsage(),
            'memory' => $this->metrics->getMemoryUsage(),
            'disk' => $this->metrics->getDiskUsage()
        ];
    }
}

Event Handler:

use App\Framework\Core\Events\OnEvent;
use App\Framework\LiveComponents\Polling\PollExecutedEvent;

final readonly class HealthMonitor
{
    #[OnEvent(PollExecutedEvent::class)]
    public function onHealthChecked(PollExecutedEvent $event): void
    {
        if ($event->poll->event === 'server.health.checked') {
            $health = $event->result;

            if ($health['cpu'] > 80) {
                $this->alerting->sendAlert('High CPU usage detected');
            }
        }
    }
}

Poll mit stopOnError

final readonly class CriticalMonitor
{
    #[Poll(
        interval: 10000,
        stopOnError: true  // Stop polling if error occurs
    )]
    public function checkCriticalService(): array
    {
        // Will stop polling if this throws
        return $this->criticalService->healthCheck();
    }
}

Closure-Based Polling

Ideal für Template-spezifische, dynamische Polls ohne dedizierte Klassen.

Basic Usage in Templates

use App\Framework\LiveComponents\Polling\PollableClosure;

// In Controller
final readonly class DashboardController
{
    #[Route('/dashboard', Method::GET)]
    public function dashboard(): ViewResult
    {
        return new ViewResult('dashboard', [
            // ✅ PREFERRED: First-class callable syntax (...)
            'notifications' => new PollableClosure(
                closure: $this->notificationService->getUnread(...),
                interval: 1000
            ),

            'serverStatus' => new PollableClosure(
                closure: $this->healthService->getStatus(...),
                interval: 5000
            )
        ]);
    }
}

Warum ... bevorzugen?

  • Kürzer und lesbarer
  • Bessere Performance (keine zusätzliche Closure)
  • IDE kann Methodensignatur besser analysieren
  • PHP 8.1+ Standard für first-class callables

Advanced Closure Examples

Mit Parametern (closure wrapper notwendig):

// ✅ First-class callable mit Parametern
'userActivity' => new PollableClosure(
    closure: fn() => $this->activityService->getRecent($userId, 10),
    interval: 2000
)

// Wenn Methode Parameter braucht, MUSS closure wrapper verwendet werden

Mit Event Dispatch:

'systemHealth' => new PollableClosure(
    closure: $this->monitoring->getHealth(...),
    interval: 10000,
    event: 'system.health.polled'  // Dispatches event
)

Mit stopOnError:

'criticalData' => new PollableClosure(
    closure: $this->dataService->getCritical(...),
    interval: 1000,
    stopOnError: true  // Stops on first error
)

Disabled by Default:

'debugInfo' => new PollableClosure(
    closure: $this->debug->getInfo(...),
    interval: 500,
    enabled: false  // Disabled until explicitly enabled
)

Automatic Template Integration

Der PollableClosureTransformer macht Closures automatisch pollbar:

// Template: dashboard.view.php
<div class="notifications">
    <h3>Notifications ({{ $notifications['count'] }})</h3>
    <!-- Data wird initial gerendert -->
    <!-- JavaScript polled automatisch /poll/{pollId} -->
</div>

<div class="status">
    <p>Server Status: {{ $serverStatus['status'] }}</p>
    <!-- Wird automatisch alle 5s aktualisiert -->
</div>

Generated JavaScript (automatisch):

// Automatisch generiert vom PollableClosureTransformer
(function() {
    function poll_notifications() {
        fetch('/poll/{pollId}', {
            method: 'GET',
            headers: {
                'X-Requested-With': 'XMLHttpRequest',
                'Accept': 'application/json'
            }
        })
        .then(response => response.json())
        .then(data => {
            // Update DOM elements with data-poll="notifications"
            const elements = document.querySelectorAll('[data-poll="notifications"]');
            elements.forEach(el => el.textContent = data);
        });
    }

    // Start polling every 1000ms
    setInterval(poll_notifications, 1000);
    poll_notifications(); // Initial call
})();

Manual Closure Registration

use App\Framework\LiveComponents\Polling\PollableClosureRegistry;
use App\Framework\LiveComponents\ValueObjects\ComponentId;

$registry = $container->get(PollableClosureRegistry::class);

// Register closure for specific component
$registration = $registry->register(
    templateKey: 'notifications',
    closure: new PollableClosure(
        closure: $this->notificationService->getUnread(...),
        interval: 1000
    ),
    componentId: ComponentId::fromString('dashboard-component')
);

// Get endpoint URL
$endpoint = $registration->getEndpoint();  // "/poll/{pollId}"

// Execute closure
$result = $registry->execute($registration->pollId);

Unified Poll Controller

Beide Poll-Typen nutzen denselben Endpoint:

GET /poll/{pollId}

PollId Format unterscheidet Typen:

  • closure.* → Closure-based Poll
  • attribute.* → Attribute-based Poll

Beispiel Request:

# Closure-based poll
curl https://localhost/poll/closure.notifications.abc123

# Attribute-based poll
curl https://localhost/poll/attribute.App\\NotificationComponent::checkNotifications

Response Format:

{
  "count": 5,
  "latest": [
    {"id": 1, "message": "New order"},
    {"id": 2, "message": "Payment received"}
  ]
}

Poll Discovery

List all registered polls

use App\Framework\LiveComponents\Polling\PollService;

$pollService = $container->get(PollService::class);

// Get all attribute-based polls
$allPolls = $pollService->getAllPolls();

foreach ($allPolls as $item) {
    $poll = $item['poll'];
    $discovered = $item['discovered'];

    echo sprintf(
        "Poll: %s::%s - Interval: %dms - Enabled: %s\n",
        $discovered->className->getFullyQualified(),
        $discovered->methodName->toString(),
        $poll->interval,
        $poll->enabled ? 'Yes' : 'No'
    );
}

Get closure-based polls

use App\Framework\LiveComponents\Polling\PollableClosureRegistry;

$registry = $container->get(PollableClosureRegistry::class);

// Get all registered closures
$registrations = $registry->getAll();

foreach ($registrations as $registration) {
    echo sprintf(
        "Closure Poll: %s - Interval: %dms\n",
        $registration->templateKey,
        $registration->closure->interval
    );
}

// Get enabled closures only
$enabled = $registry->getEnabled();

Execution Patterns

Execute all enabled attribute polls

$executor = $container->get(PollExecutor::class);

// Execute all enabled polls
$results = $executor->executeAllEnabledPolls();

foreach ($results as $result) {
    if ($result->isSuccess()) {
        echo "{$result->getPollId()} succeeded in {$result->executionTime->toMilliseconds()}ms\n";
    } else {
        echo "{$result->getPollId()} failed: {$result->error?->getMessage()}\n";
    }
}

Execute closure polls

$registry = $container->get(PollableClosureRegistry::class);

// Execute specific closure by PollId
$pollId = PollId::fromString('closure.notifications.abc123');
$result = $registry->execute($pollId);

// Process result
echo json_encode($result);

Integration with Scheduler

Schedule attribute polls

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

$scheduler = $container->get(SchedulerService::class);
$executor = $container->get(PollExecutor::class);

// Schedule poll execution
$scheduler->schedule(
    'execute-all-polls',
    IntervalSchedule::every(Duration::fromSeconds(1)),
    fn() => ['executed' => count($executor->executeAllEnabledPolls())]
);

Schedule closure polls

$scheduler->schedule(
    'execute-closure-polls',
    IntervalSchedule::every(Duration::fromSeconds(1)),
    function() use ($registry) {
        $enabled = $registry->getEnabled();
        $results = [];

        foreach ($enabled as $registration) {
            try {
                $results[] = $registry->execute($registration->pollId);
            } catch (\Throwable $e) {
                // Handle error
            }
        }

        return ['executed' => count($results)];
    }
);

Performance Monitoring

Get execution statistics

$stats = $executor->getExecutionStats();

echo "Total polls: {$stats['total_polls']}\n";
echo "Enabled polls: {$stats['enabled_polls']}\n";

echo "\nPolls by interval:\n";
foreach ($stats['polls_by_interval'] as $interval => $count) {
    echo "  {$interval}ms: {$count} polls\n";
}

Monitor poll performance

use App\Framework\Core\Events\OnEvent;

final readonly class PollPerformanceMonitor
{
    #[OnEvent(PollExecutedEvent::class)]
    public function onPollExecuted(PollExecutedEvent $event): void
    {
        $duration = $event->executionTime;

        // Log slow polls
        if ($duration->toMilliseconds() > 100) {
            $this->logger->warning('Slow poll detected', [
                'poll' => $event->getPollId(),
                'duration_ms' => $duration->toMilliseconds()
            ]);
        }

        // Track metrics
        $this->metrics->timing(
            "poll.{$event->methodName->toString()}.duration",
            $duration->toMilliseconds()
        );
    }
}

Best Practices

Wann Attribute, wann Closures?

Attribute (#[Poll]) verwenden für:

  • Wiederverwendbare Business-Logic-Polls
  • Polls, die von mehreren Templates genutzt werden
  • Komplexe Polls mit Event-Dispatch
  • Service-Layer Polls (Health Checks, Monitoring)

Closures (PollableClosure) verwenden für:

  • Template-spezifische Polls
  • Einmalige, nicht wiederverwendbare Polls
  • Dynamische Polls mit Template-Kontext
  • Quick Prototyping ohne dedizierte Klassen

Interval Guidelines

  • Fast (100-1000ms): Critical real-time data, notifications
  • Medium (1000-10000ms): Health checks, status updates
  • Slow (10000-60000ms): Background sync, cache refresh
  • Very Slow (>60000ms): Analytics, cleanup tasks

Method Requirements

  • Must be public
  • Return type should be serializable (array, SerializableState, scalars)
  • Should be idempotent (safe to call multiple times)
  • Should not have side effects that break on repeated execution

Error Handling

  • Use stopOnError: true for critical polls
  • Implement proper logging in poll methods
  • Return meaningful data structures
  • Handle exceptions gracefully

Performance

  • Keep poll methods fast (<100ms ideal)
  • Avoid heavy database queries
  • Use caching where appropriate
  • Monitor execution times via events

First-Class Callable Syntax (...)

IMMER bevorzugen wenn möglich:

// ✅ BEST: First-class callable
new PollableClosure(
    closure: $this->service->getData(...),
    interval: 1000
)

// ⚠️ OK: Wenn Parameter nötig
new PollableClosure(
    closure: fn() => $this->service->getData($userId),
    interval: 1000
)

// ❌ AVOID: Unnötiger fn() wrapper
new PollableClosure(
    closure: fn() => $this->service->getData(),  // AVOID!
    interval: 1000
)

Vorteile von ... Syntax:

  • Kürzer und lesbarer
  • Keine zusätzliche Closure-Allokation
  • Bessere IDE-Unterstützung
  • Performance-Vorteil (minimal aber vorhanden)

Testing

it('executes poll and returns expected data', function () {
    $component = new NotificationComponent($this->notificationService);

    $result = $component->checkNotifications();

    expect($result)->toHaveKey('count');
    expect($result)->toHaveKey('latest');
});

it('poll attribute is discoverable', function () {
    $pollService = $this->container->get(PollService::class);

    expect($pollService->isPollable(
        NotificationComponent::class,
        'checkNotifications'
    ))->toBeTrue();
});

it('closure poll executes correctly', function () {
    $closure = new PollableClosure(
        closure: fn() => ['status' => 'ok'],
        interval: 1000
    );

    $result = $closure->execute();

    expect($result)->toHaveKey('status');
    expect($result['status'])->toBe('ok');
});

it('closure poll is registered correctly', function () {
    $registry = $this->container->get(PollableClosureRegistry::class);

    $registration = $registry->register(
        'test',
        new PollableClosure(fn() => [], 1000)
    );

    expect($registry->has($registration->pollId))->toBeTrue();
});

Framework Compliance

Readonly Classes: Poll, PollableClosure, PollResult, Events are all readonly Value Objects: Uses ClassName, MethodName, Duration, ComponentId, PollId VOs Immutability: All poll-related objects are immutable Discovery Integration: Automatic via DiscoveryRegistry (attribute-based) Event System: Full integration with EventDispatcher Type Safety: Strong typing throughout First-Class Callables: Supports PHP 8.1+ ... syntax Unified API: Single endpoint for both poll types (/poll/{pollId})