DEV Community

Cover image for Building minicli: Implementing Command Controllers
Erika Heidi
Erika Heidi

Posted on • Updated on

Building minicli: Implementing Command Controllers

Introduction

The MVC (Model, View, Controller) pattern is very popular in web applications. Controllers are responsible for handling code execution, based on which endpoint is requested from the web application. CLI applications don't have endpoints, but we can implement a similar workflow by routing command execution through Command Controllers.

In the first tutorial of this series, we've bootstrapped a PHP application for the command line interface (CLI), using a single entry point and registering commands through anonymous functions. In this new tutorial, we will refactor minicli to use Command Controllers.

This is Part 2 of the Building Minicli series.

Before Getting Started

You'll need php-cli and Composer to follow this tutorial.

If you haven't followed the first part of this series, you can download version 0.1.0 of erikaheidi/minicli to bootstrap your setup:

wget https://github.com/erikaheidi/minicli/archive/0.1.0.zip
unzip 0.1.0.zip
cd minicli
Enter fullscreen mode Exit fullscreen mode

Then, run Composer to set up autoload. This won't install any package, because minicli has no dependencies.

composer dump-autoload
Enter fullscreen mode Exit fullscreen mode

Run the application with:

php minicli
Enter fullscreen mode Exit fullscreen mode

or

chmod +x minicli
./minicli
Enter fullscreen mode Exit fullscreen mode

1. Outsourcing Command Registration to a CommandRegistry Class

To get started with our refactoring, we'll create a new class to handle the work of registering and locating commands for the application. This work is currently handled by the App class, but we'll outsource it to a class named CommandRegistry.

Create the new class using your editor of choice. For simplicity, in this tutorial we'll be using nano:

nano lib/CommandRegistry.php
Enter fullscreen mode Exit fullscreen mode

Copy the following content to your CommandRegistry class:

<?php

namespace Minicli;

class CommandRegistry
{
    protected $registry = [];

    public function registerCommand($name, $callable)
    {
        $this->registry[$name] = $callable;
    }

    public function getCommand($command)
    {
        return isset($this->registry[$command]) ? $this->registry[$command] : null;
    }
}
Enter fullscreen mode Exit fullscreen mode

Note: the getCommand method uses a ternary operator as a shorthand if/else. It returns null in case a command is not found.

Save and close the file when you're done.

Now, edit the file App.php and replace the current content with the following code, which incorporates the CommandRegistry class for registering commands:

<?php

namespace Minicli;

class App
{
    protected $printer;

    protected $command_registry;

    public function __construct()
    {
        $this->printer = new CliPrinter();
        $this->command_registry = new CommandRegistry();
    }

    public function getPrinter()
    {
        return $this->printer;
    }

    public function registerCommand($name, $callable)
    {
        $this->command_registry->register($name, $callable);
    }

    public function runCommand(array $argv = [])
    {
        $command_name = "help";

        if (isset($argv[1])) {
            $command_name = $argv[1];
        }

        $command = $this->command_registry->getCommand($command_name);
        if ($command === null) {
            $this->getPrinter()->display("ERROR: Command \"$command_name\" not found.");
            exit;
        }

        call_user_func($command, $argv);
    }
}
Enter fullscreen mode Exit fullscreen mode

If you run the application now with ./minicli, there should be no changes, and you should still be able to run both the hello and help commands.

2. Implementing Command Controllers

Now we'll go further with the refactoring of commands, moving specific command procedures to dedicated CommandController classes.

2.1 Creating a CommandController Model

The first thing we need to do is to set up an abstract model that can be inherited by several commands. This will allow us to have a few default implementations while enforcing a set of features through abstract methods that need to be implemented by the children (concrete) classes.

This model should define at least one mandatory method to be called by the App class on a given concrete CommandController, when that command is invoked by a user on the command line.

Open a new file on your text editor:

nano lib/CommandController.php
Enter fullscreen mode Exit fullscreen mode

Copy the following contents to this file. This is how our initial CommandController abstract class should look like:

<?php

namespace Minicli;

abstract class CommandController
{
    protected $app;

    abstract public function run($argv);

    public function __construct(App $app)
    {
        $this->app = $app;
    }

