11
0
Fork 0
mirror of https://github.com/n3w/helpers-cli-input.git synced 2025-12-19 12:43:23 +00:00

Initial commit

This commit is contained in:
Alannah Kearney 2019-05-20 15:08:41 +10:00
commit 0620d00f08
24 changed files with 1054 additions and 0 deletions

View file

@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input;
use pointybeard\Helpers\Functions\Flags;
abstract class AbstractInputHandler implements Interfaces\InputHandlerInterface
{
protected $options = [];
protected $arguments = [];
protected $collection = null;
abstract protected function parse(): bool;
public function bind(InputCollection $inputCollection, bool $skipValidation = false): bool
{
// Do the binding stuff here
$this->options = [];
$this->arguments = [];
$this->collection = $inputCollection;
$this->parse();
if (true !== $skipValidation) {
$this->validate();
}
return true;
}
private static function checkRequiredAndRequiredValue(AbstractInputType $input, array $context): void
{
if (!isset($context[$input->name()])) {
if (Flags\is_flag_set($input->flags(), AbstractInputType::FLAG_REQUIRED)) {
throw new Exceptions\RequiredInputMissingException($input);
}
} elseif (Flags\is_flag_set($input->flags(), AbstractInputType::FLAG_VALUE_REQUIRED) && (null == $context[$input->name()] || true === $context[$input->name()])) {
throw new Exceptions\RequiredInputMissingValueException($input);
}
}
public function validate(): void
{
// Do basic missing option and value checking here
foreach ($this->collection->getOptions() as $input) {
self::checkRequiredAndRequiredValue($input, $this->options);
}
// Option validation.
foreach ($this->collection->getoptions() as $o) {
$result = false;
if (!array_key_exists($o->name(), $this->options)) {
$result = $o->default();
} else {
if (null === $o->validator()) {
$result = $o->default();
continue;
} elseif ($o->validator() instanceof \Closure) {
$validator = new Validator($o->validator());
} elseif ($o->validator() instanceof Validator) {
$validator = $o->validator();
} else {
throw new \Exception("Validator for option {$o->name()} must be NULL or an instance of either Closure or Input\Validator.");
}
$result = $validator->validate($o, $this);
}
$this->options[$o->name()] = $result;
}
// Argument validation.
foreach ($this->collection->getArguments() as $a) {
self::checkRequiredAndRequiredValue($a, $this->arguments);
if (isset($this->arguments[$a->name()]) && null !== $a->validator()) {
if ($a->validator() instanceof \Closure) {
$validator = new Validator($a->validator());
} elseif ($a->validator() instanceof Validator) {
$validator = $a->validator();
} else {
throw new \Exception("Validator for argument {$a->name()} must be NULL or an instance of either Closure or Input\Validator.");
}
$validator->validate($a, $this);
}
}
}
public function getArgument(string $name): ?string
{
return $this->arguments[$name] ?? null;
}
public function getOption(string $name)
{
return $this->options[$name] ?? null;
}
public function getArguments(): array
{
return $this->arguments;
}
public function getOptions(): array
{
return $this->options;
}
public function getCollection(): ?InputCollection
{
return $this->collection;
}
}

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input;
abstract class AbstractInputType implements Interfaces\InputTypeInterface
{
protected static $type;
protected $name;
protected $flags;
protected $description;
protected $validator;
protected $value;
public function __construct($name, int $flags = null, string $description = null, object $validator = null)
{
$this->name = $name;
$this->flags = $flags;
$this->description = $description;
$this->validator = $validator;
}
public function __call($name, array $args = [])
{
return $this->$name;
}
public function getType(): string
{
return strtolower((new \ReflectionClass(static::class))->getShortName());
}
}

View file

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input\Exceptions;
class InputHandlerNotFoundException extends \Exception
{
public function __construct(string $handler, string $command, $code = 0, \Exception $previous = null)
{
return parent::__construct(sprintf('The input handler %s could not be located.', $handler), $code, $previous);
}
}

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input\Exceptions;
class RequiredArgumentMissingException extends \Exception
{
private $argument;
public function __construct(string $argument, $code = 0, \Exception $previous = null)
{
$this->argument = strtoupper($argument);
return parent::__construct("missing argument {$this->argument}.", $code, $previous);
}
public function getArgumentName(): string
{
return $this->argument;
}
}

