Skip to content

Commit c450277

Browse files
authored
Merge pull request #123 from php-school/optional-arg-support
Initial support for optional arguments for commands
2 parents 6fa4db4 + f470479 commit c450277

File tree

9 files changed

+423
-169
lines changed

9 files changed

+423
-169
lines changed

composer.lock

Lines changed: 175 additions & 151 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/CommandArgument.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace PhpSchool\PhpWorkshop;
4+
5+
/**
6+
* @author Aydin Hassan <aydin@hotmail.co.uk>
7+
*/
8+
class CommandArgument
9+
{
10+
/**
11+
* @var string
12+
*/
13+
private $name;
14+
15+
/**
16+
* @var bool
17+
*/
18+
private $optional;
19+
20+
/**
21+
* @param string $name The name of the argument
22+
* @param bool $optional Whether it is required or not
23+
*/
24+
public function __construct($name, $optional = false)
25+
{
26+
$this->name = $name;
27+
$this->optional = $optional;
28+
}
29+
30+
/**
31+
* @param string $name
32+
* @return static
33+
*/
34+
public static function optional($name)
35+
{
36+
return new static($name, true);
37+
}
38+
39+
/**
40+
* @param string $name
41+
* @return static
42+
*/
43+
public static function required($name)
44+
{
45+
return new static($name);
46+
}
47+
48+
/**
49+
* @return string
50+
*/
51+
public function getName()
52+
{
53+
return $this->name;
54+
}
55+
56+
/**
57+
* @return bool
58+
*/
59+
public function isRequired()
60+
{
61+
return !$this->isOptional();
62+
}
63+
64+
/**
65+
* @return bool
66+
*/
67+
public function isOptional()
68+
{
69+
return $this->optional;
70+
}
71+
}

src/CommandDefinition.php

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace PhpSchool\PhpWorkshop;
44

5+
use PhpSchool\PhpWorkshop\Exception\InvalidArgumentException;
6+
57
/**
68
* Represents a command in the workshop framework. Simply consists of a
79
* command name, required arguments and either a service name or callable to
@@ -18,9 +20,9 @@ class CommandDefinition
1820
private $name;
1921

2022
/**
21-
* @var string[]
23+
* @var CommandArgument[]
2224
*/
23-
private $args;
25+
private $args = [];
2426

2527
/**
2628
* @var string|callable
@@ -29,14 +31,51 @@ class CommandDefinition
2931

3032
/**
3133
* @param string $name The name of the command (this is how the student would invoke the command from the cli)
32-
* @param string[] $args A list of required arguments. This must be an array of strings.
34+
* @param string[]|CommandArgument[] $args A list of arguments. Must be an array of strings or `CommandArgument`'s.
3335
* @param string|callable $commandCallable The name of a callable container entry or an actual PHP callable.
3436
*/
3537
public function __construct($name, array $args, $commandCallable)
3638
{
3739
$this->name = $name;
38-
$this->args = $args;
3940
$this->commandCallable = $commandCallable;
41+
42+
array_walk($args, function ($arg) {
43+
$this->addArgument($arg);
44+
});
45+
}
46+
47+
/**
48+
* @param string|CommandArgument $argument
49+
* @return $this
50+
*/
51+
public function addArgument($argument)
52+
{
53+
if (!is_string($argument) && !$argument instanceof CommandArgument) {
54+
throw InvalidArgumentException::notValidParameter(
55+
'argument',
56+
['string', CommandArgument::class],
57+
$argument
58+
);
59+
}
60+
61+
if (is_string($argument)) {
62+
$argument = new CommandArgument($argument);
63+
}
64+
65+
if (count($this->args) === 0) {
66+
$this->args[] = $argument;
67+
return $this;
68+
}
69+
70+
$previousArgument = end($this->args);
71+
if ($previousArgument->isOptional() && $argument->isRequired()) {
72+
throw new InvalidArgumentException(sprintf(
73+
'A required argument cannot follow an optional argument'
74+
));
75+
}
76+
77+
$this->args[] = $argument;
78+
return $this;
4079
}
4180

