Look Mama: No Console Coupling! Bye Symfony, Welcome Aura Cli!

First don’t loose sight of the Dependency Injection part on any project! This is the tip of the iceberg. If you have buried it under the gravel of a foreign abstraction or a framework imposed layer, then beware! you are loosing a bit of control that may be dangerous for decoupling. The best thing is when the DI and other components are truly decoupled. Your services are meant to be decoupled that is what makes DI great! If that is not the case then blame your framework in the full sense of the word.

Let me give you a clear example. Let me show you how we can build an easy decoupled console app with only 3 dependencies that are completely decoupled.

Suppose in our composer.json we require a DI, a cli component that has support to extract the context values from the prompt, and a dispatcher so we can decouple this first step of extraction with the actual implementation of services which will be each command. Welcome to AuraPHP 2.0 components!

    "require": {
        "aura/di": "2.*@dev",
        "aura/cli": "2.*@dev",
        "aura/dispatcher": "2.*@dev"
    },

Let’s now start sketching our console.php script:

<?php
 
use Aura\Di\ContainerBuilder;
use Cordoval\Console\Config;
use Cordoval\Console\Runner;
 
require __DIR__.'/vendor/autoload.php';
 
exit((new ContainerBuilder())
    ->newInstance([], [Config::class])
    ->newInstance(Runner::class)
    ->run()
);

Yes, it is very simple and looks beautiful. Let’s explain what is happening under the hood. Remember I said to keep DI layer the top one by any means? Well that is the container!

To build the services inside our container we pass a Config class where we define our services using the DI component:

<?php
 
namespace Cordoval\Console;
 
use Aura\Cli\_Config\Common;
use Aura\Cli\Context;
use Aura\Cli\Stdio;
use Aura\Di\Container;
use Aura\Dispatcher\Dispatcher;
use Cordoval\Console\Task\Help;
 
class Config extends Common
{
    public function define(Container $di)
    {
        parent::define($di);
 
        $di->set('cordoval:stdio', $di->lazyNew(Stdio::class));
        $di->set('cordoval:context', $di->lazyNew(Context::class));
        $di->set(
            'cordoval:dispatcher',
            $di->lazyNew(Dispatcher::class, ['object_param' => 'command'])
        );
 
        $di->params[Runner::class] = [
            'dispatcher' => $di->lazyGet('cordoval:dispatcher'),
            'context' => $di->lazyGet('cordoval:context'),
        ];
 
        $di->params[Help::class] = [
            'context' => $di->lazyGet('cordoval:context'),
            'stdio' => $di->lazyGet('cordoval:stdio'),
        ];
    }
 
    public function modify(Container $di)
    {
        $di->get('cordoval:dispatcher')
            ->setObject('help', $di->lazyNew(Help::class))
        ;
    }
}

Notice the beauty of service definition and lazy loading. The first method is called first before freezing the container. Then it is all about modifying services. The base class from which we extend defines the basic building blocks from the global variables. This is the context information and is important for the cli operation.

<?php
namespace Aura\Cli\_Config;
 
use Aura\Di\Config;
use Aura\Di\Container;
 
class Common extends Config
{
    public function define(Container $di)
    {
        /**
         * Aura\Cli\Context\Argv
         */
        $di->params['Aura\Cli\Context\Argv'] = array(
            'values' => (isset($_SERVER['argv']) ? $_SERVER['argv'] : array()),
        );
 
        /**
         * Aura\Cli\Context\Env
         */
        $di->params['Aura\Cli\Context\Env'] = array(
            'values' => $_ENV,
        );
 
        /**
         * Aura\Cli\Context\Server
         */
        $di->params['Aura\Cli\Context\Server'] = array(
            'values' => $_SERVER,
        );
 
        /**
         * Aura\Cli\Stdio
         */
        $di->params['Aura\Cli\Stdio'] = array(
            'stdin' => $di->lazyNew('Aura\Cli\Stdio\Handle', array(
                'name' => 'php://stdin',
                'mode' => 'r',
            )),
            'stdout' => $di->lazyNew('Aura\Cli\Stdio\Handle', array(
                'name' => 'php://stdout',
                'mode' => 'w+',
            )),
            'stderr' => $di->lazyNew('Aura\Cli\Stdio\Handle', array(
                'name' => 'php://stderr',
                'mode' => 'w+',
            )),
            'formatter' => $di->lazyNew('Aura\Cli\Stdio\Formatter'),
        );
    }
}

Now let’s rewind back to our script at the beginning:

exit((new ContainerBuilder())
    ->newInstance([], [Config::class])
    ->newInstance(Runner::class)
    ->run()
);