View file

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input\Exceptions;
use pointybeard\Helpers\Cli\Input;
class RequiredInputMissingException extends \Exception
{
private $input;
public function __construct(Input\AbstractInputType $input, $code = 0, \Exception $previous = null)
{
$this->input = $input;
return parent::__construct(sprintf(
'missing %s %s%s',
$input->getType(),
'option' == $input->getType() ? '-' : '',
'option' == $input->getType() ? $input->name() : strtoupper($input->name())
), $code, $previous);
}
public function getInput(): Input\AbstractInputType
{
return $this->input;
}
}

View file

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input\Exceptions;
use pointybeard\Helpers\Cli\Input;
class RequiredInputMissingValueException extends \Exception
{
private $input;
public function __construct(Input\AbstractInputType $input, $code = 0, \Exception $previous = null)
{
$this->input = $input;
return parent::__construct(sprintf(
'%s %s%s is missing a value',
$input->getType(),
'option' == $input->getType() ? '-' : '',
'option' == $input->getType() ? $input->name() : strtoupper($input->name())
), $code, $previous);
}
public function getInput(): Input\AbstractInputType
{
return $this->input;
}
}

View file

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input\Exceptions;
class UnableToLoadInputHandlerException extends \Exception
{
public function __construct(string $name, $code = 0, \Exception $previous = null)
{
return parent::__construct(sprintf('The input handler %s could not be loaded. Returned: %s', $name, $previous->getMessage()), $code, $previous);
}
}

174
src/Input/Handlers/Argv.php Normal file
View file

@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input\Handlers;
use pointybeard\Helpers\Cli\Input;
use pointybeard\Helpers\Functions\Flags;
class Argv extends Input\AbstractInputHandler
{
private $argv = null;
const OPTION_LONG = 'long';
const OPTION_SHORT = 'short';
const ARGUMENT = 'argument';
public function __construct(array $argv = null)
{
if (null === $argv) {
$argv = $_SERVER['argv'];
}
// Remove the script name
array_shift($argv);
$this->argv = self::expandOptions($argv);
}
/**
* This will look for combined options, e.g -vlt and expand them to -v -l -t.
*/
protected static function expandOptions(array $args): array
{
$result = [];
foreach ($args as $a) {
switch (self::findType($a)) {
case self::OPTION_SHORT:
// If the name is longer than a 2 characters
// it will mean it's a combination of flags. e.g.
// -vlt 12345 is the same as -v -l -t 12345
if (strlen($a) > 2) {
// Strip the leading hyphen (-)
$a = substr($a, 1);
for ($ii = 0; $ii < strlen($a); ++$ii) {
$result[] = "-{$a[$ii]}";
}
break;
}
// no break
default:
$result[] = $a;
break;
}
}
return $result;
}
protected function parse(): bool
{
// So some parsing here.
$it = new \ArrayIterator($this->argv);
$position = 0;
while ($it->valid()) {
$token = $it->current();
switch (self::findType($token)) {
case self::OPTION_LONG:
$opt = substr($token, 2);
if (false !== strstr($opt, '=')) {
list($name, $value) = explode('=', $opt, 2);
} else {
$name = $opt;
$value = true;
}
$o = $this->collection->findOption($name);
$this->options[
$o instanceof Input\AbstractInputType
? $o->name()
: $name
] = $value;
break;
case self::OPTION_SHORT:
$name = substr($token, 1);
// Determine if we're expecting a value.
// It also might have a long option equivalent, so we need
// to look for that too.
$o = $this->collection->findOption($name);
// This could also be an incrementing value
// and needs to be added up. E.g. e.g. -vvv or -v -v -v
// would be -v => 3
if ($o instanceof Input\AbstractInputType && Flags\is_flag_set($o->flags(), Input\AbstractInputType::FLAG_TYPE_INCREMENTING)) {
$value = isset($this->options[$name])
? $this->options[$name] + 1
: 1
;
// Not incrementing, so resume default behaviour
} else {
// We'll need to look ahead and see what the next value is.
// Ignore it if the next item is another option
// Advance the pointer to grab the next value
$it->next();
$value = $it->current();
// See if the next item is another option and of it is,
// rewind the iterator and set value to 'true'. Also,
// if this option doesn't expect a value (no FLAG_VALUE_REQUIRED or FLAG_VALUE_OPTIONAL flag set), don't capture the next value.
if (null === $value || self::isOption($value) || !($o instanceof Input\AbstractInputType) || (
!Flags\is_flag_set($o->flags(), Input\AbstractInputType::FLAG_VALUE_REQUIRED) && !Flags\is_flag_set($o->flags(), Input\AbstractInputType::FLAG_VALUE_OPTIONAL)
)) {
$value = true;
$it->seek($position);
}
}
$this->options[
$o instanceof Input\AbstractInputType
? $o->name()
: $name
] = $value;
break;
case self::ARGUMENT:
default:
// Arguments are positional, so we need to keep a track
// of the index and look at the collection for an argument
// with the same index
$a = $this->collection->getArgumentsByIndex(count($this->arguments));
$this->arguments[
$a instanceof Input\AbstractInputType
? $a->name()
: count($this->arguments)
] = $token;
break;
}
$it->next();
++$position;
}
return true;
}
private static function isOption(string $value): bool
{
return '-' == $value[0];
}
private static function findType(string $value): string
{
if (0 === strpos($value, '--')) {
return self::OPTION_LONG;
} elseif (self::isOption($value)) {
return self::OPTION_SHORT;
} else {
return self::ARGUMENT;
}
}
}

