> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Geplante Aufgaben in Paketen registrieren und pflegen

> Anhand von Schedule und Hooks nach der Auflösung in Laravel 13 erläutert: wie Sie geplante Aufgaben in Paketen explizit aktivierbar gestalten und was es bei doppelter Registrierung, Lock-Namen, Konfigurationsänderungen und Tests zu beachten gilt.

Wenn Sie einem Paket eine regelmäßige Synchronisation oder einen Heartbeat hinzufügen, trennen Sie die **Bereitstellung der Verarbeitung** von der **Registrierung im Schedule**. Die bloße Installation soll die geplanten Aufgaben der nutzenden Anwendung nicht vermehren; stattdessen entscheiden die Nutzer selbst über Aktivierung, Ausführungsfrequenz und das Verhalten auf mehreren Servern.

Diese Seite stützt sich auf den offiziellen Quellcode von Laravel Framework `v13.39.0` und überträgt die Grundlagen aus [Task-Scheduling](https://kawax.biz/de/scheduling.md) auf die Paketentwicklung. Wie Provider geladen werden, erfahren Sie unter [Interne Struktur der automatischen Paket-Erkennung](https://kawax.biz/de/advanced/package-discovery.md); zum Veröffentlichen der Konfiguration siehe [Paketkonfiguration zusammenführen und cachen](https://kawax.biz/de/advanced/package-config-merging.md).

## Nach der Auflösung von Schedule registrieren

In einer gewöhnlichen Laravel-Anwendung registriert der `FoundationServiceProvider` `Schedule::class` als Singleton. Die Erzeugung ruft `resolveConsoleSchedule()` des Console Kernels auf und wendet die Zeitzone der Anwendung sowie den Cache-Store für den Scheduler an.

Ein Paket erweitert diesen gemeinsam genutzten Schedule, statt einfach einen eigenen `new Schedule()` zu erzeugen. Der folgende Provider geht davon aus, dass `Acme\Courier\` per Composer-PSR-4 auf `src/` abgebildet ist und das Paket eine `config/courier.php` enthält.

```php theme={null}
<?php

namespace Acme\Courier;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->mergeConfigFrom(__DIR__.'/../config/courier.php', 'courier');

        $this->app->singleton(CourierSchedule::class);
    }

    public function boot(): void
    {
        if (! $this->app->runningInConsole()) {
            return;
        }

        $this->callAfterResolving(Schedule::class, function (Schedule $schedule): void {
            $this->app->make(CourierSchedule::class)->register($schedule);
        });
    }
}
```

`callAfterResolving()` wartet auf die künftige Auflösung, falls der Service noch nicht aufgelöst ist; ist er bereits aufgelöst, holt es ihn und ruft den Callback zusätzlich sofort auf. So gelingt die Registrierung auch dann, wenn ein anderer Provider den Schedule bereits vorher verwendet hat. Details finden Sie unter [Hooks nach der Service-Auflösung in Paketen](https://kawax.biz/de/advanced/package-service-resolution.md).

`runningInConsole()` prüft nicht auf einen bestimmten Artisan-Befehl. In diesem Beispiel wird die Nutzung des Schedule über HTTP ausgeschlossen. Außerdem darf der Provider, der den Hook registriert, selbst kein Deferred Provider sein: Läuft `boot()` nicht rechtzeitig, wird auch der Hook nicht registriert.

```mermaid theme={null}
flowchart TD
    A["boot() des Paket-Providers"] --> B["Hook nach Auflösung von Schedule registrieren"]
    B --> C{"Schedule bereits aufgelöst?"}
    C -->|No| D["Auf erste Auflösung warten"]
    C -->|Yes| E["Vorhandenes Singleton holen"]
    D --> F["Aktivierung und bisherige Registrierung prüfen"]
    E --> F
    F -->|Aktiviert und nicht registriert| G["Event-Definition hinzufügen"]
    F -->|Deaktiviert oder bereits registriert| H["Nichts hinzufügen"]
    G --> I["Späteres schedule:run<br>prüft Ausführungsbedingungen"]
```

> [!NOTE]
>
> Die Registrierung eines Events und die Ausführung einer Aufgabe sind zwei verschiedene Dinge. Das Laden dieses Providers führt den Heartbeat nicht sofort aus. Die Registrierung allein startet auch weder cron noch `schedule:work`.

## Nutzer aktivieren explizit

Die paketseitige `config/courier.php` ist standardmäßig deaktiviert.

```php theme={null}
<?php

