DEV Community

Carl Alexander
Carl Alexander

Posted on • Originally published at carlalexander.ca on

How to autowire a standalone Symfony console application

Working with the Symfony console component has always been a delightful experience. It makes it super easy to build a console application. You might need one while working with a framework. (Like Symfony or Laravel) Or you might just need to build a standalone console application.

That said, there are differences between building a standalone console application and building one with a framework. The biggest one is the use of dependency injection. Most standalone Symfony console applications that you can find out there don't use it.

That's a shame because the Symfony dependency injection component is full of awesome features. For the Ymir console application, I decided to look at how to build a console application using it. It's made building console applications even better.

A quick overview of the Symfony dependency injection component

Let's take a brief look at how to use the Symfony dependency injection component before integrating it. Central to the component is the service container. To build the service container, you need to use the ContainerBuilder class.

Now, you can manually build the container by defining parameters and services yourself. That said, it's much more common to use the Symfony config component to do it. The component lets you use YAML (with the help of the YAML component, XML or plain PHP files to configure the container.

This is what we're trying to do in this article. We want to configure a dependency injection container using a YAML file. (Feel free to use the file type that you prefer!) And then we want to compile the container and start the application using it.

$container->compile();

$container->get(Application::class)->run();
Enter fullscreen mode Exit fullscreen mode

Above is all the code that you'd need to start the console application. No need to add any commands or deal with any dependencies. The configured container would have everything wired and ready to go for you.

Composer dependencies

With this small intro out of the way, Let's start with Composer dependencies that we'll need for this console application. We've mentioned them all already.

{
    "require": {
        "symfony/config": "^4.4",
        "symfony/console": "^4.4",
        "symfony/dependency-injection": "^4.4",
        "symfony/yaml": "^4.4"
    }
}
Enter fullscreen mode Exit fullscreen mode

Above is the relevant section of the composer.json file for these dependencies. We need four Symfony components. These are the config, console, dependency injection and YAML components.

As mentioned earlier, the YAML component is optional. If you want to use PHP or XML to define your services, you don't need it. But for this article, we will use YAML for the services configuration so the dependency is necessary.

Another thing is that we're going to use version 4.4 of the Symfony components. This is current long-term support version of Symfony. So the code that you'll see will be good until the end of 2023. (And possibly longer.)

Creating the executable

Once the dependencies added and downloaded, we can start working on the console application. The primary interaction point with it is an executable file. Below is the base executable file that we're going to use for our console application:

#!/usr/bin/env php
<?php

require __DIR__.'/vendor/autoload.php';
Enter fullscreen mode Exit fullscreen mode

Our PHP script starts with #! which is known as a hashbang. A hashbang tells a Unix-like operating system that it should interpret a text file as an executable file. (You also need to make it executable using chmod +x.) Following the hashbang is the interpreter that we want Unix to use.

Here, we want that interpreter to be PHP. We could use /usr/bin/php, but instead we used /usr/bin/env php. The reason to use env is that the script will not work if PHP isn't in the /usr/bin directory. env will search for the php executable and then launch it.

env is also useful if you have multiple PHP interpreters installed. This is especially common in local development. In that scenario, env will always pick the default PHP interpreter that you've chosen.

After the hashbang, the rest of our executable looks like a regular PHP script. We have the <?php opening tag. We then require the autoload file generated by Composer.

Adding the container

Now that we have our base executable, the next step is to add the dependency injection container. We want to configure and compile the container in the executable before running the console application. Let's look at what that looks like.

#!/usr/bin/env php
<?php

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

require __DIR__.'/vendor/autoload.php';

$container = new ContainerBuilder();

$loader = new YamlFileLoader($container, new FileLocator());
$loader->load(__DIR__.'/config/services.yml');

$container->compile();
Enter fullscreen mode Exit fullscreen mode

We first start by instantiating a ContainerBuilder object. We then use that ContainerBuilder object to create a YamlFileLoader object. This is the configuration loader that we're going to use to configure our dependency injection container.

The YamlFileLoader object will load the services.yml file found in the config directory. The directory and file names are a personal choice. You can name the YAML file whatever you want and put it wherever you want really.

After loading it, we call the compiler method. This compiles the ContainerBuilder object and turns it into a Container object. You cannot use the container until you've done this.

Adding the base dependency injection container configuration file

Now, you could also load multiple configuration files if you wanted. But for this article, we're just going to use services.yml. Here's what it contains so far.

services:
  _defaults:
    autowire: true
Enter fullscreen mode Exit fullscreen mode

The _defaults section is where you define the default configuration options for the services in your container. The most important one is autowire which you want to set to true. This is what turns on the amazing autowiring feature.

When autowire is true, the container will take over the handling of the dependencies for your services. It'll read the type hints of the constructor of each service. It'll then pass it the right dependencies when it creates the object the first time. (It's a really magical thing!)

Creating the Application class

Now that we have our compiled Container object, we can move on to our base Application class. You can see it below extending the Symfony Application class. We renamed it to BaseApplication to prevent a naming conflict.

namespace Console;

use Symfony\Component\Console\Application as BaseApplication;

class Application extends BaseApplication
{
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Empty like this, a console application doesn't do much. If you ran the list command for it, you'd get this:

There are only two available commands: help and list. This is how all Symfony console applications start out with. So what we really need to do now is add commands.

Adding commands to the application

Adding commands to an application is straightforward. All that you have to do is call the add method with the Symfony Command object you want to add. In our Application class, this could look something like this:

namespace Console;

use Symfony\Component\Console\Application as BaseApplication;

class Application extends BaseApplication
{
    /**
     * Constructor.
     */
    public function __construct(array $commands = [])
    {
        foreach ($commands as $command) {
            $this->add($command);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see above, we added a constructor to the Application class. The constructor has a single parameter called commands. We use it to pass an array of Command objects to the constructor. We then loop through each one and add them using the add method.

Right now, we'd have to supply that array of Command objects to the constructor ourselves. But what we really want is for the dependency injection container to handle all that for us. It should find all the Command objects and pass them to the Application constructor.

Creating a service tag for command objects

To do that, we're going to use a service tag to identify all our Command objects. We can do that easily using the _instanceof option. Here's what it looks like in the services.yml file.

services:
  _defaults:
    autowire: true

  _instanceof:
    Symfony\Component\Console\Command\Command:
      tags: ['command']
Enter fullscreen mode Exit fullscreen mode

We added the _instanceof section below the _default one. Under that section, you want to add the fully qualified name of the class or interface that we want to add a tag to. For this, we want to use the fully qualified of the Symfony Command class because it doesn't have an interface. That fully qualified name is Symfony\Component\Console\Command\Command.

Below that, we have the tags that we want all the Symfony Command objects to have. We just have one for this. It's command.

Using the service tag

Next, we need to configure the dependency injection container. Right now, it doesn't know that we want it to pass all the Command objects to the Application class constructor. To tell it to use our service tag, we have to modify the services.yml file like so:

services:
  _defaults:
    autowire: true

  _instanceof:
    Symfony\Component\Console\Command\Command:
      tags: ['command']

  Console\Application:
    public: true
    arguments:
      - !tagged command
Enter fullscreen mode Exit fullscreen mode

Above you can see that we added the fully qualified name of the Application class as a new section. Below, we added the arguments section which lets you define the arguments to pass to a class constructor. We then used !tagged command as the first argument.

At this point, if you try to run your console application, you'll get an error like this:

PHP Fatal error:  Uncaught TypeError: Argument 1 passed to Console\Application::__construct() must be of the type array, object given
Enter fullscreen mode Exit fullscreen mode

That's because the dependency injection container doesn't pass an array to our Application class constructor. It passes an object implementing the Traversable interface. This is a built-in PHP interface that lets an object work with a foreach loop.

namespace Console;

use Symfony\Component\Console\Application as BaseApplication;

class Application extends BaseApplication
{
    /**
     * Constructor.
     */
    public function __construct(iterable $commands = [])
    {
        foreach ($commands as $command) {
            $this->add($command);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

To solve this issue, we need to change the type hint of the Application class constructor. If you want, you can use the Traversable interface. A better option is the iterable pseudo-type which is what the code above uses. But regardless of the one you use, once you update the constructor, the error will be gone.

Adding the application to the executable

At this point, you have everything that you need to run a console application using the dependency injection container. We just need to update the executable so we get the Application object from the container. You can see that below.

#!/usr/bin/env php
<?php

use Console\Application;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

require __DIR__.'/vendor/autoload.php';

$container = new ContainerBuilder();

$loader = new YamlFileLoader($container, new FileLocator());
$loader->load(__DIR__.'/config/services.yml');

$container->compile();

exit($container->get(Application::class)->run());
Enter fullscreen mode Exit fullscreen mode

At the top, we added the use statement to import the Application class. But really, what we care about at the bottom. It's the call to the exit function.

The exit function just terminated the PHP script using the given status. The status just comes from getting the Application object from the container. And then we call the run method which runs the console application and terminates.

To see a fully working version of a console application using this technique, I invite you to look at the code of the Ymir console application on GitHub.

Source

Top comments (0)