Building a console application using Symfony Console component

The other day I came across a challenge posted on github.io for a command line bot that moves in a two dimensional plane, I found the idea to be interesting and thought I’d give it a shot, and post the workflow I followed.

Since I’m going to be using Symfony, I’ll start by creating a composer.json file:

$ mkdir bot
$ cd bot
$ echo {} > composer.json

Next, require the console component:

composer require symfony/console

I’m going to take a TDD(Test Driven Development) approach on this, so we need to pull in phpunit

composer require --dev phpunit/phpunit

We’ll start with theBot class:

<?php

namespace App\Core;

class Bot
{

/*
* @var Position
*/
protected $position;

public function __construct()
{
$this->position = new Position();
}
}

It has a property called $position That is an instance of the class Position This class looks like this:

<?php

namespace App\Core;

class Position
{
protected $direction;

protected $x;

protected $y;

protected $directionNames;

function __construct()
{
$this->direction = 'n';
$this->x = 0;
$this->y = 0;

$this->directionNames = [
'n' => 'north',
'e' => 'east',
's' => 'south',
'w' => 'west'
];
}
}

This class will be responsible for the postioning of the bot, it hold its current $x,$y coordinates and the $direction it is facing. We initiate these values to 0,0 and n repectively. There is also an array $directionNames that is indexed by the four direction the bot can be facing n, e, s, w This is just a helper to return the full name of the direction that we will use later.

Now we need to set up a test class that will be responsible for the phpunit testing. Since the functionality isn’t that much and not that complicated, I’m going to use one test class, and it will be under /tests/ directory. For now this class will have:

<?php
use PHPUnit\Framework\TestCase;

class BotTest extends TestCase
{
    protected $bot;

public function setUp()
{
parent::setUp();
$this->bot = new \App\Core\Bot();
}
}

Now we can start with the actual functionality of the bot, the bot should be able to walk by a specified number of steps. The test for that:

/** @test */
public function it_walks_1_step()
{
$this->bot->walk(1);
$this->assertEquals("X: 0 Y: 1 Direction: north", $this->bot
->getPosition());
}

I have the habit of running the test as soon as I write it (by typing this in the command line: phpunit ./tests) and see where it fails first and fix it, then run it again and fix it again, and so on. Obviously this will fail because we don’t have a walk method in the Bot class. So we’ll add it:

public function walk($steps = 1)
{
$this->position->walk($steps);
return $this;
}

Since the Position Class is responsible for the positioning of the bot, we’ll just delegate to a walk method on the Position class. and this method will actually do the walking:

public function walk($steps = 1)
{
switch ($this->direction) {
case 'n':
$this->y += $steps;
break;
case 'e':
$this->x += $steps;
break;
case 's':
$this->y -= $steps;
break;
case 'w':
$this->x -= $steps;
break;
}
return $this;
}

This method will simply check the current direction the bot is facing, and then “walk” in the appropriate direction, and by “walk” I mean increment or decrement the Xs or Ys coordinates by the number of the provided $steps .

Next failure will be calling $this->bot->getPosition(). We want this function to return a text representation of the position object, this sounds like a good use case for the __toString() function:

public function getPosition()
{
return $this->position->__toString();
}

Of course we need to override the function in the Position class:

function __toString()
{
return "X: {$this->x} Y: {$this->y} Direction: {$this
->getDirection(true)}";
}

Now the getDirection() function will make use of that array of direction names we have in the Position class:

public function getDirection($fullName = false)
{
return $fullName ?
$this->directionNames[$this->direction] :
$this->direction;
}

Now the test we wrote should pass:

vagrant@homestead:~/Code/bot$ phpunit ./tests/ --filter it_walks_1_step
PHPUnit 6.1.1 by Sebastian Bergmann and contributors.
.                                                                   1 / 1 (100%)
Time: 110 ms, Memory: 2.00MB
OK (1 test, 1 assertion)

Now that we established that the bot is able to walk around the plane, we need to work on the turning, it should be able to turn to the right (its right) and to the left (its left):

/** @test */
public function it_turns_to_the_right()
{
$this->bot->turnRight();
$this->assertEquals('e', $this->bot->getDirection());
}

/** @test */
public function it_turns_to_the_left()
{
$this->bot->turnLeft();
$this->assertEquals('w', $this->bot->getDirection());
}

So it looks like we need two new methods in the Bot class: turnRight() and turnLeft() :

public function turnRight()
{
$this->position->turn('right');
return $this;
}

public function turnLeft()
{
$this->position->turn('left');
return $this;
}

They both delegate to the same function turn($to) on the position object. Now this bit is somehow tricky, the turning should result in a change in the $direction of the position of the bot. But this change is not only tied to the direction the bot is turning (left or right), but also the current direction the bot is facing (north, east, south or west). Say the bot is facing north and we tell it to turn right, then it should -after turning- face east, but it was facing east and we tell it to turn right, then it will be facing south after turning.

There are many ways to tackle this, what I went for is to define a function for every scenario the bot might be in when turning, like so:

protected function northToRight()
{
$this->direction = 'e';
}

protected function northToLeft()
{
$this->direction = 'w';
}
protected function eastToRight()
{
$this->direction = 's';
}
protected function eastToLeft()
{
$this->direction = 'n';
}
protected function southToRight()
{
$this->direction = 'w';
}
protected function southToLeft()
{
$this->direction = 'e';
}
protected function westToRight()
{
$this->direction = 'n';
}
protected function westToLeft()
{
$this->direction = 's';
}