We already know that we created a container and plug services on it. The services are all that we have seen above. Things useful to turn prompt context into parameters and the name of the command to call, and things useful also to dispatch to that command, yes the dispatcher. And of course the command itself to which we now turn. So the instance of the DI container instantiates in turn the Runner class. This is our service defined in our namespace Cordoval\Console\Runner. Basically the specification of this for us is a service that will take in the Context service and the Dispatcher and uses these to gather the parameters and command name and dispatches to the callable that is the other command registered. The help command. We execute the run() method on the Runner as you can see:

<?php
 
namespace Cordoval\Console;
 
use Aura\Dispatcher\Dispatcher;
use Aura\Cli\Context;
 
/**
 * It loads cli context from prompt and dispatches to registered callable commands
 */
final class Runner
{
    private $dispatcher;
    private $context;
 
    public function __construct(Dispatcher $dispatcher, Context $context)
    {
        $this->dispatcher = $dispatcher;
        $this->context = $context;
    }
 
    public function run()
    {
        list($params, $command) = $this->loadContext();
 
        return (int) $this->dispatcher->__invoke($params, $command);
    }
 
    public function loadContext()
    {
        $params = $this->context->argv->get();
        array_shift($params);
        $command = array_shift($params);
 
        return [$params, $command];
    }
}

So this becomes very simple to read. Basically we load the context parameters and decipher the command name invoked, that is “`php console.php help“` will do invoke our help command.

        $di->params[Help::class] = [
            'context' => $di->lazyGet('cordoval:context'),
            'stdio' => $di->lazyGet('cordoval:stdio'),
        ];

Notice that we define our class as a parameter in the DI container and lazyload its 2 dependencies to be injected. That is the service period. The DI is amazingly versatile to do the heavy lifting for us. The second part in the modify() method is so to bind the callable Help::class to be invoked by name ‘help’:

        $di->get('cordoval:dispatcher')
            ->setObject('help', $di->lazyNew(Help::class))
        ;

It is wonderful now 🙂 Our command will be invoked whenever there is a dispatch on that name for that callable. Just picture command handlers here and you will start to grin.

Here is our command Help:class:

<?php
 
namespace Cordoval\Console\Task;
 
use Aura\Cli\Stdio;
use Aura\Cli\Context;
use Aura\Cli\Status;
 
class Help
{
    private $context;
    private $stdio;
 
    public function __construct(Context $context, Stdio $stdio)
    {
        $this->context = $context;
        $this->stdio = $stdio;
    }
 
    public function __invoke()
    {
        $this->stdio->outln('all good boss');
 
        return Status::USAGE;
    }
}

Dead simple, let it just be a callable. Is like an interactor, ready to be tested like one completely decoupled. The importance thing to see here is you are not relying on magical links between the console part (aka the Runner::class) and the command. The input/output stuff is encapsulated at the level that it should be. There is no messy confusion, all is crystal clear for you to adapt/modify. In fact, I did just that because AuraPHP already provides an example of Aura.Cli_Kernel and Aura.Cli_Project which tie some dependencies including a logger and other things minor. But in reality is that this is just for educational purposes for you to bootstrap quicker the project and understand better the internals of AuraPHP 2.0.

However, after seeing this there is no need if you understand the essence of this. You can do away with complexity and keep it simple. You can decouple your commands from any framework, the DI from AuraPHP is just a sauce on top of the real domain of your commands. Your code is free from strange interfaces other than the basic final-like classes such as Context, Stdio, and the ones you decide to keep injecting.

As a matter of fact, we are trying to rewrite GushPHP with libraries like this, because is much healthier for maintenance long term and inner quality. Symfony has its place but when it comes to decoupling AuraPHP 2.0 is superior. We still can use symfony components though I see no need really for now. Furthermore, you can now keep using tools like PhpSpec to concentrate on your classes, the collaborators of your commands, rather than concentrating on a TestCommand class or what not, or in functional tests which don’t make any sense when your commands can way much more benefit from a decoupled components first approach.

Enjoy and retweet please! Thanks!

3 thoughts on “Look Mama: No Console Coupling! Bye Symfony, Welcome Aura Cli!

  1. Shouldn’t the Context and Stdio be passed to the __invoke() method instead?
    The constructor is about passing mandatory DI, but the context (args) and Stdio (input/output) may change (there not really services, but more like a Request) so passing them to the constructor will make the Command as service, limited to one execution.

    I’m currently working on a project that uses one action per controller (endpoint) and only uses the Constructor for mandatory services DI and immutable Configuration.

    The execution happens with __invoke(Request $request) and returns the Response.
    So the endpoint can be executed multiple times without remembering the previous state. The great thing about this way of working is the predictability, “I” choose what Request is passed trough and not asking anything like the RequestStack or scope what the “current” request is.

    Even some of the internal helpers methods, like resolving criteria of calling a Factory for getting a new Pager all get the Request explicitly passed. So no global state remembering for something that may change during execution and is not unique.

Leave a Reply to Sebastiaan Stok Cancel reply

Your email address will not be published. Required fields are marked *