feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready
This commit is contained in:
609
docs/examples/poll-system-usage.md
Normal file
609
docs/examples/poll-system-usage.md
Normal file
@@ -0,0 +1,609 @@
|
||||
# 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}`)
|
||||
Reference in New Issue
Block a user