610 lines
15 KiB
Markdown
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}`)
|