So now we just need to figure out the function name to call according to the current position and where to turn, so a function that returns a function name to call:

protected function getTurnFunctionName($to)
{
return $this->getDirection(true) . "To" . ucfirst($to);
}

Say the bot is facing south, $to is left, then this function will return southToLeft so we just call southToLeft. that will result in changing the bot direction to the east. Now vack to the turn($to) function, it should do the following:

public function turn($to)
{
$functionName = $this->getTurnFunctionName($to);
$this->$functionName();
}

Now the second test should pass as well, we can add a couple more tests for the turning:

/** @test */
public function it_does_a_360_turn_clockwise()
{
$this->bot
->turnRight()
->turnRight()
->turnRight()
->turnRight();
$this->assertEquals('n', $this->bot->getDirection());
}

/** @test */
public function it_does_a_360_turn_anti_clockwise()
{
$this->bot
->turnLeft()
->turnLeft()
->turnLeft()
->turnLeft();
$this->assertEquals('n', $this->bot->getDirection());
}

And for the sake of variety :)

/** @test */
public function it_can_face_south()
{
$this->bot
->turnLeft()
->turnLeft();
$this->assertEquals('s', $this->bot->getDirection());
}

/** @test */
public function it_can_face_west()
{
$this->bot
->turnRight()
->turnRight()
->turnRight();
$this->assertEquals('w', $this->bot->getDirection());
}

Now the core functionality is done, we just need an entry point from the console application into the bot class, where we can receive a command string for the bot to move according to. In the requirement of the bot they said:

For example, the walking code of RW15RW1 means
MAQE Bot starts at 0, 0 facing up North.
MAQE Bot turns right (facing East) and walk straight 15 positions.
MAQE Bot turns another right (now facing South) and walk straight 1 position.

Let’s have a move($command) method in the Bot class, this method will receive the move command string, it will parse it, validate it and run it.

So first we need the function that will parse the command string and turn it into a series (AKA: an array) of methods that we can invoke, the function has comment blocks for each step:

protected function getMoveCommands($command)
{
$methods = [];
/*
* Split the command string into characters and loop over them.
*/
foreach (str_split($command) as $char) {
switch ($char) {
case 'R':
case 'L':
/*
* In case it was either R or L, then it is a turn command.
* So we need to call the turn method and a parameter indicating
* the direction of the turn (either 'right' or 'left').
*/
$currentMethod = [];
$currentMethod['method'] = 'turn';
$currentMethod['param'] = $char == 'R' ? 'right' : 'left';
$methods[] = $currentMethod;
break;
case 'W':
/*
* A walk command, so just push it into the methods array.
*/
$methods[]['method'] = 'walk';
break;
default:
/*
* If it's not a turn command or a walk command, then it is
* a number of steps for the walk command, and for it to be
* a valid number of steps, it must be a numeric value, the
* methods array must not be empty, and the last command that
* was pushed into the methods array must be a walk command.
* Anything other than that means that the command string is not
* formatted correctly, so we throw an exception and terminate.
*/
$lastMethodIndex = count($methods) - 1;
if (!is_numeric($char) || count($methods) == 0 || $methods[$lastMethodIndex]['method'] !== 'walk') {
throw new \Exception('The move command is not in a valid format');
}
/*
* We check if param key was set on the last pushed command in the
* methods array for the case of having a two digit number of steps,
* So in case that key was already set, that means we need to concatenate
* the two digits.
*/
$methods[$lastMethodIndex]['param'] = isset($methods[$lastMethodIndex]['param']) ? $methods[$lastMethodIndex]['param'] . $char : $char;
break;
}
}
return $methods;
}

Then, the actual move() method will be:

public function move($commandString)
{
$commands = $this->getMoveCommands($commandString);
foreach ($commands as $command) {
$methodName = $command['method'];
$param = $command['param'];
$this->position->$methodName($param);
}
return $this->getPosition();
}

The entry point for the application from the command line will be index.php. In this file we simply require the autoload file generated by composer, new up the application with a simple description and version number, add the BotCommand class to the $app, and then run it:

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

require __DIR__ . '/vendor/autoload.php';
use Symfony\Component\Console\Application;

$app = new Application("The moving bot", 1.0);

$app->add(new \App\Commands\BotCommand());

$app->run();

The actual BotCommand class looks like this:

<?php

namespace App\Commands;

use App\Core\Bot;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class BotCommand extends Command
{
/**
* Set the configuration for the command
*/
protected function configure()
{
$this->setName('bot:move')
->setDescription('Move the bot')
->setHelp('Use this command to move the bot in a given fashion')
->addArgument('direction', InputArgument::REQUIRED, 'The direction string for the bot to move');
}

/**
* Execute the command
* @param InputInterface $input
* @param OutputInterface $output
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$bot = new Bot();
$bot->move($input->getArgument('direction'));
$output->writeln($bot->getPosition());

}
}

Samples of running the app:

vagrant@homestead:~/Code/bot$ php index.php bot:move RW15RW1
X: 15 Y: -1 Direction: south
vagrant@homestead:~/Code/bot$ php index.php bot:move LLLLLW99RRRRRW88LLLRL
X: -99 Y: 88 Direction: east

If we provide a move command that is not correct:

vagrant@homestead:~/Code/bot$ php index.php bot:move L13W1R
[Exception]
The move command is not in a valid format
bot:move <direction>

The full code can be found here, if you have any suggestion or you see any mistake I might have made, please tell me in a response or submit a PR on GitHub.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.