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

610 lines
15 KiB
Markdown

# 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
```php
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
```php
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
```php
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**:
```php
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
```php
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
```php
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)**:
```php
// ✅ 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**:
```php
'systemHealth' => new PollableClosure(
closure: $this->monitoring->getHealth(...),
interval: 10000,
event: 'system.health.polled' // Dispatches event
)
```
**Mit stopOnError**:
```php
'criticalData' => new PollableClosure(
closure: $this->dataService->getCritical(...),
interval: 1000,
stopOnError: true // Stops on first error
)
```
**Disabled by Default**:
```php
'debugInfo' => new PollableClosure(
closure: $this->debug->getInfo(...),
interval: 500,
enabled: false // Disabled until explicitly enabled
)
```
### Automatic Template Integration
**Der `PollableClosureTransformer` macht Closures automatisch pollbar:**
```php
// 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)**:
```javascript
// 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
```php
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**:
```bash
# Closure-based poll
curl https://localhost/poll/closure.notifications.abc123
# Attribute-based poll
curl https://localhost/poll/attribute.App\\NotificationComponent::checkNotifications
```
**Response Format**:
```json
{
"count": 5,
"latest": [
{"id": 1, "message": "New order"},
{"id": 2, "message": "Payment received"}
]
}
```
---
## Poll Discovery
### List all registered polls
```php
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
```php
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
```php
$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
```php
$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
```php
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
```php
$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
```php
$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
```php
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:**
```php
// ✅ 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
```php
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}`)