Skip to content

Commit

Permalink
Separate command and options
Browse files Browse the repository at this point in the history
Having separate value objects for options makes it easier to understand
what options are available to a command and simplifies the validation
process.

This change was implemented as separate classes so that usage can be
upgraded incrementally and version 2.0 can remove the deprecated things.
  • Loading branch information
shadowhand committed Apr 5, 2016
1 parent 241c723 commit 95410de
Show file tree
Hide file tree
Showing 13 changed files with 438 additions and 104 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ This project adheres to [Semantic Versioning](http://semver.org/).

_..._

## 1.3.0 - 2016-04-05

- Added `OptionsTrait` and `OptionsInterface`
- Added `Command` abstract class
- Deprecated `AbstractCommand` and `CommandInterface`

## 1.2.0 - 2016-03-22

- Added `getHttpStatus` to command exception
Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"sparkphp/command": "self.version"
},
"require": {
"php": ">=5.5"
"php": ">=5.5",
"equip/assist": "^0.1.2",
"equip/data": "^2.3"
},
"require-dev": {
"phpunit/phpunit": "^4.8|^5.0"
Expand Down
3 changes: 3 additions & 0 deletions src/AbstractCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace Equip\Command;

/**
* @deprecated since 1.3.0 in favor of Command
*/
abstract class AbstractCommand implements CommandInterface
{
/**
Expand Down
50 changes: 50 additions & 0 deletions src/Command.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace Equip\Command;

abstract class Command
{
/**
* @var OptionsInterface
*/
private $options;

/**
* Execute the command using the current options.
*
* @return mixed
*/
abstract public function execute();

/**
* Get the currently defined options.
*
* @return OptionsInterface
*
* @throws CommandException
* If no options have been added to the command.
*/
final public function options()
{
if (!$this->options) {
throw CommandException::needsOptions($this);
}

return $this->options;
}

/**
* Get a copy with new options.
*
* @param OptionsInterface $options
*
* @return static
*/
final public function withOptions(OptionsInterface $options)
{
$copy = clone $this;
$copy->options = $options;

return $copy;
}
}
16 changes: 16 additions & 0 deletions src/CommandException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,24 @@

class CommandException extends RuntimeException
{
const NO_OPTIONS = 2000;
const MISSING_OPTION = 2000;

/**
* @param Command $command
*
* @return static
*
* @since 1.3.0
*/
public static function needsOptions(Command $command)
{
return new static(sprintf(
'No options have been set for the `%s` command',
get_class($command)
), static::NO_OPTIONS);
}

/*
* @param array $names
*
Expand Down
3 changes: 3 additions & 0 deletions src/CommandInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace Equip\Command;

/**
* @deprecated since 1.3.0 in favor of Command
*/
interface CommandInterface
{
/**
Expand Down
40 changes: 40 additions & 0 deletions src/OptionInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace Equip\Command;

interface OptionsInterface
{
/**
* Map of all options and required state.
*
* return [
* 'user_id' => true, // required
* 'email' => true, // required
* 'name' => false, // optional
* 'access' => null, // optional, has a default
* ];
*
* @return array
*/
public function options();

/**
* Map of option default values.
*
* return [
* 'access' => AccessControl::LIMITED,
* ];
*
* @return array
*/
public function defaultOptions();

/**
* Get all defined options as an array.
*
* @throws \Equip\Command\CommandException If any required options are missing
*
* @return array
*/
public function toArray();
}
39 changes: 39 additions & 0 deletions src/OptionsInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace Equip\Command;

/**
* A general purpose value object for command options.
*
* When constructed, all required options must be passed.
*
* If any required options are missing, a `CommandException` must be thrown.
*/
interface OptionsInterface
{
/**
* Get a list of all required options.
*
* return [
* 'email',
* 'password',
* ];
*
* @return array
*/
public function required();

/**
* Get a list of all valid options.
*
* @return array
*/
public function valid();

/**
* Get all options as an array.
*
* @return array
*/
public function toArray();
}
66 changes: 66 additions & 0 deletions src/OptionsTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace Equip\Command;

use Equip;
use Equip\Data\Traits\ProtectedValueObjectTrait;

trait OptionsTrait /* implements OptionsInterface */
{
use ProtectedValueObjectTrait;

/**
* Check that all required options are defined, then hydrate.
*
* @param array $values
*/
public function __construct(array $values)
{
$values = Equip\grab($values, $this->valid());

$this->ensureRequired($values);

foreach ($values as $key => $value) {
$this->$key = $value;
}
}

/**
* Ensure that all required options are included in the given values.
*
* @param array $values
*
* @return void
*
* @throws CommandException
* If any required options have not been defined.
*/
private function ensureRequired(array $values)
{
$defined = array_filter($values, static function ($value) {
return $value !== null;
});

$missing = array_diff($this->required(), array_keys($defined));

if ($missing) {
throw CommandException::missingOptions($missing);
}
}

/**
* @inheritdoc
*/
public function valid()
{
return array_keys($this->toArray());
}

/**
* @inheritdoc
*/
public function toArray()
{
return get_object_vars($this);
}
}
Loading

0 comments on commit 95410de

Please sign in to comment.