Aura 2 Command-Line Helper


Today we’re going to build a command-line helper, using the Aura Cli component. Command-line applications provide low-level entry into task automation, and there are a few things about my workflow I would like to automate!

While much effort has been spent in the pursuit of accuracy; there’s a good chance you could stumble across a curly quote in a code listing, or some other egregious errata. Please make a note and I will fix where needed.
I have also uploaded this code to Github. You need simply follow the configuration instructions in this tutorial, after downloading the source code, and the application should run fine.
This assumes, of course, that you know how to do that sort of thing. If not; this shouldn’t be the first place you learn about making PHP applications.
https://github.com/formativ/tutorial-aura-2-command-line-helper.
If you spot differences between this tutorial and that source code, please raise it here or as a GitHub issue. Your help is greatly appreciated.

Creating composer.json


When working with full-stack frameworks, you’ll often get find composer.json already partially complete. Aura puts components first, meaning most of the time we will be the ones creating the composer.json files.

Go to your project folder and type:

composer init
This assumes you have Composer installed. If not, get it at: https://getcomposer.org/download.

Composer will ask a few questions so that it can populate the composer.json file. You can answer however you want, but I would recommend not setting your require or dev-require in this step. It’s easier to add those by hand…

Installing The CLI Component


Installing the CLI component is as easy as adding the following lines to your composer.json file:

"require" : {
"aura/cli" : "2.*@dev"
},
This was extracted from composer.json.

…and telling Composer to update.

composer update

Creating The Entry-Point


Our command-line helper is going to act like most other command-line utilities in that it will look and function more like an executable than a PHP script. Sure, it’s PHP below the hood, but others don’t need to know that!

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

echo "Dot, reporting for duty!";
This file should be saved as dot.

I’ve chosen to call the “executable” script dot. This is a reference to the dot-files I have running on my system, which often help by performing automated tasks.

The first line tells linux/OSX to run this script as though it contains PHP, which it does. It might not run until we give it permission to run as an executable:

chmod +x dot

This should allow you to call it, and see the simple message it outputs:

./dot

Using The CLI Component


Let’s get a bit more in-depth with the CLI component…

include("vendor/autoload.php");

use Aura\Cli\CliFactory;

$factory = new CliFactory();

$context = $factory->newContext($GLOBALS);
This was extracted from dot.

We begin by including the Composer autoloader script, creating a CliFactory and creating a new Context. The context is the means by which we can get environmental and user data. We provide it the $GLOBALS array so that it can pull out the environment, server and command line argument data.

We also need to get a handle on the output stream:

$stdio = $factory->newStdio();
$stdio->outln("Dot, reporting for duty!");
This was extracted from dot.

We interact with the command-line using the Stdio class. It gives us methods which we can use to output messages and/or errors and to accept user input.

Creating Commands


We want to be able to create multiple commands, which can be issued from a single entry-point. A good way to achieve this is to create our own factory class, which can distinguish between the different commands available and issue the correct one on request.

We’re going to use our own namespace, and make use of PSR-4 for loading our classes. To make this happen, add the following lines to your composer.json file:

"autoload" : {
"psr-4" : {
"Formativ\\Dot\\" : "source/"
}
}
This file should be saved as composer.json.

The exact namespace will obviously differ for you, but the important part of it is that PSR-4 autoloading requires an open-ended namespace prefix (i.e. ending in \\). This lets Composer know that it can load classes who fall within that namespace prefix from the defined path.

Explained another way, this example code tells composer that classes beginning in Formativ\Dot can be loaded from the source/ directory.

I’ve created the following factory class, as a starting point:

<?php

namespace Formativ\Dot;

use Aura\Cli\Context;
use Aura\Cli\Stdio;

class Factory
{
protected $context;

protected $stdio;

public function __construct(Context $context, Stdio $stdio)
{
$this->context = $context;
$this->stdio = $stdio;
}

public function create()
{
// TODO
}
}
This file should be saved as source/Factory.php.

This example demonstrates the principle of Dependency Injection. That means this Factory class doesn’t create the dependencies it needs internally but rather accepts them from outside. This is a good thing as it simplifies the internals of the Factory class and leads to better decoupling.

Decoupling is the process of making classes less dependent on the specifics of other classes.

The code which invokes this class then becomes:

