diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..08526d4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +/tests export-ignore +/examples export-ignore +/.gitignore export-ignore +/.travis.yml export-ignore +/phpunit.xml export-ignore +/phpcs.xml export-ignore +/PULL_REQUEST_TEMPLATE.md export-ignore +/CODE_OF_CONDUCT.md export-ignore +/CONTRIBUTING.md export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1502b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +vendor/ +composer.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f7c093f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +language: php +php: + - '7.2' + - '7.3' + +before_script: + - yes '' | pecl install yaml + - composer install + +script: + - composer validate --strict + - vendor/bin/phpunit --coverage-text + - vendor/bin/phpcs src/ tests/ + +cache: + directories: + - $HOME/.composer/cache diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..78af89d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 1.0.0 - 2020-02-27 + +### Added +- Created the initial implementation of the command package. + +# Versions +- [1.0.0 > Unreleased](https://github.com/ulrack/command/compare/1.0.0...HEAD) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..040dd3e --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at info@jyxon.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7921a76 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,28 @@ +# Contributing + +To contribute to this package, please keep to these guidelines. + +- Fork the package. +- Create a branch per feature. +- Commit your changes to these branches. +- Create a pull request per feature to the master branch of the original repository. + +## Pull requests + +Pull request should follow these rules, before they can get accepted. + +- Follow the [pull request template](PULL_REQUEST_TEMPLATE.md). +- Contains a short but complete description. +- Has passed all test command listed bellow. + +## Running Tests + +``` bash +$ vendor/bin/phpunit --coverage-text +$ vendor/bin/phpcs src/ tests/ +``` + +## Notes + +Multiple commits per feature are allowed, but please provide a good description in your pull request. +This description will be used to squash your feature into the master branch. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..457440e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Jyxon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..ce6ccf9 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,18 @@ +# (bugfix / feature / refactor / etc.): Short descriptive title + +## Description + +Fixes # + +Changes proposed in this pull request: +- +- +- + +Make sure you can check the following boxes before submitting the pull request: +- [] The proposed changes work with the dependencies from composer. +- [] The proposed changes have been thoroughly tested. +- [] The proposed changes don't alter the code in such a manner that it conflicts with the initial purpose of the package. + +Optional checkbox +- [] Backwards incompatible diff --git a/README.md b/README.md new file mode 100644 index 0000000..1032f40 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +[![Build Status](https://travis-ci.com/ulrack/command.svg?branch=master)](https://travis-ci.com/ulrack/command) + +# Ulrack Command + +This package supplies command routing for PHP applications. + +## Installation + +To install the package run the following command: + +``` +composer require ulrack/command +``` + +## Usage + +### [Command configuration](src/Dao/CommandConfiguration.php) +Command configuration is done through an object. +The main command configuration should be an empty instance of this class. +This main object can then be supplied to the [router](src/Common/Router/RouterInterface.php). + +All sub-commands can be added to their respective command configuration instance. +This can be infinitely deep. +All command configuration instances have the `no-interaction` and `help` flag configured by default. +Sub-commands of a parent command can be executed by separating them by a space. + +### [Router](src/Component/Router/CommandRouter.php) +The command router performs the routing of commands. +It creates an instance of the `service` and executes it. +This service object must implement the [CommandInterface](src/Common/Command/CommandInterface.php). + +To find out more about how services work, see the `ulrack/services` package. + +### Input and Output +The [input](src/Component/Command/Input.php) is an object which provides the input to the implementation of the command. +The [output](src/Component/Command/Output.php) provides a standard set of methods for displaying output to the user of the application. +The input instance can be created by providing the `$argv` to the `create` method of the [InputFactory](src/Factory/InputFactory.php). + +### Standard commands +The package contains two standard commands for displaying a command [list](src/Command/ListCommandsCommand.php) +and showing a command [explanation](src/Command/HelpCommand.php). + +## Examples + +An example can be found in the [examples](examples) directory. + +## Change log + +Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) and [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) for details. + +## MIT License + +Copyright (c) Jyxon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7a83dcb --- /dev/null +++ b/composer.json @@ -0,0 +1,52 @@ +{ + "name": "ulrack/command", + "description": "Command routing for PHP applications.", + "keywords": ["cli", "command"], + "type": "library", + "license": "MIT", + "prefer-stable": true, + "minimum-stability": "stable", + "require": { + "php": "^7.2", + "ulrack/cli": "^1.0", + "ulrack/configuration": "^1.0", + "ulrack/services": "^1.0", + "ulrack/task": "^1.0", + "ulrack/validator": "^1.0" + }, + "authors": [{ + "name": "Ulrack", + "homepage": "https://www.ulrack.com/", + "role": "Developer" + }], + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { + "Ulrack\\Command\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Ulrack\\Command\\Tests\\": "tests/" + } + }, + "archive": { + "exclude": [ + "/examples", + "/tests", + "/.gitignore", + "/.travis.yml", + "/phpunit.xml", + "/phpcs.xml", + "/PULL_REQUEST_TEMPLATE.md", + "/CODE_OF_CONDUCT.md", + "/CONTRIBUTING.md" + ] + }, + "require-dev": { + "phpunit/phpunit": "^8.0", + "squizlabs/php_codesniffer": "^3.4" + } +} diff --git a/configuration/schema/command.schema.json b/configuration/schema/command.schema.json new file mode 100644 index 0000000..b0c67fb --- /dev/null +++ b/configuration/schema/command.schema.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "$id": "command.schema.json", + "type": "object", + "properties": { + "command": { + "type": "array", + "items": { + "type": "string" + } + }, + "service": { + "type": "string" + }, + "description": { + "type": "string" + }, + "flags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "long": { + "type": "string" + }, + "short": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "anyOf": [ + { + "required": ["long"] + }, + { + "required": ["short"] + } + ] + } + }, + "parameters": { + "type": "array", + "items": { + "type": "object", + "properties": { + "long": { + "type": "string" + }, + "short": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["string", "number", "array"] + }, + "hidden": { + "type": "boolean", + "default": false + }, + "options": { + "type": "array" + }, + "required": { + "type": "boolean", + "default": false + } + }, + "anyOf": [ + { + "required": ["long", "type"] + }, + { + "required": ["short", "type"] + } + ] + } + } + }, + "required": ["command", "service"] +} \ No newline at end of file diff --git a/examples/command-showcase.php b/examples/command-showcase.php new file mode 100644 index 0000000..e08ec10 --- /dev/null +++ b/examples/command-showcase.php @@ -0,0 +1,173 @@ +create($argv); + +$configuration = new CommandConfiguration(); + +$inputter = new CommandConfiguration( + 'services.inputter', + 'Dumps all the input you supplied to the command.' +); + +$inputter->addCommandConfiguration( + 'foo', + new CommandConfiguration( + 'services.foo', + 'A duplicate of the parent.', + [ + // @see: configuration/schema/command.schema.json + [ + 'long' => 'parameter', + 'short' => 'p', + 'type' => 'string', + 'description' => 'A parameter', + 'required' => true + ] + ] + ) +); + +$configuration->addCommandConfiguration( + 'inputter', + $inputter +); + +// A simplified version of the Service factory for this example. +$serviceFactory = new class implements ServiceFactoryInterface { + /** + * Retrieve the interpreted value of a service. + * + * @param string $key + * + * @return mixed + */ + public function create(string $key) + { + return new class implements CommandInterface { + /** + * Executes the command. + * + * @param InputInterface $input + * @param OutputInterface $output + * + * @return void + */ + public function __invoke( + InputInterface $input, + OutputInterface $output + ): void { + $output->outputText('Command: ', false); + $output->outputText(implode( + ' ', + $input->getCommand() + ), true, 'title'); + $output->writeLine(''); + + $output->outputText('The parameters you supplied:'); + $parameters = $input->getParameters(); + if (count($parameters) > 0) { + $output->outputExplainedList($parameters); + $output->writeLine(''); + } else { + $output->outputText('None.'); + } + + $output->outputText('The flags you supplied:'); + $flags = $input->getFlags(); + if (count($flags) > 0) { + $output->outputList($flags); + $output->writeLine(''); + } else { + $output->outputText('Nothing.'); + } + } + }; + } + + /** + * Adds an extension to the service factory. + * + * @param string $key + * @param string $class + * @param array $parameters + * + * @return void + */ + public function addExtension( + string $key, + string $class, + array $parameters = [] + ): void { + return; + } + + /** + * Adds a hook to the key connected to an extension. + * + * @param string $key + * @param string $class + * @param int $sortOrder + * @param array $parameters + * + * @return void + */ + public function addHook( + string $key, + string $class, + int $sortOrder, + array $parameters = [] + ): void { + return; + } +}; + +$ioFactory = new IoFactory(); +$theme = (new DefaultTheme( + new ThemeGenerator( + new ThemeFactory( + $ioFactory + ) + ) +))->getTheme(); + +$elementFactory = new ElementFactory($theme, $ioFactory); + +$formGenerator = new FormGenerator( + new FormFactory($ioFactory, $theme), + $elementFactory +); + +$router = new CommandRouter( + $configuration, + $serviceFactory, + new ElementFactory($theme, $ioFactory, true), + $ioFactory, + new Output( + $formGenerator, + $ioFactory, + $theme, + $elementFactory + ), + $formGenerator +); + +exit($router->__invoke($input)); diff --git a/locator.php b/locator.php new file mode 100644 index 0000000..0940d89 --- /dev/null +++ b/locator.php @@ -0,0 +1,9 @@ + + + + + + + + + + + + + diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..e1e548a --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,15 @@ + + + + tests + + + + + src + + + diff --git a/src/Command/HelpCommand.php b/src/Command/HelpCommand.php new file mode 100644 index 0000000..10f4efc --- /dev/null +++ b/src/Command/HelpCommand.php @@ -0,0 +1,147 @@ +commandConfiguration = $commandConfiguration; + } + + /** + * Executes the command. + * + * @param InputInterface $input + * @param OutputInterface $output + * + * @return void + */ + public function __invoke( + InputInterface $input, + OutputInterface $output + ): void { + $output->outputText( + 'Command: ' . implode(' ', $input->getCommand()), + true, + 'title' + ); + + $description = $this->commandConfiguration->getDescription(); + if (!empty($description)) { + $output->outputText('Description: ' . $description); + } + + if (count($this->commandConfiguration->getParameters()) > 0) { + $output->writeLine(''); + + $output->outputText( + 'Parameters: ', + true, + 'title' + ); + + $output->outputExplainedList( + $this->constructParametersList() + ); + } + + if (count($this->commandConfiguration->getFlags()) > 0) { + $output->writeLine(''); + + $output->outputText( + 'Flags: ', + true, + 'title' + ); + + $output->outputExplainedList( + $this->constructFlagsList() + ); + } + } + + /** + * Constructs the explained flags list. + * + * @return array + */ + private function constructFlagsList(): array + { + $list = []; + + foreach ($this->commandConfiguration->getFlags() as $flag) { + $options = []; + + if (isset($flag['long'])) { + $options[] = $flag['long']; + } + + if (isset($flag['short'])) { + $options[] = $flag['short']; + } + + $list[sprintf( + '[%s]', + implode('|', $options) + )] = $flag['description'] ?? ''; + } + + return $list; + } + + /** + * Constructs the explained parameters list. + * + * @return array + */ + private function constructParametersList(): array + { + $list = []; + + foreach ($this->commandConfiguration->getParameters() as $parameter) { + $options = []; + + if (isset($parameter['long'])) { + $options[] = $parameter['long']; + } + + if (isset($parameter['short'])) { + $options[] = $parameter['short']; + } + + $list[sprintf( + '[%s](%s)%s', + implode('|', $options), + $parameter['type'], + isset($parameter['required']) && $parameter['required'] + ? '*' + : '' + )] = $parameter['description'] ?? ''; + } + + return $list; + } +} diff --git a/src/Command/ListCommandsCommand.php b/src/Command/ListCommandsCommand.php new file mode 100644 index 0000000..89e927d --- /dev/null +++ b/src/Command/ListCommandsCommand.php @@ -0,0 +1,182 @@ +commandConfiguration = $commandConfiguration; + } + + /** + * Executes the command. + * + * @param InputInterface $input + * @param OutputInterface $output + * + * @return void + */ + public function __invoke( + InputInterface $input, + OutputInterface $output + ): void { + $output->outputText( + 'Command list: ' . implode(' ', $input->getCommand()), + true, + 'title' + ); + + $description = $this->commandConfiguration->getDescription(); + if (!empty($description)) { + $output->outputText('Description: ' . $description); + } + + // Output an empty line. + $output->writeLine(''); + + $output->outputExplainedList( + $this->constructCommandList($this->commandConfiguration), + 'command-explained-list-key', + 'command-explained-list-description' + ); + + if (count($this->commandConfiguration->getParameters()) > 0) { + $output->writeLine(''); + + $output->outputText( + 'Parameters: ', + true, + 'title' + ); + + $output->outputExplainedList( + $this->constructParametersList() + ); + } + + if (count($this->commandConfiguration->getFlags()) > 0) { + $output->writeLine(''); + + $output->outputText( + 'Flags: ', + true, + 'title' + ); + + $output->outputExplainedList( + $this->constructFlagsList() + ); + } + } + + /** + * Constructs the explained flags list. + * + * @return array + */ + private function constructFlagsList(): array + { + $list = []; + + foreach ($this->commandConfiguration->getFlags() as $flag) { + $options = []; + + if (isset($flag['long'])) { + $options[] = $flag['long']; + } + + if (isset($flag['short'])) { + $options[] = $flag['short']; + } + + $list[sprintf( + '[%s]', + implode('|', $options) + )] = $flag['description'] ?? ''; + } + + return $list; + } + + /** + * Constructs the explained parameters list. + * + * @return array + */ + private function constructParametersList(): array + { + $list = []; + + foreach ($this->commandConfiguration->getParameters() as $parameter) { + $options = []; + + if (isset($parameter['long'])) { + $options[] = $parameter['long']; + } + + if (isset($parameter['short'])) { + $options[] = $parameter['short']; + } + + $list[sprintf( + '[%s](%s)%s', + implode('|', $options), + $parameter['type'], + isset($parameter['required']) && $parameter['required'] + ? '*' + : '' + )] = $parameter['description'] ?? ''; + } + + return $list; + } + + /** + * Constructs the command list from the configuration. + * + * @param CommandConfigurationInterface $configuration + * @param string $prefix + * + * @return array + */ + private function constructCommandList( + CommandConfigurationInterface $configuration, + string $prefix = '' + ): array { + $list = []; + + foreach ($configuration->getCommands() as $command) { + $subConfiguration = $configuration->getCommand($command); + $list[$prefix . $command] = $subConfiguration->getDescription(); + $list = array_merge($list, $this->constructCommandList( + $subConfiguration, + $prefix . $command . '.' + )); + } + + return $list; + } +} diff --git a/src/Common/Command/CommandInterface.php b/src/Common/Command/CommandInterface.php new file mode 100644 index 0000000..4c32684 --- /dev/null +++ b/src/Common/Command/CommandInterface.php @@ -0,0 +1,23 @@ +command = $command; + $this->parameters = $parameters; + $this->flags = $flags; + } + + /** + * Loads configuration into the input. + * + * @param CommandConfigurationInterface $commandConfiguration + * + * @return void + */ + public function loadConfiguration( + CommandConfigurationInterface $commandConfiguration + ): void { + $this->commandConfiguration = $commandConfiguration; + } + + /** + * Checks whether the flag is set. + * + * @param string $flag + * + * @return bool + */ + public function hasParameter(string $parameter): bool + { + if (array_key_exists($parameter, $this->parameters)) { + return true; + } + + if ($this->commandConfiguration !== null) { + foreach ($this->commandConfiguration + ->getParameters() as $parameterInput) { + $long = $parameterInput['long'] ?? ''; + $short = $parameterInput['short'] ?? ''; + if ($long === $parameter || $short === $parameter) { + if (array_key_exists($long, $this->parameters) + || array_key_exists($short, $this->parameters)) { + return true; + } + } + } + } + + return false; + } + + /** + * Sets the value of a parameter. + * + * @param string $parameter + * @param mixed $value + * + * @return void + */ + public function setParameter(string $parameter, $value): void + { + $this->parameters[$parameter] = $value; + } + + /** + * Retrieves the parameter from the input. + * + * @param string $parameter + * + * @return mixed + */ + public function getParameter(string $parameter) + { + if (array_key_exists($parameter, $this->parameters)) { + return $this->parameters[$parameter]; + } + + if ($this->commandConfiguration !== null) { + foreach ($this->commandConfiguration->getParameters() as $parameterInput) { + $long = $parameterInput['long'] ?? ''; + $short = $parameterInput['short'] ?? ''; + if ($long === $parameter || $short === $parameter) { + if (array_key_exists($long, $this->parameters)) { + return $this->parameters[$long]; + } elseif (array_key_exists($short, $this->parameters)) { + return $this->parameters[$short]; + } + } + } + } + + return null; + } + + /** + * Checks whether the flag is set. + * + * @param string $flag + * + * @return bool + */ + public function isSetFlag(string $flag): bool + { + if (in_array($flag, $this->flags)) { + return true; + } + + if ($this->commandConfiguration !== null) { + foreach ($this->commandConfiguration->getFlags() as $configFlag) { + if ($configFlag['long'] === $flag + || $configFlag['short'] === $flag) { + if (in_array($configFlag['long'], $this->flags) + || in_array($configFlag['short'], $this->flags)) { + return true; + } + } + } + } + + return false; + } + + /** + * Returns the command. + * + * @return string[] + */ + public function getCommand(): array + { + return $this->command; + } + + /** + * Returns the parameters. + * + * @return array + */ + public function getParameters(): array + { + return $this->parameters; + } + + /** + * Returns the flags. + * + * @return array + */ + public function getFlags(): array + { + return $this->flags; + } +} diff --git a/src/Component/Command/Output.php b/src/Component/Command/Output.php new file mode 100644 index 0000000..b9313fa --- /dev/null +++ b/src/Component/Command/Output.php @@ -0,0 +1,263 @@ +formGenerator = $formGenerator; + $this->writer = $ioFactory->createStandardWriter(); + $this->theme = $theme; + $this->elementFactory = $elementFactory; + } + + /** + * Writes the output to the user. + * + * @param string $input + * @param string $style + * + * @return void + */ + public function write(string $input, string $style = 'text'): void + { + $style = $this->theme->getStyle($style); + $style->apply(); + $this->writer->write($input); + $style->reset(); + } + + /** + * Writes the output in a line to the user. + * + * @param string $input + * @param string $style + * + * @return void + */ + public function writeLine(string $input, string $style = 'text'): void + { + $style = $this->theme->getStyle($style); + $style->apply(); + $this->writer->writeLine($input); + $style->reset(); + } + + /** + * Writes a mutable line to the user. + * The next writer will overwrite this line. + * + * @param string $input + * @param string $style + * + * @return void + */ + public function overWrite(string $input, string $style = 'text'): void + { + $style = $this->theme->getStyle($style); + $style->apply(); + $this->writer->overWrite($input); + $style->reset(); + } + + /** + * Retrieves the form generator. + * + * @return FormGeneratorInterface + */ + public function getFormGenerator(): FormGeneratorInterface + { + return $this->formGenerator; + } + + /** + * Output a text element. + * + * @param string $content + * @param bool $newLine + * @param string $styleKey + * + * @return void + */ + public function outputText( + string $content, + bool $newLine = true, + string $styleKey = 'text' + ): void { + $this->elementFactory->createText( + $content, + $newLine, + $styleKey + )->render(); + } + + /** + * Outputs a table. + * + * @param string[] $keys + * @param array $items + * @param string $tableCharacters + * @param string $style + * @param string $boxStyle + * @param string $keyStyle + * + * @return void + */ + public function outputTable( + array $keys, + array $items, + string $tableCharacters = 'table-characters', + string $style = 'table-style', + string $boxStyle = 'table-box-style', + string $keyStyle = 'table-key-style' + ): void { + $this->elementFactory->createTable( + $keys, + $items, + $tableCharacters, + $style, + $boxStyle, + $keyStyle + )->render(); + } + + /** + * Output a list. + * + * @param array $items + * @param string $style + * + * @return void + */ + public function outputList( + array $items, + string $style = 'list' + ): void { + $this->elementFactory->createList( + $items, + $style + )->render(); + } + + /** + * Output an explained list. + * + * @param array $items + * @param string $keyStyle + * @param string $descriptionStyle + * + * @return void + */ + public function outputExplainedList( + array $items, + string $keyStyle = 'explained-list-key', + string $descriptionStyle = 'explained-list-description' + ): void { + $this->elementFactory->createExplainedList( + $items, + $keyStyle, + $descriptionStyle + )->render(); + } + + /** + * Output a block. + * + * @param string $content + * @param string $style + * @param string $padding + * @param string $margin + * + * @return void + */ + public function outputBlock( + string $content, + string $style = 'block', + string $padding = 'block-padding', + string $margin = 'block-margin' + ): void { + $this->elementFactory->createBlock( + $content, + $style, + $padding, + $margin + )->render(); + } + + /** + * Output a progress bar. + * + * @param TaskListInterface $taskList + * @param string $progressCharacters + * @param string $textStyle + * @param string $progressStyle + * + * @return void + */ + public function outputProgressBar( + TaskListInterface $taskList, + string $progressCharacters = 'progress-characters', + string $textStyle = 'progress-text', + string $progressStyle = 'progress-bar' + ): void { + $this->elementFactory->createProgressBar( + $taskList, + $progressCharacters, + $textStyle, + $progressStyle + )->render(); + } +} diff --git a/src/Component/Router/CommandRouter.php b/src/Component/Router/CommandRouter.php new file mode 100644 index 0000000..58f749c --- /dev/null +++ b/src/Component/Router/CommandRouter.php @@ -0,0 +1,357 @@ +commandConfiguration = $commandConfiguration; + $this->serviceFactory = $serviceFactory; + $this->errorElementFactory = $errorElementFactory; + $this->ioFactory = $ioFactory; + $this->output = $output; + $this->formGenerator = $formGenerator; + } + + /** + * Resolves the input to a command, executes it and returns the exit code. + * + * @param InputInterface $input + * + * @return int + */ + public function __invoke(InputInterface $input): int + { + $command = $input->getCommand(); + $originalCommand = $command; + try { + $command = $this->findCommand($originalCommand); + $input->loadConfiguration($command); + if ($input->isSetFlag('no-interaction')) { + $this->ioFactory->setAllowReading(false); + } + + $serviceKey = $command->getService(); + if ($input->isSetFlag('help')) { + (new HelpCommand($command))->__invoke($input, $this->output); + + return 0; + } + + if ($serviceKey !== '') { + try { + foreach ($this->getMissingParameters( + $input, + $command + ) as $key => $value) { + $input->setParameter($key, $value); + } + } catch (MisconfiguredCommandException $exception) { + throw new CommandCanNotExecuteException( + $originalCommand, + $exception->getMessage() + ); + } + + /** @var CommandInterface $command */ + $this->serviceFactory + ->create($serviceKey) + ->__invoke($input, $this->output); + + return 0; + } + + if (count($command->getCommands()) > 0) { + (new ListCommandsCommand($command))->__invoke( + $input, + $this->output + ); + + return 0; + } + + throw new CommandCanNotExecuteException( + $originalCommand, + 'Service definition not configured.' + ); + } catch (Throwable $exception) { + $this->errorElementFactory->createBlock( + $exception->getMessage(), + 'error-block' + ); + + $code = $exception->getCode(); + + return is_string($code) || !$code ? 1 : $code; + } + } + + /** + * Resolves the command iteratively. + * + * @param array $command + * + * @return CommandConfigurationInterface + * + * @throws CommandNotFoundException When the command can not be found. + */ + private function findCommand( + array $command + ): CommandConfigurationInterface { + $configuration = $this->commandConfiguration; + $originalCommand = $command; + foreach ($command as $key => $item) { + if ($configuration->hasCommand($item)) { + $configuration = $configuration->getCommand($item); + + continue; + } + + throw new CommandNotFoundException($originalCommand, $key); + } + + return $configuration; + } + + /** + * Generates a form asks the user to fill in the blanks. + * + * @param InputInterface $input + * + * @return array + * + * @throws MisconfiguredCommandException When the command configuration is incorrect. + */ + private function getMissingParameters( + InputInterface $input, + CommandConfigurationInterface $configuration + ): array { + $missing = false; + $this->formGenerator->init( + 'Missing parameters', + 'The following parameters were required and missing. '. + 'Please fill them in before execution procceeds.' + ); + + foreach ($configuration->getParameters() as $parameter) { + if (isset($parameter['required']) + && $parameter['required']) { + if (!$input->hasParameter( + $parameter['long'] ?? $parameter['short'] + )) { + $missing = true; + if (isset($parameter['hidden']) + && $parameter['hidden']) { + $this->createHiddenField($parameter); + + continue; + } elseif (isset($parameter['options']) + && is_array($parameter['options'])) { + $this->createAutocompletingField($parameter); + + continue; + } + + $this->createOpenField($parameter); + } + } + } + + $form = $this->formGenerator->getForm(); + + if ($missing) { + $form->render(); + + return $form->getInput(); + } + + return []; + } + + /** + * Creates the hidden field for the form generator. + * + * @param array $parameter + * + * @return void + * + * @throws MisconfiguredCommandException When the parameter is not configured correctly. + */ + private function createHiddenField(array $parameter): void + { + if ($parameter['type'] === 'array') { + $this->formGenerator->addHiddenArrayField( + $parameter['long'] ?? $parameter['short'], + true + ); + + return; + } elseif (in_array($parameter['type'], ['string', 'number'])) { + $this->formGenerator->addHiddenField( + $parameter['long'] ?? $parameter['short'], + true, + sprintf( + 'This field is required, and must be a %s', + $parameter['type'] + ), + $parameter['type'] === 'string' + ? new StringValidator() + : new PatternValidator('[0-9]+') + ); + + return; + } + + throw new MisconfiguredCommandException(); + } + + /** + * Creates the autocompleting field for the form generator. + * + * @param array $parameter + * + * @return void + * + * @throws MisconfiguredCommandException When the parameter is not configured correctly. + */ + private function createAutocompletingField(array $parameter): void + { + if ($parameter['type'] === 'array') { + $this->formGenerator->addAutocompletingArrayField( + $parameter['long'] ?? $parameter['short'], + $parameter['options'], + true + ); + + return; + } elseif (in_array($parameter['type'], ['string', 'number'])) { + $this->formGenerator->addAutocompletingField( + $parameter['long'] ?? $parameter['short'], + $parameter['options'], + true + ); + + return; + } + + throw new MisconfiguredCommandException(); + } + + /** + * Creates the autocompleting field for the form generator. + * + * @param array $parameter + * + * @return void + * + * @throws MisconfiguredCommandException When the parameter is not configured correctly. + */ + private function createOpenField(array $parameter): void + { + if ($parameter['type'] === 'array') { + $this->formGenerator->addOpenArrayField( + $parameter['long'] ?? $parameter['short'], + true + ); + + return; + } elseif (in_array($parameter['type'], ['string', 'number'])) { + $this->formGenerator->addOpenField( + $parameter['long'] ?? $parameter['short'], + true, + sprintf( + 'This field is required, and must be a %s', + $parameter['type'] + ), + $parameter['type'] === 'string' + ? new StringValidator() + : new NumberValidator() + ); + + return; + } + + throw new MisconfiguredCommandException(); + } +} diff --git a/src/Dao/CommandConfiguration.php b/src/Dao/CommandConfiguration.php new file mode 100644 index 0000000..cafcd16 --- /dev/null +++ b/src/Dao/CommandConfiguration.php @@ -0,0 +1,170 @@ +service = $service; + $this->description = $description; + $this->parameters = $parameters; + $this->flags = array_merge( + $flags, + [ + [ + 'long' => 'help', + 'short' => 'h', + 'description' => 'Explains the command.' + ], + [ + 'long' => 'no-interaction', + 'short' => 'ni', + 'description' => 'Prevents interaction during the execution of a command.' + ] + ] + ); + } + + /** + * Adds a nested command configuration to the configuration. + * + * @param string $command + * @param CommandConfigurationInterface $configuration + * + * @return void + */ + public function addCommandConfiguration( + string $command, + CommandConfigurationInterface $configuration + ): void { + $this->commands[$command] = $configuration; + } + + /** + * Retrieves the allowed flags for the command. + * + * @return array + */ + public function getFlags(): array + { + return $this->flags; + } + + /** + * Retrieves the allowed parameters. + * + * @return array + */ + public function getParameters(): array + { + return $this->parameters; + } + + /** + * Retrieves a command configuration nested inside the configuration. + * + * @param string $command + * + * @return CommandConfigurationInterface + */ + public function getCommand(string $command): CommandConfigurationInterface + { + return $this->commands[$command]; + } + + /** + * Retrieves a command configuration nested inside the configuration. + * + * @param string $command + * + * @return bool + */ + public function hasCommand(string $command): bool + { + return isset($this->commands[$command]); + } + + /** + * Retrieves the service key. + * + * @return string + */ + public function getService(): string + { + return $this->service; + } + + /** + * Retrieves all nested commands. + * + * @return string[] + */ + public function getCommands(): array + { + return array_keys($this->commands); + } + + /** + * Retrieves the description for the command configuration. + * + * @return string + */ + public function getDescription(): string + { + return $this->description; + } +} diff --git a/src/Exception/CommandCanNotExecuteException.php b/src/Exception/CommandCanNotExecuteException.php new file mode 100644 index 0000000..423e71b --- /dev/null +++ b/src/Exception/CommandCanNotExecuteException.php @@ -0,0 +1,30 @@ + 0) { + $expArg = explode('=', $argument); + + if (substr($expArg[0], -2, 2) === '[]') { + // Array markdown is used. + $parameters[rtrim( + ltrim($expArg[0], '-'), + '[]' + )][] = $expArg[1]; + + continue; + } + + $parameters[ltrim($expArg[0], '-')] = $expArg[1]; + + continue; + } + + // No subsequent value starting without a "-" + // It must be a flag + if (empty($arguments[0]) + || substr($arguments[0], 0, 1) === '-' + ) { + $flags[] = ltrim($argument, '-'); + + continue; + } + + // There was a subsequent value starting without a "-" + // It counts as a parameter + $argument = ltrim($argument, '-'); + + if (substr($argument, -2, 2) === '[]') { + // Array markdown is used. + $parameters[rtrim($argument, '[]')][] = array_shift( + $arguments + ); + + continue; + } + + $parameters[$argument] = array_shift($arguments); + + continue; + } + + // Additional arguments go into the command array + $command[] = $argument; + } + + return [$command, $parameters, $flags]; + } +} diff --git a/tests/Command/HelpCommandTest.php b/tests/Command/HelpCommandTest.php new file mode 100644 index 0000000..f68d91b --- /dev/null +++ b/tests/Command/HelpCommandTest.php @@ -0,0 +1,76 @@ +createMock(CommandConfigurationInterface::class); + $subject = new HelpCommand($commandConfiguration); + + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); + + $commandConfiguration->expects(static::once()) + ->method('getDescription') + ->willReturn('foo'); + + $commandConfiguration->expects(static::exactly(2)) + ->method('getParameters') + ->willReturn([ + [ + 'long' => 'foo', + 'short' => 'f', + 'type' => 'string', + 'required' => true, + 'description' => 'foo description' + ], + [ + 'long' => 'bar', + 'short' => 'b', + 'type' => 'number', + 'required' => false, + 'description' => 'foo description' + ] + ]); + + $commandConfiguration->expects(static::exactly(2)) + ->method('getFlags') + ->willReturn([ + [ + 'long' => 'baz', + 'short' => 'b', + 'description' => 'baz description' + ], + [ + 'long' => 'qux', + 'short' => 'q', + 'description' => 'qux description' + ] + ]); + + $subject->__invoke($input, $output); + } +} diff --git a/tests/Command/ListCommandsCommandTest.php b/tests/Command/ListCommandsCommandTest.php new file mode 100644 index 0000000..c67c332 --- /dev/null +++ b/tests/Command/ListCommandsCommandTest.php @@ -0,0 +1,94 @@ +createMock(CommandConfigurationInterface::class); + $subject = new ListCommandsCommand($commandConfiguration); + + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); + + $commandConfiguration->expects(static::once()) + ->method('getDescription') + ->willReturn('foo'); + + $commandConfiguration->expects(static::once()) + ->method('getCommands') + ->willReturn(['foo']); + + $commandConfiguration->expects(static::once()) + ->method('getCommand') + ->with('foo') + ->willReturn( + $this->createConfiguredMock( + CommandConfigurationInterface::class, + [ + 'getDescription' => 'foo description', + 'getCommands' => [] + ] + ) + ); + + $commandConfiguration->expects(static::exactly(2)) + ->method('getParameters') + ->willReturn([ + [ + 'long' => 'foo', + 'short' => 'f', + 'type' => 'string', + 'required' => true, + 'description' => 'foo description' + ], + [ + 'long' => 'bar', + 'short' => 'b', + 'type' => 'number', + 'required' => false, + 'description' => 'foo description' + ] + ]); + + $commandConfiguration->expects(static::exactly(2)) + ->method('getFlags') + ->willReturn([ + [ + 'long' => 'baz', + 'short' => 'b', + 'description' => 'baz description' + ], + [ + 'long' => 'qux', + 'short' => 'q', + 'description' => 'qux description' + ] + ]); + + $subject->__invoke($input, $output); + } +} diff --git a/tests/Component/Command/InputTest.php b/tests/Component/Command/InputTest.php new file mode 100644 index 0000000..2774ff1 --- /dev/null +++ b/tests/Component/Command/InputTest.php @@ -0,0 +1,132 @@ + 'baz']; + $flags = ['qux']; + $subject = new Input($command, $parameters, $flags); + + $this->assertEquals($command, $subject->getCommand()); + $this->assertEquals($parameters, $subject->getParameters()); + $this->assertEquals($flags, $subject->getFlags()); + $this->assertEquals(true, $subject->isSetFlag('qux')); + $this->assertEquals(true, $subject->hasParameter('bar')); + $this->assertEquals('baz', $subject->getParameter('bar')); + } + + /** + * Test if the variable is going to be set correctly. + * + * @covers ::hasParameter + * @covers ::setParameter + * @covers ::__construct + * + * @return void + */ + public function testSetter(): void + { + $subject = new Input([], [], []); + + $this->assertEquals(false, $subject->hasParameter('baz')); + $subject->setParameter('baz', 'foo'); + $this->assertEquals(true, $subject->hasParameter('baz')); + } + + /** + * Test if the command configuration works as expected. + * + * @covers ::loadConfiguration + * @covers ::hasParameter + * @covers ::isSetFlag + * @covers ::getParameter + * @covers ::__construct + * + * @return void + */ + public function testLoadingCommandConfiguration(): void + { + $command = ['foo']; + $parameters = ['bar' => 'baz', 'ba' => 'b']; + $flags = ['qux']; + $subject = new Input($command, $parameters, $flags); + + // Pre configuration checks. + $this->assertEquals(false, $subject->hasParameter('b')); + $this->assertEquals(null, $subject->getParameter('b')); + $this->assertEquals(false, $subject->hasParameter('baz')); + $this->assertEquals(null, $subject->getParameter('baz')); + $this->assertEquals(false, $subject->isSetFlag('q')); + + // Add the configuration. + $commandConfiguration = $this->createMock( + CommandConfigurationInterface::class + ); + + $commandConfiguration->expects(static::exactly(4)) + ->method('getParameters') + ->willReturn([ + [ + 'long' => 'bar', + 'short' => 'b', + 'type' => 'number', + 'required' => false, + 'description' => 'bar description' + ], + [ + 'long' => 'baz', + 'short' => 'ba', + 'type' => 'string', + 'required' => true, + 'description' => 'baz description' + ] + ]); + + $commandConfiguration->expects(static::once()) + ->method('getFlags') + ->willReturn([ + [ + 'long' => 'qux', + 'short' => 'q', + 'description' => 'qux description' + ] + ]); + + $subject->loadConfiguration($commandConfiguration); + + // Verify that the aliases can now be resolved. + $this->assertEquals(true, $subject->hasParameter('b')); + $this->assertEquals(true, $subject->hasParameter('baz')); + $this->assertEquals(true, $subject->isSetFlag('q')); + $this->assertEquals('baz', $subject->getParameter('b')); + $this->assertEquals('b', $subject->getParameter('baz')); + } +} diff --git a/tests/Component/Command/OutputTest.php b/tests/Component/Command/OutputTest.php new file mode 100644 index 0000000..771acec --- /dev/null +++ b/tests/Component/Command/OutputTest.php @@ -0,0 +1,333 @@ +createMock(WriterInterface::class); + $ioFactory = $this->createMock(IoFactoryInterface::class); + $ioFactory->expects(static::once()) + ->method('createStandardWriter') + ->willReturn($writer); + + $subject = new Output( + $this->createMock(FormGeneratorInterface::class), + $ioFactory, + $this->createMock(ThemeInterface::class), + $this->createMock(ElementFactoryInterface::class) + ); + + $input = 'foo'; + + $writer->expects(static::once()) + ->method('write') + ->with($input); + + $subject->write($input); + } + + /** + * @covers ::writeLine + * @covers ::__construct + * + * @return void + */ + public function testWriteLine(): void + { + $writer = $this->createMock(WriterInterface::class); + $ioFactory = $this->createMock(IoFactoryInterface::class); + $ioFactory->expects(static::once()) + ->method('createStandardWriter') + ->willReturn($writer); + + $subject = new Output( + $this->createMock(FormGeneratorInterface::class), + $ioFactory, + $this->createMock(ThemeInterface::class), + $this->createMock(ElementFactoryInterface::class) + ); + + $input = 'foo'; + + $writer->expects(static::once()) + ->method('writeLine') + ->with($input); + + $subject->writeLine($input); + } + + /** + * @covers ::overWrite + * @covers ::__construct + * + * @return void + */ + public function testOverWrite(): void + { + $writer = $this->createMock(WriterInterface::class); + $ioFactory = $this->createMock(IoFactoryInterface::class); + $ioFactory->expects(static::once()) + ->method('createStandardWriter') + ->willReturn($writer); + + $subject = new Output( + $this->createMock(FormGeneratorInterface::class), + $ioFactory, + $this->createMock(ThemeInterface::class), + $this->createMock(ElementFactoryInterface::class) + ); + + $input = 'foo'; + + $writer->expects(static::once()) + ->method('overWrite') + ->with($input); + + $subject->overWrite($input); + } + + /** + * @covers ::getFormGenerator + * @covers ::__construct + * + * @return void + */ + public function testGetFormGenerator(): void + { + $formGenerator = $this->createMock(FormGeneratorInterface::class); + $subject = new Output( + $formGenerator, + $this->createMock(IoFactoryInterface::class), + $this->createMock(ThemeInterface::class), + $this->createMock(ElementFactoryInterface::class) + ); + + $this->assertEquals($formGenerator, $subject->getFormGenerator()); + } + + /** + * @covers ::outputText + * @covers ::__construct + * + * @return void + */ + public function testOutputText(): void + { + $elementFactory = $this->createMock(ElementFactoryInterface::class); + $element = $this->createMock(ElementInterface::class); + $subject = new Output( + $this->createMock(FormGeneratorInterface::class), + $this->createMock(IoFactoryInterface::class), + $this->createMock(ThemeInterface::class), + $elementFactory + ); + + $content = 'foo'; + $newLine = false; + $styleKey = 'baz'; + + $elementFactory->expects(static::once()) + ->method('createText') + ->with($content, $newLine, $styleKey) + ->willReturn($element); + + $element->expects(static::once()) + ->method('render'); + + $subject->outputText($content, $newLine, $styleKey); + } + + /** + * @covers ::outputTable + * @covers ::__construct + * + * @return void + */ + public function testOutputTable(): void + { + $elementFactory = $this->createMock(ElementFactoryInterface::class); + $element = $this->createMock(ElementInterface::class); + $subject = new Output( + $this->createMock(FormGeneratorInterface::class), + $this->createMock(IoFactoryInterface::class), + $this->createMock(ThemeInterface::class), + $elementFactory + ); + + $keys = ['foo-1']; + $items = ['foo-1' => 'bar-1']; + $tableCharacters = 'foo'; + $style = 'bar'; + $boxStyle = 'baz'; + $keyStyle = 'qux'; + + $elementFactory->expects(static::once()) + ->method('createTable') + ->with($keys, $items, $tableCharacters, $style, $boxStyle, $keyStyle) + ->willReturn($element); + + $element->expects(static::once()) + ->method('render'); + + $subject->outputTable($keys, $items, $tableCharacters, $style, $boxStyle, $keyStyle); + } + + /** + * @covers ::outputList + * @covers ::__construct + * + * @return void + */ + public function testOutputList(): void + { + $elementFactory = $this->createMock(ElementFactoryInterface::class); + $element = $this->createMock(ElementInterface::class); + $subject = new Output( + $this->createMock(FormGeneratorInterface::class), + $this->createMock(IoFactoryInterface::class), + $this->createMock(ThemeInterface::class), + $elementFactory + ); + + $items = ['foo']; + $style = 'bar'; + + $elementFactory->expects(static::once()) + ->method('createList') + ->with($items, $style) + ->willReturn($element); + + $element->expects(static::once()) + ->method('render'); + + $subject->outputList($items, $style); + } + + /** + * @covers ::outputExplainedList + * @covers ::__construct + * + * @return void + */ + public function testOutputExplainedList(): void + { + $elementFactory = $this->createMock(ElementFactoryInterface::class); + $element = $this->createMock(ElementInterface::class); + $subject = new Output( + $this->createMock(FormGeneratorInterface::class), + $this->createMock(IoFactoryInterface::class), + $this->createMock(ThemeInterface::class), + $elementFactory + ); + + $items = ['foo' => ['description' => 'bar']]; + $keyStyle = 'baz'; + $descriptionStyle = 'qux'; + + $elementFactory->expects(static::once()) + ->method('createExplainedList') + ->with($items, $keyStyle, $descriptionStyle) + ->willReturn($element); + + $element->expects(static::once()) + ->method('render'); + + $subject->outputExplainedList($items, $keyStyle, $descriptionStyle); + } + + /** + * @covers ::outputBlock + * @covers ::__construct + * + * @return void + */ + public function testOutputBlock(): void + { + $elementFactory = $this->createMock(ElementFactoryInterface::class); + $element = $this->createMock(ElementInterface::class); + $subject = new Output( + $this->createMock(FormGeneratorInterface::class), + $this->createMock(IoFactoryInterface::class), + $this->createMock(ThemeInterface::class), + $elementFactory + ); + + $content = 'foo'; + $style = 'bar'; + $padding = 'baz'; + $margin = 'qux'; + + $elementFactory->expects(static::once()) + ->method('createBlock') + ->with($content, $style, $padding, $margin) + ->willReturn($element); + + $element->expects(static::once()) + ->method('render'); + + $subject->outputBlock($content, $style, $padding, $margin); + } + + /** + * @covers ::outputProgressBar + * @covers ::__construct + * + * @return void + */ + public function testOutputProgressBar(): void + { + $elementFactory = $this->createMock(ElementFactoryInterface::class); + $element = $this->createMock(ElementInterface::class); + $subject = new Output( + $this->createMock(FormGeneratorInterface::class), + $this->createMock(IoFactoryInterface::class), + $this->createMock(ThemeInterface::class), + $elementFactory + ); + + $taskList = $this->createMock(TaskListInterface::class); + $progressCharacters = 'foo'; + $textStyle = 'bar'; + $progressStyle = 'baz'; + + $elementFactory->expects(static::once()) + ->method('createProgressBar') + ->with($taskList, $progressCharacters, $textStyle, $progressStyle) + ->willReturn($element); + + $element->expects(static::once()) + ->method('render'); + + $subject->outputProgressBar( + $taskList, + $progressCharacters, + $textStyle, + $progressStyle + ); + } +} diff --git a/tests/Component/Router/CommandRouterTest.php b/tests/Component/Router/CommandRouterTest.php new file mode 100644 index 0000000..f850bb4 --- /dev/null +++ b/tests/Component/Router/CommandRouterTest.php @@ -0,0 +1,261 @@ +createMock(ServiceFactoryInterface::class); + $errorElementFactory = $this->createMock(ElementFactoryInterface::class); + $ioFactory = $this->createMock(IoFactoryInterface::class); + $output = $this->createMock(OutputInterface::class); + $formGenerator = $this->createMock(FormGeneratorInterface::class); + $subject = new CommandRouter( + $commandConfiguration, + $serviceFactory, + $errorElementFactory, + $ioFactory, + $output, + $formGenerator + ); + + $form = $this->createMock(FormInterface::class); + $formGenerator->method('getForm')->willReturn($form); + $form->method('getInput')->willReturn(['foo' => 'bar']); + $serviceFactory->method('create')->willReturn( + $this->createMock(CommandInterface::class) + ); + + $this->assertEquals($expected, $subject->__invoke($input)); + } + + /** + * Provides configurations for resolving routes. + * + * @return array + */ + public function routingProvider(): array + { + $fooConfig = new CommandConfiguration(); + $fooConfig->addCommandConfiguration( + 'foo', + new CommandConfiguration() + ); + + return [ + // Command not execute test + [ + $this->createMock(InputInterface::class), + $this->createMock(CommandConfigurationInterface::class), + 126 + ], + // Show help command + [ + new Input([], [], ['help']), + $this->createMock(CommandConfigurationInterface::class), + 0 + ], + // Show list command + [ + new Input(), + $fooConfig, + 0 + ], + // Command not found in command + [ + new Input(['foo', 'bar']), + $fooConfig, + 127 + ], + // Missing open parameter with form. + [ + new Input(['foo'], [], []), + $this->createCommandConfiguration('foo', 'bar', [[ + 'long' => 'foo', + 'type' => 'foo', + 'required' => true + ]]), + 126 + ], + // Missing hidden parameter with form. + [ + new Input(['foo'], [], []), + $this->createCommandConfiguration('foo', 'bar', [[ + 'long' => 'foo', + 'type' => 'foo', + 'required' => true, + 'hidden' => true, + ]]), + 126 + ], + // Missing option parameter with form. + [ + new Input(['foo'], [], []), + $this->createCommandConfiguration('foo', 'bar', [[ + 'long' => 'foo', + 'type' => 'foo', + 'required' => true, + 'options' => [], + ]]), + 126 + ], + // Missing option no interaction. + [ + new Input(['foo'], [], ['no-interaction']), + $this->createCommandConfiguration('foo', 'bar', [[ + 'long' => 'foo', + 'type' => 'number', + 'required' => true, + ]]), + 1 + ], + // Missing open option. + [ + new Input(['foo'], [], []), + $this->createCommandConfiguration('foo', 'bar', [[ + 'long' => 'foo', + 'type' => 'string', + 'required' => true, + ]]), + 0 + ], + // Missing array option. + [ + new Input(['foo'], [], []), + $this->createCommandConfiguration('foo', 'bar', [[ + 'long' => 'foo', + 'type' => 'array', + 'required' => true, + ]]), + 0 + ], + // Missing autocompleting option. + [ + new Input(['foo'], [], []), + $this->createCommandConfiguration('foo', 'bar', [[ + 'long' => 'foo', + 'type' => 'string', + 'required' => true, + 'options' => [] + ]]), + 0 + ], + // Missing array autocompleting option. + [ + new Input(['foo'], [], []), + $this->createCommandConfiguration('foo', 'bar', [[ + 'long' => 'foo', + 'type' => 'array', + 'required' => true, + 'options' => [] + ]]), + 0 + ], + // Missing hidden option. + [ + new Input(['foo'], [], []), + $this->createCommandConfiguration('foo', 'bar', [[ + 'long' => 'foo', + 'type' => 'string', + 'required' => true, + 'hidden' => true + ]]), + 0 + ], + // Missing hidden array option. + [ + new Input(['foo'], [], []), + $this->createCommandConfiguration('foo', 'bar', [[ + 'short' => 'foo', + 'type' => 'array', + 'required' => true, + 'hidden' => true + ]]), + 0 + ], + // Nothing missing. + [ + new Input(['foo'], [], []), + $this->createCommandConfiguration('foo', 'bar', [[ + 'short' => 'foo', + 'type' => 'array', + 'required' => false, + 'hidden' => true + ]]), + 0 + ], + ]; + } + + /** + * Creates a command configuration. + * + * @param string $name + * @param string $service + * @param array $config + * + * @return CommandConfigurationInterface + */ + private function createCommandConfiguration( + string $name, + string $service, + array $configuration + ): CommandConfigurationInterface { + $config = new CommandConfiguration(); + $config->addCommandConfiguration( + $name, + new CommandConfiguration( + $service, + 'description', + $configuration + ) + ); + + return $config; + } +} diff --git a/tests/Dao/CommandConfigurationTest.php b/tests/Dao/CommandConfigurationTest.php new file mode 100644 index 0000000..40b0ac2 --- /dev/null +++ b/tests/Dao/CommandConfigurationTest.php @@ -0,0 +1,66 @@ + 'value']; + $flags = [ + [ + 'long' => 'flag', + 'short' => 'f', + 'description' => 'A flag' + ] + ]; + + $allFlags = array_merge( + $flags, + (new CommandConfiguration())->getFlags() + ); + + $subject = new CommandConfiguration($service, $description, $parameters, $flags); + + $this->assertEquals($allFlags, $subject->getFlags()); + $this->assertEquals($parameters, $subject->getParameters()); + $this->assertEquals(false, $subject->hasCommand('command')); + $this->assertEquals($service, $subject->getService()); + $this->assertEquals([], $subject->getCommands()); + $this->assertEquals($description, $subject->getDescription()); + + $command = 'command'; + $configuration = $this->createMock(CommandConfigurationInterface::class); + $subject->addCommandConfiguration($command, $configuration); + + $this->assertEquals(true, $subject->hasCommand('command')); + $this->assertEquals($configuration, $subject->getCommand('command')); + $this->assertEquals([$command], $subject->getCommands()); + } +} diff --git a/tests/Factory/InputFactoryTest.php b/tests/Factory/InputFactoryTest.php new file mode 100644 index 0000000..ccfcff7 --- /dev/null +++ b/tests/Factory/InputFactoryTest.php @@ -0,0 +1,74 @@ +create($arguments); + + $this->assertInstanceOf( + InputInterface::class, + $result + ); + + $this->assertEquals( + ['foo'], + $result->getCommand() + ); + + $this->assertEquals( + [ + 'bar' => 'qux', + 'baz' => ['bar', 'foo'], + 'foo' => 'bar' + ], + $result->getParameters() + ); + + $this->assertEquals( + ['flag'], + $result->getFlags() + ); + } +}