mirror of
https://github.com/n3w/helpers-cli-input.git
synced 2025-12-21 21:53:23 +00:00
Compare commits
39 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6fe64321e | ||
|
|
9622c98258 | ||
|
|
2d87f04dff | ||
|
|
1e903ea6e1 | ||
|
|
326a979302 | ||
|
|
d501a680ff | ||
|
|
e542f6fd94 | ||
|
|
46b87d3d62 | ||
|
|
27c04d1d43 | ||
|
|
f7a9b66590 | ||
|
|
05d283b6d2 | ||
|
|
90db5c2047 | ||
|
|
9f5c5c7d4d | ||
|
|
1100d5d959 | ||
|
|
0cbbbdff66 | ||
|
|
7069019301 | ||
|
|
302e4378fb | ||
|
|
900f9f2885 | ||
|
|
ad21e667c3 | ||
|
|
419a4a9f79 | ||
|
|
866a2e1c9e | ||
|
|
32ed646220 | ||
|
|
695b4ac75a | ||
|
|
c5aa26b19d | ||
|
|
50e56a8649 | ||
|
|
e71ea40616 | ||
|
|
4117ed266c | ||
|
|
b89f2a6008 | ||
|
|
96bf279a70 | ||
|
|
a4a8d5e351 | ||
|
|
9e27b52991 | ||
|
|
b49ee5559d | ||
|
|
b36bfeac3d | ||
|
|
9ac0e21d04 | ||
|
|
95e93ec1bc | ||
|
|
6e987bb4eb | ||
|
|
25f3fb8014 | ||
|
|
8d3141c79f | ||
|
|
7c06febefe |
24 changed files with 728 additions and 353 deletions
86
CHANGELOG.md
86
CHANGELOG.md
|
|
@ -3,10 +3,92 @@
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
**View all [Unreleased][] changes here**
|
## [1.2.2][]
|
||||||
|
#### Changed
|
||||||
|
- Minor improvement to logic in `AbstractInputHandler::validateInput()`. Ensures that an input with a validator, but with a default value and no user suplied input, will have the default value used.
|
||||||
|
|
||||||
|
## [1.2.1][]
|
||||||
|
#### Changed
|
||||||
|
- Updated `InputHandlerFactory` and `InputTypeFactory` to work with changes in `pointybeard/helpers-foundation-factory` 1.0.2
|
||||||
|
|
||||||
|
## [1.2.0][]
|
||||||
|
#### Added
|
||||||
|
- Added `InputTypeFilterIterator` class
|
||||||
|
- Added `UnrecognisedInputException` exception
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
- `InputCollection` now implements `Iterator` and `Countable` (implementing required methods)
|
||||||
|
- Removed use of `$type` in `InputCollection`
|
||||||
|
- Added `InputCollection::getItemsExcludeByType()`
|
||||||
|
- `InputCollection::getItemsByType()` and `InputCollection::getItems()` now returns an `Iterator`
|
||||||
|
- Renamed `InputCollection::append()` to `add()` and added `$position` flag
|
||||||
|
- Added `POSITION_APPEND` and `POSITION_PREPEND` flags to `InputCollection`
|
||||||
|
- Made `getCollection()`, `getInput()`, `find()`, and `validate()` in `AbstractInputHandler` final
|
||||||
|
- Removed all categorisation of items by type in `AbstractInputHandler::$input`
|
||||||
|
- Abstracted most of `AbstractInputHandler::validate()` into it's own protected method called `validateInput()`
|
||||||
|
- Removed `$skipValidation` argument from `AbstractInputHandler::bind()` and relaced with `$flags`
|
||||||
|
- Added `FLAG_BIND_SKIP_VALIDATION`, `FLAG_VALIDATION_SKIP_REQUIRED`, `FLAG_VALIDATION_SKIP_CUSTOM`, and `FLAG_VALIDATION_SKIP_UNRECOGNISED` flags to `AbstractInputHandler`
|
||||||
|
- Added check in `AbstractInputHandler::validate()` to look for unrecognised options and arguments
|
||||||
|
- Removed `InputHandlerFactory::FLAG_SKIP_VALIDATION` from `InputHandlerFactory`
|
||||||
|
- Passing flags in call from `InputHandlerFactory::build()` to `AbstractInputHandler::bind()`
|
||||||
|
- Updated `InputHandlerInterface::bind()` and `validate()` methods to support flags
|
||||||
|
|
||||||
|
## [1.1.4][]
|
||||||
|
#### Fixed
|
||||||
|
- Fixed misnamed variable in `InputCollection::merge()`
|
||||||
|
|
||||||
|
## [1.1.3][]
|
||||||
|
#### Fixed
|
||||||
|
- Fixed logic bug that prevented `$index` and `$type` from being set in `InputCollection::append()`. This means replaceing items in an `InputCollection` now works as expected
|
||||||
|
|
||||||
|
## [1.1.2][]
|
||||||
|
#### Added
|
||||||
|
- Added `InputValidationFailedException` exception
|
||||||
|
- Added `InputTypeInterface::getDisplayName()` method to standardise how the name of an `InputTypeInterface` class wants to display it's name
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
- Updated validation logic for inputs that have a validator, no default, and are not set
|
||||||
|
- Throwing `InputValidationFailedException` exception when validation fails
|
||||||
|
- Updated `RequiredInputMissingException` and `RequiredInputMissingValueException` exceptions to use `InputTypeInterface::getDisplayName()` when producing their message
|
||||||
|
- Removed unused `RequiredArgumentMissingException` exception
|
||||||
|
|
||||||
|
## [1.1.1][]
|
||||||
|
#### Changed
|
||||||
|
- `AbstractInputHandler::find()` returns NULL if it cannot find any input with the supplied name. It is easier to test for NULL than it is to catch an exception
|
||||||
|
|
||||||
|
## [1.1.0][]
|
||||||
|
#### Added
|
||||||
|
- Expanded input types to include `Flag`, `IncrementingFlag`, and `LongOption`
|
||||||
|
- Added `InputTypeFactory` to help with loading input type classes
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
- Updated to work with more than just `Argument` and `Option` input types. Makes use of `InputTypeFactory` to allow addition of new types as needed
|
||||||
|
|
||||||
|
## [1.0.2][]
|
||||||
|
#### Changed
|
||||||
|
- Updated example to reflect changes to `manpage()` function in `pointybeard/helpers-functions-cli` package
|
||||||
|
- Refactoring and improvemnts to `Argument::__toString()` and `Option::__toString()`
|
||||||
|
|
||||||
|
## [1.0.2][]
|
||||||
|
#### Fixed
|
||||||
|
- Fixed `InputCollection::getArgumentsByIndex()` so it returns NULL if the index does not exist instead of throwing an E_NOTICE message
|
||||||
|
|
||||||
|
## [1.0.1][]
|
||||||
|
#### Changed
|
||||||
|
- Updated example to use `Cli\manpage()` provided by the `pointybeard/helpers-functions-cli` package
|
||||||
|
|
||||||
## 1.0.0
|
## 1.0.0
|
||||||
#### Added
|
#### Added
|
||||||
- Initial release
|
- Initial release
|
||||||
|
|
||||||
[Unreleased]: https://github.com/pointybeard/helpers-cli-input/compare/1.0.0...integration
|
[1.2.2]: https://github.com/pointybeard/helpers-functions-cli/compare/1.2.1...1.2.2
|
||||||
|
[1.2.1]: https://github.com/pointybeard/helpers-functions-cli/compare/1.2.0...1.2.1
|
||||||
|
[1.2.0]: https://github.com/pointybeard/helpers-functions-cli/compare/1.1.4...1.2.0
|
||||||
|
[1.1.4]: https://github.com/pointybeard/helpers-functions-cli/compare/1.1.3...1.1.4
|
||||||
|
[1.1.3]: https://github.com/pointybeard/helpers-functions-cli/compare/1.1.2...1.1.3
|
||||||
|
[1.1.2]: https://github.com/pointybeard/helpers-functions-cli/compare/1.1.1...1.1.2
|
||||||
|
[1.1.1]: https://github.com/pointybeard/helpers-functions-cli/compare/1.1.0...1.1.1
|
||||||
|
[1.1.0]: https://github.com/pointybeard/helpers-functions-cli/compare/1.0.3...1.1.0
|
||||||
|
[1.0.3]: https://github.com/pointybeard/helpers-functions-cli/compare/1.0.2...1.0.3
|
||||||
|
[1.0.2]: https://github.com/pointybeard/helpers-functions-cli/compare/1.0.1...1.0.2
|
||||||
|
[1.0.1]: https://github.com/pointybeard/helpers-functions-cli/compare/1.0.0...1.0.1
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# PHP Helpers: Command-line Input and Input Type Handlers
|
# PHP Helpers: Command-line Input and Input Type Handlers
|
||||||
|
|
||||||
- Version: v1.0.0
|
- Version: v1.2.2
|
||||||
- Date: May 20 2019
|
- Date: Aug 06 2019
|
||||||
- [Release notes](https://github.com/pointybeard/helpers-cli-input/blob/master/CHANGELOG.md)
|
- [Release notes](https://github.com/pointybeard/helpers-cli-input/blob/master/CHANGELOG.md)
|
||||||
- [GitHub repository](https://github.com/pointybeard/helpers-cli-input)
|
- [GitHub repository](https://github.com/pointybeard/helpers-cli-input)
|
||||||
|
|
||||||
|
|
@ -9,7 +9,7 @@ Collection of classes for handling argv (and other) input when calling command-l
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
This library is installed via [Composer](http://getcomposer.org/). To install, use `composer require pointybeard/helpers-cli-input` or add `"pointybeard/helpers-cli-input": "~1.0"` to your `composer.json` file.
|
This library is installed via [Composer](http://getcomposer.org/). To install, use `composer require pointybeard/helpers-cli-input` or add `"pointybeard/helpers-cli-input": "~1.2.0"` to your `composer.json` file.
|
||||||
|
|
||||||
And run composer to update your dependencies:
|
And run composer to update your dependencies:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "pointybeard/helpers-cli-input",
|
"name": "pointybeard/helpers-cli-input",
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Collection of classes for handling argv (and other) input when calling command-line scripts. Helps with parsing, collecting and validating arguments, options, and flags.",
|
"description": "Collection of classes for handling argv (and other) input when calling command-line scripts. Helps with parsing, collecting and validating arguments, options, and flags.",
|
||||||
"homepage": "https://github.com/pointybeard/helpers-cli-input",
|
"homepage": "https://github.com/pointybeard/helpers-cli-input",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -17,11 +16,6 @@
|
||||||
"pointybeard/helpers-foundation-factory": "~1.0",
|
"pointybeard/helpers-foundation-factory": "~1.0",
|
||||||
"pointybeard/helpers-functions-flags": "~1.0"
|
"pointybeard/helpers-functions-flags": "~1.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
|
||||||
"phpunit/phpunit": "^8",
|
|
||||||
"pointybeard/helpers-functions-strings": "~1.0",
|
|
||||||
"pointybeard/helpers-cli-colour": "~1.0"
|
|
||||||
},
|
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/pointybeard/helpers-cli-input/issues",
|
"issues": "https://github.com/pointybeard/helpers-cli-input/issues",
|
||||||
"wiki": "https://github.com/pointybeard/helpers-cli-input/wiki"
|
"wiki": "https://github.com/pointybeard/helpers-cli-input/wiki"
|
||||||
|
|
|
||||||
|
|
@ -4,146 +4,72 @@ declare(strict_types=1);
|
||||||
include __DIR__.'/../vendor/autoload.php';
|
include __DIR__.'/../vendor/autoload.php';
|
||||||
|
|
||||||
use pointybeard\Helpers\Cli\Input;
|
use pointybeard\Helpers\Cli\Input;
|
||||||
use pointybeard\Helpers\Functions\Flags;
|
use pointybeard\Helpers\Functions\Cli;
|
||||||
use pointybeard\Helpers\Functions\Strings;
|
|
||||||
use pointybeard\Helpers\Cli\Colour\Colour;
|
|
||||||
|
|
||||||
// Define what we are expecting to get from the command line
|
// Define what we are expecting to get from the command line
|
||||||
$collection = (new Input\InputCollection())
|
$collection = (new Input\InputCollection())
|
||||||
->append(new Input\Types\Argument(
|
->add(
|
||||||
'action',
|
Input\InputTypeFactory::build('Argument')
|
||||||
Input\AbstractInputType::FLAG_REQUIRED,
|
->name('action')
|
||||||
'The name of the action to perform'
|
->flags(Input\AbstractInputType::FLAG_REQUIRED)
|
||||||
))
|
->description('The name of the action to perform')
|
||||||
->append(new Input\Types\Option(
|
)
|
||||||
'v',
|
->add(
|
||||||
null,
|
Input\InputTypeFactory::build('IncrementingFlag')
|
||||||
Input\AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_TYPE_INCREMENTING,
|
->name('v')
|
||||||
'verbosity level. -v (errors only), -vv (warnings and errors), -vvv (everything).',
|
->flags(Input\AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_TYPE_INCREMENTING)
|
||||||
null,
|
->description('verbosity level. -v (errors only), -vv (warnings and errors), -vvv (everything).')
|
||||||
0
|
->validator(new Input\Validator(
|
||||||
))
|
function (Input\AbstractInputType $input, Input\AbstractInputHandler $context) {
|
||||||
->append(new Input\Types\Option(
|
// Make sure verbosity level never goes above 3
|
||||||
'd',
|
return min(3, (int) $context->find('v'));
|
||||||
'data',
|
}
|
||||||
Input\AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_VALUE_REQUIRED,
|
))
|
||||||
'Path to the input JSON data',
|
)
|
||||||
function (Input\AbstractInputType $input, Input\AbstractInputHandler $context) {
|
->add(
|
||||||
// Make sure -d (--data) is a valid file that can be read
|
Input\InputTypeFactory::build('LongOption')
|
||||||
$file = $context->getOption('d');
|
->name('data')
|
||||||
|
->short('d')
|
||||||
|
->flags(Input\AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_VALUE_REQUIRED)
|
||||||
|
->description('Path to the input JSON data')
|
||||||
|
->validator(new Input\Validator(
|
||||||
|
function (Input\AbstractInputType $input, Input\AbstractInputHandler $context) {
|
||||||
|
// Make sure -d (--data) is a valid file that can be read
|
||||||
|
$file = $context->find('data');
|
||||||
|
|
||||||
if (!is_readable($file)) {
|
if (!is_readable($file)) {
|
||||||
throw new \Exception('The file specified via option -d (--data) does not exist or is not readable.');
|
throw new \Exception('The file specified via option --data does not exist or is not readable.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now make sure it is valid JSON
|
// Now make sure it is valid JSON
|
||||||
try {
|
try {
|
||||||
$json = json_decode(file_get_contents($file), false, 512, JSON_THROW_ON_ERROR);
|
$json = json_decode(file_get_contents($file), false, 512, JSON_THROW_ON_ERROR);
|
||||||
} catch (JsonException $ex) {
|
} catch (JsonException $ex) {
|
||||||
throw new \Exception(sprintf('The file specified via option -d (--data) does not appear to be a valid JSON ddocument. Returned: %s: %s', $ex->getCode(), $ex->getMessage()));
|
throw new \Exception(sprintf('The file specified via option --data does not appear to be a valid JSON ddocument. Returned: %s: %s', $ex->getCode(), $ex->getMessage()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $json;
|
return $json;
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
|
)
|
||||||
;
|
;
|
||||||
|
|
||||||
// Get the supplied input. Passing the collection will make the handler bind values
|
// Get the supplied input. Passing the collection will make the handler bind values
|
||||||
// and validate the input according to our collection
|
// and validate the input according to our collection
|
||||||
$argv = Input\InputHandlerFactory::build('Argv', $collection);
|
try {
|
||||||
|
$argv = Input\InputHandlerFactory::build('Argv', $collection);
|
||||||
// Example of using an input collection to generate a usage string
|
} catch (\Exception $ex) {
|
||||||
function usage(Input\InputCollection $collection): string {
|
echo 'Error when attempting to bind values to collection. Returned: '.$ex->getMessage().PHP_EOL;
|
||||||
$arguments = [];
|
exit;
|
||||||
foreach ($collection->getArguments() as $a) {
|
|
||||||
$arguments[] = strtoupper(
|
|
||||||
// Wrap with square brackets if it's not required
|
|
||||||
Flags\is_flag_set(Input\AbstractInputType::FLAG_OPTIONAL, $a->flags()) ||
|
|
||||||
!Flags\is_flag_set(Input\AbstractInputType::FLAG_REQUIRED, $a->flags())
|
|
||||||
? "[{$a->name()}]"
|
|
||||||
: $a->name()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$arguments = trim(implode($arguments, ' '));
|
|
||||||
return sprintf(
|
|
||||||
"Usage: php -f example.php -- [OPTIONS]... %s%s",
|
|
||||||
$arguments,
|
|
||||||
strlen($arguments) > 0 ? '...' : ''
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example of using an input collection to generate a manual page
|
var_dump($argv->find('action'));
|
||||||
function manpage(Input\InputCollection $collection) : string {
|
|
||||||
|
|
||||||
$arguments = $options = [];
|
|
||||||
|
|
||||||
foreach ($collection->getArguments() as $a) {
|
|
||||||
$arguments[] = (string) $a;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($collection->getOptions() as $o) {
|
|
||||||
$options[] = (string) $o;
|
|
||||||
}
|
|
||||||
|
|
||||||
$arguments = implode($arguments, PHP_EOL.' ');
|
|
||||||
$options = implode($options, PHP_EOL.' ');
|
|
||||||
|
|
||||||
return sprintf('%s 1.0.0, %s
|
|
||||||
%s
|
|
||||||
|
|
||||||
Mandatory values for long options are mandatory for short options too.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
%s
|
|
||||||
|
|
||||||
Options:
|
|
||||||
%s
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
php -f example/example.php -- -vvv -d example/example.json import
|
|
||||||
',
|
|
||||||
basename(__FILE__),
|
|
||||||
Strings\utf8_wordwrap(
|
|
||||||
"An example script for the PHP Helpers: Command-line Input and Input Type Handlers composer library (pointybeard/helpers-cli-input)."
|
|
||||||
),
|
|
||||||
usage($collection),
|
|
||||||
$arguments,
|
|
||||||
$options
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display the manual in green text
|
|
||||||
echo Colour::colourise(manpage($collection), Colour::FG_GREEN) . PHP_EOL . PHP_EOL;
|
|
||||||
|
|
||||||
/*
|
|
||||||
example.php 1.0.0, An example script for the PHP Helpers: Command-line Input and Input Type Handlers
|
|
||||||
composer library (pointybeard/helpers-cli-input).
|
|
||||||
Usage: php -f example.php -- [OPTIONS]... ACTION...
|
|
||||||
|
|
||||||
Mandatory values for long options are mandatory for short options too.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
ACTION The name of the action to perform
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-v verbosity level. -v (errors only), -vv (warnings
|
|
||||||
and errors), -vvv (everything).
|
|
||||||
-d, --data=VALUE Path to the input JSON data
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
php -f example/example.php -- -vvv -d example/example.json import
|
|
||||||
*/
|
|
||||||
|
|
||||||
var_dump($argv->getArgument('action'));
|
|
||||||
// string(6) "import"
|
// string(6) "import"
|
||||||
|
|
||||||
var_dump($argv->getOption('v'));
|
var_dump($argv->find('v'));
|
||||||
//int(3)
|
//int(3)
|
||||||
|
|
||||||
var_dump($argv->getOption('s'));
|
var_dump($argv->find('data'));
|
||||||
//bool(true)
|
|
||||||
|
|
||||||
var_dump($argv->getOption('d'));
|
|
||||||
// class stdClass#11 (1) {
|
// class stdClass#11 (1) {
|
||||||
// public $fruit =>
|
// public $fruit =>
|
||||||
// array(2) {
|
// array(2) {
|
||||||
|
|
@ -153,3 +79,6 @@ var_dump($argv->getOption('d'));
|
||||||
// string(6) "banana"
|
// string(6) "banana"
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
var_dump($argv->find('nope-doesnt-exist'));
|
||||||
|
// NULL
|
||||||
|
|
|
||||||
|
|
@ -5,26 +5,49 @@ declare(strict_types=1);
|
||||||
namespace pointybeard\Helpers\Cli\Input;
|
namespace pointybeard\Helpers\Cli\Input;
|
||||||
|
|
||||||
use pointybeard\Helpers\Functions\Flags;
|
use pointybeard\Helpers\Functions\Flags;
|
||||||
|
use pointybeard\Helpers\Functions\Debug;
|
||||||
|
|
||||||
abstract class AbstractInputHandler implements Interfaces\InputHandlerInterface
|
abstract class AbstractInputHandler implements Interfaces\InputHandlerInterface
|
||||||
{
|
{
|
||||||
protected $options = [];
|
/**
|
||||||
protected $arguments = [];
|
* Will skip all validation when bind() is executed. Ignores all other flags
|
||||||
|
* @var integer
|
||||||
|
*/
|
||||||
|
const FLAG_BIND_SKIP_VALIDATION = 0x0001;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will skip the required input and required values check
|
||||||
|
* @var integer
|
||||||
|
*/
|
||||||
|
const FLAG_VALIDATION_SKIP_REQUIRED = 0x0002;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will skip running custom validators
|
||||||
|
* @var integer
|
||||||
|
*/
|
||||||
|
const FLAG_VALIDATION_SKIP_CUSTOM = 0x0004;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will skip checking if an input is in the collection
|
||||||
|
* @var integer
|
||||||
|
*/
|
||||||
|
const FLAG_VALIDATION_SKIP_UNRECOGNISED = 0x0008;
|
||||||
|
|
||||||
|
protected $input = [];
|
||||||
protected $collection = null;
|
protected $collection = null;
|
||||||
|
|
||||||
abstract protected function parse(): bool;
|
abstract protected function parse(): bool;
|
||||||
|
|
||||||
public function bind(InputCollection $inputCollection, bool $skipValidation = false): bool
|
final public function bind(InputCollection $inputCollection, ?int $flags = null): bool
|
||||||
{
|
{
|
||||||
// Do the binding stuff here
|
// Do the binding stuff here
|
||||||
$this->options = [];
|
$this->input = [];
|
||||||
$this->arguments = [];
|
|
||||||
$this->collection = $inputCollection;
|
$this->collection = $inputCollection;
|
||||||
|
|
||||||
$this->parse();
|
$this->parse();
|
||||||
|
|
||||||
if (true !== $skipValidation) {
|
if (!Flags\is_flag_set($flags, self::FLAG_BIND_SKIP_VALIDATION)) {
|
||||||
$this->validate();
|
$this->validate($flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -41,76 +64,86 @@ abstract class AbstractInputHandler implements Interfaces\InputHandlerInterface
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function validate(): void
|
protected function validateInput(AbstractInputType $input, ?int $flags) {
|
||||||
{
|
if(!Flags\is_flag_set($flags, self::FLAG_VALIDATION_SKIP_REQUIRED)) {
|
||||||
// Do basic missing option and value checking here
|
self::checkRequiredAndRequiredValue($input, $this->input);
|
||||||
foreach ($this->collection->getOptions() as $input) {
|
|
||||||
self::checkRequiredAndRequiredValue($input, $this->options);
|
|
||||||
}
|
}
|
||||||
|
// There is a default value and input has not been set. Assign the
|
||||||
|
// default value to the result.
|
||||||
|
if (
|
||||||
|
null !== $input->default() &&
|
||||||
|
null === $this->find($input->name())
|
||||||
|
) {
|
||||||
|
$result = $input->default();
|
||||||
|
|
||||||
// Option validation.
|
// Input has been set AND it has a validator. Run the validator over the
|
||||||
foreach ($this->collection->getoptions() as $o) {
|
// input. Note, this will be skipped if FLAG_VALIDATION_SKIP_CUSTOM is
|
||||||
$result = false;
|
// set
|
||||||
|
} elseif (null !== $this->find($input->name()) && null !== $input->validator() && !Flags\is_flag_set($flags, self::FLAG_VALIDATION_SKIP_CUSTOM)) {
|
||||||
|
$validator = $input->validator();
|
||||||
|
|
||||||
if (!array_key_exists($o->name(), $this->options)) {
|
if ($validator instanceof \Closure) {
|
||||||
$result = $o->default();
|
$validator = new Validator($validator);
|
||||||
} else {
|
} elseif (!($validator instanceof Validator)) {
|
||||||
if (null === $o->validator()) {
|
throw new \Exception("Validator for '{$input->name()}' must be NULL or an instance of either Closure or Input\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;
|
try {
|
||||||
|
$result = $validator->validate($input, $this);
|
||||||
|
} catch (\Exception $ex) {
|
||||||
|
throw new Exceptions\InputValidationFailedException($input, 0, $ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No default, but may or may not have been set so assign whatever value
|
||||||
|
// it might have to the result
|
||||||
|
} else {
|
||||||
|
$result = $this->find($input->name());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Argument validation.
|
return $result;
|
||||||
foreach ($this->collection->getArguments() as $a) {
|
}
|
||||||
self::checkRequiredAndRequiredValue($a, $this->arguments);
|
|
||||||
|
|
||||||
if (isset($this->arguments[$a->name()]) && null !== $a->validator()) {
|
protected function isInputRecognised(string $name): bool {
|
||||||
if ($a->validator() instanceof \Closure) {
|
return null === $this->collection->find($name) ? false : true;
|
||||||
$validator = new Validator($a->validator());
|
}
|
||||||
} elseif ($a->validator() instanceof Validator) {
|
|
||||||
$validator = $a->validator();
|
final public function validate(?int $flags = null): void
|
||||||
} else {
|
{
|
||||||
throw new \Exception("Validator for argument {$a->name()} must be NULL or an instance of either Closure or Input\Validator.");
|
if(!Flags\is_flag_set($flags, self::FLAG_VALIDATION_SKIP_UNRECOGNISED)) {
|
||||||
|
foreach($this->input as $name => $value) {
|
||||||
|
if(false == static::isInputRecognised((string)$name)) {
|
||||||
|
throw new Exceptions\UnrecognisedInputException("'{$name}' is not recognised");
|
||||||
}
|
}
|
||||||
|
|
||||||
$validator->validate($a, $this);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ($this->collection->getItems() as $input) {
|
||||||
|
$this->input[$input->name()] = static::validateInput($input, $flags);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getArgument(string $name): ?string
|
final public function find(string $name)
|
||||||
{
|
{
|
||||||
return $this->arguments[$name] ?? null;
|
if (isset($this->input[$name])) {
|
||||||
|
return $this->input[$name];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the collection to see if anything responds to $name
|
||||||
|
foreach ($this->collection->getItems() as $item) {
|
||||||
|
if ($item->respondsTo($name) && isset($this->input[$item->name()])) {
|
||||||
|
return $this->input[$item->name()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getOption(string $name)
|
final public function getInput(): array
|
||||||
{
|
{
|
||||||
return $this->options[$name] ?? null;
|
return $this->input;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getArguments(): array
|
final public function getCollection(): ?InputCollection
|
||||||
{
|
|
||||||
return $this->arguments;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getOptions(): array
|
|
||||||
{
|
|
||||||
return $this->options;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCollection(): ?InputCollection
|
|
||||||
{
|
{
|
||||||
return $this->collection;
|
return $this->collection;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,22 +12,40 @@ abstract class AbstractInputType implements Interfaces\InputTypeInterface
|
||||||
protected $flags;
|
protected $flags;
|
||||||
protected $description;
|
protected $description;
|
||||||
protected $validator;
|
protected $validator;
|
||||||
|
protected $default;
|
||||||
|
|
||||||
protected $value;
|
protected $value;
|
||||||
|
|
||||||
public function __construct($name, int $flags = null, string $description = null, object $validator = null)
|
public function __construct(string $name = null, int $flags = null, string $description = null, object $validator = null, $default = null)
|
||||||
{
|
{
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
$this->flags = $flags;
|
$this->flags = $flags;
|
||||||
$this->description = $description;
|
$this->description = $description;
|
||||||
$this->validator = $validator;
|
$this->validator = $validator;
|
||||||
|
$this->default = $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __call($name, array $args = [])
|
public function __call($name, array $args = [])
|
||||||
|
{
|
||||||
|
if (empty($args)) {
|
||||||
|
return $this->$name;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->$name = $args[0];
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __get($name)
|
||||||
{
|
{
|
||||||
return $this->$name;
|
return $this->$name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function respondsTo(string $name): bool
|
||||||
|
{
|
||||||
|
return $name == $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
public function getType(): string
|
public function getType(): string
|
||||||
{
|
{
|
||||||
return strtolower((new \ReflectionClass(static::class))->getShortName());
|
return strtolower((new \ReflectionClass(static::class))->getShortName());
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace pointybeard\Helpers\Cli\Input\Exceptions;
|
namespace pointybeard\Helpers\Cli\Input\Exceptions;
|
||||||
|
|
||||||
class InputHandlerNotFoundException extends \Exception
|
class InputNotFoundException extends \Exception
|
||||||
{
|
{
|
||||||
public function __construct(string $handler, string $command, $code = 0, \Exception $previous = null)
|
public function __construct(string $name, $code = 0, \Exception $previous = null)
|
||||||
{
|
{
|
||||||
return parent::__construct(sprintf('The input handler %s could not be located.', $handler), $code, $previous);
|
return parent::__construct(sprintf('Input %s could not be found.', $name), $code, $previous);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
src/Input/Exceptions/InputValidationFailedException.php
Normal file
15
src/Input/Exceptions/InputValidationFailedException.php
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace pointybeard\Helpers\Cli\Input\Exceptions;
|
||||||
|
|
||||||
|
use pointybeard\Helpers\Cli\Input\AbstractInputType;
|
||||||
|
|
||||||
|
class InputValidationFailedException extends \Exception
|
||||||
|
{
|
||||||
|
public function __construct(AbstractInputType $input, $code = 0, \Exception $previous = null)
|
||||||
|
{
|
||||||
|
return parent::__construct(sprintf('Validation failed for %s. Returned: %s', $input->getDisplayName(), $previous->getMessage()), $code, $previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
<?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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -15,10 +15,8 @@ class RequiredInputMissingException extends \Exception
|
||||||
$this->input = $input;
|
$this->input = $input;
|
||||||
|
|
||||||
return parent::__construct(sprintf(
|
return parent::__construct(sprintf(
|
||||||
'missing %s %s%s',
|
'missing %s',
|
||||||
$input->getType(),
|
$input->getDisplayName()
|
||||||
'option' == $input->getType() ? '-' : '',
|
|
||||||
'option' == $input->getType() ? $input->name() : strtoupper($input->name())
|
|
||||||
), $code, $previous);
|
), $code, $previous);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,8 @@ class RequiredInputMissingValueException extends \Exception
|
||||||
$this->input = $input;
|
$this->input = $input;
|
||||||
|
|
||||||
return parent::__construct(sprintf(
|
return parent::__construct(sprintf(
|
||||||
'%s %s%s is missing a value',
|
'a value is required for %s',
|
||||||
$input->getType(),
|
$input->getDisplayName()
|
||||||
'option' == $input->getType() ? '-' : '',
|
|
||||||
'option' == $input->getType() ? $input->name() : strtoupper($input->name())
|
|
||||||
), $code, $previous);
|
), $code, $previous);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
9
src/Input/Exceptions/UnrecognisedInputException.php
Normal file
9
src/Input/Exceptions/UnrecognisedInputException.php
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace pointybeard\Helpers\Cli\Input\Exceptions;
|
||||||
|
|
||||||
|
class UnrecognisedInputException extends \Exception
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -3,9 +3,10 @@
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace pointybeard\Helpers\Cli\Input\Handlers;
|
namespace pointybeard\Helpers\Cli\Input\Handlers;
|
||||||
|
use pointybeard\Helpers\Cli\Input\Exceptions;
|
||||||
use pointybeard\Helpers\Cli\Input;
|
use pointybeard\Helpers\Cli\Input;
|
||||||
use pointybeard\Helpers\Functions\Flags;
|
use pointybeard\Helpers\Functions\Flags;
|
||||||
|
use pointybeard\Helpers\Functions\Debug;
|
||||||
|
|
||||||
class Argv extends Input\AbstractInputHandler
|
class Argv extends Input\AbstractInputHandler
|
||||||
{
|
{
|
||||||
|
|
@ -61,12 +62,35 @@ class Argv extends Input\AbstractInputHandler
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function findOptionInCollection(string $name): ?Input\AbstractInputType
|
||||||
|
{
|
||||||
|
$option = $this->collection->find($name);
|
||||||
|
if(!($option instanceof Input\AbstractInputType)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $option;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function findArgumentInCollection(int $index, string $token): ?Input\AbstractInputType
|
||||||
|
{
|
||||||
|
$arguments = $this->collection->getItemsByType('Argument');
|
||||||
|
$position = 0;
|
||||||
|
foreach($arguments as $a) {
|
||||||
|
if($position == $index) {
|
||||||
|
return $a;
|
||||||
|
}
|
||||||
|
$position++;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
protected function parse(): bool
|
protected function parse(): bool
|
||||||
{
|
{
|
||||||
// So some parsing here.
|
|
||||||
$it = new \ArrayIterator($this->argv);
|
$it = new \ArrayIterator($this->argv);
|
||||||
|
|
||||||
$position = 0;
|
$position = 0;
|
||||||
|
$argumentCount = 0;
|
||||||
|
|
||||||
while ($it->valid()) {
|
while ($it->valid()) {
|
||||||
$token = $it->current();
|
$token = $it->current();
|
||||||
|
|
@ -82,9 +106,9 @@ class Argv extends Input\AbstractInputHandler
|
||||||
$value = true;
|
$value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$o = $this->collection->findOption($name);
|
$o = $this->findOptionInCollection($name);
|
||||||
|
|
||||||
$this->options[
|
$this->input[
|
||||||
$o instanceof Input\AbstractInputType
|
$o instanceof Input\AbstractInputType
|
||||||
? $o->name()
|
? $o->name()
|
||||||
: $name
|
: $name
|
||||||
|
|
@ -98,14 +122,14 @@ class Argv extends Input\AbstractInputHandler
|
||||||
// Determine if we're expecting a value.
|
// Determine if we're expecting a value.
|
||||||
// It also might have a long option equivalent, so we need
|
// It also might have a long option equivalent, so we need
|
||||||
// to look for that too.
|
// to look for that too.
|
||||||
$o = $this->collection->findOption($name);
|
$o = $this->findOptionInCollection($name);
|
||||||
|
|
||||||
// This could also be an incrementing value
|
// This could also be an incrementing value
|
||||||
// and needs to be added up. E.g. e.g. -vvv or -v -v -v
|
// and needs to be added up. E.g. e.g. -vvv or -v -v -v
|
||||||
// would be -v => 3
|
// would be -v => 3
|
||||||
if ($o instanceof Input\AbstractInputType && Flags\is_flag_set($o->flags(), Input\AbstractInputType::FLAG_TYPE_INCREMENTING)) {
|
if ($o instanceof Input\AbstractInputType && Flags\is_flag_set($o->flags(), Input\AbstractInputType::FLAG_TYPE_INCREMENTING)) {
|
||||||
$value = isset($this->options[$name])
|
$value = isset($this->input[$name])
|
||||||
? $this->options[$name] + 1
|
? $this->input[$name] + 1
|
||||||
: 1
|
: 1
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
@ -128,7 +152,7 @@ class Argv extends Input\AbstractInputHandler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->options[
|
$this->input[
|
||||||
$o instanceof Input\AbstractInputType
|
$o instanceof Input\AbstractInputType
|
||||||
? $o->name()
|
? $o->name()
|
||||||
: $name
|
: $name
|
||||||
|
|
@ -141,12 +165,14 @@ class Argv extends Input\AbstractInputHandler
|
||||||
// Arguments are positional, so we need to keep a track
|
// Arguments are positional, so we need to keep a track
|
||||||
// of the index and look at the collection for an argument
|
// of the index and look at the collection for an argument
|
||||||
// with the same index
|
// with the same index
|
||||||
$a = $this->collection->getArgumentsByIndex(count($this->arguments));
|
$a = $this->findArgumentInCollection($argumentCount, $token);
|
||||||
$this->arguments[
|
|
||||||
|
$this->input[
|
||||||
$a instanceof Input\AbstractInputType
|
$a instanceof Input\AbstractInputType
|
||||||
? $a->name()
|
? $a->name()
|
||||||
: count($this->arguments)
|
: $argumentCount
|
||||||
] = $token;
|
] = $token;
|
||||||
|
++$argumentCount;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
$it->next();
|
$it->next();
|
||||||
|
|
|
||||||
|
|
@ -4,116 +4,150 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace pointybeard\Helpers\Cli\Input;
|
namespace pointybeard\Helpers\Cli\Input;
|
||||||
|
|
||||||
class InputCollection
|
class InputCollection implements \Iterator, \Countable
|
||||||
{
|
{
|
||||||
private $arguments = [];
|
private $items = [];
|
||||||
private $options = [];
|
private $position = 0;
|
||||||
|
|
||||||
|
public const POSITION_APPEND = 0x0001;
|
||||||
|
public const POSITION_PREPEND = 0x0002;
|
||||||
|
|
||||||
// Prevents the class from being instanciated
|
// Prevents the class from being instanciated
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
|
$this->position = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function append(Interfaces\InputTypeInterface $input, bool $replace = false): self
|
public function current(): mixed
|
||||||
{
|
{
|
||||||
$class = new \ReflectionClass($input);
|
return $this->items[$this->position];
|
||||||
$this->{'append'.$class->getShortName()}($input, $replace);
|
}
|
||||||
|
|
||||||
|
public function key(): scalar
|
||||||
|
{
|
||||||
|
return $this->position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function next(): void
|
||||||
|
{
|
||||||
|
++$this->position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rewind(): void
|
||||||
|
{
|
||||||
|
$this->position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function valid(): bool
|
||||||
|
{
|
||||||
|
return isset($this->items[$this->position]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function count() : int {
|
||||||
|
return count($this->items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exists(string $name, &$index=null): bool {
|
||||||
|
return (null !== $this->find($name, null, null, $index));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remove(string $name): self {
|
||||||
|
if(!$this->exists($name, $index)) {
|
||||||
|
throw new \Exception("Input '{$name}' does not exist in this collection");
|
||||||
|
}
|
||||||
|
unset($this->items[$index]);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function add(Interfaces\InputTypeInterface $input, bool $replace = false, int $position=self::POSITION_APPEND): self
|
||||||
|
{
|
||||||
|
if($this->exists($input->name(), $index) && !$replace) {
|
||||||
|
throw new \Exception(
|
||||||
|
(new \ReflectionClass($input))->getShortName()." '{$input->name()}' already exists in this collection"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (true == $replace && null !== $index) {
|
||||||
|
$this->items[$index] = $input;
|
||||||
|
} else {
|
||||||
|
if($position == self::POSITION_PREPEND) {
|
||||||
|
array_unshift($this->items, $input);
|
||||||
|
} else {
|
||||||
|
array_push($this->items, $input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findArgument(string $name, ?int &$index = null): ?AbstractInputType
|
public function find(string $name, array $restrictToType = null, array $excludeType = null, &$index = null): ?AbstractInputType
|
||||||
{
|
{
|
||||||
foreach ($this->arguments as $index => $a) {
|
foreach ($this->items as $index => $input) {
|
||||||
if ($a->name() == $name) {
|
// Check if we're restricting to or excluding specific types
|
||||||
return $a;
|
if (null !== $restrictToType && !in_array($input->getType(), $restrictToType)) {
|
||||||
|
continue;
|
||||||
|
} elseif (null !== $excludeType && in_array($input->getType(), $excludeType)) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($input->respondsTo($name)) {
|
||||||
|
return $input;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$index = null;
|
$index = null;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findOption(string $name, ?int &$index = null): ?AbstractInputType
|
public function getTypes(): array
|
||||||
{
|
{
|
||||||
$type = 1 == strlen($name) ? 'name' : 'long';
|
$types = [];
|
||||||
|
foreach($this->items as $input) {
|
||||||
foreach ($this->options as $index => $o) {
|
$types[] = $input->getType();
|
||||||
if ($o->$type() == $name) {
|
|
||||||
return $o;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return array_unique($types);
|
||||||
$index = null;
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function appendArgument(Interfaces\InputTypeInterface $argument, bool $replace = false): void
|
public function getItems(): \Iterator
|
||||||
{
|
{
|
||||||
if (null !== $this->findArgument($argument->name(), $index) && !$replace) {
|
return (new \ArrayObject($this->items))->getIterator();
|
||||||
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
|
public function getItemsByType(string $type): \Iterator
|
||||||
{
|
{
|
||||||
if (null !== $this->findOption($option->name(), $index) && !$replace) {
|
return new InputTypeFilterIterator(
|
||||||
throw new \Exception("Option -{$option->name()} already exists in collection");
|
$this->getItems(),
|
||||||
}
|
[$type],
|
||||||
if (true == $replace && null !== $index) {
|
InputTypeFilterIterator::FILTER_INCLUDE
|
||||||
$this->options[$index] = $option;
|
);
|
||||||
} else {
|
|
||||||
$this->options[] = $option;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getArgumentsByIndex(int $index): ?AbstractInputType
|
public function getItemsExcludeByType(string $type): \Iterator
|
||||||
{
|
{
|
||||||
return $this->arguments[$index];
|
return new InputTypeFilterIterator(
|
||||||
|
$this->getItems(),
|
||||||
|
[$type],
|
||||||
|
InputTypeFilterIterator::FILTER_EXCLUDE
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getArguments(): array
|
public function getItemByIndex(int $index): ?AbstractInputType
|
||||||
{
|
{
|
||||||
return $this->arguments;
|
return $this->items[$index] ?? null;
|
||||||
}
|
|
||||||
|
|
||||||
public function getOptions(): array
|
|
||||||
{
|
|
||||||
return $this->options;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function merge(self ...$collections): self
|
public static function merge(self ...$collections): self
|
||||||
{
|
{
|
||||||
$arguments = [];
|
|
||||||
$options = [];
|
|
||||||
|
|
||||||
|
$iterator = new \AppendIterator;
|
||||||
foreach ($collections as $c) {
|
foreach ($collections as $c) {
|
||||||
$arguments = array_merge($arguments, $c->getArguments());
|
$iterator->append($c->getItems());
|
||||||
$options = array_merge($options, $c->getOptions());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$mergedCollection = new self();
|
$mergedCollection = new self();
|
||||||
|
foreach ($iterator as $input) {
|
||||||
$it = new \AppendIterator();
|
$mergedCollection->add($input, true);
|
||||||
$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;
|
return $mergedCollection;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,32 +9,37 @@ use pointybeard\Helpers\Foundation\Factory;
|
||||||
|
|
||||||
final class InputHandlerFactory extends Factory\AbstractFactory
|
final class InputHandlerFactory extends Factory\AbstractFactory
|
||||||
{
|
{
|
||||||
const FLAG_SKIP_VALIDATION = 0x0001;
|
public function getTemplateNamespace(): string
|
||||||
|
|
||||||
public static function getTemplateNamespace(): string
|
|
||||||
{
|
{
|
||||||
return __NAMESPACE__.'\\Handlers\\%s';
|
return __NAMESPACE__.'\\Handlers\\%s';
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getExpectedClassType(): ?string
|
public function getExpectedClassType(): ?string
|
||||||
{
|
{
|
||||||
return __NAMESPACE__.'\\Interfaces\\InputHandlerInterface';
|
return __NAMESPACE__.'\\Interfaces\\InputHandlerInterface';
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function build(string $name, InputCollection $collection = null, int $flags = null): Interfaces\InputHandlerInterface
|
public static function build(string $name, ...$arguments): object
|
||||||
{
|
{
|
||||||
|
|
||||||
|
// Since passing flags is optional, we can use array_pad
|
||||||
|
// to ensure there are always at least 2 elements in $arguments
|
||||||
|
[$collection, $flags] = array_pad($arguments, 2, null);
|
||||||
|
|
||||||
|
$factory = new self;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$handler = self::instanciate(
|
$handler = $factory->instanciate(
|
||||||
self::generateTargetClassName($name)
|
$factory->generateTargetClassName($name)
|
||||||
);
|
);
|
||||||
} catch (\Exception $ex) {
|
} catch (\Exception $ex) {
|
||||||
throw new Exceptions\UnableToLoadInputHandlerException($name, 0, $ex);
|
throw new Exceptions\UnableToLoadInputHandlerException($name, 0, $ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($collection instanceof InputCollection) {
|
if (null !== $collection) {
|
||||||
$handler->bind(
|
$handler->bind(
|
||||||
$collection,
|
$collection,
|
||||||
Flags\is_flag_set($flags, self::FLAG_SKIP_VALIDATION)
|
$flags
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
20
src/Input/InputTypeFactory.php
Normal file
20
src/Input/InputTypeFactory.php
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace pointybeard\Helpers\Cli\Input;
|
||||||
|
|
||||||
|
use pointybeard\Helpers\Foundation\Factory;
|
||||||
|
|
||||||
|
final class InputTypeFactory extends Factory\AbstractFactory
|
||||||
|
{
|
||||||
|
public function getTemplateNamespace(): string
|
||||||
|
{
|
||||||
|
return __NAMESPACE__.'\\Types\\%s';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExpectedClassType(): ?string
|
||||||
|
{
|
||||||
|
return __NAMESPACE__.'\\Interfaces\\InputTypeInterface';
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/Input/InputTypeFilterIterator.php
Normal file
39
src/Input/InputTypeFilterIterator.php
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace pointybeard\Helpers\Cli\Input;
|
||||||
|
|
||||||
|
class InputTypeFilterIterator extends \FilterIterator {
|
||||||
|
|
||||||
|
private $types;
|
||||||
|
private $mode;
|
||||||
|
|
||||||
|
public const FILTER_INCLUDE = 0x0001;
|
||||||
|
public const FILTER_EXCLUDE = 0x0002;
|
||||||
|
|
||||||
|
public function __construct(\Iterator $iterator, array $types=[], int $mode=self::FILTER_INCLUDE)
|
||||||
|
{
|
||||||
|
parent::__construct($iterator);
|
||||||
|
|
||||||
|
$this->types = array_map('strtolower', $types);
|
||||||
|
$this->mode = $mode;
|
||||||
|
|
||||||
|
}
|
||||||
|
public function accept()
|
||||||
|
{
|
||||||
|
$input = $this->getInnerIterator()->current();
|
||||||
|
|
||||||
|
switch($this->mode) {
|
||||||
|
case self::FILTER_EXCLUDE:
|
||||||
|
return !in_array($input->getType(), $this->types);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case self::FILTER_INCLUDE:
|
||||||
|
default:
|
||||||
|
return in_array($input->getType(), $this->types);
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,19 +8,13 @@ use pointybeard\Helpers\Cli\Input;
|
||||||
|
|
||||||
interface InputHandlerInterface
|
interface InputHandlerInterface
|
||||||
{
|
{
|
||||||
public function bind(Input\InputCollection $inputCollection, bool $skipValidation = false): bool;
|
public function bind(Input\InputCollection $inputCollection, ?int $flags = null): bool;
|
||||||
|
|
||||||
public function validate(): void;
|
public function validate(?int $flags = null): void;
|
||||||
|
|
||||||
public function getArgument(string $name): ?string;
|
public function find(string $name);
|
||||||
|
|
||||||
// note that the return value of getOption() isn't always going to be
|
public function getInput(): array;
|
||||||
// a string like getArgument()
|
|
||||||
public function getOption(string $name);
|
|
||||||
|
|
||||||
public function getArguments(): array;
|
|
||||||
|
|
||||||
public function getOptions(): array;
|
|
||||||
|
|
||||||
public function getCollection(): ?Input\InputCollection;
|
public function getCollection(): ?Input\InputCollection;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,4 +16,10 @@ interface InputTypeInterface
|
||||||
const FLAG_TYPE_INCREMENTING = 0x0400;
|
const FLAG_TYPE_INCREMENTING = 0x0400;
|
||||||
|
|
||||||
public function getType(): string;
|
public function getType(): string;
|
||||||
|
|
||||||
|
public function respondsTo(string $name): bool;
|
||||||
|
|
||||||
|
public function __toString(): string;
|
||||||
|
|
||||||
|
public function getDisplayName(): string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,20 +6,72 @@ namespace pointybeard\Helpers\Cli\Input\Types;
|
||||||
|
|
||||||
use pointybeard\Helpers\Cli\Input;
|
use pointybeard\Helpers\Cli\Input;
|
||||||
use pointybeard\Helpers\Functions\Strings;
|
use pointybeard\Helpers\Functions\Strings;
|
||||||
|
use pointybeard\Helpers\Functions\Cli;
|
||||||
|
|
||||||
class Argument extends Input\AbstractInputType
|
class Argument extends Input\AbstractInputType
|
||||||
{
|
{
|
||||||
public function __toString()
|
public function __construct(string $name = null, int $flags = null, string $description = null, object $validator = null, $default = null)
|
||||||
{
|
{
|
||||||
$name = strtoupper($this->name());
|
if (null === $validator) {
|
||||||
|
$validator = function (Input\AbstractInputType $input, Input\AbstractInputHandler $context) {
|
||||||
$first = str_pad(sprintf('%s ', $name), 20, ' ');
|
// This dummy validator is necessary otherwise the argument
|
||||||
|
// value is ALWAYS set to default (most often NULL) regardless
|
||||||
$second = Strings\utf8_wordwrap_array($this->description(), 40);
|
// of if the argument was set or not
|
||||||
for ($ii = 1; $ii < count($second); ++$ii) {
|
return $context->find($input->name());
|
||||||
$second[$ii] = str_pad('', 22, ' ', \STR_PAD_LEFT).$second[$ii];
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return $first.implode($second, PHP_EOL);
|
parent::__construct($name, $flags, $description, $validator, $default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDisplayName(): string
|
||||||
|
{
|
||||||
|
return strtoupper($this->name());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
// MAGIC VALUES!!! OH MY.....
|
||||||
|
$padCharacter = ' ';
|
||||||
|
$paddingBufferSize = 0.15; // 15%
|
||||||
|
$argumentNamePaddedWidth = 20;
|
||||||
|
$argumentNameMinimumPaddingWidth = 4;
|
||||||
|
$minimumWindowWidth = 80;
|
||||||
|
|
||||||
|
// Get the window dimensions but restrict width to minimum
|
||||||
|
// of $minimumWindowWidth
|
||||||
|
$window = Cli\get_window_size();
|
||||||
|
$window['cols'] = max($minimumWindowWidth, $window['cols']);
|
||||||
|
|
||||||
|
// This shrinks the total line length (derived by the window width) by
|
||||||
|
// $paddingBufferSize
|
||||||
|
$paddingBuffer = (int) ceil($window['cols'] * $paddingBufferSize);
|
||||||
|
|
||||||
|
// Create a string of $padCharacter which is prepended to each secondary
|
||||||
|
// line
|
||||||
|
$secondaryLineLeadPadding = str_pad(
|
||||||
|
'',
|
||||||
|
$argumentNamePaddedWidth,
|
||||||
|
$padCharacter,
|
||||||
|
STR_PAD_LEFT
|
||||||
|
);
|
||||||
|
|
||||||
|
$first = Strings\mb_str_pad(
|
||||||
|
$this->getDisplayName().str_repeat($padCharacter, $argumentNameMinimumPaddingWidth),
|
||||||
|
$argumentNamePaddedWidth,
|
||||||
|
$padCharacter
|
||||||
|
);
|
||||||
|
|
||||||
|
$second = Strings\utf8_wordwrap_array(
|
||||||
|
$this->description(),
|
||||||
|
$window['cols'] - $argumentNamePaddedWidth - $paddingBuffer
|
||||||
|
);
|
||||||
|
|
||||||
|
// Skip the first item (notice $ii starts at value of '1')
|
||||||
|
for ($ii = 1; $ii < count($second); ++$ii) {
|
||||||
|
$second[$ii] = $secondaryLineLeadPadding.$second[$ii];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $first.implode(PHP_EOL, $second);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
src/Input/Types/Flag.php
Normal file
18
src/Input/Types/Flag.php
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace pointybeard\Helpers\Cli\Input\Types;
|
||||||
|
|
||||||
|
use pointybeard\Helpers\Functions\Flags;
|
||||||
|
|
||||||
|
class Flag extends Option
|
||||||
|
{
|
||||||
|
public function __construct(string $name = null, int $flags = null, string $description = null, object $validator = null, $default = false)
|
||||||
|
{
|
||||||
|
if (Flags\is_flag_set($flags, self::FLAG_VALUE_REQUIRED) || Flags\is_flag_set($flags, self::FLAG_VALUE_OPTIONAL)) {
|
||||||
|
throw new \Exception('The flags FLAG_VALUE_REQUIRED and FLAG_VALUE_OPTIONAL cannot be used on an input of type Flag');
|
||||||
|
}
|
||||||
|
parent::__construct($name, null, $flags, $description, $validator, $default);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/Input/Types/IncrementingFlag.php
Normal file
18
src/Input/Types/IncrementingFlag.php
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace pointybeard\Helpers\Cli\Input\Types;
|
||||||
|
|
||||||
|
use pointybeard\Helpers\Functions\Flags;
|
||||||
|
|
||||||
|
class IncrementingFlag extends Flag
|
||||||
|
{
|
||||||
|
public function __construct(string $name = null, int $flags = null, string $description = null, object $validator = null, $default = 0)
|
||||||
|
{
|
||||||
|
if (Flags\is_flag_set($flags, self::FLAG_TYPE_INCREMENTING)) {
|
||||||
|
$flags = $flags | self::FLAG_TYPE_INCREMENTING;
|
||||||
|
}
|
||||||
|
parent::__construct($name, null, $flags, $description, $validator, $default);
|
||||||
|
}
|
||||||
|
}
|
||||||
89
src/Input/Types/LongOption.php
Normal file
89
src/Input/Types/LongOption.php
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace pointybeard\Helpers\Cli\Input\Types;
|
||||||
|
|
||||||
|
use pointybeard\Helpers\Functions\Flags;
|
||||||
|
use pointybeard\Helpers\Functions\Strings;
|
||||||
|
use pointybeard\Helpers\Functions\Cli;
|
||||||
|
use pointybeard\Helpers\Cli\Input;
|
||||||
|
|
||||||
|
class LongOption extends Input\AbstractInputType
|
||||||
|
{
|
||||||
|
protected $short;
|
||||||
|
|
||||||
|
public function __construct(string $name = null, string $short = null, int $flags = null, string $description = null, object $validator = null, $default = false)
|
||||||
|
{
|
||||||
|
$this->short = $short;
|
||||||
|
parent::__construct($name, $flags, $description, $validator, $default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function respondsTo(string $name): bool
|
||||||
|
{
|
||||||
|
return $name == $this->name || $name == $this->short;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDisplayName(): string
|
||||||
|
{
|
||||||
|
$short =
|
||||||
|
null !== $this->short()
|
||||||
|
? '-'.$this->short().', '
|
||||||
|
: null
|
||||||
|
;
|
||||||
|
|
||||||
|
return sprintf('%s--%s', $short, $this->name());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
// MAGIC VALUES!!! OH MY.....
|
||||||
|
$padCharacter = ' ';
|
||||||
|
$paddingBufferSize = 0.15; // 15%
|
||||||
|
$optionNamePaddedWidth = 30;
|
||||||
|
$minimumWindowWidth = 80;
|
||||||
|
$secondaryLineIndentlength = 2;
|
||||||
|
|
||||||
|
// Get the window dimensions but restrict width to minimum
|
||||||
|
// of $minimumWindowWidth
|
||||||
|
$window = Cli\get_window_size();
|
||||||
|
$window['cols'] = max($minimumWindowWidth, $window['cols']);
|
||||||
|
|
||||||
|
// This shrinks the total line length (derived by the window width) by
|
||||||
|
// $paddingBufferSize
|
||||||
|
$paddingBuffer = (int) ceil($window['cols'] * $paddingBufferSize);
|
||||||
|
|
||||||
|
// Create a string of $padCharacter which is prepended to each secondary
|
||||||
|
// line
|
||||||
|
$secondaryLineLeadPadding = str_pad(
|
||||||
|
'',
|
||||||
|
$optionNamePaddedWidth,
|
||||||
|
$padCharacter,
|
||||||
|
STR_PAD_LEFT
|
||||||
|
);
|
||||||
|
|
||||||
|
$long = $this->getDisplayName();
|
||||||
|
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 = Strings\mb_str_pad(
|
||||||
|
$long, // -O, --LONG,
|
||||||
|
$optionNamePaddedWidth,
|
||||||
|
$padCharacter
|
||||||
|
);
|
||||||
|
|
||||||
|
$second = Strings\utf8_wordwrap_array(
|
||||||
|
$this->description(),
|
||||||
|
$window['cols'] - $optionNamePaddedWidth - $paddingBuffer
|
||||||
|
);
|
||||||
|
|
||||||
|
for ($ii = 1; $ii < count($second); ++$ii) {
|
||||||
|
$second[$ii] = $secondaryLineLeadPadding.$second[$ii];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $first.implode(PHP_EOL, $second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,37 +4,57 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace pointybeard\Helpers\Cli\Input\Types;
|
namespace pointybeard\Helpers\Cli\Input\Types;
|
||||||
|
|
||||||
use pointybeard\Helpers\Functions\Flags;
|
|
||||||
use pointybeard\Helpers\Functions\Strings;
|
use pointybeard\Helpers\Functions\Strings;
|
||||||
|
use pointybeard\Helpers\Functions\Cli;
|
||||||
use pointybeard\Helpers\Cli\Input;
|
use pointybeard\Helpers\Cli\Input;
|
||||||
|
|
||||||
class Option extends Input\AbstractInputType
|
class Option extends Input\AbstractInputType
|
||||||
{
|
{
|
||||||
protected $long;
|
public function getDisplayName(): string
|
||||||
protected $default;
|
|
||||||
|
|
||||||
public function __construct(string $name, string $long = null, int $flags = null, string $description = null, object $validator = null, $default = false)
|
|
||||||
{
|
{
|
||||||
$this->default = $default;
|
return '-'.$this->name();
|
||||||
$this->long = $long;
|
|
||||||
parent::__construct($name, $flags, $description, $validator);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __toString()
|
public function __toString(): string
|
||||||
{
|
{
|
||||||
$long = null !== $this->long() ? ', --'.$this->long() : null;
|
// MAGIC VALUES!!! OH MY.....
|
||||||
if (null != $long) {
|
$padCharacter = ' ';
|
||||||
if (Flags\is_flag_set($this->flags(), self::FLAG_VALUE_REQUIRED)) {
|
$paddingBufferSize = 0.15; // 15%
|
||||||
$long .= '=VALUE';
|
$optionNamePaddedWidth = 30;
|
||||||
} elseif (Flags\is_flag_set($this->flags(), self::FLAG_VALUE_OPTIONAL)) {
|
$minimumWindowWidth = 80;
|
||||||
$long .= '[=VALUE]';
|
$secondaryLineIndentlength = 2;
|
||||||
}
|
|
||||||
}
|
// Get the window dimensions but restrict width to minimum
|
||||||
$first = str_pad(sprintf('-%s%s ', $this->name(), $long), 36, ' ');
|
// of $minimumWindowWidth
|
||||||
|
$window = Cli\get_window_size();
|
||||||
|
$window['cols'] = max($minimumWindowWidth, $window['cols']);
|
||||||
|
|
||||||
|
// This shrinks the total line length (derived by the window width) by
|
||||||
|
// $paddingBufferSize
|
||||||
|
$paddingBuffer = (int) ceil($window['cols'] * $paddingBufferSize);
|
||||||
|
|
||||||
|
// Create a string of $padCharacter which is prepended to each secondary
|
||||||
|
// line
|
||||||
|
$secondaryLineLeadPadding = str_pad(
|
||||||
|
'',
|
||||||
|
$optionNamePaddedWidth,
|
||||||
|
$padCharacter,
|
||||||
|
STR_PAD_LEFT
|
||||||
|
);
|
||||||
|
|
||||||
|
$first = Strings\mb_str_pad(
|
||||||
|
$this->getDisplayName(),
|
||||||
|
$optionNamePaddedWidth,
|
||||||
|
$padCharacter
|
||||||
|
);
|
||||||
|
|
||||||
|
$second = Strings\utf8_wordwrap_array(
|
||||||
|
$this->description(),
|
||||||
|
$window['cols'] - $optionNamePaddedWidth - $paddingBuffer
|
||||||
|
);
|
||||||
|
|
||||||
$second = Strings\utf8_wordwrap_array($this->description(), 40);
|
|
||||||
for ($ii = 1; $ii < count($second); ++$ii) {
|
for ($ii = 1; $ii < count($second); ++$ii) {
|
||||||
$second[$ii] = str_pad('', 38, ' ', \STR_PAD_LEFT).$second[$ii];
|
$second[$ii] = $secondaryLineLeadPadding.$second[$ii];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $first.implode($second, PHP_EOL);
|
return $first.implode($second, PHP_EOL);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue