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()
+ );
+ }
+}