    protected function getApp()
    {
        return $this->app;
    }
}
Enter fullscreen mode Exit fullscreen mode

Any class that inherits from CommandController will inherit the getApp method, but it will be required to implement a run method and handle the command execution.

2.2 Creating a Concrete Command Controller

Now we'll create our first Command Controller concrete class: HelloController. This class will replace the current definition of the hello command, from an anonymous function to a CommandController object.

Remember how we created two separate namespaces within our Composer file, one for the framework and one for the application? Because this code is very specific to the application being developed, we'll use the App namespace now.

First, create a new folder named Command inside the app namespace directory:

mkdir app/Command
Enter fullscreen mode Exit fullscreen mode

Open a new file in your text editor:


nano app/Command/HelloController.php
Enter fullscreen mode Exit fullscreen mode

Copy the following contents to your controller. This is how the new HelloController class should look like:

<?php

namespace App\Command;

use Minicli\CommandController;

class HelloController extends CommandController
{
    public function run($argv)
    {
        $name = isset ($argv[2]) ? $argv[2] : "World";
        $this->getApp()->getPrinter()->display("Hello $name!!!");
    }
}
Enter fullscreen mode Exit fullscreen mode

There's not much going on here. We reused the same code from before, but now it's placed in a separate class that inherits from CommandController. The App object is now accessible through a method getApp, inherited from the parent abstract class CommandController.

2.3 Updating CommandRegistry to Use Controllers

We have defined a simple architecture for our Command Controllers based on inheritance, but we still need to update the CommandRegistry class to handle these changes.

Having the ability to separate commands into their own classes is great for maintainability, but for simple commands you might still prefer to use anonymous functions.

The following code implements the registration of Command Controllers in a way that keeps compatibility with the previous method of defining commands using anonymous functions. Open the CommandRegistry.php file using your editor of choice:

nano lib/CommandRegistry.php
Enter fullscreen mode Exit fullscreen mode

Update the current contents of the CommandRegistry class with the following code:

<?php

namespace Minicli;

class CommandRegistry
{
    protected $registry = [];

    protected $controllers = [];

    public function registerController($command_name, CommandController $controller)
    {
        $this->controllers = [ $command_name => $controller ];
    }

    public function registerCommand($name, $callable)
    {
        $this->registry[$name] = $callable;
    }

    public function getController($command)
    {
        return isset($this->controllers[$command]) ? $this->controllers[$command] : null;
    }

    public function getCommand($command)
    {
        return isset($this->registry[$command]) ? $this->registry[$command] : null;
    }

    public function getCallable($command_name)
    {
        $controller = $this->getController($command_name);

        if ($controller instanceof CommandController) {
            return [ $controller, 'run' ];
        }

        $command = $this->getCommand($command_name);
        if ($command === null) {
            throw new \Exception("Command \"$command_name\" not found.");
        }

        return $command;
    }
}
Enter fullscreen mode Exit fullscreen mode

Because we now have both Command Controllers and simple callback functions registered within the Application, we've implemented a method named getCallable that will be responsible for figuring out which code should be called when a command is invoked. This method throws an exception in case a command can't be found. The way we've implemented it, Command Controllers will always take precedence over single commands registered through anonymous functions.

Save and close the file when you're done replacing the old code.

2.4 Updating the App class

We still need to update the App class to handle all the recent changes.

Open the file containing the App class:

nano lib/App.php
Enter fullscreen mode Exit fullscreen mode

Replace the current contents of the App.php file with the following code:

<?php

namespace Minicli;

class App
{
    protected $printer;

    protected $command_registry;

    public function __construct()
    {
        $this->printer = new CliPrinter();
        $this->command_registry = new CommandRegistry();
    }

    public function getPrinter()
    {
        return $this->printer;
    }

    public function registerController($name, CommandController $controller)
    {
        $this->command_registry->registerController($name, $controller);
    }

    public function registerCommand($name, $callable)
    {
        $this->command_registry->registerCommand($name, $callable);
    }

