BISO bietet ein Background-Task-System an, welches Hintergrundarbeiten definieren und ausführen kann. Es basiert auf der php-tr Bibliothek, die via Gaia ins Projekt eingebunden ist.
Typische Anwendungsfälle:
Ein Task definiert den Workflow (die Abfolge der Schritte). Ein Step implementiert die eigentliche Arbeit. Für einfache Tasks kann die Task-Klasse gleichzeitig auch das Step-Interface implementieren.
Der BisoTask ist die BISO-eigene Basisklasse, die von Task erbt und folgende Hilfsmethoden bereitstellt:
| Methode | Beschreibung |
|---|---|
setUserId(?int $userId) |
Setzt die Benutzer-ID im Payload (für Benachrichtigungen) |
getUserId(): ?int |
Liest die Benutzer-ID aus dem Payload |
log(string $line, string $level = 'info') |
Schreibt in PSR-Logger und in den Append-Only-Log |
setProgress(float $progress) |
Setzt den Fortschritt (0–100) |
getProgress(): float |
Liest den Fortschritt |
notifyUser(string $message, bool $attachLog = true) |
Sendet eine Abschluss-E-Mail an den Initiator |
Für einfache Tasks implementiert die Klasse sowohl BisoTask als auch das Step-Interface:
<?php
declare(strict_types=1);
namespace Biso\App\tasks;
use ByLexus\TaskRunner\Attribute\CleanupAfter;
use ByLexus\TaskRunner\Result\StepResult;
use ByLexus\TaskRunner\Step;
use ByLexus\TaskRunner\Task;
use Override;
use Psr\Log\LoggerInterface;
#[CleanupAfter(successful: new \DateInterval('P3D'), unsuccessful: new \DateInterval('P14D'))]
final class SimpleTask extends BisoTask implements Step {
public function __construct(?LoggerInterface $logger = null) {
parent::__construct(logger: $logger);
}
#[Override]
public function displayName(): string {
return 'SimpleTask';
}
/**
* Workflow-Definition: gibt den nächsten Step zurück. Null = Ende.
* Für Single-Step-Tasks: beim ersten Aufruf sich selbst zurückgeben.
*/
#[Override]
public function nextStep(?Step $actStep = null): ?Step {
if ($actStep === null) {
return $this;
}
return null;
}
/**
* Eigentliche Arbeitslogik des Steps.
*/
#[Override]
public function execute(Task $task): StepResult {
$this->log('SimpleTask läuft...');
// ... Arbeit verrichten ...
$this->notifyUser('SimpleTask abgeschlossen.');
return StepResult::succeeded(message: 'Erfolgreich abgeschlossen.');
}
}
Für komplexere Workflows werden Task und Steps getrennt implementiert. Der Task definiert die Abfolge, die Steps implementieren die Arbeit.
Step-Klasse (eigentliche Arbeit, mit Cancel-Unterstützung):
<?php
declare(strict_types=1);
namespace Biso\App\tasks;
use ByLexus\TaskRunner\Result\StepResult;
use ByLexus\TaskRunner\Step;
use ByLexus\TaskRunner\Task;
final class VerarbeitungStep implements Step {
public function execute(Task $task): StepResult {
$payload = $task->getPayload();
$ids = (array)($payload->ids ?? []);
foreach ($ids as $i => $id) {
// Zwischendurch auf Abbruch prüfen:
$task->reload();
if ($task->isCancelRequested()) {
return StepResult::cancelled(
message: $task->getCancelReason() ?? 'Abgebrochen.'
);
}
// ... ID verarbeiten ...
$task->appendLog("ID {$id} verarbeitet.");
// Fortschritt speichern:
$task->getPayload()->progress = ($i + 1) / count($ids) * 100.0;
$task->persistPayload();
}
// Ergebnis für den nächsten Step im Payload ablegen:
$task->getPayload()->verarbeitungAbgeschlossen = true;
return StepResult::succeeded(message: 'Verarbeitung abgeschlossen.');
}
}
Task-Klasse (Workflow-Definition):
<?php
declare(strict_types=1);
namespace Biso\App\tasks;
use ByLexus\TaskRunner\Attribute\CleanupAfter;
use ByLexus\TaskRunner\Step;
use Override;
use Psr\Log\LoggerInterface;
#[CleanupAfter(successful: new \DateInterval('P3D'), unsuccessful: new \DateInterval('P14D'))]
final class KomplexerTask extends BisoTask {
public function __construct(?LoggerInterface $logger = null) {
parent::__construct(logger: $logger);
}
#[Override]
public function displayName(): string {
return 'KomplexerTask';
}
/**
* Workflow-Definition: nextStep() wird vom Runner nach jedem abgeschlossenen Step aufgerufen.
* $actStep ist null beim ersten Aufruf, danach die zuletzt ausgeführte Step-Instanz.
*/
#[Override]
public function nextStep(?Step $actStep = null): ?Step {
if ($actStep === null) {
return new VerarbeitungStep();
}
if ($actStep instanceof VerarbeitungStep) {
return new AbschlussStep();
}
return null;
}
}
Tasks werden über den Gaia Dependency Injector erstellt und via TaskEnvironment eingereiht. In einem Controller-Action:
<?php
use ByLexus\TaskRunner\Task;
use ByLexus\TaskRunner\TaskEnvironment;
use Gaia\Router\GaiaAutoWire;
class MeinController extends GaiaController {
public function startTaskAction(TaskEnvironment $taskEnv, GaiaAutoWire $autowire): array {
$user = $this->getUser();
$task = $autowire->createInstance(KomplexerTask::class);
// Payload setzen (Eingabedaten für den Task):
$task->setUserId($user->getId());
$task->setPayload((object)[
'user_id' => $user->getId(),
'ids' => [1, 2, 3, 4, 5],
'total' => 5,
]);
$taskEnv->enqueue($task, Task::PRIO_VERY_LOW);
return ['task_id' => $task->getId()];
}
}
Verfügbare Prioritäten: Task::PRIO_VERY_LOW, Task::PRIO_LOW, Task::PRIO_NORMAL, Task::PRIO_HIGH.
Der Payload ist ein stdClass-Objekt, das auf dem Task gespeichert wird und als Datencontainer zwischen Steps dient:
// Payload schreiben:
$task->getPayload()->meinFeld = 'wert';
$task->persistPayload(); // in DB speichern
// Payload lesen:
$wert = $task->getPayload()->meinFeld ?? null;
Mit BisoTask stehen Hilfsmethoden für häufige Felder bereit (setUserId, setProgress etc.).
BisoTask::notifyUser() sendet nach Abschluss eine E-Mail an den Initiator. Die E-Mail-Adresse wird aus der user_id im Payload ermittelt:
#[Override]
public function execute(Task $task): StepResult {
// ... Arbeit ...
$this->notifyUser('1234 Fälle wurden verarbeitet.');
return StepResult::succeeded();
}
Optionaler zweiter Parameter $attachLog = true hängt den Append-Only-Log als Textdatei an.
Für den Mail-Versand muss in config.php ein SMTP-Sender definiert sein:
Config::set('smtp_sender', 'absender@domain.com');
Der Queue-Runner wird via biso-cli (das die Gaia-CLI verwendet) ausgeführt:
# Alle Tasks abarbeiten, dann beenden:
./biso-cli queue process-single
# Dauerhaft laufen und auf neue Tasks warten:
./biso-cli queue process-loop
# Tasks auflisten (optional mit Status-Filter):
./biso-cli queue list
./biso-cli queue list --status=running
./biso-cli queue list --status=failed
# Task abbrechen:
./biso-cli queue cancel <task-id> "Grund des Abbruchs"
Mehrere Runner können parallel betrieben werden. Jeder Runner identifiziert sich mit einer Runner-ID und stellt sicher, dass er nur nicht beanspruchte Tasks verarbeitet:
./biso-cli queue process-single --runner-id=worker-1
./biso-cli queue process-single --runner-id=worker-2
./biso-cli queue process-single --runner-id=worker-3
Der Queue-Runner wird via Crunz-Scheduler automatisch gestartet. Die Anzahl paralleler Worker ist konfigurierbar:
// config.php
Config::set('crunz.scheduler.queue.workers', 3);
Die Scheduler-Konfiguration liegt in backend/scheduler/tasks/QueueRunnerTasks.php. Sie startet die konfigurierten Worker-Prozesse jede Minute.
Tasks werden nach Abschluss automatisch aus der Datenbank gelöscht. Der Zeitpunkt wird pro Task-Klasse mit dem #[CleanupAfter]-Attribut definiert:
#[CleanupAfter(successful: new \DateInterval('P3D'), unsuccessful: new \DateInterval('P14D'))]
final class MeinTask extends BisoTask implements Step {
// ...
}
Im obigen Beispiel werden erfolgreiche Tasks nach 3 Tagen, fehlgeschlagene nach 14 Tagen gelöscht.