View file

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input;
class InputCollection
{
private $arguments = [];
private $options = [];
// Prevents the class from being instanciated
public function __construct()
{
}
public function append(Interfaces\InputTypeInterface $input, bool $replace = false): self
{
$class = new \ReflectionClass($input);
$this->{'append'.$class->getShortName()}($input, $replace);
return $this;
}
public function findArgument(string $name, ?int &$index = null): ?AbstractInputType
{
foreach ($this->arguments as $index => $a) {
if ($a->name() == $name) {
return $a;
}
}
$index = null;
return null;
}
public function findOption(string $name, ?int &$index = null): ?AbstractInputType
{
$type = 1 == strlen($name) ? 'name' : 'long';
foreach ($this->options as $index => $o) {
if ($o->$type() == $name) {
return $o;
}
}
$index = null;
return null;
}
private function appendArgument(Interfaces\InputTypeInterface $argument, bool $replace = false): void
{
if (null !== $this->findArgument($argument->name(), $index) && !$replace) {
throw new \Exception("Argument {$argument->name()} already exists in collection");
}
if (true == $replace && null !== $index) {
$this->arguments[$index] = $argument;
} else {
$this->arguments[] = $argument;
}
}
private function appendOption(Interfaces\InputTypeInterface $option, bool $replace = false): void
{
if (null !== $this->findOption($option->name(), $index) && !$replace) {
throw new \Exception("Option -{$option->name()} already exists in collection");
}
if (true == $replace && null !== $index) {
$this->options[$index] = $option;
} else {
$this->options[] = $option;
}
}
public function getArgumentsByIndex(int $index): ?AbstractInputType
{
return $this->arguments[$index];
}
public function getArguments(): array
{
return $this->arguments;
}
public function getOptions(): array
{
return $this->options;
}
public static function merge(self ...$collections): self
{
$arguments = [];
$options = [];
foreach ($collections as $c) {
$arguments = array_merge($arguments, $c->getArguments());
$options = array_merge($options, $c->getOptions());
}
$mergedCollection = new self();
$it = new \AppendIterator();
$it->append(new \ArrayIterator($arguments));
$it->append(new \ArrayIterator($options));
foreach ($it as $input) {
try {
$mergedCollection->append($input, true);
} catch (\Exception $ex) {
// Already exists, so skip it.
}
}
return $mergedCollection;
}
}

View file

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input;
use pointybeard\Helpers\Functions\Flags;
use pointybeard\Helpers\Foundation\Factory;
final class InputHandlerFactory extends Factory\AbstractFactory
{
const FLAG_SKIP_VALIDATION = 0x0001;
public static function getTemplateNamespace(): string
{
return __NAMESPACE__.'\\Handlers\\%s';
}
public static function getExpectedClassType(): ?string
{
return __NAMESPACE__.'\\Interfaces\\InputHandlerInterface';
}
public static function build(string $name, InputCollection $collection = null, int $flags = null): Interfaces\InputHandlerInterface
{
try {
$handler = self::instanciate(
self::generateTargetClassName($name)
);
} catch (\Exception $ex) {
throw new Exceptions\UnableToLoadInputHandlerException($name, 0, $ex);
}
if ($collection instanceof InputCollection) {
$handler->bind(
$collection,
Flags\is_flag_set($flags, self::FLAG_SKIP_VALIDATION)
);
}
return $handler;
}
}

