v13.35.0 and goes deeper into the commands() registration example from Package development basics. For implementing file generation, see GeneratorCommand; for participating in optimization tasks, see Integrating with optimize.
Register commands explicitly from your provider
Commands in a package are registered by class name in the service provider’sboot() method. Auto-discovery of the application’s app/Console/Commands and registration of classes provided by a package are separate entry points.
runningInConsole() checks whether the application is running in a console environment, not whether a particular command is being executed. If you also support calling Artisan::call() from within HTTP requests, account for the possibility that this condition prevents registration, and define the usage contract, including whether to register unconditionally. For running commands programmatically, see Artisan.
commands() does not run the command on the spot
ServiceProvider::commands() registers a closure that uses the given class names with Illuminate\Console\Application::starting(). At this point, the command’s handle() is not called.
With the standard Console Kernel, the registered startup callbacks run later when the Artisan Application is created. There, resolveCommands() passes each class to resolve() and proceeds with command registration.
The key point here is that a command is not necessarily instantiated at the same time it is executed. For regular classes that do not use the dedicated deferred registration path described later, resolve() calls the container’s make(), so the constructor and its dependencies are resolved at registration time.
As a result, the constructor of a package command may run even when you only execute php artisan list or some other command. Placing commands() in boot() does not by itself guarantee that instantiation is delayed until execution.
Inject runtime dependencies into handle()
As an example, let’s create a command that only displays whether the package configuration is enabled or disabled. As in the official Laravel 13 Artisan documentation, the name and description are defined with theSignature and Description attributes.
handle(). Because Command::execute() uses the container’s call() on the command’s execution path, type-hinted dependencies are injected.
When you use external API clients or synchronization services, you can inject them in the same place if they are only needed at execution time. However, if constructing the service itself or another provider’s boot() has side effects, those are not automatically prevented.
If you define your own constructor, also call
parent::__construct(). Laravel’s parent constructor configures the command from attributes, the signature, the description, and so on.
Signature and deferred registration are separate mechanisms
TheApplication::resolve() we examined has a branch that reads the name of Symfony’s AsCommand attribute and saves the mapping between the name and the class in a command map. In this branch, the container’s make() is not called on the spot. The Console Kernel later calls setContainerCommandLoader() to set up a loader that uses this map.
On the other hand, Laravel’s Signature attribute used in the example above is separate from this AsCommand check. Adding Signature alone does not put the class on this deferred registration path.
Treat this difference as something to keep in mind when reading the internal implementation. Start by keeping construction cost and side effects small with the official command definition and dependency injection into handle(). Even when a deferred loader is present, command information may be needed for listings, help, and so on, so do not design on the assumption that “the command is never instantiated except when it is selected and run.”
Do not defer command registration itself
Lazily loading service bindings and making a command known to Artisan are two different things. If a provider implementsDeferrableProvider, its boot() may not run at all until the target service is needed.
If you put command registration only in that boot() and the provider is not loaded before Artisan starts, the startup callback that announces the command is not registered either. Even if a service you moved into handle() dependency injection would load the deferred provider, that does not solve the problem that occurs before Artisan finds the command.
To make sure commands are exposed, you can place registration in an eagerly loaded provider and separate only the heavy service bindings into a different deferred provider. For the specific requirements, see Deferred service providers.
What to check when updating
Command names, arguments, options, output, and exit codes are public interfaces used by your users’ shell scripts and CI. Do not change them as casually as internal class changes; check compatibility and migration steps.
For the
StatusCommand above, you can verify the output and exit code as follows in a test environment where the provider is loaded. For the testing setup, see Package testing; for details on assertions, see Console Tests.
Primary sources
- Laravel official documentation: Registering package commands
- Laravel official documentation: Command structure and dependency injection
- ServiceProvider: commands()
- Console Application: starting(), bootstrap(), and resolve()
- Console Kernel: getArtisan() and loader registration
- Command: constructor and execute()
- Official tests: command resolution and alias registration with AsCommand