return [
    'schedule' => [
        'enabled' => false,
        'cron' => '*/5 * * * *',
        'timezone' => 'UTC',
        'overlap_minutes' => 10,
        'on_one_server' => false,
    ],
];
```

In der nutzenden Anwendung wird die Konfiguration veröffentlicht bzw. angelegt und `schedule.enabled` auf `true` gesetzt. In diesem Beispiel werden die Werte bei der Registrierung gelesen. Als Standardzeitzone dient UTC; wer eine andere Zeitzone wählt, muss doppelte oder ausgelassene Ausführungen durch die Sommerzeit berücksichtigen.

Da die Konfiguration nur [flach zusammengeführt](https://kawax.biz/de/advanced/package-config-merging.md) wird, gibt auch der Registrierer für jeden Schlüssel einen Standardwert an. So lässt sich die Aufgabe mit den Grundeinstellungen registrieren, selbst wenn ein teilweises Überschreiben andere Schlüssel des Arrays `schedule` entfernt.

## Doppelte Registrierung im selben Schedule verhindern

Die folgende `src/CourierSchedule.php` registriert einen minimalen Heartbeat, der einen Log-Eintrag schreibt. Mithilfe einer `WeakMap` sorgt sie dafür, dass **dieser Registrierer einem Schedule-Objekt nur einmal etwas hinzufügt**.

```php theme={null}
<?php

namespace Acme\Courier;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Contracts\Config\Repository;
use Psr\Log\LoggerInterface;
use WeakMap;

final class CourierSchedule
{
    public const EVENT_NAME = 'acme-courier:heartbeat';

    private WeakMap $registered;

    public function __construct(private Repository $config)
    {
        $this->registered = new WeakMap();
    }

    public function register(Schedule $schedule): void
    {
        if (! $this->config->get('courier.schedule.enabled', false)
            || isset($this->registered[$schedule])) {
            return;
        }

        $event = $schedule->call(static function (LoggerInterface $logger): void {
            $logger->info('Courier heartbeat');
        })
            ->name(self::EVENT_NAME)
            ->cron($this->config->get('courier.schedule.cron', '*/5 * * * *'))
            ->timezone($this->config->get('courier.schedule.timezone', 'UTC'))
            ->withoutOverlapping($this->config->get('courier.schedule.overlap_minutes', 10));

        if ($this->config->get('courier.schedule.on_one_server', false)) {
            $event->onOneServer();
        }

        $this->registered[$schedule] = true;
    }
}
```

Da dieser Registrierer ein Singleton ist, teilt er den Registrierungsstatus auch dann, wenn er aus dem Provider-Hook erneut geholt wird. Einen neuen Schedule kann er dagegen weiterhin befüllen. Vermeiden Sie ein prozessweites statisches „bereits registriert“-Flag, das auch neue Container oder Schedules in Tests ausschließen würde.

`Schedule::call()`, `exec()` und `job()` fügen Events zu einem Array hinzu. Es handelt sich nicht um APIs, die Events mit gleichem Namen finden und ersetzen oder Duplikate entfernen.

> [!WARNING]
>
> `withoutOverlapping()` behebt keine doppelte Registrierung. Ist dieselbe Aufgabe zweimal registriert und wird der Lock nach Ende der ersten Ausführung freigegeben, kann auch das zweite Event ausgeführt werden. Auch `onOneServer()` entfernt keine doppelten Registrierungen.
>
> Die obige WeakMap verhindert nur Duplikate dieses Registrierers. Fügen Nutzer dieselbe Aufgabe in `routes/console.php` hinzu, deaktivieren sie die automatische Registrierung des Pakets, sodass die Registrierung an genau einer Stelle verantwortet wird.

## Event-Namen betreffen auch die Lock-Kompatibilität

`name()` ist ein Alias von `description()`, doch die Auswirkung auf den Lock-Namen hängt von der Art des Events ab. Nachfolgend die Standard-Mutex-Namen.

| Event | Grundlage des Mutex-Namens | Rolle von `name()` |
| - | - | - |
| Event aus `command()` bzw. `exec()` | Cron-Ausdruck und normalisierter Befehlsstring | Beschreibung zur Anzeige. Nicht im Standard-Mutex-Namen enthalten |
| CallbackEvent aus `call()` bzw. `job()` | Description des Events | Anzeigename und Identifikation des Locks. Der Cron-Ausdruck ist nicht enthalten |

Geben Sie einem CallbackEvent **vor** `withoutOverlapping()` oder `onOneServer()` **einen nicht leeren Namen**. Ist kein Name gesetzt, werfen diese Methoden eine Exception.

`job()` verwendet normalerweise etwa den Namen der Job-Klasse als Anfangsnamen. Registrieren Sie denselben Job jedoch mit unterschiedlichen Argumenten oder Zielen, geben Sie pro Ziel explizit einen Namen an. Allein durch andere Argumente ergibt sich nicht automatisch ein anderer Anfangsname.

```php theme={null}
use Acme\Courier\Jobs\SyncCatalogJob;
use Illuminate\Support\Facades\Schedule;