use Aura\Cli\CliFactory;
use Formativ\Dot\Factory;

$cliFactory = new CliFactory();

$helperFactory = new Factory(
$cliFactory->newContext($GLOBALS),
$cliFactory->newStdio()
);
This was extracted from dot.

We’ll use a common interface for our command classes (the classes this factory outputs), which resembles the following:

<?php

namespace Formativ\Dot;

interface CommandInterface
{
public function help();
public function attach();
public function handle();
}
This file should be saved as source/CommandInterface.php.

This interface defines three methods we will require of each command. The help() method will (hopefully!) output some helpful text, which describes what the command does and how it can be used. The attach() method will register the command-line arguments and flags, specific to each command. The handle() method will perform the command’s logic.

Creating An Update Command


Let’s work on the implementation of a new command:

<?php

namespace Formativ\Dot\Command;

use Aura\Cli\Context;
use Aura\Cli\Stdio;
use Formativ\Dot\CommandInterface;

class Update
implements CommandInterface
{
protected $context;

protected $stdio;

public function __construct(Context $context, Stdio $stdio)
{
$this->context = $context;
$this->stdio = $stdio;
}

public function help()
{
// TODO
}

public function attach()
{
// TODO
}

public function handle()
{
// TODO
}
}
This file should be saved as source/Command/Update.php.

You’ll notice that we’ve used the same Dependency Injection in this class. That’s because the factory needs to pass the dependencies along to the individual commands. The factory doesn’t need them directly, but will will create command instances, so we should inject them into the factory and the commands.

We can extend our Factory class to handle update commands:

<?php

namespace Formativ\Dot;

use Aura\Cli\Context;
use Aura\Cli\Stdio;

class Factory
{
protected $context;

protected $stdio;

protected $commands = [];

protected function addCommand($alias, $class)
{
$this->commands[$alias] = $class;
return $this;
}

public function __construct(Context $context, Stdio $stdio)
{
$this->context = $context;
$this->stdio = $stdio;

$this->addCommand(
"update",
"Formativ\Dot\Command\Update"
);
}

public function create()
{
$getopt = $this->context->getopt([]);
$command = $getopt->get(1);

if (isset($this->commands[$command])) {
$class = $this->commands[$command];
return new $class($this->context, $this->stdio);
}

return null;
}
}
This file should be saved as source/Factory.php.

We’ve added a commands array, which maps command names to the classes which will handle them. So far there’s only an update command, but we’ll extend this later on.

The create() method uses the context’s getopt() method to observe a specific set of command-line arguments and flags. We pass a blank array because we’re only interested in the very first argument. Our commands will expand on the getopt() method’s syntax!

The first getopt->get() argument is always the name of the command.

Next, we will update our update command class to provide a help message:

<?php

namespace Formativ\Dot\Command;

use Aura\Cli\Context;
use Aura\Cli\Stdio;
use Formativ\Dot\CommandInterface;

class Update
implements CommandInterface
{
protected $context;

protected $stdio;

protected $getopt;

public function __construct(Context $context, Stdio $stdio)
{
$this->context = $context;
$this->stdio = $stdio;
}

public function help()
{
$this->stdio->outln(
"update - Updates installed package managers."
);

return $this;
}

public function attach()
{
$this->getopt = $this->context->getopt([
"h,help"
]);

return $this;
}

public function handle()
{
if ($this->getopt->get("-h")) {
return $this->help();
}
}
}
This file should be saved as source/Command/Update.php.

We’ve added the help option to the update command, using h,help. This tells the CLI component to look for the flags h and help. Since neither of these require a value, their value is set to true when retrieved through getopt->get().

If either is given to the update command, we immediately return the help text. This short-circuits any further processing.

Let’s complete the update command:

public function handle()
{
if ($this->getopt->get("-h")) {
return $this->help();
}

$this->stdio->outln("Updating Homebrew...");
shell_exec("brew update");
shell_exec("brew upgrade");

$this->stdio->outln("Updating Composer...");
shell_exec("composer self-update");
shell_exec("composer global update");

$this->stdio->outln("Updated.");

return $this;
}
This was extracted from source/Command/Update.php.

After the conditional output of the help message, the update command issues updates for home-brew and composer (two package managers I use on OSX). It also prints three progress messages.

