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

# Registrazione e manutenzione dei task pianificati dei pacchetti

> Analizzare Schedule e gli hook post-risoluzione di Laravel 13 e spiegare come progettare l'attivazione esplicita dei task pianificati di un pacchetto, la doppia registrazione, i nomi dei lock, le modifiche alla configurazione e i test.

Per aggiungere a un pacchetto una sincronizzazione periodica o un heartbeat, separate **la fornitura dell'elaborazione** dalla **registrazione nello scheduler**. La semplice installazione non deve aggiungere task periodici all'applicazione che usa il pacchetto: progettatelo in modo che sia l'utente a scegliere l'attivazione, la frequenza di esecuzione e il comportamento su più server.

Questa pagina esamina il codice sorgente ufficiale di Laravel Framework `v13.39.0` e applica allo sviluppo di pacchetti le operazioni di base del [Task scheduling](https://kawax.biz/it/scheduling.md). Per il caricamento dei provider consultate [Struttura interna del package auto-discovery](https://kawax.biz/it/advanced/package-discovery.md), per la pubblicazione della configurazione [Merge e cache della configurazione dei pacchetti](https://kawax.biz/it/advanced/package-config-merging.md).

## Registrare dopo la risoluzione di Schedule

In una normale applicazione Laravel, `FoundationServiceProvider` registra `Schedule::class` come singleton. La sua creazione chiama `resolveConsoleSchedule()` del Console Kernel, che applica il fuso orario dell'applicazione e lo store di cache usato dallo scheduler.

Il pacchetto estende questo Schedule condiviso, evitando di creare un proprio `new Schedule()` e fermarsi lì. Il provider seguente presuppone che `Acme\Courier\` corrisponda a `src/` tramite il PSR-4 di Composer e che il pacchetto contenga `config/courier.php`.

```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()`, se il servizio non è ancora risolto, attende la risoluzione futura; se è già risolto, lo recupera e invoca subito anche la callback. In questo modo la registrazione avviene anche se un altro provider ha usato Schedule prima. Per i dettagli consultate [Hook di risoluzione dei servizi nei pacchetti](https://kawax.biz/it/advanced/package-service-resolution.md).

`runningInConsole()` non verifica uno specifico comando Artisan. In questo esempio si esclude l'uso di Schedule tramite HTTP. Inoltre, il provider che registra l'hook non deve essere un provider differito: se `boot()` non viene eseguito entro il momento necessario, nemmeno l'hook viene registrato.

```mermaid theme={null}
flowchart TD
    A["boot() del provider del pacchetto"] --> B["Registra l'hook post-risoluzione di Schedule"]
    B --> C{"Schedule è già risolto?"}
    C -->|No| D["Attende la prima risoluzione"]
    C -->|Yes| E["Recupera il singleton esistente"]
    D --> F["Verifica attivazione e registrazione già avvenuta"]
    E --> F
    F -->|Attivo e non registrato| G["Aggiunge la definizione dell'evento"]
    F -->|Disattivo o già registrato| H["Non aggiunge nulla"]
    G --> I["Il successivo schedule:run<br>valuta le condizioni di esecuzione"]
```

> [!NOTE]
>
> Registrare un evento ed eseguire un task sono due cose distinte. Caricare questo provider non esegue immediatamente l'heartbeat. La sola registrazione non avvia nemmeno cron o `schedule:work`.

## L'utente attiva esplicitamente il task

Il file `config/courier.php` del pacchetto imposta lo stato iniziale come disattivato.

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

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

Nell'applicazione che usa il pacchetto si pubblica o si crea la configurazione e si imposta `schedule.enabled` a `true`. In questo esempio i valori vengono letti al momento della registrazione. Il fuso orario predefinito è UTC; se ne scegliete un altro, tenete conto delle esecuzioni duplicate o mancanti dovute all'ora legale.

Poiché la configurazione viene [unita in modo superficiale](https://kawax.biz/it/advanced/package-config-merging.md), anche il registratore specifica il valore predefinito di ogni chiave. Così, anche se una sovrascrittura parziale elimina le altre chiavi dell'array `schedule`, la registrazione avviene con le impostazioni di base.

## Evitare la doppia registrazione nello stesso Schedule

Il seguente `src/CourierSchedule.php` registra un heartbeat minimo che scrive una riga di log. Grazie a `WeakMap`, **questo registratore aggiunge l'evento allo stesso oggetto Schedule una sola volta**.

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

Poiché questo registratore è un singleton, anche se viene recuperato di nuovo dall'hook del provider condivide lo stato di registrazione. D'altra parte, può registrare l'evento in un nuovo Schedule. Evitate un flag statico "registrato" valido per l'intero processo, che escluderebbe anche gli Schedule di nuovi container o dei test.

`Schedule::call()`, `exec()` e `job()` aggiungono l'evento a un array. Non sono API che trovano e sostituiscono un evento con lo stesso nome o che eliminano i duplicati.

> [!WARNING]
>
> `withoutOverlapping()` non corregge le registrazioni duplicate. Se lo stesso task viene registrato due volte, una volta terminata la prima esecuzione e rilasciato il lock, anche il secondo evento può essere eseguito. Neppure `onOneServer()` è una funzione che rimuove le doppie registrazioni.
>
> La WeakMap sopra impedisce solo i duplicati di questo registratore. Se l'utente aggiunge lo stesso task in `routes/console.php`, disattivate la registrazione automatica del pacchetto e mantenete la responsabilità della registrazione in un solo punto.

## Il nome dell'evento influisce anche sulla compatibilità dei lock

`name()` è un alias di `description()`, ma l'effetto sul nome del lock dipende dal tipo di evento. Di seguito i nomi di mutex predefiniti.

| Evento | Valore da cui deriva il nome del mutex | Ruolo di `name()` |
| - | - | - |
| Event di `command()` ed `exec()` | Espressione cron e stringa del comando normalizzata | Descrizione visualizzata. Non è incluso nel nome del mutex predefinito |
| CallbackEvent di `call()` e `job()` | Description dell'evento | Nome visualizzato e identificazione del lock. L'espressione cron non è inclusa |

Per i CallbackEvent, assegnate **un nome non vuoto prima** di `withoutOverlapping()` o `onOneServer()`. Se il nome non è impostato, questi metodi lanciano un'eccezione.

`job()` di norma usa come nome iniziale il nome della classe del job o simili. Tuttavia, se registrate lo stesso job con argomenti o destinatari diversi, specificate esplicitamente un nome per ciascun destinatario. Cambiare solo gli argomenti non rende automaticamente diverso il nome iniziale.

```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` è un esempio di classe job fornita dal pacchetto. Per l'implementazione dei job consultate [Code e job](https://kawax.biz/it/queues.md).

Modificando il nome, il comando o l'espressione cron, a seconda del tipo cambia anche l'identificatore del lock. In un deploy in cui convivono processi della vecchia e della nuova versione, la stessa elaborazione potrebbe acquisire lock diversi. Non trattatelo come una semplice modifica del testo visualizzato: gestite il nome del task come una specifica pubblica stabile.

### Distinguere onOneServer e withoutOverlapping

* `onOneServer()` sceglie il server incaricato dell'esecuzione per quell'orario. Il `CacheSchedulingMutex` esaminato aggiunge ore e minuti al nome del mutex dell'evento e lo mantiene con una scadenza di un'ora. È diverso dal lock anti-sovrapposizione che viene rilasciato al termine del task.
* `withoutOverlapping()` impedisce la sovrapposizione mentre l'esecuzione precedente è ancora in corso. La scadenza indicata è espressa in minuti e il valore predefinito è 1.440 minuti. Con una scadenza troppo breve, il lock scade prima che l'elaborazione finisca.
* Quando `job()` accoda un job, il completamento dell'evento pianificato non coincide con il completamento dell'elaborazione del job. Il controllo dei duplicati nei worker va progettato separatamente, ad esempio con i job univoci o il middleware `WithoutOverlapping` lato job.

Agli utenti che attivano `onOneServer()` indicate come requisito una cache condivisa tra `database`, `memcached`, `dynamodb` e `redis`, come specificato dalla documentazione ufficiale. I lock non garantiscono l'esecuzione exactly-once della logica di business: serve anche un'elaborazione che non si rompa se rieseguita.

## Risolvere le dipendenze pesanti al momento dell'esecuzione

Il `LoggerInterface` dell'heartbeat visto sopra viene risolto da `CallbackEvent` tramite `call()` del container al momento dell'esecuzione dell'evento. È una configurazione che non scrive log né crea client durante la registrazione dell'hook.

| Modalità di definizione | Attenzione al momento della registrazione |
| - | - |
| Passare una closure a `call()` | Il corpo viene chiamato in seguito. Tuttavia, gli oggetti creati all'esterno e catturati sono già creati |
| `call(new Task)` | `new Task` viene eseguito al momento della registrazione |
| `command(CommandClass::class)` | Schedule crea la classe tramite il container e ne ricava nome e descrizione |
| `job(JobClass::class)` | La creazione del job avviene nella callback dell'evento, rimandata fino all'esecuzione |

Evitare connessioni esterne e logica di business nel costruttore è importante anche per la [Registrazione dei comandi Artisan di un pacchetto e risoluzione delle dipendenze](https://kawax.biz/it/advanced/package-commands.md).

Inoltre, poiché `call()` e `job()` sono CallbackEvent, non è possibile usare `runInBackground()`. Delegate alle code le elaborazioni lunghe oppure, se necessario, scegliete l'esecuzione in background di `command()`.

## Non sottrarre le impostazioni globali dell'applicazione

`Schedule::useCache()` cambia lo store dell'oggetto mutex posseduto da Schedule. Non è un'impostazione limitata agli eventi del pacchetto: influisce anche sugli eventi che condividono lo stesso mutex. Non chiamatelo incondizionatamente nel `boot()` del pacchetto, ma trattatelo come un'impostazione scelta dall'applicazione che lo usa.

Allo stesso modo, il pacchetto non deve modificare di propria iniziativa `Schedule::alwaysOnOneServer()`, la sospensione dello scheduler o la politica di esecuzione durante la modalità di manutenzione.

## Dopo una modifica della configurazione, creare un nuovo Schedule

Nell'esempio, `enabled`, l'espressione cron e il fuso orario sono impostazioni lette al momento della registrazione. Cambiare solo i valori di `config()` per uno Schedule già creato non aggiunge né rimuove eventi e non rilegge la configurazione. Anche il controllo della WeakMap non serve a riconfigurare.

Nei deploy che usano la cache della configurazione, ricostruite la cache e verificate `schedule:list` in un processo che carica la nuova configurazione. Verificate anche che scheduler a lunga esecuzione o `schedule:run` che eseguono task sotto il minuto non mantengano il vecchio codice o la vecchia configurazione. Per l'interruzione dei task sotto il minuto durante il deploy consultate [Task scheduling](https://kawax.biz/it/scheduling.md).

## Testare la registrazione separatamente dall'esecuzione

Quello che segue è un esempio di `tests/CourierScheduleTest.php` che usa l'autoload Composer del pacchetto e PHPUnit. Il mutex è sostituito da un test double, così da verificare il risultato della registrazione senza cache esterne né cron reale.

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

Questo è un test unitario del registratore. L'auto-discovery del provider, il merge della configurazione e l'integrazione con il Console Kernel vanno verificati separatamente con [Orchestra Testbench](https://kawax.biz/it/advanced/package-testing.md) o con un'applicazione di verifica.

| Caso da verificare | Criterio di completamento |
| - | - |
| Installazione con la configurazione iniziale | I task pianificati del pacchetto non vengono registrati |
| `schedule:list` dopo l'attivazione | Esiste un solo evento, con frequenza e fuso orario corretti |
| Avvio del provider prima e dopo la risoluzione di Schedule | In entrambi i casi la registrazione viene applicata |
| Solo registrazione | Non iniziano né la creazione né l'elaborazione dei servizi di business |
| Esecuzione isolata del corpo del task | Le dipendenze necessarie vengono risolte e si possono verificare l'elaborazione e le eccezioni attese |
| Più server ed elaborazioni lunghe | Verificare separatamente il prerequisito della cache condivisa e i due tipi di lock |
| Aggiornamento con cache della configurazione | Nel nuovo processo non restano vecchi task né doppie registrazioni |

Per far passare i test di registrazione, non eseguite tutti i task dell'applicazione con `schedule:run`. Separando i test del corpo dei task dai test di integrazione dello scheduler in un ambiente controllato, eviterete gli effetti collaterali di task periodici non correlati.

## Pagine correlate

> [card] [Task scheduling](https://kawax.biz/it/scheduling.md)
>
> Frequenza di esecuzione, configurazione di cron, delega alle code e gestione durante i deploy.

> [card] [Hook di risoluzione dei servizi nei pacchetti](https://kawax.biz/it/advanced/package-service-resolution.md)
>
> Applicazione a servizi non ancora risolti o già risolti e precauzioni in caso di nuova registrazione.

## Fonti primarie consultate

* [Documentazione ufficiale di Laravel: Task scheduling](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/scheduling.md)
* [Documentazione ufficiale di Laravel: sviluppo di pacchetti](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/packages.md)
* [FoundationServiceProvider: registrazione del singleton di Schedule](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php)
* [Console Kernel: creazione di Schedule e impostazioni predefinite](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: aggiunta di eventi, creazione differita dei job e condivisione dei mutex](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Console/Scheduling/Schedule.php)
* [Event: nome del mutex degli eventi di comando](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Console/Scheduling/Event.php)
* [CallbackEvent: obbligatorietà del nome e risoluzione delle dipendenze in esecuzione](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Console/Scheduling/CallbackEvent.php)
* [CacheEventMutex: lock anti-sovrapposizione e conversione della scadenza](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Console/Scheduling/CacheEventMutex.php)
* [CacheSchedulingMutex: lock per orario di esecuzione](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Console/Scheduling/CacheSchedulingMutex.php)
* [Test ufficiali: registrazione degli eventi e fuso orario](https://github.com/laravel/framework/blob/v13.39.0/tests/Console/ConsoleEventSchedulerTest.php)
* [Test ufficiali: creazione differita dal nome della classe del job](https://github.com/laravel/framework/blob/v13.39.0/tests/Console/Scheduling/ScheduleTest.php)


## Related topics

- [Cache dei pacchetti e integrazione con optimize](https://kawax.biz/it/advanced/package-optimization.md)
- [Registrazione e test dei transport di posta nei pacchetti](https://kawax.biz/it/advanced/package-mail-transports.md)
- [Registrazione dei comandi Artisan di un pacchetto e risoluzione delle dipendenze](https://kawax.biz/it/advanced/package-commands.md)
- [Registrazione e cache dei listener di eventi di un pacchetto](https://kawax.biz/it/advanced/package-events.md)
- [Task scheduling](https://kawax.biz/it/scheduling.md)


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