    public function runCommand(array $argv = [], $default_command = 'help')
    {
        $command_name = $default_command;

        if (isset($argv[1])) {
            $command_name = $argv[1];
        }

        try {
            call_user_func($this->command_registry->getCallable($command_name), $argv);
        } catch (\Exception $e) {
            $this->getPrinter()->display("ERROR: " . $e->getMessage());
            exit;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

First, we've implemented a method to allow users to register Command Controllers after instantiating an App object: registerController. This method will outsource the command registration to the CommandRegistry object. Then, we've update the runCommand method to use getCallable, catching a possible exception in a try / catch block.

Save and close the file when you're done editing.

2.5 Registering the HelloController Command Controller

The minicli script is still using the old method of defining commands through anonymous functions. We'll now update this file to use our new HelloController Command Controller, but we we'll keep the help command registration the same way it was before, registered as an anonymous function.

Open the minicli script:

nano minicli
Enter fullscreen mode Exit fullscreen mode

This is how the updated minicli script will look like now:

#!/usr/bin/php
<?php

if (php_sapi_name() !== 'cli') {
    exit;
}

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

use Minicli\App;

$app = new App();

$app->registerController('hello', new \App\Command\HelloController($app));

$app->registerCommand('help', function (array $argv) use ($app) {
    $app->getPrinter()->display("usage: minicli hello [ your-name ]");
});

$app->runCommand($argv);

Enter fullscreen mode Exit fullscreen mode

After updating the file with the new code, you should be able to run the application the same way as you run it before, and it should behave exactly the same:

./minicli
Enter fullscreen mode Exit fullscreen mode

The difference is that now you have two ways of creating commands: by registering an anonymous function with registerCommand, or by creating a Controller class that inherits from CommandController. Using a Controller class will keep your code more organized and maintainable, but you can still use the "short way" with anonymous functions for quick hacks and simple scripts.

Conclusion & Next Steps

In this post, we refactored minicli to support commands defined in classes, with an architecture that uses Command Controllers. While this is working well for now, a Controller should be able to handle more than one command; this would make it easier for us to implement command structures like this:

command [ subcommand ] [ action ] [ params ]
command [ subcommand 1 ] [ subcommand n ] [ params ]
Enter fullscreen mode Exit fullscreen mode

In the next part of this series, we'll refactor minicli to support subcommands.

What do you think? How would you implement that?

Cheers and see you soon! \,,/

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
All files used in this tutorial can be found here: erikaheidi/minicli:v0.1.2

Top comments (6)

Collapse
 
ppetermann profile image
ppetermann

Quick suggestion: you are using App like a service locator, it would be better to inject the printer into the controller.

(And generally when injecting, it's a good practice to type hint interfaces rather than implementations).

Also, when you register your command you already instantiat it, that, when having multiple commands will lead to unnecessary code being instantiated - a better practice would be to wrap the instantiation in a closure that's used to get the implementation when actually needed, rather than on registration.

Collapse
 
erikaheidi profile image
Erika Heidi

Oh, thank you so much for sharing these suggestions! I'll work on at least some of these ideas for the next release. :thumbs_up:

Collapse
 
richardhowes profile image
richardhowes

First of all, thank you! I learnt so much!!

I wanted to point out (especially for noobs like me) an error in section [1] where this:

$this->command_registry->register($name, $callable);

Should be:

$this->command_registry->registerCommand($name, $callable);

Hope this helps someone, although it's also a good test of whether you know what the code is actually doing since that is a bug that should not be too difficult to find.

Collapse
 
rescatado182 profile image
Diego Pinzón

Excellent tutorial Erika, we always have a lot ideas, but implement them it's the harder part. I really learn so many useful things, like register functions from Classes (Controller on this) and anonymus. I hope you keeping posted those articles.

One thing, I watch on the beginning, that you call on App class, on registerCommand function, the register method, but, this is not exits, so, I supposed that is registerCommand instead, 'cause is working for me.

Thanks again!!

Collapse
 
lawondyss profile image
Ladislav Vondráček

Two suggestions:
1) Code "isset($foo[$bar]) ? $foo[$bar] : null" can be short to "$foo[$bar] ?? null"
2) Try using Null object pattern for missing controller

Collapse
 
ihorvorotnov profile image
Ihor Vorotnov

Both are good points