Laravel Artisan Command Dependency Injection

21st January 2024

tl;dr Don’t inject dependencies for Artisan commands using the __construct() method, use handle() instead.

Under Construction

The Laravel service container is a magical thing. It allows us to resolve dependencies out of thin air, without worrying about how those dependencies are constructed. Combined with constructor property promotion, added to PHP in version 8, all we need to do is;

1public function __construct(
2 private readonly FooRepository $fooRepository,
3 private readonly BarService $barService,
4 private readonly BazInterface $bazInterface,
5) {
6}

We need to make sure there aren’t any circular dependencies, but otherwise it’s something we can "set and forget". However; there is one place it can have unintended consequences: Artisan commands.

I Command You

If you use php artisan make:command to scaffold a new command, Laravel adds a placeholder __construct() method, ready and waiting for you to inject any classes the command relies on.

7use Illuminate\Console\Command;
8 
9class TestCommand extends Command
10{
11 ...
12 * The name and signature of the console command.
13 *
14 * @var string
15 */
16 protected $signature = 'app:test-command';
17 
18 /**
19 * The console command description.
20 *
21 * @var string
22 */
23 protected $description = 'Command description';
24 
25 /**
26 * Create a new command instance.
27 *
28 * @return void
29 */
30 public function __construct()
31 {
32 parent::__construct();
33 }
34 
35 ...
36 * Execute the console command.
37 *
38 * @return int
39 */
40 public function handle()
41 {
42 return 0;
43 }
44}

In most cases it’s fairly clear where those constructors are called, but with Artisan commands it may be happening more often than you realise. To demonstrate, let’s add our good friend dd() to the __construct() method of our command.

7use Illuminate\Console\Command;
8 
9class TestCommand extends Command
10{
11 ...
12 * The name and signature of the console command.
13 *
14 * @var string
15 */
16 protected $signature = 'app:test-command';
17 
18 /**
19 * The console command description.
20 *
21 * @var string
22 */
23 protected $description = 'Command description';
24 
25 /**
26 * Create a new command instance.
27 *
28 * @return void
29 */
30 public function __construct()
31 {
32 parent::__construct();
33 
34 dd('Boom!');
35 }
36 
37 ...
38 * Execute the console command.
39 *
40 * @return int
41 */
42 public function handle()
43 {
44 return 0;
45 }
46}

If we call our new command, using php artisan app:test-command then the result isn’t surprising;

1php artisan app:test-command
2"Boom!" // app/Console/Commands/TestCommand.php:34

As we move away from that command though, it becomes a little more unexpected;

1php artisan list
2"Boom!" // app/Console/Commands/TestCommand.php:34

This command lists out all the Artisan commands that are available. The list includes the signature and description of our test command, so maybe it isn’t that unexpected. How about this one;

1php artisan migrate
2"Boom!" // app/Console/Commands/TestCommand.php:34

Migrating our database doesn't seem like it should have anything to do with our new command.

Whenever you run a command that begins with php artisan it loads the console kernel. This constructs an instance of every single command in our application. And what happens when we construct an instance of every command? Thanks to the service container, it also resolves an instance of each and every one of their dependencies. Most of those dependencies may be quick to resolve, but as a project grows, we can end up with hundreds or even thousands of commands.

I know what you may be thinking; "I don’t run those commands very often". Don’t forget, this will also affect;

  • php artisan schedule:run - called every minute if you have it defined in crontab.
  • php artisan queue:work - whenever you start a queue worker and whenever they are restarted by --max-jobs or --max-time.
  • php artisan serve - whenever you spin up a local development server.

The Artisan console is a quick way to run application code and, just like other areas of our application, we’d like to keep it that way! Thankfully, there is an easy solution; enter handle().

How To Handle() Dependencies

The main functionality of an Artisan command is contained within the handle() method. This is the method that actually gets called when we run a command. The handle() method also supports dependency injection, but doesn't get called when we run anything other than that command.

One downside of using the handle() method for dependency injection is that we can no longer use constructor property promotion, or mark the dependencies as read-only. So we have to go back to the old-fashioned method of declaring properties;

7use Illuminate\Console\Command;
8 
9class TestCommand extends Command
10{
11 ...
12 * The name and signature of the console command.
13 *
14 * @var string
15 */
16 protected $signature = 'app:test-command';
17 
18 /**
19 * The console command description.
20 *
21 * @var string
22 */
23 protected $description = 'Command description';
24 
25 private FooRepository $fooRepository;
26 
27 private BarService $barService;
28 
29 private BazInterface $bazInterface;
30 
31 /**
32 * Execute the console command.
33 *
34 * @return int
35 */
36 public function handle(
37 FooRepository $fooRepository,
38 BarService $barService,
39 BazInterface $bazInterface,
40 ) {
41 $this->fooRepository = $fooRepository;
42 $this->barService = $barService;
43 $this->bazInterface = $bazInterface;
44 
45 return 0;
46 }
47}

Some of you will probably cringe at returning to these cave-man techniques. For us, keeping Artisan commands light and fast is more important. Let me know your thoughts on the trade-offs between these two approaches. Thanks for reading!

Code highlighting powered by Torchlight