The system commands and executed with the shell_exec() function. This function doesn’t immediately output the results of the command (like similar PHP functions do!) but rather it returns the resulting string. We’re not using the resulting strings, but we could…

We invoke this command, from the entry-point file, using the following code:

$command = $helperFactory->create();

if ($command) {
$command->attach();
$command->handle();
}
This was extracted from dot.

Creating A Few Git Commands


There are a number of common Git commands I use, on a daily basis:

  1. git pull
  2. git add -a
  3. git commit -a -m “message”
  4. git push -u origin master

If I had to give these simpler names, they would probably be:

  1. pull
  2. stage
  3. commit
  4. push

Let’s implement these in order:

<?php

namespace Formativ\Dot\Command;

use Aura\Cli\Context;
use Aura\Cli\Stdio;
use Formativ\Dot\CommandInterface;

class Pull
implements CommandInterface
{
protected $context;

protected $stdio;

protected $getopt;

public function __construct(Context $context, Stdio $stdio)
{
$this->context = $context;
$this->stdio = $stdio;
}

public function help()
{
$this->stdio->outln(
"pull - Pulls the latest files from a remote branch."
);

return $this;
}

public function attach()
{
$this->getopt = $this->context->getopt([
"h,help",
"remote",
"branch"
]);

return $this;
}

public function handle()
{
if ($this->getopt->get("-h")) {
return $this->help();
}

$remote = $this->getopt->get("remote", "origin");
$branch = $this->getopt->get("remote", "master");

$command = "git pull %s %s";

$this->stdio->outln("Pulling files...");

shell_exec(
sprintf($command, $remote, $branch)
);

$this->stdio->outln("Pulled.");

return $this;
}
}
This file should be saved as source/Command/Pull.php.
<?php

namespace Formativ\Dot\Command;

use Aura\Cli\Context;
use Aura\Cli\Stdio;
use Formativ\Dot\CommandInterface;

class Stage
implements CommandInterface
{
protected $context;

protected $stdio;

protected $getopt;

public function __construct(Context $context, Stdio $stdio)
{
$this->context = $context;
$this->stdio = $stdio;
}

public function help()
{
$this->stdio->outln(
"stages - Stages files in the current repository."
);

return $this;
}

public function attach()
{
$this->getopt = $this->context->getopt([
"h,help"
]);

return $this;
}

public function handle()
{
if ($this->getopt->get("-h")) {
return $this->help();
}

$this->stdio->outln("Staging files...");
shell_exec("git add -A");

$this->stdio->outln("Staged.");

return $this;
}
}
This file should be saved as source/Command/Stage.php.
<?php

namespace Formativ\Dot\Command;

use Aura\Cli\Context;
use Aura\Cli\Stdio;
use Formativ\Dot\CommandInterface;

class Commit
implements CommandInterface
{
protected $context;

protected $stdio;

protected $getopt;

public function __construct(Context $context, Stdio $stdio)
{
$this->context = $context;
$this->stdio = $stdio;
}

public function help()
{
$this->stdio->outln(
"commit - Commits files in the current repository."
);

return $this;
}

public function attach()
{
$this->getopt = $this->context->getopt([
"h,help"
]);

return $this;
}

public function handle()
{
if ($this->getopt->get("-h")) {
return $this->help();
}

$message = $this->getopt->get(2);

if ($message == null) {
$this->stdio->outln("Message required.");
return;
}

$message = addslashes($message);

$this->stdio->outln("Committing files...");
shell_exec("git commit -a -m '" . $message . "'");

$this->stdio->outln("Committed.");

return $this;
}
}
This file should be saved as source/Command/Commit.php.
<?php

namespace Formativ\Dot\Command;

use Aura\Cli\Context;
use Aura\Cli\Stdio;
use Formativ\Dot\CommandInterface;