4281
/**
@@ -52,7 +91,7 @@ public function getName()
5291
/**
5392
* Get the list of required arguments.
5493
*
55-
* @return array
94+
* @return CommandArgument[]
5695
*/
5796
public function getRequiredArgs()
5897
{

src/CommandRouter.php

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ private function addCommand(CommandDefinition $c)
8484
*
8585
* @param array $args
8686
* @return int
87-
* @throws CliRouteNotExists
87+
* @throws CliRouteNotExistsException
8888
*/
8989
public function route(array $args = null)
9090
{
@@ -109,15 +109,30 @@ public function route(array $args = null)
109109
$commandName = $command;
110110
}
111111
$command = $this->commands[$commandName];
112-
if (count($args) !== count($command->getRequiredArgs())) {
113-
$receivedArgs = count($args);
114-
$missingArgs = array_slice($command->getRequiredArgs(), $receivedArgs);
115-
throw new MissingArgumentException($commandName, $missingArgs);
116-
}
112+
113+
$this->checkRequiredArgs($commandName, $command->getRequiredArgs(), $args);
117114

118115
return $this->resolveCallable($command, array_merge([$appName], $args));
119116
}
120117

118+
/**
119+
* @param string $commandName
120+
* @param array $definitionArgs
121+
* @param array $givenArgs
122+
*/
123+
private function checkRequiredArgs($commandName, array $definitionArgs, array $givenArgs)
124+
{
125+
while (null !== ($definitionArg = array_shift($definitionArgs))) {
126+
$arg = array_shift($givenArgs);
127+
128+
if (null == $arg && !$definitionArg->isOptional()) {
129+
throw new MissingArgumentException($commandName, array_map(function (CommandArgument $argument) {
130+
return $argument->getName();
131+
}, array_merge([$definitionArg], $definitionArgs)));
132+
}
133+
}
134+
}
135+
121136
/**
122137
* Get the closest command to the one typed, but only if there is 3 or less
123138
* characters different
@@ -187,6 +202,6 @@ private function resolveCallable(CommandDefinition $command, array $args)
187202
*/
188203
private function callCommand(callable $command, array $arguments)
189204
{
190-
return call_user_func_array($command, $arguments);
205+
return $command(...$arguments);
191206
}
192207
}

test/Asset/PatchableExercise.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ public function getProblem()
5454
*/
5555
public function tearDown()
5656
{
57-
5857
}
5958

6059
/**

test/Check/ComposerCheckTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ public function setUp()
4040

4141
$this->assertTrue($this->check->canRun(ExerciseType::CGI()));
4242
$this->assertTrue($this->check->canRun(ExerciseType::CLI()));
43-
4443
}
4544

4645
public function testExceptionIsThrownIfNotValidExercise()

test/CommandArgumentTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace PhpSchool\PhpWorkshopTest;
4+
5+
use PhpSchool\PhpWorkshop\CommandArgument;
6+
use PHPUnit_Framework_TestCase;
7+
8+
/**
9+
* @author Aydin Hassan <aydin@hotmail.co.uk>
10+
*/
11+
class CommandArgumentTest extends PHPUnit_Framework_TestCase
12+
{
13+
public function testRequiredArgument()
14+
{
15+
$arg = new CommandArgument('arg1');
16+
$this->assertSame('arg1', $arg->getName());
17+
$this->assertFalse($arg->isOptional());
18+
}
19+
20+
public function testOptionalArgument()
21+
{
22+
$arg = new CommandArgument('arg1', true);
23+
$this->assertSame('arg1', $arg->getName());
24+
$this->assertTrue($arg->isOptional());
25+
}
26+
}

test/CommandDefinitionTest.php

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,68 @@
22

33
namespace PhpSchool\PhpWorkshopTest;
44

5+
use PhpSchool\PhpWorkshop\CommandArgument;
6+
use PhpSchool\PhpWorkshop\Exception\InvalidArgumentException;
57
use PHPUnit_Framework_TestCase;
68
use PhpSchool\PhpWorkshop\CommandDefinition;
79

