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

# Geplande taken van een package registreren en onderhouden

> Een analyse van Schedule en hooks na het resolven in Laravel 13: hoe je geplande taken van een package expliciet laat inschakelen, en waar je op moet letten bij dubbele registratie, locknamen, configuratiewijzigingen en tests.

Wil je een package uitbreiden met een periodieke synchronisatie of een heartbeat, scheid dan **het aanbieden van de verwerking** van **het registreren in de schedule**. Alleen het installeren van het package mag de geplande taken van de gebruikende applicatie niet uitbreiden; ontwerp het zo dat gebruikers zelf kiezen of de taak wordt ingeschakeld, hoe vaak die draait en hoe die zich gedraagt op meerdere servers.

Op deze pagina bekijken we de officiële broncode van Laravel Framework `v13.39.0` en passen we de basisbewerkingen uit [Taakplanning](https://kawax.biz/nl/scheduling.md) toe op packageontwikkeling. Zie [De interne structuur van package discovery](https://kawax.biz/nl/advanced/package-discovery.md) voor het laden van providers en [Packageconfiguratie samenvoegen en cachen](https://kawax.biz/nl/advanced/package-config-merging.md) voor het publiceren van configuratie.

## Registreren nadat Schedule is geresolved

In een gewone Laravel-applicatie registreert `FoundationServiceProvider` `Schedule::class` als singleton. Het aanmaken daarvan roept `resolveConsoleSchedule()` van de Console Kernel aan, die de tijdzone van de applicatie en de cachestore voor de scheduler toepast.

Een package breidt deze gedeelde Schedule uit en maakt niet simpelweg een eigen `new Schedule()` aan. De volgende provider gaat uit van een opzet waarin `Acme\Courier\` via Composer PSR-4 naar `src/` verwijst en het package een `config/courier.php` heeft.

```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()` wacht op een toekomstige resolutie als de service nog niet geresolved is, en haalt de service op en roept de callback direct aan als die al geresolved is. Zo wordt de registratie ook toegepast als een andere provider Schedule al eerder heeft gebruikt. Zie [Hooks na het resolven van packageservices](https://kawax.biz/nl/advanced/package-service-resolution.md) voor details.

`runningInConsole()` controleert niet op een specifiek Artisan-command. In dit voorbeeld sluiten we het gebruik van Schedule via HTTP uit. Maak bovendien de provider die de hook registreert zelf geen deferred provider. Als `boot()` niet op tijd draait, wordt ook de hook niet geregistreerd.

```mermaid theme={null}
flowchart TD
    A["boot() van de packageprovider"] --> B["Hook na het resolven van Schedule registreren"]
    B --> C{"Is Schedule al geresolved?"}
    C -->|Nee| D["Wachten op de eerste resolutie"]
    C -->|Ja| E["Bestaande singleton ophalen"]
    D --> F["Inschakelinstelling en registratiecontrole"]
    E --> F
    F -->|Ingeschakeld en niet geregistreerd| G["Eventdefinitie toevoegen"]
    F -->|Uitgeschakeld of al geregistreerd| H["Niet toevoegen"]
    G --> I["Latere schedule:run<br>evalueert de uitvoeringsvoorwaarden"]
```

> [!NOTE]
>
> Het registreren van een event en het uitvoeren van een taak zijn twee verschillende dingen. Het laden van deze provider voert de heartbeat niet ter plekke uit. Alleen registreren start ook geen cron of `schedule:work`.

## Gebruikers expliciet laten inschakelen

De `config/courier.php` van het package staat standaard uitgeschakeld.

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

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

In de gebruikende applicatie publiceer of maak je de configuratie aan en zet je `schedule.enabled` op `true`. In dit voorbeeld worden de waarden bij het registreren gelezen. De tijdzone staat standaard op UTC; kies je een andere tijdzone, houd dan rekening met dubbele of overgeslagen uitvoeringen door de zomertijd.

Omdat configuratie [ondiep wordt samengevoegd](https://kawax.biz/nl/advanced/package-config-merging.md), geeft ook de registrar voor elke sleutel een standaardwaarde op. Zo kan de taak met de basisinstellingen worden geregistreerd, zelfs als een gedeeltelijke overschrijving de andere sleutels van de `schedule`-array laat verdwijnen.

## Dubbele registratie in dezelfde Schedule voorkomen

De volgende `src/CourierSchedule.php` registreert een minimale heartbeat die één logregel schrijft. Met `WeakMap` zorgen we ervoor dat **deze registrar maar één keer iets toevoegt aan hetzelfde Schedule-object**.

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

Omdat deze registrar een singleton is, wordt de controle of er al geregistreerd is gedeeld, ook als de hook van de provider hem opnieuw ophaalt. Een nieuwe Schedule kan daarentegen wel worden geregistreerd. Gebruik geen statische vlag "al geregistreerd" voor het hele proces, die ook nieuwe containers of de Schedule in tests zou uitsluiten.

`Schedule::call()`, `exec()` en `job()` voegen een event toe aan een array. Het zijn geen API's die een event met dezelfde naam opzoeken en vervangen of dubbele events verwijderen.

> [!WARNING]
>
> `withoutOverlapping()` lost dubbele registratie niet op. Als dezelfde taak twee keer is geregistreerd en de lock wordt vrijgegeven zodra de eerste uitvoering klaar is, kan het latere event ook worden uitgevoerd. Ook `onOneServer()` is geen functie om dubbele registraties te verwijderen.
>
> De WeakMap hierboven voorkomt alleen duplicaten van deze registrar. Als gebruikers dezelfde taak aan `routes/console.php` toevoegen, schakel dan de automatische registratie van het package uit en leg de verantwoordelijkheid voor de registratie op één plek.

## De eventnaam beïnvloedt ook de compatibiliteit van locks

`name()` is een alias van `description()`, maar het effect op de locknaam verschilt per type event. Hieronder staan de standaard mutexnamen.

| Event | Waarde waarop de mutexnaam is gebaseerd | Rol van `name()` |
| - | - | - |
| Event van `command()` en `exec()` | Cron-expressie en genormaliseerde commandstring | Beschrijving voor weergave. Maakt geen deel uit van de standaard mutexnaam |
| CallbackEvent van `call()` en `job()` | De description van het event | Weergavenaam en identificatie van de lock. De cron-expressie maakt er geen deel van uit |

Geef een CallbackEvent een **niet-lege naam vóór** `withoutOverlapping()` of `onOneServer()`. Is er geen naam ingesteld, dan gooien deze methoden een exception.

`job()` gebruikt normaal de naam van de jobklasse of iets dergelijks als initiële naam. Registreer je dezelfde job met verschillende argumenten of doelen, geef dan expliciet per doel een naam op. Alleen het wijzigen van de argumenten maakt van de initiële naam niet automatisch een andere naam.

```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` is een voorbeeld van een jobklasse die het package aanbiedt. Zie [Queues en jobs](https://kawax.biz/nl/queues.md) voor het implementeren van jobs.

Als je de naam, het command of de cron-expressie wijzigt, verandert afhankelijk van het type ook de identificatie van de lock. Bij een deployment waarin processen van de oude en nieuwe versie naast elkaar draaien, kunnen ze voor dezelfde verwerking verschillende locks verkrijgen. Behandel dit niet als een simpele wijziging van een weergavetekst, maar beheer taaknamen als een stabiele publieke specificatie.

### onOneServer en withoutOverlapping los van elkaar bekijken

* `onOneServer()` kiest welke server de uitvoering op dat tijdstip voor zijn rekening neemt. De onderzochte `CacheSchedulingMutex` voegt het uur en de minuut toe aan de mutexnaam van het event en houdt die een uur vast. Dit staat los van de lock tegen overlapping, die wordt vrijgegeven wanneer de taak eindigt.
* `withoutOverlapping()` voorkomt overlapping zolang de vorige uitvoering nog loopt. De opgegeven vervaltijd is in minuten, standaard 1.440 minuten. Is de vervaltijd te kort, dan verloopt de lock voordat de verwerking klaar is.
* Als `job()` iets in de queue plaatst, betekent het voltooien van het schedule-event niet dat de job is verwerkt. Ontwerp de controle op dubbele uitvoering in de worker apart, bijvoorbeeld met unieke jobs of de `WithoutOverlapping`-middleware aan de kant van de job.

Wijs gebruikers die `onOneServer()` inschakelen erop dat een gedeelde cacheconfiguratie met `database`, `memcached`, `dynamodb` of `redis`, zoals de officiële documentatie voorschrijft, een vereiste is. Locks garanderen geen exactly-once voor de bedrijfslogica; de verwerking moet ook bestand zijn tegen opnieuw uitvoeren.

## Zware afhankelijkheden pas bij uitvoering resolven

De `LoggerInterface` van de heartbeat hierboven wordt bij het uitvoeren van het event door `CallbackEvent` geresolved via `call()` van de container. In deze opzet wordt er bij het registreren van de hook niets gelogd en geen client aangemaakt.

| Manier van definiëren | Aandachtspunt bij registratie |
| - | - |
| Een closure doorgeven aan `call()` | De body wordt later aangeroepen. Objecten die je daarbuiten aanmaakt en capturet, zijn echter al aangemaakt |
| `call(new Task)` | `new Task` wordt bij het registreren uitgevoerd |
| `command(CommandClass::class)` | Schedule maakt de klasse aan via de container en haalt de naam en beschrijving op |
| `job(JobClass::class)` | Het aanmaken van de job wacht tot de uitvoering, binnen de callback van het event |

Een ontwerp zonder externe verbindingen of bedrijfslogica in de constructor is ook belangrijk bij [Artisan-commands van een package registreren en afhankelijkheden resolven](https://kawax.biz/nl/advanced/package-commands.md).

Omdat `call()` en `job()` CallbackEvents zijn, kun je `runInBackground()` niet gebruiken. Delegeer tijdrovende verwerking aan de queue, of kies indien nodig voor uitvoering op de achtergrond met `command()`.

## De configuratie van de hele applicatie niet overnemen

`Schedule::useCache()` wijzigt de store van het mutex-object dat Schedule bevat. Dit is geen instelling die alleen de events van het package raakt; ook events die dezelfde mutex delen worden beïnvloed. Roep het niet onvoorwaardelijk aan in de `boot()` van het package, maar behandel het als een instelling die de gebruikende applicatie kiest.

Op dezelfde manier mag een package niet op eigen houtje `Schedule::alwaysOnOneServer()`, het pauzeren van de scheduler of het uitvoeringsbeleid tijdens de onderhoudsmodus wijzigen.

## Na een configuratiewijziging een nieuwe Schedule aanmaken

`enabled`, de cron-expressie en de tijdzone in het voorbeeld zijn instellingen die bij het registreren worden gelezen. Alleen de waarde van `config()` wijzigen voor een al aangemaakte Schedule voegt geen events toe of verwijdert ze, en leest de configuratie ook niet opnieuw in. De WeakMap-controle is evenmin bedoeld om opnieuw te configureren.

Bouw bij deployments die de configuratiecache gebruiken de cache opnieuw op, en controleer `schedule:list` in een proces dat de nieuwe configuratie inleest. Controleer ook voor langlopende schedulers en voor `schedule:run` die taken van minder dan een minuut uitvoert, dat er geen oude code of configuratie achterblijft. Zie [Taakplanning](https://kawax.biz/nl/scheduling.md) voor het onderbreken van sub-minuuttaken tijdens een deployment.

## Registratie los van uitvoering testen

Hieronder staat een voorbeeld van `tests/CourierScheduleTest.php` dat de Composer-autoload van het package en PHPUnit gebruikt. De mutex wordt vervangen door een test double, zodat je het resultaat van de registratie controleert zonder externe cache of echte cron.

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

Deze test is een unittest van de registrar. Controleer package discovery, het samenvoegen van configuratie en de integratie met de Console Kernel apart met [Orchestra Testbench](https://kawax.biz/nl/advanced/package-testing.md) of een testapplicatie.

| Te controleren geval | Voorwaarde voor voltooiing |
| - | - |
| Installatie met de standaardconfiguratie | De geplande taken van het package worden niet geregistreerd |
| Inschakelen en `schedule:list` | Er is precies één event, met de juiste frequentie en tijdzone |
| Provider opstarten vóór en na het resolven van Schedule | In beide gevallen wordt de registratie toegepast |
| Alleen registreren | Er worden geen bedrijfsservices aangemaakt en er start geen verwerking |
| De taakbody afzonderlijk uitvoeren | De benodigde afhankelijkheden worden geresolved en de verwachte verwerking en exceptions zijn te controleren |
| Meerdere servers en langlopende verwerking | De voorwaarde van een gedeelde cache en de twee soorten locks worden afzonderlijk gecontroleerd |
| Update met configuratiecache | In het nieuwe proces blijven geen oude taken of dubbele registraties achter |

Voer niet alle taken van de gebruikende applicatie uit met `schedule:run` alleen om de registratietests te laten slagen. Door tests van de taakbody te scheiden van integratietests van de scheduler in een gecontroleerde omgeving, vermijd je bijwerkingen van niet-gerelateerde geplande taken.

## Gerelateerde pagina's

> [card] [Taakplanning](https://kawax.biz/nl/scheduling.md)
>
> Bekijk uitvoeringsfrequenties, het instellen van cron, delegeren aan de queue en het beheer tijdens deployments.

> [card] [Hooks na het resolven van packageservices](https://kawax.biz/nl/advanced/package-service-resolution.md)
>
> Bekijk het toepassen op nog niet en al wel geresolvede services en de aandachtspunten bij herhaalde registratie.

## Geraadpleegde primaire bronnen

* [Officiële Laravel-documentatie: Taakplanning](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/scheduling.md)
* [Officiële Laravel-documentatie: Packageontwikkeling](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/packages.md)
* [FoundationServiceProvider: Schedule registreren als singleton](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php)
* [Console Kernel: Schedule aanmaken en standaardinstellingen](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: events toevoegen, jobs uitgesteld aanmaken en de mutex delen](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Console/Scheduling/Schedule.php)
* [Event: mutexnaam van command-events](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Console/Scheduling/Event.php)
* [CallbackEvent: verplichte naam en het resolven van afhankelijkheden bij uitvoering](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Console/Scheduling/CallbackEvent.php)
* [CacheEventMutex: lock tegen overlapping en omrekening van de vervaltijd](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Console/Scheduling/CacheEventMutex.php)
* [CacheSchedulingMutex: lock per uitvoeringstijdstip](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Console/Scheduling/CacheSchedulingMutex.php)
* [Officiële tests: registratie van events en tijdzones](https://github.com/laravel/framework/blob/v13.39.0/tests/Console/ConsoleEventSchedulerTest.php)
* [Officiële tests: uitgesteld aanmaken via de naam van de jobklasse](https://github.com/laravel/framework/blob/v13.39.0/tests/Console/Scheduling/ScheduleTest.php)


## Related topics

- [Event listeners van een package registreren en cachen](https://kawax.biz/nl/advanced/package-events.md)
- [Artisan-commands van een package registreren en afhankelijkheden resolven](https://kawax.biz/nl/advanced/package-commands.md)
- [Packagecaches integreren in optimize](https://kawax.biz/nl/advanced/package-optimization.md)
- [Introductie tot Laravel Nightwatch](https://kawax.biz/nl/blog/nightwatch-introduction.md)
- [RPC (Remote Procedure Call)](https://kawax.biz/nl/packages/laravel-copilot-sdk/rpc.md)


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