View file

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input\Interfaces;
use pointybeard\Helpers\Cli\Input;
interface InputHandlerInterface
{
public function bind(Input\InputCollection $inputCollection, bool $skipValidation = false): bool;
public function validate(): void;
public function getArgument(string $name): ?string;
// note that the return value of getOption() isn't always going to be
// a string like getArgument()
public function getOption(string $name);
public function getArguments(): array;
public function getOptions(): array;
public function getCollection(): ?Input\InputCollection;
}

View file

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input\Interfaces;
interface InputTypeInterface
{
const FLAG_REQUIRED = 0x0001;
const FLAG_OPTIONAL = 0x0002;
const FLAG_VALUE_REQUIRED = 0x0004;
const FLAG_VALUE_OPTIONAL = 0x0008;
const FLAG_TYPE_STRING = 0x0100;
const FLAG_TYPE_INT = 0x0200;
const FLAG_TYPE_INCREMENTING = 0x0400;
public function getType(): string;
}

View file

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input\Interfaces;
use pointybeard\Helpers\Cli\Input;
interface InputValidatorInterface
{
public function validate(Input\AbstractInputType $input, Input\AbstractInputHandler $context);
}

View file

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input\Types;
use pointybeard\Helpers\Cli\Input;
use pointybeard\Helpers\Functions\Strings;
class Argument extends Input\AbstractInputType
{
public function __toString()
{
$name = strtoupper($this->name());
$first = str_pad(sprintf('%s ', $name), 20, ' ');
$second = Strings\utf8_wordwrap_array($this->description(), 40);
for ($ii = 1; $ii < count($second); ++$ii) {
$second[$ii] = str_pad('', 22, ' ', \STR_PAD_LEFT).$second[$ii];
}
return $first.implode($second, PHP_EOL);
}
}

View file

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input\Types;
use pointybeard\Helpers\Functions\Flags;
use pointybeard\Helpers\Functions\Strings;
use pointybeard\Helpers\Cli\Input;
class Option extends Input\AbstractInputType
{
protected $long;
protected $default;
public function __construct(string $name, string $long = null, int $flags = null, string $description = null, object $validator = null, $default = false)
{
$this->default = $default;
$this->long = $long;
parent::__construct($name, $flags, $description, $validator);
}
public function __toString()
{
$long = null !== $this->long() ? ', --'.$this->long() : null;
if (null != $long) {
if (Flags\is_flag_set($this->flags(), self::FLAG_VALUE_REQUIRED)) {
$long .= '=VALUE';
} elseif (Flags\is_flag_set($this->flags(), self::FLAG_VALUE_OPTIONAL)) {
$long .= '[=VALUE]';
}
}
$first = str_pad(sprintf('-%s%s ', $this->name(), $long), 36, ' ');
$second = Strings\utf8_wordwrap_array($this->description(), 40);
for ($ii = 1; $ii < count($second); ++$ii) {
$second[$ii] = str_pad('', 38, ' ', \STR_PAD_LEFT).$second[$ii];
}
return $first.implode($second, PHP_EOL);
}
}

38
src/Input/Validator.php Normal file
View file

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace pointybeard\Helpers\Cli\Input;
class Validator implements Interfaces\InputValidatorInterface
{
private $func;
public function __construct(\Closure $func)
{
// Check the closure used for validation meets requirements
$params = (new \ReflectionFunction($func))->getParameters();
// Must have exactly 2 params
if (2 != count($params)) {
throw new \Exception('Closure passed to Validator::__construct() is invalid: Must have exactly 2 parameters.');
}
// First must be 'input' and be of type pointybeard\Helpers\Cli\Input\AbstractInputType
if ('input' != $params[0]->getName() || __NAMESPACE__.'\AbstractInputType' != (string) $params[0]->getType()) {
throw new \Exception('Closure passed to Validator::__construct() is invalid: First parameter must match '.__NAMESPACE__."\AbstractInputType \$input. Provided with ".(string) $params[0]->getType()." \${$params[0]->getName()}");
}
// Second must be 'context' and be of type pointybeard\Helpers\Cli\Input\AbstractInputHandler
if ('context' != $params[1]->getName() || __NAMESPACE__.'\AbstractInputHandler' != (string) $params[1]->getType()) {
throw new \Exception('Closure passed to Validator::__construct() is invalid: Second parameter must match '.__NAMESPACE__."\AbstractInputHandler \$context. Provided with ".(string) $params[1]->getType()." \${$params[1]->getName()}");
}
$this->func = $func;
}
public function validate(AbstractInputType $input, AbstractInputHandler $context)
{
return ($this->func)($input, $context);
}
}