810
/**
9-
* Class CommandDefinitionTest
10-
* @package PhpSchool\PhpWorkshopTest
1111
* @author Aydin Hassan <aydin@hotmail.co.uk>
1212
*/
1313
class CommandDefinitionTest extends PHPUnit_Framework_TestCase
1414
{
1515

16-
public function testGettersSetters()
16+
public function testGettersSettersWithStringArgs()
1717
{
1818
$callable = function () {
1919
};
2020
$definition = new CommandDefinition('animal', ['name'], $callable);
2121

2222
$this->assertSame($definition->getName(), 'animal');
23-
$this->assertSame(['name'], $definition->getRequiredArgs());
23+
24+
$requiredArgs = $definition->getRequiredArgs();
25+
26+
$this->assertCount(1, $requiredArgs);
27+
$this->assertInstanceOf(CommandArgument::class, $requiredArgs[0]);
28+
$this->assertSame('name', $requiredArgs[0]->getName());
29+
$this->assertSame($callable, $definition->getCommandCallable());
30+
}
31+
32+
public function testGettersSettersWithObjArgs()
33+
{
34+
$callable = function () {
35+
};
36+
$definition = new CommandDefinition('animal', [new CommandArgument('name')], $callable);
37+
38+
$this->assertSame($definition->getName(), 'animal');
39+
40+
$requiredArgs = $definition->getRequiredArgs();
41+
42+
$this->assertCount(1, $requiredArgs);
43+
$this->assertInstanceOf(CommandArgument::class, $requiredArgs[0]);
44+
$this->assertSame('name', $requiredArgs[0]->getName());
2445
$this->assertSame($callable, $definition->getCommandCallable());
2546
}
47+
48+
public function testExceptionIsThrowWhenTryingToAddRequiredArgAfterOptionalArg()
49+
{
50+
$this->expectException(InvalidArgumentException::class);
51+
$this->expectExceptionMessage('A required argument cannot follow an optional argument');
52+
53+
$definition = new CommandDefinition('animal', [], 'strlen');
54+
$definition
55+
->addArgument(CommandArgument::optional('optional-arg'))
56+
->addArgument(CommandArgument::required('required-arg'));
57+
}
58+
59+
public function testExceptionIsThrownWithWrongParameterToAddArgument()
60+
{
61+
$this->expectException(InvalidArgumentException::class);
62+
$msg = 'Parameter: "argument" can only be one of: "string", "PhpSchool\PhpWorkshop\CommandArgument" ';
63+
$msg .= 'Received: "stdClass"';
64+
65+
$this->expectExceptionMessage($msg);
66+
$definition = new CommandDefinition('animal', [], 'strlen');
67+
$definition->addArgument(new \stdClass);
68+
}
2669
}

test/CommandRouterTest.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Interop\Container\ContainerInterface;
66
use InvalidArgumentException;
7+
use PhpSchool\PhpWorkshop\CommandArgument;
78
use PHPUnit_Framework_TestCase;
89
use PhpSchool\PhpWorkshop\CommandDefinition;
910
use PhpSchool\PhpWorkshop\CommandRouter;
@@ -291,4 +292,41 @@ public function testRouteCommandSpeltIncorrectlyStillRoutes()
291292
);
292293
$router->route(['app', 'verifu', 'some-exercise', 'program.php']);
293294
}
295+
296+
public function testRouteCommandWithOptionalArgument()
297+
{
298+
$mock = $this->getMockBuilder('stdClass')
299+
->setMethods(['__invoke'])
300+
->getMock();
301+
302+
$mock->expects($this->at(0))
303+
->method('__invoke')
304+
->with('app', 'some-exercise')
305+
->will($this->returnValue(true));
306+
307+
$mock->expects($this->at(1))
308+
->method('__invoke')
309+
->with('app', 'some-exercise', 'program.php')
310+
->will($this->returnValue(true));
311+
312+
$c = $this->createMock(ContainerInterface::class);
313+
$router = new CommandRouter(
314+
[
315+
new CommandDefinition(
316+
'verify',
317+
[
318+
'exercise',
319+
new CommandArgument('program', true),
320+
new CommandArgument('some-other-arg', true)
321+
],
322+
$mock
323+
)
324+
],
325+
'verify',
326+
$c
327+
);
328+
$router->route(['app', 'verify', 'some-exercise']);
329+
$router->route(['app', 'verify', 'some-exercise', 'program.php']);
330+
$router->route(['app', 'verify', 'some-exercise', 'program.php', 'some-other-arg-value']);
331+
}
294332
}

0 commit comments

Comments
 (0)