Schedule::job(new SyncCatalogJob('tenant-a'))
    ->name('acme-courier:catalog:tenant-a')
    ->everyFiveMinutes()
    ->onOneServer();

Schedule::job(new SyncCatalogJob('tenant-b'))
    ->name('acme-courier:catalog:tenant-b')
    ->everyFiveMinutes()
    ->onOneServer();
```

`SyncCatalogJob` ist ein Beispiel für eine vom Paket bereitgestellte Job-Klasse. Wie Jobs implementiert werden, erfahren Sie unter [Queues und Jobs](https://kawax.biz/de/queues.md).

Ändern Sie Name, Befehl oder Cron-Ausdruck, ändert sich je nach Event-Art auch der Bezeichner des Locks. Bei Deployments, in denen Prozesse der alten und neuen Version parallel laufen, kann dieselbe Verarbeitung dadurch unterschiedliche Locks erhalten. Behandeln Sie dies nicht wie eine bloße Änderung des Anzeigetexts, sondern pflegen Sie Aufgabennamen als stabile, öffentliche Spezifikation.

### onOneServer und withoutOverlapping getrennt betrachten

* `onOneServer()` wählt den Server aus, der die Ausführung zu diesem Zeitpunkt übernimmt. Der untersuchte `CacheSchedulingMutex` hängt Stunde und Minute an den Mutex-Namen des Events an und hält ihn eine Stunde lang. Das ist etwas anderes als ein Überlappungs-Lock, der mit dem Ende der Aufgabe freigegeben wird.
* `withoutOverlapping()` verhindert Überschneidungen, solange die vorherige Ausführung noch läuft. Die angegebene Ablaufzeit wird in Minuten angegeben, standardmäßig 1.440 Minuten. Ist sie zu kurz, verfällt der Lock, bevor die Verarbeitung endet.
* Stellt `job()` den Job in eine Queue, bedeutet der Abschluss des Schedule-Events nicht, dass der Job abgearbeitet ist. Die Duplikatkontrolle auf dem Worker gestalten Sie separat, etwa mit Unique Jobs oder der Middleware `WithoutOverlapping` auf Seiten des Jobs.

Nutzern, die `onOneServer()` aktivieren, nennen Sie als Voraussetzung einen gemeinsam genutzten Cache mit `database`, `memcached`, `dynamodb` oder `redis`, wie es die offizielle Dokumentation vorgibt. Locks garantieren keine Exactly-once-Ausführung der Geschäftslogik; die Verarbeitung muss auch bei erneuter Ausführung konsistent bleiben.

## Schwere Abhängigkeiten zur Laufzeit auflösen

Das `LoggerInterface` des obigen Heartbeats löst `CallbackEvent` bei der Ausführung des Events über `call()` des Containers auf. Bei der Registrierung des Hooks wird weder geloggt noch ein Client erzeugt.

| Definitionsweise | Hinweis zur Registrierung |
| - | - |
| Closure an `call()` übergeben | Der Rumpf wird später aufgerufen. Außerhalb erzeugte und per capture übernommene Objekte existieren jedoch bereits |
| `call(new Task)` | `new Task` wird bereits bei der Registrierung ausgeführt |
| `command(CommandClass::class)` | Schedule erzeugt die Klasse über den Container und liest Name und Beschreibung aus |
| `job(JobClass::class)` | Die Erzeugung des Jobs wartet bis zur Ausführung im Callback des Events |

Externe Verbindungen oder Geschäftslogik aus dem Konstruktor herauszuhalten, ist auch bei [Artisan-Befehle in Paketen registrieren und Abhängigkeiten auflösen](https://kawax.biz/de/advanced/package-commands.md) wichtig.

Da `call()` und `job()` CallbackEvents erzeugen, steht `runInBackground()` nicht zur Verfügung. Langwierige Verarbeitung delegieren Sie an eine Queue oder nutzen bei Bedarf die Hintergrundausführung von `command()`.

## Anwendungsweite Einstellungen nicht an sich reißen

`Schedule::useCache()` ändert den Store des Mutex-Objekts, das der Schedule hält. Diese Einstellung gilt nicht nur für die Events des Pakets, sondern betrifft auch alle anderen Events, die denselben Mutex teilen. Rufen Sie sie nicht bedingungslos im `boot()` des Pakets auf, sondern behandeln Sie sie als Einstellung, die die nutzende Anwendung wählt.

Ebenso sollte ein Paket `Schedule::alwaysOnOneServer()`, das Pausieren des Schedulers oder das Ausführungsverhalten im Wartungsmodus nicht eigenmächtig ändern.

## Nach Konfigurationsänderungen einen neuen Schedule erzeugen

`enabled`, Cron-Ausdruck und Zeitzone im Beispiel sind Einstellungen zum Zeitpunkt der Registrierung. Ändern Sie lediglich den Wert von `config()` für einen bereits erzeugten Schedule, werden weder Events hinzugefügt oder entfernt noch die Konfiguration neu eingelesen. Auch die WeakMap-Prüfung dient nicht der Neukonfiguration.

Bei Deployments mit Konfigurations-Cache bauen Sie den Cache neu auf und prüfen `schedule:list` in einem Prozess, der die neue Konfiguration lädt. Stellen Sie auch für lang laufende Scheduler und für `schedule:run` mit laufenden Aufgaben unter einer Minute sicher, dass kein alter Code und keine alte Konfiguration zurückbleiben. Zum Unterbrechen von Sub-Minuten-Aufgaben beim Deployment siehe [Task-Scheduling](https://kawax.biz/de/scheduling.md).

## Registrierung getrennt von der Ausführung testen

Das folgende Beispiel `tests/CourierScheduleTest.php` nutzt das Composer-Autoloading des Pakets und PHPUnit. Die Mutexe werden durch Test-Doubles ersetzt, sodass das Registrierungsergebnis ohne externen Cache und ohne echtes cron geprüft wird.

```php theme={null}
<?php

