v13.39.0 and applies the basics of Task scheduling to package development. For how providers are loaded, see Package auto-discovery internals; for publishing configuration, see Package config merging and caching.
Register after Schedule is resolved
In a typical Laravel application,FoundationServiceProvider registers Schedule::class as a singleton. Its factory calls the Console Kernel’s resolveConsoleSchedule(), which applies the application’s timezone and the scheduler’s cache store.
A package should extend this shared Schedule rather than creating its own new Schedule() and stopping there. The following provider assumes that Acme\Courier\ maps to src/ via Composer PSR-4 and that the package has a config/courier.php.
callAfterResolving() waits for future resolution if the service is not yet resolved, and if it is already resolved, it retrieves the instance and invokes the callback immediately. This lets you register even if another provider has already used Schedule. For details, see Package service resolution hooks.
runningInConsole() does not detect a specific Artisan command. In this example it excludes uses of Schedule over HTTP. Also, do not make the provider that registers the hook a deferred provider. If boot() does not run by the time it is needed, the hook will not be registered either.
Registering an event and running a task are separate. Loading this provider does not run the heartbeat on the spot. Registration alone does not start cron or
schedule:work either.Let users enable it explicitly
The package’sconfig/courier.php is disabled by default.
schedule.enabled to true. In this example, the values are read at registration time. The timezone defaults to UTC; if you choose a different timezone, also account for runs being duplicated or skipped due to daylight saving time.
Because configuration is merged shallowly, the registrar also specifies a default for each key. Even if a partial override removes the other keys from the schedule array, registration can proceed with the base settings.
Prevent duplicate registration on the same Schedule
The followingsrc/CourierSchedule.php registers a minimal heartbeat that writes a single log entry. A WeakMap ensures that this registrar adds to the same Schedule object only once.
Schedule::call(), exec(), and job() append events to an array. They are not APIs that find and replace an event with the same name or deduplicate events.
Event names also affect lock compatibility
name() is an alias for description(), but its effect on the lock name differs by event type. The following are the default mutex names.
For a CallbackEvent, assign a non-empty name before
withoutOverlapping() or onOneServer(). If no name is set, these methods throw an exception.
job() normally uses something like the job class name as the initial name. However, when registering the same job with different arguments or targets, give each target an explicit name. Changing only the arguments does not automatically give the initial name a different value.
SyncCatalogJob is an example job class provided by the package. For how to implement jobs, see Queues.
Changing the name, command, or cron expression also changes the lock identifier, depending on the event type. During a deployment where old and new versions of processes coexist, the same work may acquire different locks. Do not treat this like a mere change to display text; manage task names as a stable public specification.
Think about onOneServer and withoutOverlapping separately
onOneServer()selects which server is responsible for the run at that scheduled time. TheCacheSchedulingMutexwe examined appends the hour and minute to the event’s mutex name and holds it with a one-hour expiration. This is separate from the overlap-prevention lock that is released when the task finishes.withoutOverlapping()prevents overlap while a previous run is still in progress. The expiration you specify is in minutes, and the default is 1,440 minutes. If the expiration is too short, the lock expires before the work finishes.- When
job()dispatches to a queue, completion of the scheduled event does not mean the job has finished processing. Design overlap control on the worker separately, for example with unique jobs or theWithoutOverlappingmiddleware on the job side.
onOneServer(), document a shared cache using database, memcached, dynamodb, or redis, as specified by the official documentation, as a requirement. Locks do not guarantee exactly-once execution of business logic, so the work must also be safe to run again.
Resolve heavy dependencies at run time
TheLoggerInterface in the heartbeat above is resolved when the event runs, by CallbackEvent using the container’s call(). This setup does not write logs or create clients when the hook is registered.
A design that keeps external connections and business logic out of constructors is also important in Registering package Artisan commands and resolving dependencies.
Also, because
call() and job() create CallbackEvents, runInBackground() cannot be used with them. Delegate long-running work to a queue, or choose background execution with command() if needed.
Do not take over application-wide settings
Schedule::useCache() changes the store of the mutex objects held by Schedule. It is not a setting scoped to the package’s events only; it also affects events that share the same mutex. Do not call it unconditionally in the package’s boot(); treat it as a setting the consuming application chooses.
Likewise, the package should not unilaterally change Schedule::alwaysOnOneServer(), pausing the scheduler, or the policy for running during maintenance mode.
Create a new Schedule after configuration changes
In the example,enabled, the cron expression, and the timezone are registration-time settings. Simply changing config() values against an already created Schedule does not add or remove events or reload settings. The WeakMap check is not meant for reconfiguration either.
For deployments that use the configuration cache, rebuild the cache and check schedule:list in a process that loads the new configuration. Also verify that long-running schedulers, and schedule:run processes running sub-minute tasks, do not keep old code or old configuration around. For interrupting sub-minute tasks during deployment, see Task scheduling.
Test registration separately from execution
The following is an exampletests/CourierScheduleTest.php that uses the package’s Composer autoloading and PHPUnit. It replaces the mutexes with test doubles and verifies the registration result without an external cache or real cron.
Do not run all of the consuming application’s tasks with
schedule:run just to make the registration tests pass. Separating tests of the task body from scheduler integration tests in a controlled environment avoids side effects from unrelated scheduled work.
Related pages
Task scheduling
Review run frequencies, setting up cron, delegating to queues, and operations during deployment.
Package service resolution hooks
Review applying hooks to unresolved and already resolved services, and caveats when re-registering.
Primary sources consulted
- Laravel official documentation: Task scheduling
- Laravel official documentation: Package development
- FoundationServiceProvider: registering the Schedule singleton
- Console Kernel: creating Schedule and default settings
- ServiceProvider: callAfterResolving()
- Schedule: adding events, lazy job creation, and sharing mutexes
- Event: mutex names for command events
- CallbackEvent: name requirement and run-time dependency resolution
- CacheEventMutex: overlap-prevention locks and expiration conversion
- CacheSchedulingMutex: per-run-time locks
- Official tests: event registration and timezones
- Official tests: lazy creation from job class names