Skip to main content
When you want to add Artisan commands for diagnostics, synchronization, or installation to your package, think about command registration, instance construction, and execution as separate steps. If you resolve services that are only needed at execution time in the constructor, you may end up affecting the startup of unrelated Artisan commands. This page examines the implementation in Laravel Framework 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’s boot() method. Auto-discovery of the application’s app/Console/Commands and registration of classes provided by a package are separate entry points.
This example assumes that the provider is loaded through package discovery or by explicit registration on the application side. Simply placing a command class in your package does not automatically register the package’s provider. 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 the Signature and Description attributes.
This example does not add a constructor and instead receives the config Repository as an argument to 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.
Do not connect to external APIs, update the database, or overwrite published files in the constructor. Package users need to be able to use list and help right after installation, even when connection settings are not yet complete.
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

The Application::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 implements DeferrableProvider, 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.
In addition, for commands that use heavy services, also verify that the service’s business methods are not called when running listing or help. Checking only that a command can be registered will not detect its impact on unrelated commands.

Primary sources

Last modified on October 12, 2026