namespace Acme\Courier\Tests;

use Acme\Courier\CourierSchedule;
use Illuminate\Config\Repository;
use Illuminate\Console\Scheduling\EventMutex;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Console\Scheduling\SchedulingMutex;
use Illuminate\Container\Container;
use PHPUnit\Framework\TestCase;

class CourierScheduleTest extends TestCase
{
    private Container $previousContainer;
    private Repository $config;
    private CourierSchedule $registrar;

    protected function setUp(): void
    {
        parent::setUp();

        $this->previousContainer = Container::getInstance();
        $container = new Container();
        Container::setInstance($container);

        $container->instance(EventMutex::class, $this->createMock(EventMutex::class));
        $container->instance(SchedulingMutex::class, $this->createMock(SchedulingMutex::class));

        $this->config = new Repository();
        $this->registrar = new CourierSchedule($this->config);
    }

    protected function tearDown(): void
    {
        Container::setInstance($this->previousContainer);

        parent::tearDown();
    }

    public function test_disabled_by_default(): void
    {
        $schedule = new Schedule();
        $this->registrar->register($schedule);

        $this->assertCount(0, $schedule->events());
    }

    public function test_partial_config_uses_defaults_without_duplicate_registration(): void
    {
        $this->config->set('courier.schedule.enabled', true);
        $schedule = new Schedule();

        $this->registrar->register($schedule);
        $this->registrar->register($schedule);

        $this->assertCount(1, $schedule->events());
        $event = $schedule->events()[0];
        $this->assertSame(CourierSchedule::EVENT_NAME, $event->description);
        $this->assertSame('*/5 * * * *', $event->expression);
        $this->assertSame('UTC', $event->timezone);
        $this->assertTrue($event->withoutOverlapping);
        $this->assertSame(10, $event->expiresAt);
        $this->assertFalse($event->onOneServer);
    }

    public function test_explicit_schedule_settings_are_applied(): void
    {
        $this->config->set('courier.schedule', [
            'enabled' => true,
            'cron' => '0 * * * *',
            'timezone' => 'Asia/Tokyo',
            'overlap_minutes' => 30,
            'on_one_server' => true,
        ]);

        $schedule = new Schedule();
        $this->registrar->register($schedule);
        $event = $schedule->events()[0];

        $this->assertSame('0 * * * *', $event->expression);
        $this->assertSame('Asia/Tokyo', $event->timezone);
        $this->assertSame(30, $event->expiresAt);
        $this->assertTrue($event->onOneServer);
    }

