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 13, 2016
1 parent 241c723 commit a6e3932
Show file tree
Hide file tree
Showing 11 changed files with 516 additions and 96 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).

_..._

## 1.3.0 - 2016-04-05

- Added `Command` and `Options` abstract classes
- Deprecated `AbstractCommand` and `CommandInterface`

## 1.2.0 - 2016-03-22

- Added `getHttpStatus` to command exception
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
67 changes: 67 additions & 0 deletions src/Command.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

namespace Equip\Command;

/**
* A general purpose command class.
*
* @since 1.3.0
*/
abstract class Command
{
/**
* @var OptionsInterface
*/
private $options;

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

/**
* Allow usage as a callable.
*
* @see Command::execute()
*
* @return mixed
*/
final public function __invoke()
{
return $this->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
26 changes: 26 additions & 0 deletions src/ImmutableException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Equip\Command;

use LogicException;

class ImmutableException extends LogicException
{
const CANNOT_MODIFY = 1;

/**
* @param object $object
*
* @return static
*/
public static function cannotModify($object)
{
return new static(
sprintf(
'Object `%s` is immutable and cannot be modified',
get_class($object)
),
static::CANNOT_MODIFY
);
}
}
129 changes: 129 additions & 0 deletions src/Options.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

namespace Equip\Command;

/**
* A general purpose value object for command options.
*
* When constructed, all required options must be passed.
*
* @since 1.3.0
*/
abstract class Options
{
/**
* Check that all required options are defined, then hydrate.
*
* @param array $values
*/
final public function __construct(array $values)
{
// Remove any values that are invalid for this set of options
$values = array_intersect_key(
$values,
array_flip($this->valid())
);

$this->ensureRequired($values);

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

/**
* Get a list of all required options.
*
* return [
* 'email',
* 'password',
* ];
*
* @return array
*/
abstract public function required();

/**
* Get a list of all valid options.
*
* @return array
*/
public function valid()
{
return array_keys($this->toArray());
}

/**
* Get all options as an array.
*
* @return array
*/
public function toArray()
{
return get_object_vars($this);
}

/**
* Provide read-only access to attributes.
*
* @param string $key
*
* @return mixed
*/
final public function __get($key)
{
return $this->{$key};
}

/**
* Prevent modification.
*
* @throws ImmutableException
*
* @param string $key
* @param mixed $value
*
* @return void
*/
final public function __set($key, $value)
{
throw ImmutableException::cannotModify($this);
}

/**
* Prevent modification.
*
* @throws ImmutableException
*
* @param string $key
*
* @return void
*/
final public function __unset($key)
{
throw ImmutableException::cannotModify($this);
}

/**
* 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);
}
}
}
Loading

0 comments on commit a6e3932

Please sign in to comment.