[inhalt]
Projekt BISO 3 - Handbuch

Background-Task-System

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:

Architektur

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

Implementation von Task-Klassen

Einfacher Single-Step-Task

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.');
    }
}

Multi-Step-Task

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;
    }
}

Task in die Queue einreihen

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.

Payload

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.).

Benutzer-Benachrichtigung

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');

Ausführen des Queue-Runners

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 parallel

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

Scheduling

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.

Automatisches Aufräumen

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.