    public function test_setting_changes_require_a_new_schedule(): void
    {
        $this->config->set('courier.schedule.enabled', true);
        $first = new Schedule();
        $this->registrar->register($first);

        $this->config->set('courier.schedule.cron', '0 * * * *');
        $this->registrar->register($first);

        $second = new Schedule();
        $this->registrar->register($second);

        $this->assertCount(1, $first->events());
        $this->assertCount(1, $second->events());
        $this->assertSame('*/5 * * * *', $first->events()[0]->expression);
        $this->assertSame('0 * * * *', $second->events()[0]->expression);
    }
}
```

Dieser Test ist ein Unit-Test des Registrierers. Die automatische Erkennung des Providers, das Zusammenführen der Konfiguration und die Integration in den Console Kernel prüfen Sie separat mit [Orchestra Testbench](https://kawax.biz/de/advanced/package-testing.md) oder einer Testanwendung.

| Zu prüfender Fall | Erfüllungskriterium |
| - | - |
| Installation mit Standardkonfiguration | Es werden keine geplanten Aufgaben des Pakets registriert |
| `schedule:list` nach Aktivierung | Genau ein Event ist vorhanden, Frequenz und Zeitzone sind korrekt |
| Provider-Start vor bzw. nach der Auflösung von Schedule | In beiden Fällen wird die Registrierung übernommen |
| Nur Registrierung durchführen | Geschäftsservices werden weder erzeugt noch beginnt eine Verarbeitung |
| Aufgabenrumpf einzeln ausführen | Benötigte Abhängigkeiten werden aufgelöst, erwartete Verarbeitung bzw. Exceptions lassen sich prüfen |
| Mehrere Server und lange Verarbeitung | Voraussetzung eines gemeinsamen Caches und beide Lock-Arten getrennt verifizieren |
| Update mit Konfigurations-Cache | In neuen Prozessen bleiben weder alte Aufgaben noch doppelte Registrierungen zurück |

Führen Sie nicht alle Aufgaben der nutzenden Anwendung mit `schedule:run` aus, nur um den Registrierungstest zu bestehen. Indem Sie Tests des Aufgabenrumpfs und Scheduler-Integrationstests in einer kontrollierten Umgebung trennen, vermeiden Sie Nebenwirkungen unbeteiligter geplanter Aufgaben.

## Verwandte Seiten

> [card] [Task-Scheduling](https://kawax.biz/de/scheduling.md)
>
> Ausführungsfrequenz, Einrichtung von cron, Delegation an Queues und Betrieb beim Deployment.

> [card] [Hooks nach der Service-Auflösung in Paketen](https://kawax.biz/de/advanced/package-service-resolution.md)
>
> Anwendung auf noch nicht aufgelöste und bereits aufgelöste Services sowie Hinweise zur erneuten Registrierung.

## Herangezogene Primärquellen

* [Offizielle Laravel-Dokumentation: Task-Scheduling](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/scheduling.md)
* [Offizielle Laravel-Dokumentation: Paketentwicklung](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/packages.md)
* [FoundationServiceProvider: Registrierung von Schedule als Singleton](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php)
* [Console Kernel: Erzeugung von Schedule und Standardeinstellungen](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Foundation/Console/Kernel.php)
* [ServiceProvider: callAfterResolving()](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/ServiceProvider.php)
* [Schedule: Hinzufügen von Events, verzögerte Erzeugung von Jobs, gemeinsamer Mutex](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Console/Scheduling/Schedule.php)
* [Event: Mutex-Namen von Befehls-Events](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Console/Scheduling/Event.php)
* [CallbackEvent: Pflicht zum Namen und Auflösung von Abhängigkeiten zur Laufzeit](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Console/Scheduling/CallbackEvent.php)
* [CacheEventMutex: Überlappungs-Lock und Umrechnung der Ablaufzeit](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Console/Scheduling/CacheEventMutex.php)
* [CacheSchedulingMutex: Lock pro Ausführungszeitpunkt](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Console/Scheduling/CacheSchedulingMutex.php)
* [Offizielle Tests: Registrierung von Events und Zeitzonen](https://github.com/laravel/framework/blob/v13.39.0/tests/Console/ConsoleEventSchedulerTest.php)
* [Offizielle Tests: Verzögerte Erzeugung über den Job-Klassennamen](https://github.com/laravel/framework/blob/v13.39.0/tests/Console/Scheduling/ScheduleTest.php)


## Related topics

- [Versionskompatibilität von Paketen verwalten](https://kawax.biz/de/advanced/package-versioning.md)
- [Laravel-Paketentwicklung](https://kawax.biz/de/advanced/package-development.md)
- [Event-Listener in Paketen registrieren und cachen](https://kawax.biz/de/advanced/package-events.md)
- [Mail-Transports in Paketen registrieren und testen](https://kawax.biz/de/advanced/package-mail-transports.md)
- [Artisan-Befehle in Paketen registrieren und Abhängigkeiten auflösen](https://kawax.biz/de/advanced/package-commands.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.