class Push
implements CommandInterface
{
protected $context;

protected $stdio;

protected $getopt;

public function __construct(Context $context, Stdio $stdio)
{
$this->context = $context;
$this->stdio = $stdio;
}

public function help()
{
$this->stdio->outln(
"push - Pushes the latest files to a remote branch."
);

return $this;
}

public function attach()
{
$this->getopt = $this->context->getopt([
"h,help",
"remote",
"branch",
"f,force",
"u,upstream"
]);

return $this;
}

public function handle()
{
if ($this->getopt->get("-h")) {
return $this->help();
}

$remote = $this->getopt->get("remote", "origin");
$branch = $this->getopt->get("remote", "master");

$force = "";

if ($this->getopt->get("force")) {
$force = "-f";
}

$upstream = "";

if ($this->getopt->get("upstream")) {
$upstream = "-u";
}

$this->stdio->outln("Pushing files...");

$command = "git push %s %s %s %s";

shell_exec(
sprintf($command, $remote, $branch, $force, $upstream)
);

$this->stdio->outln("Pushed.");

return $this;
}
}
This file should be saved as source/Command/Push.php.
public function __construct(Context $context, Stdio $stdio)
{
$this->context = $context;
$this->stdio = $stdio;

$this->addCommand(
"update",
"Formativ\Dot\Command\Update"
);

$this->addCommand(
"pull",
"Formativ\Dot\Command\Pull"
);

$this->addCommand(
"stage",
"Formativ\Dot\Command\Stage"
);

$this->addCommand(
"commit",
"Formativ\Dot\Command\Commit"
);

$this->addCommand(
"push",
"Formativ\Dot\Command\Push"
);
}
This was extracted from source/Factory.php.

These commands use system_exec() function calls, in much the same way as the update command. The pull and push commands accept a remote server name (which defaults to origin) and a remote branch name (which defaults to master). The push command also accepts force and upstream flags for setting those things in Git.

The commit command expects a message argument, and will halt execution if one is not provided. The pull and push commands also demonstrate the use of the sprintf() function to perform string interpolation in more complex shell commands.

These git-centric commands don’t do much more than the underlying git commands, and they exhibit some repeated code which can undoubtedly be abstracted. The point is not to dwell on the examples but rather to understand how the CLI component might be used to build command-line applications.

Some examples of how these commands can be used:

./dot pull --remote origin
./dot stage
./dot commit "First commit."
./dot push --branch master -fu
You can learn more about Git commands at: http://git-scm.com/book/en/Git-Basics.

Displaying The List Of Commands


Let’s alter the Factory class to display a list of commands if none are matched:

public function create()
{
$getopt = $this->context->getopt([]);
$command = $getopt->get(1);

if (isset($this->commands[$command])) {
$class = $this->commands[$command];
return new $class($this->context, $this->stdio);
}

$this->stdio->outln("Available commands:");

foreach ($this->commands as $key => $value) {
$instance = new $value($this->context, $this->stdio);
$instance->help();
}

return null;
}
This was extracted from source/Factory.php.

Finally, we should provide a simple mechanism for installing our command-line helper. We can create another command for this:

<?php

namespace Formativ\Dot\Command;

use Aura\Cli\Context;
use Aura\Cli\Stdio;
use Formativ\Dot\CommandInterface;

class Install
implements CommandInterface
{
protected $context;

protected $stdio;

protected $getopt;

public function __construct(Context $context, Stdio $stdio)
{
$this->context = $context;
$this->stdio = $stdio;
}

public function help()
{
$this->stdio->outln(
"install - Installs this command-line helper."
);

return $this;
}

public function attach()
{
$this->getopt = $this->context->getopt([
"h,help"
]);

return $this;
}

public function handle()
{
if ($this->getopt->get("-h")) {
return $this->help();
}

$dir = dirname(dirname(__DIR__));

$source = $dir . "/dot";
$target = "/usr/local/bin/dot";

$command = "ln -s %s %s";

$this->stdio->outln("Installing...");

shell_exec(
sprintf($command, $source, $target)
);

shell_exec("chmod +x " . $target);

$this->stdio->outln("Installed.");

return $this;
}
}
This file should be saved as source/Command/Install.php.
$this->addCommand(
"install",
"Formativ\Dot\Command\Install"
);
This was extracted from source/Factory.php.

The install command creates a symbolic link between the dot script we’ve been writing, and the local executable path (of most linux/OSX systems). This means we can use the dot command-line helper from anywhere in the system.

In addition, it add that executable flag that we needed to be able to execute the script.

You can learn more about arguments and flags at: https://github.com/auraphp/Aura.Cli#getopt-support.

Conclusion


Today we looked at how to create a command-line helper, using the Aura CLI component. Don’t let my simple examples stop you from unleashing your imagination with this component!

If you enjoyed this tutorial; it would be helpful if you could click the Recommend button.