PK r?V3VK K phpunit.xml.distnu W+A
tests
src
PK r?Vƻ
.editorconfignu W+A # This file is for unifying the coding style for different editors and IDEs
# editorconfig.org
root = true
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[**.php]
indent_style = space
indent_size = 4
PK r?V1 1 LICENSEnu W+A Copyright (c) 2016 Consolidation Org Developers
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.
PK r?V;8 8 CONTRIBUTING.mdnu W+A # Contributing to Consolidation
Thank you for your interest in contributing to the Consolidation effort! Consolidation aims to provide reusable, loosely-coupled components useful for building command-line tools. Consolidation is built on top of Symfony Console, but aims to separate the tool from the implementation details of Symfony.
Here are some of the guidelines you should follow to make the most of your efforts:
## Code Style Guidelines
Consolidation adheres to the [PSR-2 Coding Style Guide](http://www.php-fig.org/psr/psr-2/) for PHP code.
## Pull Request Guidelines
Every pull request is run through:
- phpcs -n --standard=PSR2 src
- phpunit
- [Scrutinizer](https://scrutinizer-ci.com/g/consolidation-org/annotated-command/)
It is easy to run the unit tests and code sniffer locally; simply ensure that `./vendor/bin` is in your `$PATH`, cd to the root of the project directory, and run `phpcs` and `phpunit` as shown above. To automatically fix coding standard errors, run:
- phpcbf --standard=PSR2 src
After submitting a pull request, please examine the Scrutinizer report. It is not required to fix all Scrutinizer issues; you may ignore recommendations that you disagree with. The spacing patches produced by Scrutinizer do not conform to PSR2 standards, and therefore should never be applied. DocBlock patches may be applied at your discression. Things that Scrutinizer identifies as a bug nearly always needs to be addressed.
Pull requests must pass phpcs and phpunit in order to be merged; ideally, new functionality will also include new unit tests.
PK r?V~ .travis.ymlnu W+A language: php
branches:
# Only test the master branch and SemVer tags.
only:
- master
- /^[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+.*$/
php:
- 5.4
- 5.5
- 5.6
- 7.0
sudo: false
cache:
directories:
- vendor
- $HOME/.composer/cache
before_script:
- composer install
script:
- vendor/bin/phpcs --standard=PSR2 -n src
- vendor/bin/phpunit
after_success:
- travis_retry php vendor/bin/coveralls -v
PK r?V4 4
composer.jsonnu W+A {
"name": "consolidation/annotated-command",
"description": "Initialize Symfony Console commands from annotated command class methods.",
"license": "MIT",
"authors": [
{
"name": "Greg Anderson",
"email": "greg.1.anderson@greenknowe.org"
}
],
"autoload":{
"psr-4":{
"Consolidation\\AnnotatedCommand\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Consolidation\\TestUtils\\": "tests/src"
}
},
"require": {
"php": ">=5.4.0",
"psr/log": "~1.0",
"symfony/console": "~2.5|~3.0",
"symfony/event-dispatcher": "~2.5|~3.0",
"symfony/finder": "~2.5|~3.0",
"phpdocumentor/reflection-docblock": "^2.0|^3.0.2"
},
"require-dev": {
"consolidation/output-formatters": "~1",
"phpunit/phpunit": "4.*",
"satooshi/php-coveralls": "^1.0",
"squizlabs/php_codesniffer": "2.*"
},
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
}
}
}
PK r?V3 CHANGELOG.mdnu W+A # Change Log
### 2.0.0 - 15 September 2016
- Hooks with no command name now apply to all commands defined in the same class. This is a change of behavior from the 1.x branch, where hooks with no command name applied to a command with the same method name in a *different* class.
- Update the interfaces ValidatorInterface, ProcessResultInterface and AlterResultInterface such that their methods are passed annotation data, just as the procedural callbacks are.
### 1.4.0 - 13 September 2016
- Add basic annotation hook capability, to allow hook functions to be attached to commands with arbitrary annotations.
### 1.3.0 - 8 September 2016
- Add ComandFileDiscovery::setSearchDepth(). The search depth applies to each search location, unless there are no search locations, in which case it applies to the base directory.
### 1.2.0 - 2 August 2016
- Support both the 2.x and 3.x versions of phpdocumentor/reflection-docblock.
- Support php 5.4.
- **Bug** Do not allow an @param docblock comment for the options to override the meaning of the options.
### 1.1.0 - 6 July 2016
- Introduce AnnotatedCommandFactory::createSelectedCommandsFromClassInfo() method.
### 1.0.0 - 20 May 2016
- First stable release.
PK r?V ," ," tests/testFullStack.phpnu W+A application = new Application('TestApplication', '0.0.0');
$this->application->setAutoExit(false);
}
function testValidFormats()
{
$formatter = new FormatterManager();
$commandInfo = new CommandInfo('\Consolidation\TestUtils\alpha\AlphaCommandFile', 'exampleTable');
$this->assertEquals('example:table', $commandInfo->getName());
$this->assertEquals('\Consolidation\OutputFormatters\StructuredData\RowsOfFields', $commandInfo->getReturnType());
}
function testCommandsAndHooks()
{
// First, search for commandfiles in the 'alpha'
// directory. Note that this same functionality
// is tested more thoroughly in isolation in
// testCommandFileDiscovery.php
$discovery = new CommandFileDiscovery();
$discovery
->setSearchPattern('*CommandFile.php')
->setIncludeFilesAtBase(false)
->setSearchLocations(['alpha']);
chdir(__DIR__);
$commandFiles = $discovery->discover('.', '\Consolidation\TestUtils');
$formatter = new FormatterManager();
$hookManager = new HookManager();
$commandProcessor = new CommandProcessor($hookManager);
$commandProcessor->setFormatterManager($formatter);
// Create a new factory, and load all of the files
// discovered above. The command factory class is
// tested in isolation in testAnnotatedCommandFactory.php,
// but this is the only place where
$factory = new AnnotatedCommandFactory();
$factory->setCommandProcessor($commandProcessor);
// $factory->addListener(...);
$this->addDiscoveredCommands($factory, $commandFiles, false);
$this->assertTrue($this->application->has('example:table'));
$this->assertFalse($this->application->has('without:annotations'));
// Fetch a reference to the 'example:table' command and test its valid format types
$exampleTableCommand = $this->application->find('example:table');
$returnType = $exampleTableCommand->getReturnType();
$this->assertEquals('\Consolidation\OutputFormatters\StructuredData\RowsOfFields', $returnType);
$validFormats = $formatter->validFormats($returnType);
$this->assertEquals('csv,json,list,php,print-r,sections,table,tsv,var_export,xml,yaml', implode(',', $validFormats));
// Control: run commands without hooks.
$this->assertRunCommandViaApplicationEquals('always:fail', 'This command always fails.', 13);
$this->assertRunCommandViaApplicationEquals('simulated:status', '');
$this->assertRunCommandViaApplicationEquals('example:output', 'Hello, World.');
$this->assertRunCommandViaApplicationEquals('example:cat bet alpha --flip', 'alphabet');
$this->assertRunCommandViaApplicationEquals('example:echo a b c', '');
$this->assertRunCommandViaApplicationEquals('example:message', '');
$this->assertRunCommandViaApplicationEquals('command:with-one-optional-argument', 'Hello, world');
$this->assertRunCommandViaApplicationEquals('command:with-one-optional-argument Joe', 'Hello, Joe');
// Add some hooks.
$factory->hookManager()->addValidator(new ExampleValidator());
$factory->hookManager()->addResultProcessor(new ExampleResultProcessor());
$factory->hookManager()->addAlterResult(new ExampleResultAlterer());
$factory->hookManager()->addStatusDeterminer(new ExampleStatusDeterminer());
$factory->hookManager()->addOutputExtractor(new ExampleOutputExtractor());
// Run the same commands as before, and confirm that results
// are different now that the hooks are in place.
$this->assertRunCommandViaApplicationEquals('simulated:status', '', 42);
$this->assertRunCommandViaApplicationEquals('example:output', 'Hello, World!');
$this->assertRunCommandViaApplicationEquals('example:cat bet alpha --flip', 'alphabeta');
$this->assertRunCommandViaApplicationEquals('example:echo a b c', 'a,b,c');
$this->assertRunCommandViaApplicationEquals('example:message', 'Shipwrecked; send bananas.');
$expected = <<assertRunCommandViaApplicationEquals('example:table', $expected);
$expected = <<assertRunCommandViaApplicationEquals('example:table --fields=III,II', $expected);
// Now we will once again add all commands, this time including all
// public methods. The command 'withoutAnnotations' should now be found.
$this->addDiscoveredCommands($factory, $commandFiles, true);
$this->assertTrue($this->application->has('without:annotations'));
}
public function addDiscoveredCommands($factory, $commandFiles, $includeAllPublicMethods) {
foreach ($commandFiles as $path => $commandClass) {
$this->assertFileExists($path);
if (!class_exists($commandClass)) {
include $path;
}
$commandInstance = new $commandClass();
$commandList = $factory->createCommandsFromClass($commandInstance, $includeAllPublicMethods);
foreach ($commandList as $command) {
$this->application->add($command);
}
}
}
function assertRunCommandViaApplicationEquals($cmd, $expectedOutput, $expectedStatusCode = 0)
{
$input = new StringInput($cmd);
$output = new BufferedOutput();
$statusCode = $this->application->run($input, $output);
$commandOutput = trim($output->fetch());
$expectedOutput = $this->simplifyWhitespace($expectedOutput);
$commandOutput = $this->simplifyWhitespace($commandOutput);
$this->assertEquals($expectedOutput, $commandOutput);
$this->assertEquals($expectedStatusCode, $statusCode);
}
function simplifyWhitespace($data)
{
return trim(preg_replace('#[ \t]+$#m', '', $data));
}
}
class ExampleValidator implements ValidatorInterface
{
public function validate($args, AnnotationData $annotationData)
{
if (isset($args['one']) && ($args['one'] == 'bet')) {
$args['one'] = 'beta';
return $args;
}
}
}
class ExampleResultProcessor implements ProcessResultInterface
{
public function process($result, array $args, AnnotationData $annotationData)
{
if (is_array($result) && array_key_exists('item-list', $result)) {
return implode(',', $result['item-list']);
}
}
}
class ExampleResultAlterer implements AlterResultInterface
{
public function process($result, array $args, AnnotationData $annotationData)
{
if (is_string($result) && ($result == 'Hello, World.')) {
return 'Hello, World!';
}
}
}
class ExampleStatusDeterminer implements StatusDeterminerInterface
{
public function determineStatusCode($result)
{
if (is_array($result) && array_key_exists('status-code', $result)) {
return $result['status-code'];
}
}
}
class ExampleOutputExtractor implements ExtractOutputInterface
{
public function extractOutput($result)
{
if (is_array($result) && array_key_exists('message', $result)) {
return $result['message'];
}
}
}
PK r?V֊?P P % tests/testAnnotatedCommandFactory.phpnu W+A createCommandInfo($commandFileInstance, 'testArithmatic');
$command = $commandFactory->createCommand($commandInfo, $commandFileInstance);
$this->assertInstanceOf('\Symfony\Component\Console\Command\Command', $command);
$this->assertEquals('test:arithmatic', $command->getName());
$this->assertEquals('This is the test:arithmatic command', $command->getDescription());
$this->assertEquals("This command will add one and two. If the --negate flag\nis provided, then the result is negated.", $command->getHelp());
$this->assertEquals('arithmatic', implode(',', $command->getAliases()));
$this->assertEquals('test:arithmatic [--negate] [--] ', $command->getSynopsis());
$this->assertEquals('test:arithmatic 2 2 --negate', implode(',', $command->getUsages()));
$input = new StringInput('arithmatic 2 3 --negate');
$this->assertRunCommandViaApplicationEquals($command, $input, '-5');
}
function testMyCatCommand()
{
$commandFileInstance = new \Consolidation\TestUtils\ExampleCommandFile;
$commandFactory = new AnnotatedCommandFactory();
$commandInfo = $commandFactory->createCommandInfo($commandFileInstance, 'myCat');
$command = $commandFactory->createCommand($commandInfo, $commandFileInstance);
$this->assertInstanceOf('\Symfony\Component\Console\Command\Command', $command);
$this->assertEquals('my:cat', $command->getName());
$this->assertEquals('This is the my:cat command', $command->getDescription());
$this->assertEquals("This command will concatenate two parameters. If the --flip flag\nis provided, then the result is the concatenation of two and one.", $command->getHelp());
$this->assertEquals('c', implode(',', $command->getAliases()));
$this->assertEquals('my:cat [--flip] [--] []', $command->getSynopsis());
$this->assertEquals('my:cat bet alpha --flip', implode(',', $command->getUsages()));
$input = new StringInput('my:cat bet alpha --flip');
$this->assertRunCommandViaApplicationEquals($command, $input, 'alphabet');
}
function testDefaultsCommand()
{
$commandFileInstance = new \Consolidation\TestUtils\ExampleCommandFile;
$commandFactory = new AnnotatedCommandFactory();
$commandInfo = $commandFactory->createCommandInfo($commandFileInstance, 'defaults');
$command = $commandFactory->createCommand($commandInfo, $commandFileInstance);
$this->assertInstanceOf('\Symfony\Component\Console\Command\Command', $command);
$this->assertEquals('defaults', $command->getName());
$this->assertEquals('Test default values in arguments', $command->getDescription());
$input = new StringInput('defaults');
$this->assertRunCommandViaApplicationEquals($command, $input, 'nothing provided');
$input = new StringInput('defaults ichi');
$this->assertRunCommandViaApplicationEquals($command, $input, 'only ichi');
$input = new StringInput('defaults I II');
$this->assertRunCommandViaApplicationEquals($command, $input, 'I and II');
}
function testCommandWithNoOptions()
{
$commandFileInstance = new \Consolidation\TestUtils\ExampleCommandFile;
$commandFactory = new AnnotatedCommandFactory();
$commandInfo = $commandFactory->createCommandInfo($commandFileInstance, 'commandWithNoOptions');
$command = $commandFactory->createCommand($commandInfo, $commandFileInstance);
$this->assertInstanceOf('\Symfony\Component\Console\Command\Command', $command);
$this->assertEquals('command:with-no-options', $command->getName());
$this->assertEquals('This is a command with no options', $command->getDescription());
$this->assertEquals("This command will concatenate two parameters.", $command->getHelp());
$this->assertEquals('nope', implode(',', $command->getAliases()));
$this->assertEquals('command:with-no-options []', $command->getSynopsis());
$this->assertEquals('command:with-no-options alpha bet', implode(',', $command->getUsages()));
$input = new StringInput('command:with-no-options something');
$this->assertRunCommandViaApplicationEquals($command, $input, 'somethingdefault');
}
function testCommandWithNoArguments()
{
$commandFileInstance = new \Consolidation\TestUtils\ExampleCommandFile;
$commandFactory = new AnnotatedCommandFactory();
$commandInfo = $commandFactory->createCommandInfo($commandFileInstance, 'commandWithNoArguments');
$command = $commandFactory->createCommand($commandInfo, $commandFileInstance);
$this->assertInstanceOf('\Symfony\Component\Console\Command\Command', $command);
$this->assertEquals('command:with-no-arguments', $command->getName());
$this->assertEquals('This command has no arguments--only options', $command->getDescription());
$this->assertEquals("Return a result only if not silent.", $command->getHelp());
$this->assertEquals('command:with-no-arguments [-s|--silent]', $command->getSynopsis());
$input = new StringInput('command:with-no-arguments');
$this->assertRunCommandViaApplicationEquals($command, $input, 'Hello, world');
$input = new StringInput('command:with-no-arguments -s');
$this->assertRunCommandViaApplicationEquals($command, $input, '');
$input = new StringInput('command:with-no-arguments --silent');
$this->assertRunCommandViaApplicationEquals($command, $input, '');
}
function testCommandWithShortcutOnAnnotation()
{
$commandFileInstance = new \Consolidation\TestUtils\ExampleCommandFile;
$commandFactory = new AnnotatedCommandFactory();
$commandInfo = $commandFactory->createCommandInfo($commandFileInstance, 'shortcutOnAnnotation');
$command = $commandFactory->createCommand($commandInfo, $commandFileInstance);
$this->assertInstanceOf('\Symfony\Component\Console\Command\Command', $command);
$this->assertEquals('shortcut:on-annotation', $command->getName());
$this->assertEquals('Shortcut on annotation', $command->getDescription());
$this->assertEquals("This command defines the option shortcut on the annotation instead of in the options array.", $command->getHelp());
$this->assertEquals('shortcut:on-annotation [-s|--silent]', $command->getSynopsis());
$input = new StringInput('shortcut:on-annotation');
$this->assertRunCommandViaApplicationEquals($command, $input, 'Hello, world');
$input = new StringInput('shortcut:on-annotation -s');
$this->assertRunCommandViaApplicationEquals($command, $input, '');
$input = new StringInput('shortcut:on-annotation --silent');
$this->assertRunCommandViaApplicationEquals($command, $input, '');
}
function testState()
{
$commandFileInstance = new \Consolidation\TestUtils\ExampleCommandFile('secret secret');
$commandFactory = new AnnotatedCommandFactory();
$commandInfo = $commandFactory->createCommandInfo($commandFileInstance, 'testState');
$command = $commandFactory->createCommand($commandInfo, $commandFileInstance);
$this->assertInstanceOf('\Symfony\Component\Console\Command\Command', $command);
$this->assertEquals('test:state', $command->getName());
$input = new StringInput('test:state');
$this->assertRunCommandViaApplicationEquals($command, $input, 'secret secret');
}
function testPassthroughArray()
{
$commandFileInstance = new \Consolidation\TestUtils\ExampleCommandFile;
$commandFactory = new AnnotatedCommandFactory();
$commandInfo = $commandFactory->createCommandInfo($commandFileInstance, 'testPassthrough');
$command = $commandFactory->createCommand($commandInfo, $commandFileInstance);
$this->assertInstanceOf('\Symfony\Component\Console\Command\Command', $command);
$this->assertEquals('test:passthrough', $command->getName());
$input = new StringInput('test:passthrough a b c');
$input = new PassThroughArgsInput(['x', 'y', 'z'], $input);
$this->assertRunCommandViaApplicationEquals($command, $input, 'a,b,c,x,y,z');
}
function testPassThroughNonArray()
{
$commandFileInstance = new \Consolidation\TestUtils\ExampleCommandFile;
$commandFactory = new AnnotatedCommandFactory();
$commandInfo = $commandFactory->createCommandInfo($commandFileInstance, 'myCat');
$command = $commandFactory->createCommand($commandInfo, $commandFileInstance);
$input = new StringInput('my:cat bet --flip');
$input = new PassThroughArgsInput(['x', 'y', 'z'], $input);
$this->assertRunCommandViaApplicationEquals($command, $input, 'x y zbet');
// Can't look at 'hasOption' until after the command initializes the
// option, because Symfony.
$this->assertTrue($input->hasOption('flip'));
}
function testPassThroughWithInputManipulation()
{
$commandFileInstance = new \Consolidation\TestUtils\ExampleCommandFile;
$commandFactory = new AnnotatedCommandFactory();
$commandInfo = $commandFactory->createCommandInfo($commandFileInstance, 'myRepeat');
$command = $commandFactory->createCommand($commandInfo, $commandFileInstance);
$input = new StringInput('my:repeat bet --repeat=2');
$input = new PassThroughArgsInput(['x', 'y', 'z'], $input);
$this->assertRunCommandViaApplicationEquals($command, $input, 'betx y zbetx y z');
// Symfony does not allow us to manipulate the options via setOption until
// the definition from the command object has been set up.
$input->setOption('repeat', 3);
$this->assertEquals(3, $input->getOption('repeat'));
$input->setArgument(0, 'q');
// Manipulating $input does not work -- the changes are not effective.
// The end result here should be 'qx y yqx y yqx y y'
$this->assertRunCommandViaApplicationEquals($command, $input, 'betx y zbetx y z');
}
function testHookedCommand()
{
$commandFileInstance = new \Consolidation\TestUtils\ExampleCommandFile();
$commandFactory = new AnnotatedCommandFactory();
$hookInfo = $commandFactory->createCommandInfo($commandFileInstance, 'hookTestHook');
$this->assertTrue($hookInfo->hasAnnotation('hook'));
$this->assertEquals('alter test:hook', $hookInfo->getAnnotation('hook'));
$commandFactory->registerCommandHook($hookInfo, $commandFileInstance);
$hookCallback = $commandFactory->hookManager()->get('test:hook', 'alter');
$this->assertTrue($hookCallback != null);
$this->assertEquals(1, count($hookCallback));
$this->assertEquals(2, count($hookCallback[0]));
$this->assertTrue(is_callable($hookCallback[0]));
$this->assertEquals('hookTestHook', $hookCallback[0][1]);
$commandInfo = $commandFactory->createCommandInfo($commandFileInstance, 'testHook');
$command = $commandFactory->createCommand($commandInfo, $commandFileInstance);
$this->assertInstanceOf('\Symfony\Component\Console\Command\Command', $command);
$this->assertEquals('test:hook', $command->getName());
$input = new StringInput('test:hook bar');
$this->assertRunCommandViaApplicationEquals($command, $input, '<[bar]>');
}
function testHookAllCommands()
{
$commandFileInstance = new \Consolidation\TestUtils\ExampleHookAllCommandFile();
$commandFactory = new AnnotatedCommandFactory();
$hookInfo = $commandFactory->createCommandInfo($commandFileInstance, 'alterAllCommands');
$this->assertTrue($hookInfo->hasAnnotation('hook'));
$this->assertEquals('alter', $hookInfo->getAnnotation('hook'));
$commandFactory->registerCommandHook($hookInfo, $commandFileInstance);
$hookCallback = $commandFactory->hookManager()->get('Consolidation\TestUtils\ExampleHookAllCommandFile', 'alter');
$this->assertTrue($hookCallback != null);
$this->assertEquals(1, count($hookCallback));
$this->assertEquals(2, count($hookCallback[0]));
$this->assertTrue(is_callable($hookCallback[0]));
$this->assertEquals('alterAllCommands', $hookCallback[0][1]);
$commandInfo = $commandFactory->createCommandInfo($commandFileInstance, 'doCat');
$command = $commandFactory->createCommand($commandInfo, $commandFileInstance);
$this->assertInstanceOf('\Symfony\Component\Console\Command\Command', $command);
$this->assertEquals('do:cat', $command->getName());
$input = new StringInput('do:cat bar');
$this->assertRunCommandViaApplicationEquals($command, $input, '*** bar ***');
}
function testAnnotatedHookedCommand()
{
$commandFileInstance = new \Consolidation\TestUtils\ExampleCommandFile();
$commandFactory = new AnnotatedCommandFactory();
$hookInfo = $commandFactory->createCommandInfo($commandFileInstance, 'hookTestAnnotatedHook');
$this->assertTrue($hookInfo->hasAnnotation('hook'));
$this->assertEquals('alter @hookme', $hookInfo->getAnnotation('hook'));
$commandFactory->registerCommandHook($hookInfo, $commandFileInstance);
$hookCallback = $commandFactory->hookManager()->get('@hookme', 'alter');
$this->assertTrue($hookCallback != null);
$this->assertEquals(1, count($hookCallback));
$this->assertEquals(2, count($hookCallback[0]));
$this->assertTrue(is_callable($hookCallback[0]));
$this->assertEquals('hookTestAnnotatedHook', $hookCallback[0][1]);
$commandInfo = $commandFactory->createCommandInfo($commandFileInstance, 'testAnnotationHook');
$annotationData = $commandInfo->getAnnotations();
$this->assertEquals('hookme,before,after', implode(',', $annotationData->keys()));
$this->assertEquals('@hookme,@before,@after', implode(',', array_map(function ($item) { return "@$item"; }, $annotationData->keys())));
$command = $commandFactory->createCommand($commandInfo, $commandFileInstance);
$this->assertInstanceOf('\Symfony\Component\Console\Command\Command', $command);
$this->assertEquals('test:annotation-hook', $command->getName());
$input = new StringInput('test:annotation-hook baz');
$this->assertRunCommandViaApplicationEquals($command, $input, '>(baz)<');
}
function testHookedCommandWithHookAddedLater()
{
$commandFileInstance = new \Consolidation\TestUtils\ExampleCommandFile();
$commandFactory = new AnnotatedCommandFactory();
$commandInfo = $commandFactory->createCommandInfo($commandFileInstance, 'testHook');
$command = $commandFactory->createCommand($commandInfo, $commandFileInstance);
$this->assertInstanceOf('\Symfony\Component\Console\Command\Command', $command);
$this->assertEquals('test:hook', $command->getName());
// Run the command once without the hook
$input = new StringInput('test:hook foo');
$this->assertRunCommandViaApplicationEquals($command, $input, '[foo]');
// Register the hook and run the command again
$hookInfo = $commandFactory->createCommandInfo($commandFileInstance, 'hookTestHook');
$this->assertTrue($hookInfo->hasAnnotation('hook'));
$this->assertEquals('alter test:hook', $hookInfo->getAnnotation('hook'));
$commandFactory->registerCommandHook($hookInfo, $commandFileInstance);
$hookCallback = $commandFactory->hookManager()->get('test:hook', 'alter');
$this->assertTrue($hookCallback != null);
$this->assertEquals(1, count($hookCallback));
$this->assertEquals(2, count($hookCallback[0]));
$this->assertTrue(is_callable($hookCallback[0]));
$this->assertEquals('hookTestHook', $hookCallback[0][1]);
$input = new StringInput('test:hook bar');
$this->assertRunCommandViaApplicationEquals($command, $input, '<[bar]>');
}
function testInteractAndValidate()
{
$commandFileInstance = new \Consolidation\TestUtils\ExampleCommandFile();
$commandFactory = new AnnotatedCommandFactory();
$hookInfo = $commandFactory->createCommandInfo($commandFileInstance, 'interactTestHello');
$this->assertTrue($hookInfo->hasAnnotation('hook'));
$this->assertEquals($hookInfo->getAnnotation('hook'), 'interact test:hello');
$commandFactory->registerCommandHook($hookInfo, $commandFileInstance);
$hookInfo = $commandFactory->createCommandInfo($commandFileInstance, 'validateTestHello');
$this->assertTrue($hookInfo->hasAnnotation('hook'));
$this->assertEquals($hookInfo->getAnnotation('hook'), 'validate test:hello');
$commandFactory->registerCommandHook($hookInfo, $commandFileInstance);
$hookCallback = $commandFactory->hookManager()->get('test:hello', 'validate');
$this->assertTrue($hookCallback != null);
$this->assertEquals(1, count($hookCallback));
$this->assertEquals(2, count($hookCallback[0]));
$this->assertTrue(is_callable($hookCallback[0]));
$this->assertEquals('validateTestHello', $hookCallback[0][1]);
$hookCallback = $commandFactory->hookManager()->get('test:hello', 'interact');
$this->assertTrue($hookCallback != null);
$this->assertEquals(1, count($hookCallback));
$this->assertEquals(2, count($hookCallback[0]));
$this->assertTrue(is_callable($hookCallback[0]));
$this->assertEquals('interactTestHello', $hookCallback[0][1]);
$commandInfo = $commandFactory->createCommandInfo($commandFileInstance, 'testHello');
$command = $commandFactory->createCommand($commandInfo, $commandFileInstance);
$this->assertInstanceOf('\Symfony\Component\Console\Command\Command', $command);
$this->assertEquals('test:hello', $command->getName());
$input = new StringInput('test:hello "Mickey Mouse"');
$this->assertRunCommandViaApplicationEquals($command, $input, 'Hello, Mickey Mouse.');
$input = new StringInput('test:hello');
$this->assertRunCommandViaApplicationEquals($command, $input, 'Hello, Goofey.');
$input = new StringInput('test:hello "Donald Duck"');
$this->assertRunCommandViaApplicationEquals($command, $input, "I won't say hello to Donald Duck.", 1);
$input = new StringInput('test:hello "Drumph"');
$this->assertRunCommandViaApplicationEquals($command, $input, "Irrational value error.", 1);
// Try the last test again with a display error function installed.
$commandFactory->commandProcessor()->setDisplayErrorFunction(
function ($output, $message) {
$output->writeln("*** $message ****");
}
);
$input = new StringInput('test:hello "Drumph"');
$this->assertRunCommandViaApplicationEquals($command, $input, "*** Irrational value error. ****", 1);
}
function assertRunCommandViaApplicationEquals($command, $input, $expectedOutput, $expectedStatusCode = 0)
{
$output = new BufferedOutput();
$application = new Application('TestApplication', '0.0.0');
$application->setAutoExit(false);
$application->add($command);
$statusCode = $application->run($input, $output);
$commandOutput = trim($output->fetch());
$this->assertEquals($expectedOutput, $commandOutput);
$this->assertEquals($expectedStatusCode, $statusCode);
}
}
PK r?Vg " tests/testCommandFileDiscovery.phpnu W+A setSearchPattern('*CommandFile.php')
->setSearchLocations(['alpha']);
chdir(__DIR__);
$commandFiles = $discovery->discover('.', '\Consolidation\TestUtils');
$commandFilePaths = array_keys($commandFiles);
$commandFileNamespaces = array_values($commandFiles);
// Ensure that the command files that we expected to
// find were all found. We don't find anything in
// 'beta' because only 'alpha' is in the search path.
$this->assertContains('./src/ExampleCommandFile.php', $commandFilePaths);
$this->assertContains('./src/ExampleHookAllCommandFile.php', $commandFilePaths);
$this->assertContains('./src/alpha/AlphaCommandFile.php', $commandFilePaths);
$this->assertContains('./src/alpha/Inclusive/IncludedCommandFile.php', $commandFilePaths);
// Make sure that there are no additional items found.
$this->assertEquals(4, count($commandFilePaths));
// Ensure that the command file namespaces that we expected
// to be generated all match.
$this->assertContains('\Consolidation\TestUtils\ExampleCommandFile', $commandFileNamespaces);
$this->assertContains('\Consolidation\TestUtils\ExampleHookAllCommandFile', $commandFileNamespaces);
$this->assertContains('\Consolidation\TestUtils\alpha\AlphaCommandFile', $commandFileNamespaces);
$this->assertContains('\Consolidation\TestUtils\alpha\Inclusive\IncludedCommandFile', $commandFileNamespaces);
// We do not need to test for additional namespace items, because we
// know that the length of the array_keys must be the same as the
// length of the array_values.
}
function testDeepCommandDiscovery()
{
$discovery = new CommandFileDiscovery();
$discovery
->setSearchPattern('*CommandFile.php')
->setSearchDepth(1)
->setSearchLocations([]);
chdir(__DIR__);
$commandFiles = $discovery->discover('.', '\Consolidation\TestUtils');
$commandFilePaths = array_keys($commandFiles);
$commandFileNamespaces = array_values($commandFiles);
// Ensure that the command files that we expected to
// find were all found. We find both 'alpha' and 'beta'
// items because the search locations is empty, which
// causes the search at the base directory to be deep.
// We do not find alpha/Inclusive, though, as the search
// depth is only 2, which excludes directories that are
// three levels deep.
$this->assertContains('./src/ExampleCommandFile.php', $commandFilePaths);
$this->assertContains('./src/ExampleHookAllCommandFile.php', $commandFilePaths);
$this->assertContains('./src/alpha/AlphaCommandFile.php', $commandFilePaths);
$this->assertContains('./src/beta/BetaCommandFile.php', $commandFilePaths);
// Make sure that there are no additional items found.
$this->assertEquals(4, count($commandFilePaths));
// Ensure that the command file namespaces that we expected
// to be generated all match.
$this->assertContains('\Consolidation\TestUtils\ExampleCommandFile', $commandFileNamespaces);
$this->assertContains('\Consolidation\TestUtils\ExampleHookAllCommandFile', $commandFileNamespaces);
$this->assertContains('\Consolidation\TestUtils\alpha\AlphaCommandFile', $commandFileNamespaces);
$this->assertContains('\Consolidation\TestUtils\beta\BetaCommandFile', $commandFileNamespaces);
// We do not need to test for additional namespace items, because we
// know that the length of the array_keys must be the same as the
// length of the array_values.
}
}
PK r?VV V tests/testCommandInfo.phpnu W+A $value) {
if (!is_string($value)) {
$value = var_export($value, true);
}
$result[] = "{$key}=>{$value}";
}
return implode("\n", $result);
}
/**
* Test CommandInfo command annotation parsing.
*/
function testParsing()
{
$commandInfo = new CommandInfo('\Consolidation\TestUtils\ExampleCommandFile', 'testArithmatic');
$this->assertEquals('test:arithmatic', $commandInfo->getName());
$this->assertEquals(
'This is the test:arithmatic command',
$commandInfo->getDescription()
);
$this->assertEquals(
"This command will add one and two. If the --negate flag\nis provided, then the result is negated.",
$commandInfo->getHelp()
);
$this->assertEquals('arithmatic', implode(',', $commandInfo->getAliases()));
$this->assertEquals(
'2 2 --negate=>Add two plus two and then negate.',
$this->flattenArray($commandInfo->getExampleUsages())
);
$this->assertEquals(
'The first number to add.',
$commandInfo->arguments()->getDescription('one')
);
$this->assertEquals(
'The other number to add.',
$commandInfo->arguments()->getDescription('two')
);
$this->assertEquals(
'Whether or not the result should be negated.',
$commandInfo->options()->getDescription('negate')
);
}
function testReturnValue()
{
$commandInfo = new CommandInfo('\Consolidation\TestUtils\alpha\AlphaCommandFile', 'exampleTable');
$this->assertEquals('example:table', $commandInfo->getName());
$this->assertEquals('\Consolidation\OutputFormatters\StructuredData\RowsOfFields', $commandInfo->getReturnType());
}
}
PK r?V|.E E tests/testAnnotatedCommand.phpnu W+A assertInstanceOf('\Symfony\Component\Console\Command\Command', $command);
$this->assertEquals('my:cat', $command->getName());
$this->assertEquals('This is the my:cat command implemented as an AnnotatedCommand subclass.', $command->getDescription());
$this->assertEquals("This command will concatenate two parameters. If the --flip flag\nis provided, then the result is the concatenation of two and one.", $command->getHelp());
$this->assertEquals('c', implode(',', $command->getAliases()));
// Symfony Console composes the synopsis; perhaps we should not test it. Remove if this gives false failures.
$this->assertEquals('my:cat [--flip] [--] []', $command->getSynopsis());
$this->assertEquals('my:cat bet alpha --flip', implode(',', $command->getUsages()));
$input = new StringInput('my:cat bet alpha --flip');
$this->assertRunCommandViaApplicationEquals($command, $input, 'alphabet');
}
// TODO: Make a base test class to hold this.
function assertRunCommandViaApplicationEquals($command, $input, $expectedOutput, $expectedStatusCode = 0)
{
$output = new BufferedOutput();
$application = new Application('TestApplication', '0.0.0');
$application->setAutoExit(false);
$application->add($command);
$statusCode = $application->run($input, $output);
$commandOutput = trim($output->fetch());
$this->assertEquals($expectedOutput, $commandOutput);
$this->assertEquals($expectedStatusCode, $statusCode);
}
}
PK r?Vº $ $ % tests/src/ExampleAnnotatedCommand.phpnu W+A getArgument('one');
$two = $input->getArgument('two');
$flip = $input->getOption('flip');
$result = $this->myCat($one, $two, $flip);
// We could also just use $output->writeln($result) here,
// but calling processResults enables the use of output
// formatters. Note also that if you use processResults, you
// should correctly inject the command processor into your
// annotated command via AnnotatedCommand::setCommandProcessor().
return $this->processResults($input, $output, $result);
}
}
PK r?VjW W tests/src/ExampleCommandFile.phpnu W+A state = $state;
}
/**
* This is the my:cat command
*
* This command will concatenate two parameters. If the --flip flag
* is provided, then the result is the concatenation of two and one.
*
* @param string $one The first parameter.
* @param string $two The other parameter.
* @option boolean $flip Whether or not the second parameter should come first in the result.
* @aliases c
* @usage bet alpha --flip
* Concatenate "alpha" and "bet".
*/
public function myCat($one, $two = '', $options = ['flip' => false])
{
if ($options['flip']) {
return "{$two}{$one}";
}
return "{$one}{$two}";
}
public function myRepeat($one, $two = '', $options = ['repeat' => 1])
{
return str_repeat("{$one}{$two}", $options['repeat']);
}
/**
* This is a command with no options
*
* This command will concatenate two parameters.
*
* @param $one The first parameter.
* @param $two The other parameter.
* @aliases nope
* @usage alpha bet
* Concatenate "alpha" and "bet".
*/
public function commandWithNoOptions($one, $two = 'default')
{
return "{$one}{$two}";
}
/**
* This command has no arguments--only options
*
* Return a result only if not silent.
*
* @option $silent Supress output.
*/
public function commandWithNoArguments($opts = ['silent|s' => false])
{
if (!$opts['silent']) {
return "Hello, world";
}
}
/**
* Shortcut on annotation
*
* This command defines the option shortcut on the annotation instead of in the options array.
*
* @param $opts The options
* @option $silent|s Supress output.
*/
public function shortcutOnAnnotation($opts = ['silent' => false])
{
if (!$opts['silent']) {
return "Hello, world";
}
}
/**
* This is the test:arithmatic command
*
* This command will add one and two. If the --negate flag
* is provided, then the result is negated.
*
* @param integer $one The first number to add.
* @param integer $two The other number to add.
* @option $negate Whether or not the result should be negated.
* @aliases arithmatic
* @usage 2 2 --negate
* Add two plus two and then negate.
*/
public function testArithmatic($one, $two, $options = ['negate' => false])
{
$result = $one + $two;
if ($options['negate']) {
$result = -$result;
}
// Integer return codes are exit codes (errors), so
// return a the result as a string so that it will be printed.
return "$result";
}
/**
* This is the test:state command
*
* This command tests to see if the state of the Commandfile instance
*/
public function testState()
{
return $this->state;
}
/**
* This is the test:passthrough command
*
* This command takes a variable number of parameters as
* an array and returns them as a csv.
*/
public function testPassthrough(array $params)
{
return implode(',', $params);
}
/**
* This command wraps its parameter in []; its alter hook
* then wraps the result in <>.
*/
public function testHook($parameter)
{
return "[$parameter]";
}
/**
* Wrap the results of test:hook in <>.
*
* @hook alter test:hook
*/
public function hookTestHook($result)
{
return "<$result>";
}
/**
* This test is very similar to the preceding test, except
* it uses an annotation hook instead of a named-function hook.
*
* @hookme
* @before >
* @after <
*/
public function testAnnotationHook($parameter)
{
return "($parameter)";
}
/**
* Wrap the results of test:hook in whatever the @before and @after
* annotations contain.
*
* @hook alter @hookme
*/
public function hookTestAnnotatedHook($result, $args, AnnotationData $annotationData)
{
$before = $annotationData->get('before', '-');
$after = $annotationData->get('after', '-');
return "$before$result$after";
}
public function testHello($who)
{
return "Hello, $who.";
}
public function testException($what)
{
throw new \Exception($what);
}
/**
* @hook interact test:hello
*/
public function interactTestHello($input, $output)
{
$who = $input->getArgument('who');
if (!$who) {
$input->setArgument('who', 'Goofey');
}
}
/**
* @hook validate test:hello
*/
public function validateTestHello($args)
{
if ($args['who'] == 'Donald Duck') {
return new CommandError("I won't say hello to Donald Duck.");
}
if ($args['who'] == 'Drumph') {
throw new \Exception('Irrational value error.');
}
}
/**
* Test default values in arguments
*
* @param string|null $one
* @param string|null $two
* @return string
*/
public function defaults($one = null, $two = null)
{
if ($one && $two) {
return "$one and $two";
}
if ($one) {
return "only $one";
}
return "nothing provided";
}
}
PK r?VV " tests/src/beta/BetaCommandFile.phpnu W+A 42];
}
/**
* @command example:output
*/
public function exampleOutput()
{
return 'Hello, World.';
}
/**
* @command example:cat
*/
public function exampleCat($one, $two = '', $options = ['flip' => false])
{
if ($options['flip']) {
return "{$two}{$one}";
}
return "{$one}{$two}";
}
/**
* @command example:echo
*/
public function exampleEcho(array $args)
{
return ['item-list' => $args];
}
/**
* @command example:message
*/
public function exampleMessage()
{
return ['message' => 'Shipwrecked; send bananas.'];
}
/**
* Test command with formatters
*
* @command example:table
* @field-labels
* first: I
* second: II
* third: III
* @usage try:formatters --format=yaml
* @usage try:formatters --format=csv
* @usage try:formatters --fields=first,third
* @usage try:formatters --fields=III,II
* @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields
*/
public function exampleTable($options = ['format' => 'table', 'fields' => ''])
{
$outputData = [
[ 'first' => 'One', 'second' => 'Two', 'third' => 'Three' ],
[ 'first' => 'Eins', 'second' => 'Zwei', 'third' => 'Drei' ],
[ 'first' => 'Ichi', 'second' => 'Ni', 'third' => 'San' ],
[ 'first' => 'Uno', 'second' => 'Dos', 'third' => 'Tres' ],
];
return new RowsOfFields($outputData);
}
/**
* This command has no annotations; this means that it will not be
* found when createCommandsFromClass() is called with
* '$includeAllPublicMethods' set to false.
*/
public function withoutAnnotations()
{
return 'ok';
}
/**
* @command command:with-one-optional-argument
*
* This command has just one optional argument.
*
* Return a result only if not silent.
*
* @option $silent Supress output.
*/
public function commandWithOneOptionalArgument($who = 'world', $opts = ['silent|s' => false])
{
if (!$opts['silent']) {
return "Hello, $who";
}
}
}
PK r?VΫr r / tests/src/alpha/Exclude/ExcludedCommandFile.phpnu W+A false])
{
if ($options['flip']) {
return "{$two}{$one}";
}
return "{$one}{$two}";
}
public function doRepeat($one, $two = '', $options = ['repeat' => 1])
{
return str_repeat("{$one}{$two}", $options['repeat']);
}
/**
* This hook function does not specify which command or annotation
* it is hooking; that makes it apply to every command in the same class.
*
* @hook alter
*/
public function alterAllCommands($result)
{
if (is_string($result)) {
$result = "*** $result ***";
}
return $result;
}
}
PK r?Vp<2 2
.gitignorenu W+A .DS_Store
phpunit.xml
vendor
composer.lock
build
PK r?VX*- *- README.mdnu W+A # Consolidation\AnnotatedCommand
Initialize Symfony Console commands from annotated command class methods.
[![Travis CI](https://travis-ci.org/consolidation/annotated-command.svg?branch=master)](https://travis-ci.org/consolidation/annotated-command) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/consolidation/annotated-command/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/consolidation/annotated-command/?branch=master) [![Coverage Status](https://coveralls.io/repos/github/consolidation/annotated-command/badge.svg?branch=master)](https://coveralls.io/github/consolidation/annotated-command?branch=master) [![License](https://poser.pugx.org/consolidation/annotated-command/license)](https://packagist.org/packages/consolidation/annotated-command)
**Note** If you are looking for a very fast way to write a Symfony Console-base command-line tool, you should consider using [Robo](https://github.com/consolidation/Robo). See [Using Robo as a Framework](https://github.com/consolidation/Robo/docs/framework.md). It is possible to use this project without Robo if desired, though.
## Component Status
Currently in use in [Robo](https://github.com/consolidation/Robo).
## Library Usage
This is a library intended to be used in some other project. Require from your composer.json file:
```
"require": {
"consolidation/annotated-command": "~1|~2"
},
```
In the 2.x branch, the interfaces ValidatorInterface, ProcessResultInterface and AlterResultInterface changed. If your application uses any of these interfaces, then you should require version "~2" instead. The 2.x branch is compatible with the 1.x branch in other respects.
## Motivation
Symfony Console provides a set of classes that are widely used to implement command line tools. Increasingly, it is becoming popular to use annotations to describe the characteristics of the command (e.g. its arguments, options and so on) implemented by the annotated method.
Extant commandline tools that utilize this technique include:
- [Robo](https://github.com/consolidation/Robo)
- [wp-cli](https://github.com/wp-cli/wp-cli)
- [Pantheon Terminus](https://github.com/pantheon-systems/terminus)
This library provides routines to produce the Symfony\Component\Console\Command\Command from all public methods defined in the provided class.
## Example Annotated Command Class
The public methods of the command class define its commands, and the parameters of each method define its arguments and options. The command options, if any, are declared as the last parameter of the methods. The options will be passed in as an associative array; the default options of the last parameter should list the options recognized by the command.
The rest of the parameters are arguments. Parameters with a default value are optional; those without a default value are required.
```php
class MyCommandClass
{
/**
* This is the my:cat command
*
* This command will concatenate two parameters. If the --flip flag
* is provided, then the result is the concatenation of two and one.
*
* @command my:cat
* @param integer $one The first parameter.
* @param integer $two The other parameter.
* @option $flip Whether or not the second parameter should come first in the result.
* @aliases c
* @usage bet alpha --flip
* Concatenate "alpha" and "bet".
*/
public function myCat($one, $two, $options = ['flip' => false])
{
if ($options['flip']) {
return "{$two}{$one}";
}
return "{$one}{$two}";
}
}
```
## Hooks
Commandfiles may provide hooks in addition to commands. A commandfile method that contains a @hook annotation is registered as a hook instead of a command. The format of the hook annotation is:
```
@hook type commandname|annotation
```
The commandname may be the command's primary name (e.g. `my:command`), it's method name (e.g. myCommand) or any of its aliases.
If an annotation is given instead, then this hook function will run for all commands with the specified annotation.
There are seven types of hooks supported:
- Interact
- Validate
- Command
- Process
- Alter
- Status
- Extract
Most of these also have "pre" and "post" varieties, to give more flexibility vis-a-vis hook ordering (and for consistency). Note that many validate, process and alter hooks may run, but the first status or extract hook that successfully returns a result will halt processing of further hooks of the same type.
### Interact Hook
The interact hook runs prior to argument and option validation. Required arguments and options not supplied by the user may be provided during this phase, either from configuration or by prompting the user. The hook is provided an `$input` object, which may be manipulated directly.
### Validate Hook
Validation hooks examine the arguments and options passed to a command. A validation hook may take one of several actions:
- Do nothing. This indicates that validation succeeded.
- Return an array. This alters the arguments that will be used during command execution.
- Return a CommandError. Validation fails, and execution stops. The CommandError contains a status result code and a message, which is printed.
- Throw an exception. The exception is converted into a CommandError.
Any number of validation hooks may run, but if any fails, then execution of the command stops.
### Command Hook
The command hook is called just before the command actually executes. Unlike the other hooks, this one can be applied to Symfony Console commands that were not created via the AnnotatedCommandFactory.
### Process Hook
Some commands will return an object that generates a result, rather than returning a result object directly. When this is supported, the operation should be executed during the process hook.
An example of this is implemented in Robo; if a Robo command returns a TaskInterface, then a Robo process hook will execute the task and return the result. This allows a pre-process hook to alter the task, e.g. by adding more operations to a task collection.
### Alter Hook
An alter hook changes the result object. Alter hooks should only operate on result objects of a type they explicitly recognize. They may return an object of the same type, or they may convert the object to some other type.
If something goes wrong, and the alter hooks wishes to force the command to fail, then it may either return a CommandError object, or throw an exception.
### Status Hook
The result object returned by a command may be a compound object that contains multiple bits of information about the command result. If the result object implements ExitCodeInterface, then the `getExitCode()` method of the result object is called to determine what the status result code for the command should be. If ExitCodeInterface is not implemented, then all of the status hooks attached to this command are executed; the first one that successfully returns a result will stop further execution of status hooks, and the result it returned will be used as the status result code for this operation.
If no status hook returns any result, then success is presumed.
### Extract Hook
The result object returned by a command may be a compound object that contains multiple bits of information about the command result. If the result object implements OutputDataInterface, then the `getOutputData()` method of the result object is called to determine what information should be displayed to the user as a result of the command's execution. If ExitCodeInterface is not implemented, then all of the extract hooks attached to this command are executed; the first one that successfully returns output data will stop further execution of extract hooks.
If no extract hook returns any data, then the result object itself is printed if it is a string; otherwise, no output is emitted (other than any produced by the command itself).
## Output
If a command method returns an integer, it is used as the command exit status code. If the command method returns a string, it is printed.
If the [Consolidation/OutputFormatters](https://github.com/consolidation/output-formatters) project is used, then users may specify a --format option to select the formatter to use to transform the output from whatever form the command provides to a string. To make this work, the application must provide a formatter to the AnnotatedCommandFactory. See [API Usage](#api-usage) below.
## Logging
The Annotated-Command project is completely agnostic to logging. If a command wishes to log progress, then the CommandFile class should implement LoggerAwareInterface, and the Commandline tool should inject a logger for its use via the LoggerAwareTrait `setLogger()` method. Using [Robo](https://github.com/codegyre/robo) is recommended.
## Access to Symfony Objects
If you want to use annotations, but still want access to the Symfony Command, e.g. to get a reference to the helpers in order to call some legacy code, you may create an ordinary Symfony Command that extends \Consolidation\AnnotatedCommand\AnnotatedCommand, which is a \Symfony\Component\Console\Command\Command. Omit the configure method, and place your annotations on the `execute()` method.
It is also possible to add InputInterface or OutputInterface parameters to any annotated method of a command file.
## API Usage
To use annotated commands in an application, pass an instance of your command class in to AnnotatedCommandFactory::createCommandsFromClass(). The result will be a list of Commands that may be added to your application.
```php
$myCommandClassInstance = new MyCommandClass();
$commandFactory = new AnnotatedCommandFactory();
$commandFactory->commandProcessor()->setFormatterManager(new FormatterManager());
$commandList = $commandFactory->createCommandsFromClass($myCommandClassInstance);
foreach ($commandList as $command) {
$application->add($command);
}
```
You may have more than one command class, if you wish. If so, simply call AnnotatedCommandFactory::createCommandsFromClass() multiple times.
Note that the `setFormatterManager()` operation is optional; omit this if not using [Consolidation/OutputFormatters](https://github.com/consolidation/output-formatters).
A discovery class, CommandFileDiscovery, is also provided to help find command files on the filesystem. Usage is as follows:
```php
$discovery = new CommandFileDiscovery();
$myCommandFiles = $discovery->discover($path, '\Drupal');
foreach ($myCommandFiles as $myCommandClass) {
$myCommandClassInstance = new $myCommandClass();
// ... as above
}
```
For a discussion on command file naming conventions and search locations, see https://github.com/consolidation/annotated-command/issues/12.
If different namespaces are used at different command file paths, change the call to discover as follows:
```php
$myCommandFiles = $discovery->discover(['\Ns1' => $path1, '\Ns2' => $path2]);
```
As a shortcut for the above, the method `discoverNamespaced()` will take the last directory name of each path, and append it to the base namespace provided. This matches the conventions used by Drupal modules, for example.
## Comparison to Existing Solutions
The existing solutions used their own hand-rolled regex-based parsers to process the contents of the DocBlock comments. consolidation/annotated-command uses the [phpdocumentor/reflection-docblock](https://github.com/phpDocumentor/ReflectionDocBlock) project (which is itself a regex-based parser) to interpret DocBlock contents.
PK r?Vua src/CommandProcessor.phpnu W+A hookManager = $hookManager;
}
/**
* Return the hook manager
* @return HookManager
*/
public function hookManager()
{
return $this->hookManager;
}
public function setFormatterManager(FormatterManager $formatterManager)
{
$this->formatterManager = $formatterManager;
}
public function setDisplayErrorFunction(callable $fn)
{
$this->displayErrorFunction = $fn;
}
/**
* Return the formatter manager
* @return FormatterManager
*/
public function formatterManager()
{
return $this->formatterManager;
}
public function interact(
InputInterface $input,
OutputInterface $output,
$names,
AnnotationData $annotationData
) {
return $this->hookManager()->interact($input, $output, $names, $annotationData);
}
public function process(
OutputInterface $output,
$names,
$commandCallback,
AnnotationData $annotationData,
$args
) {
$result = [];
// Recover options from the end of the args
$options = end($args);
try {
$result = $this->validateRunAndAlter(
$names,
$commandCallback,
$args,
$annotationData
);
return $this->handleResults($output, $names, $result, $annotationData, $options);
} catch (\Exception $e) {
$result = new CommandError($e->getMessage(), $e->getCode());
return $this->handleResults($output, $names, $result, $annotationData, $options);
}
}
public function validateRunAndAlter(
$names,
$commandCallback,
$args,
AnnotationData $annotationData
) {
// Validators return any object to signal a validation error;
// if the return an array, it replaces the arguments.
$validated = $this->hookManager()->validateArguments($names, $args, $annotationData);
if (is_object($validated)) {
return $validated;
}
if (is_array($validated)) {
$args = $validated;
}
// Run the command, alter the results, and then handle output and status
$result = $this->runCommandCallback($commandCallback, $args);
return $this->processResults($names, $result, $args, $annotationData);
}
public function processResults($names, $result, $args, $annotationData)
{
return $this->hookManager()->alterResult($names, $result, $args, $annotationData);
}
/**
* Handle the result output and status code calculation.
*/
public function handleResults(OutputInterface $output, $names, $result, AnnotationData $annotationData, $options = [])
{
$status = $this->hookManager()->determineStatusCode($names, $result);
// If the result is an integer and no separate status code was provided, then use the result as the status and do no output.
if (is_integer($result) && !isset($status)) {
return $result;
}
$status = $this->interpretStatusCode($status);
// Get the structured output, the output stream and the formatter
$structuredOutput = $this->hookManager()->extractOutput($names, $result);
$output = $this->chooseOutputStream($output, $status);
if ($status != 0) {
return $this->writeErrorMessage($output, $status, $structuredOutput, $result);
}
if (isset($this->formatterManager)) {
return $this->writeUsingFormatter($output, $structuredOutput, $annotationData, $options);
}
return $this->writeCommandOutput($output, $structuredOutput);
}
/**
* Run the main command callback
*/
protected function runCommandCallback($commandCallback, $args)
{
$result = false;
try {
$result = call_user_func_array($commandCallback, $args);
} catch (\Exception $e) {
$result = new CommandError($e->getMessage(), $e->getCode());
}
return $result;
}
/**
* Determine the formatter that should be used to render
* output.
*
* If the user specified a format via the --format option,
* then always return that. Otherwise, return the default
* format, unless --pipe was specified, in which case
* return the default pipe format, format-pipe.
*
* n.b. --pipe is a handy option introduced in Drush 2
* (or perhaps even Drush 1) that indicates that the command
* should select the output format that is most appropriate
* for use in scripts (e.g. to pipe to another command).
*
* @return string
*/
protected function getFormat($options)
{
$options += [
'default-format' => false,
'pipe' => false,
];
$options += [
'format' => $options['default-format'],
'format-pipe' => $options['default-format'],
];
$format = $options['format'];
if ($options['pipe']) {
$format = $options['format-pipe'];
}
return $format;
}
/**
* Determine whether we should use stdout or stderr.
*/
protected function chooseOutputStream(OutputInterface $output, $status)
{
// If the status code indicates an error, then print the
// result to stderr rather than stdout
if ($status && ($output instanceof ConsoleOutputInterface)) {
return $output->getErrorOutput();
}
return $output;
}
/**
* Call the formatter to output the provided data.
*/
protected function writeUsingFormatter(OutputInterface $output, $structuredOutput, AnnotationData $annotationData, $options)
{
$format = $this->getFormat($options);
$formatterOptions = new FormatterOptions($annotationData->getArrayCopy(), $options);
$this->formatterManager->write(
$output,
$format,
$structuredOutput,
$formatterOptions
);
return 0;
}
/**
* Description
* @param OutputInterface $output
* @param int $status
* @param string $structuredOutput
* @param mixed $originalResult
* @return type
*/
protected function writeErrorMessage($output, $status, $structuredOutput, $originalResult)
{
if (isset($this->displayErrorFunction)) {
call_user_func($this->displayErrorFunction, $output, $structuredOutput, $status, $originalResult);
} else {
$this->writeCommandOutput($output, $structuredOutput);
}
return $status;
}
/**
* If the result object is a string, then print it.
*/
protected function writeCommandOutput(
OutputInterface $output,
$structuredOutput
) {
// If there is no formatter, we will print strings,
// but can do no more than that.
if (is_string($structuredOutput)) {
$output->writeln($structuredOutput);
}
return 0;
}
/**
* If a status code was set, then return it; otherwise,
* presume success.
*/
protected function interpretStatusCode($status)
{
if (isset($status)) {
return $status;
}
return 0;
}
}
PK r?Vv7 src/AnnotatedCommandFactory.phpnu W+A commandProcessor = new CommandProcessor(new HookManager());
}
public function setCommandProcessor($commandProcessor)
{
$this->commandProcessor = $commandProcessor;
}
public function commandProcessor()
{
return $this->commandProcessor;
}
public function hookManager()
{
return $this->commandProcessor()->hookManager();
}
public function addListener($listener)
{
$this->listeners[] = $listener;
}
protected function notify($commandFileInstance)
{
foreach ($this->listeners as $listener) {
if ($listener instanceof CommandCreationListenerInterface) {
$listener->notifyCommandFileAdded($commandFileInstance);
}
if (is_callable($listener)) {
$listener($commandFileInstance);
}
}
}
public function createCommandsFromClass($commandFileInstance, $includeAllPublicMethods = true)
{
$this->notify($commandFileInstance);
$commandInfoList = $this->getCommandInfoListFromClass($commandFileInstance);
$this->registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance);
return $this->createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods);
}
public function getCommandInfoListFromClass($classNameOrInstance)
{
$commandInfoList = [];
// Ignore special functions, such as __construct and __call, and
// accessor methods such as getFoo and setFoo, while allowing
// set or setup.
$commandMethodNames = array_filter(
get_class_methods($classNameOrInstance) ?: [],
function ($m) {
return !preg_match('#^(_|get[A-Z]|set[A-Z])#', $m);
}
);
foreach ($commandMethodNames as $commandMethodName) {
$commandInfoList[] = new CommandInfo($classNameOrInstance, $commandMethodName);
}
return $commandInfoList;
}
public function createCommandInfo($classNameOrInstance, $commandMethodName)
{
return new CommandInfo($classNameOrInstance, $commandMethodName);
}
public function createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods = true)
{
return $this->createSelectedCommandsFromClassInfo(
$commandInfoList,
$commandFileInstance,
function ($commandInfo) use ($includeAllPublicMethods) {
return $includeAllPublicMethods || static::isCommandMethod($commandInfo);
}
);
}
public function createSelectedCommandsFromClassInfo($commandInfoList, $commandFileInstance, callable $commandSelector)
{
$commandList = [];
foreach ($commandInfoList as $commandInfo) {
if ($commandSelector($commandInfo)) {
$command = $this->createCommand($commandInfo, $commandFileInstance);
$commandList[] = $command;
}
}
return $commandList;
}
public static function isCommandMethod($commandInfo)
{
if ($commandInfo->hasAnnotation('hook')) {
return false;
}
return $commandInfo->hasAnnotation('command');
}
public function registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance)
{
foreach ($commandInfoList as $commandInfo) {
if ($commandInfo->hasAnnotation('hook')) {
$this->registerCommandHook($commandInfo, $commandFileInstance);
}
}
}
/**
* Register a command hook given the CommandInfo for a method.
*
* The hook format is:
*
* @hook type name type
*
* For example, the pre-validate hook for the core:init command is:
*
* @hook pre-validate core:init
*
* If no command name is provided, then this hook will affect every
* command that is defined in the same file.
*
* If no hook is provided, then we will presume that ALTER_RESULT
* is intended.
*
* @param CommandInfo $commandInfo Information about the command hook method.
* @param object $commandFileInstance An instance of the CommandFile class.
*/
public function registerCommandHook(CommandInfo $commandInfo, $commandFileInstance)
{
// Ignore if the command info has no @hook
if (!$commandInfo->hasAnnotation('hook')) {
return;
}
$hookData = $commandInfo->getAnnotation('hook');
$hook = $this->getNthWord($hookData, 0, HookManager::ALTER_RESULT);
$commandName = $this->getNthWord($hookData, 1);
// Register the hook
$callback = [$commandFileInstance, $commandInfo->getMethodName()];
$this->commandProcessor()->hookManager()->add($callback, $hook, $commandName);
}
protected function getNthWord($string, $n, $default = '', $delimiter = ' ')
{
$words = explode($delimiter, $string);
if (!empty($words[$n])) {
return $words[$n];
}
return $default;
}
public function createCommand(CommandInfo $commandInfo, $commandFileInstance)
{
$command = new AnnotatedCommand($commandInfo->getName());
$commandCallback = [$commandFileInstance, $commandInfo->getMethodName()];
$command->setCommandCallback($commandCallback);
$command->setCommandProcessor($this->commandProcessor);
$command->setCommandInfo($commandInfo);
// Annotation commands are never bootstrap-aware, but for completeness
// we will notify on every created command, as some clients may wish to
// use this notification for some other purpose.
$this->notify($command);
return $command;
}
}
PK r?V0T( ( src/AnnotatedCommand.phpnu W+A getName();
}
}
parent::__construct($name);
if ($commandInfo && $commandInfo->hasAnnotation('command')) {
$this->setCommandInfo($commandInfo);
}
}
public function setCommandCallback($commandCallback)
{
$this->commandCallback = $commandCallback;
}
public function setCommandProcessor($commandProcessor)
{
$this->commandProcessor = $commandProcessor;
}
public function getCommandProcessor()
{
// If someone is using an AnnotatedCommand, and is NOT getting
// it from an AnnotatedCommandFactory OR not correctly injecting
// a command processor via setCommandProcessor() (ideally via the
// DI container), then we'll just give each annotated command its
// own command processor. This is not ideal; preferably, there would
// only be one instance of the command processor in the application.
if (!isset($this->commandProcessor)) {
$this->commandProcessor = new CommandProcessor(new HookManager());
}
return $this->commandProcessor;
}
public function getReturnType()
{
return $this->returnType;
}
public function setReturnType($returnType)
{
$this->returnType = $returnType;
}
public function setAnnotationData($annotationData)
{
$this->annotationData = $annotationData;
}
public function setCommandInfo($commandInfo)
{
$this->setDescription($commandInfo->getDescription());
$this->setHelp($commandInfo->getHelp());
$this->setAliases($commandInfo->getAliases());
$this->setAnnotationData($commandInfo->getAnnotations());
foreach ($commandInfo->getExampleUsages() as $usage => $description) {
// Symfony Console does not support attaching a description to a usage
$this->addUsage($usage);
}
$this->setCommandArguments($commandInfo);
$this->setCommandOptions($commandInfo);
$this->setReturnType($commandInfo->getReturnType());
}
protected function setCommandArguments($commandInfo)
{
$this->setUsesInputInterface($commandInfo);
$this->setUsesOutputInterface($commandInfo);
$this->setCommandArgumentsFromParameters($commandInfo);
}
/**
* Check whether the first parameter is an InputInterface.
*/
protected function checkUsesInputInterface($params)
{
$firstParam = reset($params);
return $firstParam instanceof InputInterface;
}
/**
* Determine whether this command wants to get its inputs
* via an InputInterface or via its command parameters
*/
protected function setUsesInputInterface($commandInfo)
{
$params = $commandInfo->getParameters();
$this->usesInputInterface = $this->checkUsesInputInterface($params);
}
/**
* Determine whether this command wants to send its output directly
* to the provided OutputInterface, or whether it will returned
* structured output to be processed by the command processor.
*/
protected function setUsesOutputInterface($commandInfo)
{
$params = $commandInfo->getParameters();
$index = $this->checkUsesInputInterface($params) ? 1 : 0;
$this->usesOutputInterface =
(count($params) > $index) &&
($params[$index] instanceof OutputInterface);
}
protected function setCommandArgumentsFromParameters($commandInfo)
{
$args = $commandInfo->arguments()->getValues();
foreach ($args as $name => $defaultValue) {
$description = $commandInfo->arguments()->getDescription($name);
$hasDefault = $commandInfo->arguments()->hasDefault($name);
$parameterMode = $this->getCommandArgumentMode($hasDefault, $defaultValue);
$this->addArgument($name, $parameterMode, $description, $defaultValue);
}
}
protected function getCommandArgumentMode($hasDefault, $defaultValue)
{
if (!$hasDefault) {
return InputArgument::REQUIRED;
}
if (is_array($defaultValue)) {
return InputArgument::IS_ARRAY;
}
return InputArgument::OPTIONAL;
}
protected function setCommandOptions($commandInfo)
{
$opts = $commandInfo->options()->getValues();
foreach ($opts as $name => $val) {
$description = $commandInfo->options()->getDescription($name);
$fullName = $name;
$shortcut = '';
if (strpos($name, '|')) {
list($fullName, $shortcut) = explode('|', $name, 2);
}
if (is_bool($val)) {
$this->addOption($fullName, $shortcut, InputOption::VALUE_NONE, $description);
} else {
$this->addOption($fullName, $shortcut, InputOption::VALUE_OPTIONAL, $description, $val);
}
}
}
protected function getArgsWithPassThrough($input)
{
$args = $input->getArguments();
// When called via the Application, the first argument
// will be the command name. The Application alters the
// input definition to match, adding a 'command' argument
// to the beginning.
array_shift($args);
if ($input instanceof PassThroughArgsInput) {
return $this->appendPassThroughArgs($input, $args);
}
return $args;
}
protected function getArgsAndOptions($input)
{
if (!$input) {
return [];
}
// Get passthrough args, and add the options on the end.
$args = $this->getArgsWithPassThrough($input);
$args[] = $input->getOptions();
return $args;
}
protected function appendPassThroughArgs($input, $args)
{
$passThrough = $input->getPassThroughArgs();
$definition = $this->getDefinition();
$argumentDefinitions = $definition->getArguments();
$lastParameter = end($argumentDefinitions);
if ($lastParameter && $lastParameter->isArray()) {
$args[$lastParameter->getName()] = array_merge($args[$lastParameter->getName()], $passThrough);
} else {
$args[$lastParameter->getName()] = implode(' ', $passThrough);
}
return $args;
}
/**
* Returns all of the hook names that may be called for this command.
*
* @return array
*/
protected function getNames()
{
return array_filter(
array_merge(
$this->getNamesUsingCommands(),
[HookManager::getClassNameFromCallback($this->commandCallback)]
)
);
}
protected function getNamesUsingCommands()
{
return array_merge(
[$this->getName()],
$this->getAliases()
);
}
/**
* {@inheritdoc}
*/
protected function interact(InputInterface $input, OutputInterface $output)
{
$this->getCommandProcessor()->interact(
$input,
$output,
$this->getNames(),
$this->annotationData
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
// Get passthrough args, and add the options on the end.
$args = $this->getArgsAndOptions($input);
if ($this->usesInputInterface) {
array_unshift($args, $input);
}
if ($this->usesOutputInterface) {
array_unshift($args, $output);
}
// Validate, run, process, alter, handle results.
return $this->getCommandProcessor()->process(
$output,
$this->getNames(),
$this->commandCallback,
$this->annotationData,
$args
);
}
public function processResults(InputInterface $input, OutputInterface $output, $results)
{
$commandProcessor = $this->getCommandProcessor();
$names = $this->getNames();
$args = $this->getArgsAndOptions($input);
$results = $commandProcessor->processResults(
$names,
$results,
$args,
$this->annotationData
);
$options = end($args);
return $commandProcessor->handleResults(
$output,
$names,
$results,
$this->annotationData,
$options
);
}
}
PK r?V\. . src/CommandFileDiscovery.phpnu W+A discoverNamespaced($moduleList, '\Drupal');
*
* To discover global commands:
*
* $commandFiles = $discovery->discover($drupalRoot, '\Drupal');
*/
class CommandFileDiscovery
{
/** @var string[] */
protected $excludeList;
/** @var string[] */
protected $searchLocations;
/** @var string */
protected $searchPattern = '*Commands.php';
/** @var boolean */
protected $includeFilesAtBase = true;
/** @var integer */
protected $searchDepth = 2;
public function __construct()
{
$this->excludeList = ['Exclude'];
$this->searchLocations = [
'Command',
'CliTools', // TODO: Maybe remove
];
}
/**
* Specify whether to search for files at the base directory
* ($directoryList parameter to discover and discoverNamespaced
* methods), or only in the directories listed in the search paths.
*
* @param boolean $includeFilesAtBase
*/
public function setIncludeFilesAtBase($includeFilesAtBase)
{
$this->includeFilesAtBase = $includeFilesAtBase;
return $this;
}
/**
* Set the list of excludes to add to the finder, replacing
* whatever was there before.
*
* @param array $excludeList The list of directory names to skip when
* searching for command files.
*/
public function setExcludeList($excludeList)
{
$this->excludeList = $excludeList;
return $this;
}
/**
* Add one more location to the exclude list.
*
* @param string $exclude One directory name to skip when searching
* for command files.
*/
public function addExclude($exclude)
{
$this->excludeList[] = $exclude;
return $this;
}
/**
* Set the search depth. By default, fills immediately in the
* base directory are searched, plus all of the search locations
* to this specified depth. If the search locations is set to
* an empty array, then the base directory is searched to this
* depth.
*/
public function setSearchDepth($searchDepth)
{
$this->searchDepth = $searchDepth;
return $this;
}
/**
* Set the list of search locations to examine in each directory where
* command files may be found. This replaces whatever was there before.
*
* @param array $searchLocations The list of locations to search for command files.
*/
public function setSearchLocations($searchLocations)
{
$this->searchLocations = $searchLocations;
return $this;
}
/**
* Add one more location to the search location list.
*
* @param string $location One more relative path to search
* for command files.
*/
public function addSearchLocation($location)
{
$this->searchLocations[] = $location;
return $this;
}
/**
* Specify the pattern / regex used by the finder to search for
* command files.
*/
public function setSearchPattern($searchPattern)
{
$this->searchPattern = $searchPattern;
return $this;
}
/**
* Given a list of directories, e.g. Drupal modules like:
*
* core/modules/block
* core/modules/dblog
* modules/default_content
*
* Discover command files in any of these locations.
*
* @param string|string[] $directoryList Places to search for commands.
*
* @return array
*/
public function discoverNamespaced($directoryList, $baseNamespace = '')
{
return $this->discover($this->convertToNamespacedList((array)$directoryList), $baseNamespace);
}
/**
* Given a simple list containing paths to directories, where
* the last component of the path should appear in the namespace,
* after the base namespace, this function will return an
* associative array mapping the path's basename (e.g. the module
* name) to the directory path.
*
* Module names must be unique.
*
* @param string[] $directoryList A list of module locations
*
* @return array
*/
public function convertToNamespacedList($directoryList)
{
$namespacedArray = [];
foreach ((array)$directoryList as $directory) {
$namespacedArray[basename($directory)] = $directory;
}
return $namespacedArray;
}
/**
* Search for command files in the specified locations. This is the function that
* should be used for all locations that are NOT modules of a framework.
*
* @param string|string[] $directoryList Places to search for commands.
* @return array
*/
public function discover($directoryList, $baseNamespace = '')
{
$commandFiles = [];
foreach ((array)$directoryList as $key => $directory) {
$itemsNamespace = $this->joinNamespace([$baseNamespace, $key]);
$commandFiles = array_merge(
$commandFiles,
$this->discoverCommandFiles($directory, $itemsNamespace),
$this->discoverCommandFiles("$directory/src", $itemsNamespace)
);
}
return $commandFiles;
}
/**
* Search for command files in specific locations within a single directory.
*
* In each location, we will accept only a few places where command files
* can be found. This will reduce the need to search through many unrelated
* files.
*
* The default search locations include:
*
* .
* CliTools
* src/CliTools
*
* The pattern we will look for is any file whose name ends in 'Commands.php'.
* A list of paths to found files will be returned.
*/
protected function discoverCommandFiles($directory, $baseNamespace)
{
$commandFiles = [];
// In the search location itself, we will search for command files
// immediately inside the directory only.
if ($this->includeFilesAtBase) {
$commandFiles = $this->discoverCommandFilesInLocation(
$directory,
$this->getBaseDirectorySearchDepth(),
$baseNamespace
);
}
// In the other search locations,
foreach ($this->searchLocations as $location) {
$itemsNamespace = $this->joinNamespace([$baseNamespace, $location]);
$commandFiles = array_merge(
$commandFiles,
$this->discoverCommandFilesInLocation(
"$directory/$location",
$this->getSearchDepth(),
$itemsNamespace
)
);
}
return $commandFiles;
}
/**
* Return a Finder search depth appropriate for our selected search depth.
*
* @return string
*/
protected function getSearchDepth()
{
return $this->searchDepth <= 0 ? '== 0' : '<= ' . $this->searchDepth;
}
/**
* Return a Finder search depth for the base directory. If the
* searchLocations array has been populated, then we will only search
* for files immediately inside the base directory; no traversal into
* deeper directories will be done, as that would conflict with the
* specification provided by the search locations. If there is no
* search location, then we will search to whatever depth was specified
* by the client.
*
* @return string
*/
protected function getBaseDirectorySearchDepth()
{
if (!empty($this->searchLocations)) {
return '== 0';
}
return $this->getSearchDepth();
}
/**
* Search for command files in just one particular location. Returns
* an associative array mapping from the pathname of the file to the
* classname that it contains. The pathname may be ignored if the search
* location is included in the autoloader.
*
* @param string $directory The location to search
* @param string $depth How deep to search (e.g. '== 0' or '< 2')
* @param string $baseNamespace Namespace to prepend to each classname
*
* @return array
*/
protected function discoverCommandFilesInLocation($directory, $depth, $baseNamespace)
{
if (!is_dir($directory)) {
return [];
}
$finder = $this->createFinder($directory, $depth);
$commands = [];
foreach ($finder as $file) {
$relativePathName = $file->getRelativePathname();
$relativeNamespaceAndClassname = str_replace(
['/', '.php'],
['\\', ''],
$relativePathName
);
$classname = $this->joinNamespace([$baseNamespace, $relativeNamespaceAndClassname]);
$commandFilePath = $this->joinPaths([$directory, $relativePathName]);
$commands[$commandFilePath] = $classname;
}
return $commands;
}
/**
* Create a Finder object for use in searching a particular directory
* location.
*
* @param string $directory The location to search
* @param string $depth The depth limitation
*
* @return Finder
*/
protected function createFinder($directory, $depth)
{
$finder = new Finder();
$finder->files()
->name($this->searchPattern)
->in($directory)
->depth($depth);
foreach ($this->excludeList as $item) {
$finder->exclude($item);
}
return $finder;
}
/**
* Combine the items of the provied array into a backslash-separated
* namespace string. Empty and numeric items are omitted.
*
* @param array $namespaceParts List of components of a namespace
*
* @return string
*/
protected function joinNamespace(array $namespaceParts)
{
return $this->joinParts(
'\\',
$namespaceParts,
function ($item) {
return !is_numeric($item) && !empty($item);
}
);
}
/**
* Combine the items of the provied array into a slash-separated
* pathname. Empty items are omitted.
*
* @param array $pathParts List of components of a path
*
* @return string
*/
protected function joinPaths(array $pathParts)
{
return $this->joinParts(
'/',
$pathParts,
function ($item) {
return !empty($item);
}
);
}
/**
* Simple wrapper around implode and array_filter.
*
* @param string $delimiter
* @param array $parts
* @param callable $filterFunction
*/
protected function joinParts($delimiter, $parts, $filterFunction)
{
return implode(
$delimiter,
array_filter($parts, $filterFunction)
);
}
}
PK r?V6|Ay y src/PassThroughArgsInput.phpnu W+A passThroughArgs = $passThroughArgs;
$this->delegate = $delegate;
if (!$this->delegate) {
$this->delegate = new ArgvInput();
}
}
public function getPassThroughArgs()
{
return $this->passThroughArgs;
}
/**
* {@inheritdoc}
*/
public function getFirstArgument()
{
return $this->delegate->getFirstArgument();
}
/**
* {@inheritdoc}
*/
public function hasParameterOption($values, $onlyParams = false)
{
return $this->delegate->hasParameterOption($values, $onlyParams);
}
/**
* {@inheritdoc}
*/
public function getParameterOption($values, $default = false, $onlyParams = false)
{
return $this->delegate->getParameterOption($values, $default, $onlyParams);
}
/**
* {@inheritdoc}
*/
public function bind(InputDefinition $definition)
{
return $this->delegate->bind($definition);
}
/**
* {@inheritdoc}
*/
public function validate()
{
$this->delegate->validate();
}
/**
* {@inheritdoc}
*/
public function getArguments()
{
return $this->delegate->getArguments();
}
/**
* {@inheritdoc}
*/
public function getArgument($name)
{
return $this->delegate->getArgument($name);
}
/**
* {@inheritdoc}
*/
public function setArgument($name, $value)
{
return $this->delegate->setArgument($name, $value);
}
/**
* {@inheritdoc}
*/
public function hasArgument($name)
{
return $this->delegate->hasArgument($name);
}
/**
* {@inheritdoc}
*/
public function getOptions()
{
return $this->delegate->getOptions();
}
/**
* {@inheritdoc}
*/
public function getOption($name)
{
return $this->delegate->getOption($name);
}
/**
* {@inheritdoc}
*/
public function setOption($name, $value)
{
$this->delegate->setOption($name, $value);
}
/**
* {@inheritdoc}
*/
public function hasOption($name)
{
return $this->delegate->hasOption($name);
}
/**
* {@inheritdoc}
*/
public function isInteractive()
{
return $this->delegate->isInteractive();
}
/**
* {@inheritdoc}
*/
public function setInteractive($interactive)
{
$this->delegate->setInteractive($interactive);
}
}
PK r?V"\4 ( src/CommandCreationListenerInterface.phpnu W+A message = $message;
// Ensure the exit code is non-zero. The exit code may have
// come from an exception, and those often default to zero if
// a specific value is not provided.
$this->exitCode = $exitCode == 0 ? 1 : $exitCode;
}
public function getExitCode()
{
return $this->exitCode;
}
public function getOutputData()
{
return $this->message;
}
}
PK r?V+ src/ExitCodeInterface.phpnu W+A hooks;
}
/**
* Add a hook
*
* @param mixed $callback The callback function to call
* @param string $hook The name of the hook to add
* @param string $name The name of the command to hook
* ('*' for all)
*/
public function add(callable $callback, $hook, $name = '*')
{
if (empty($name)) {
$name = static::getClassNameFromCallback($callback);
}
$this->hooks[$name][$hook][] = $callback;
}
/**
* If a command hook does not specify any particular command
* name that it should be attached to, then it will be applied
* to every command that is defined in the same class as the hook.
* This is controlled by using the namespace + class name of
* the implementing class of the callback hook.
*/
public static function getClassNameFromCallback($callback)
{
if (!is_array($callback)) {
return '';
}
$reflectionClass = new \ReflectionClass($callback[0]);
return $reflectionClass->getName();
}
/**
* Add an interact hook
*
* @param type ValidatorInterface $validator
* @param type $name The name of the command to hook
* ('*' for all)
*/
public function addInteractor(InteractorInterface $interactor, $name = '*')
{
$this->hooks[$name][self::INTERACT][] = $interactor;
}
/**
* Add a pre-validator hook
*
* @param type ValidatorInterface $validator
* @param type $name The name of the command to hook
* ('*' for all)
*/
public function addPreValidator(ValidatorInterface $validator, $name = '*')
{
$this->hooks[$name][self::PRE_ARGUMENT_VALIDATOR][] = $validator;
}
/**
* Add a validator hook
*
* @param type ValidatorInterface $validator
* @param type $name The name of the command to hook
* ('*' for all)
*/
public function addValidator(ValidatorInterface $validator, $name = '*')
{
$this->hooks[$name][self::ARGUMENT_VALIDATOR][] = $validator;
}
/**
* Add a result processor.
*
* @param type ProcessResultInterface $resultProcessor
* @param type $name The name of the command to hook
* ('*' for all)
*/
public function addResultProcessor(ProcessResultInterface $resultProcessor, $name = '*')
{
$this->hooks[$name][self::PROCESS_RESULT][] = $resultProcessor;
}
/**
* Add a result alterer. After a result is processed
* by a result processor, an alter hook may be used
* to convert the result from one form to another.
*
* @param type AlterResultInterface $resultAlterer
* @param type $name The name of the command to hook
* ('*' for all)
*/
public function addAlterResult(AlterResultInterface $resultAlterer, $name = '*')
{
$this->hooks[$name][self::ALTER_RESULT][] = $resultAlterer;
}
/**
* Add a status determiner. Usually, a command should return
* an integer on error, or a result object on success (which
* implies a status code of zero). If a result contains the
* status code in some other field, then a status determiner
* can be used to call the appropriate accessor method to
* determine the status code. This is usually not necessary,
* though; a command that fails may return a CommandError
* object, which contains a status code and a result message
* to display.
* @see CommandError::getExitCode()
*
* @param type StatusDeterminerInterface $statusDeterminer
* @param type $name The name of the command to hook
* ('*' for all)
*/
public function addStatusDeterminer(StatusDeterminerInterface $statusDeterminer, $name = '*')
{
$this->hooks[$name][self::STATUS_DETERMINER][] = $statusDeterminer;
}
/**
* Add an output extractor. If a command returns an object
* object, by default it is passed directly to the output
* formatter (if in use) for rendering. If the result object
* contains more information than just the data to render, though,
* then an output extractor can be used to call the appopriate
* accessor method of the result object to get the data to
* rendered. This is usually not necessary, though; it is preferable
* to have complex result objects implement the OutputDataInterface.
* @see OutputDataInterface::getOutputData()
*
* @param type ExtractOutputInterface $outputExtractor
* @param type $name The name of the command to hook
* ('*' for all)
*/
public function addOutputExtractor(ExtractOutputInterface $outputExtractor, $name = '*')
{
$this->hooks[$name][self::EXTRACT_OUTPUT][] = $outputExtractor;
}
public function interact(
InputInterface $input,
OutputInterface $output,
$names,
AnnotationData $annotationData
) {
$interactors = $this->getInteractors($names, $annotationData);
foreach ($interactors as $interactor) {
$this->callInteractor($interactor, $input, $output, $annotationData);
}
}
public function validateArguments($names, $args, AnnotationData $annotationData)
{
$validators = $this->getValidators($names, $annotationData);
foreach ($validators as $validator) {
$validated = $this->callValidator($validator, $args, $annotationData);
if (is_object($validated)) {
return $validated;
}
if (is_array($validated)) {
$args = $validated;
}
}
return $args;
}
/**
* Process result and decide what to do with it.
* Allow client to add transformation / interpretation
* callbacks.
*/
public function alterResult($names, $result, $args, AnnotationData $annotationData)
{
$processors = $this->getProcessResultHooks($names, $annotationData);
foreach ($processors as $processor) {
$result = $this->callProcessor($processor, $result, $args, $annotationData);
}
$alterers = $this->getAlterResultHooks($names, $annotationData);
foreach ($alterers as $alterer) {
$result = $this->callProcessor($alterer, $result, $args, $annotationData);
}
return $result;
}
/**
* Call all status determiners, and see if any of them
* know how to convert to a status code.
*/
public function determineStatusCode($names, $result)
{
// If the result (post-processing) is an object that
// implements ExitCodeInterface, then we will ask it
// to give us the status code.
if ($result instanceof ExitCodeInterface) {
return $result->getExitCode();
}
// If the result does not implement ExitCodeInterface,
// then we'll see if there is a determiner that can
// extract a status code from the result.
$determiners = $this->getStatusDeterminers($names);
foreach ($determiners as $determiner) {
$status = $this->callDeterminer($determiner, $result);
if (isset($status)) {
return $status;
}
}
}
/**
* Convert the result object to printable output in
* structured form.
*/
public function extractOutput($names, $result)
{
if ($result instanceof OutputDataInterface) {
return $result->getOutputData();
}
$extractors = $this->getOutputExtractors($names);
foreach ($extractors as $extractor) {
$structuredOutput = $this->callExtractor($extractor, $result);
if (isset($structuredOutput)) {
return $structuredOutput;
}
}
return $result;
}
protected function getInteractors($names, AnnotationData $annotationData)
{
return $this->getHooks($names, self::INTERACT, $annotationData);
}
protected function getValidators($names, AnnotationData $annotationData)
{
return $this->getHooks($names, self::ARGUMENT_VALIDATOR, $annotationData);
}
protected function getStatusDeterminers($names)
{
return $this->getHooks($names, self::STATUS_DETERMINER);
}
protected function getProcessResultHooks($names, AnnotationData $annotationData)
{
return $this->getHooks($names, self::PROCESS_RESULT, $annotationData);
}
protected function getAlterResultHooks($names, AnnotationData $annotationData)
{
return $this->getHooks($names, self::ALTER_RESULT, $annotationData);
}
protected function getOutputExtractors($names)
{
return $this->getHooks($names, self::EXTRACT_OUTPUT);
}
protected function getCommandEvents($names)
{
return $this->getHooks($names, self::COMMAND_EVENT);
}
/**
* Get a set of hooks with the provided name(s). Include the
* pre- and post- hooks, and also include the global hooks ('*')
* in addition to the named hooks provided.
*
* @param string|array $names The name of the function being hooked.
* @param string $hook The specific hook name (e.g. alter)
*
* @return callable[]
*/
protected function getHooks($names, $hook, $annotationData = null)
{
$names = array_merge(
(array)$names,
($annotationData == null) ? [] : array_map(function ($item) {
return "@$item";
}, $annotationData->keys())
);
$names[] = '*';
return array_merge(
$this->get($names, "pre-$hook"),
$this->get($names, $hook),
$this->get($names, "post-$hook")
);
}
/**
* Get a set of hooks with the provided name(s).
*
* @param string|array $names The name of the function being hooked.
* @param string $hook The specific hook name (e.g. alter)
*
* @return callable[]
*/
public function get($names, $hook)
{
$hooks = [];
foreach ((array)$names as $name) {
$hooks = array_merge($hooks, $this->getHook($name, $hook));
}
return $hooks;
}
/**
* Get a single named hook.
*
* @param string $name The name of the hooked method
* @param string $hook The specific hook name (e.g. alter)
*
* @return callable[]
*/
protected function getHook($name, $hook)
{
if (isset($this->hooks[$name][$hook])) {
return $this->hooks[$name][$hook];
}
return [];
}
protected function callInteractor($interactor, $input, $output, AnnotationData $annotationData)
{
if ($interactor instanceof InteractorInterface) {
return $interactor->interact($input, $output, $annotationData);
}
if (is_callable($interactor)) {
return $interactor($input, $output, $annotationData);
}
}
protected function callValidator($validator, $args, AnnotationData $annotationData)
{
// TODO: Adding AnnotationData to ValidatorInterface would be
// a breaking change. Either hold off until 2.x, or make
// a new interface containing a method that takes the extra parameter.
if ($validator instanceof ValidatorInterface) {
return $validator->validate($args, $annotationData);
}
if (is_callable($validator)) {
return $validator($args, $annotationData);
}
}
protected function callProcessor($processor, $result, $args, AnnotationData $annotationData)
{
$processed = null;
// TODO: Adding AnnotationData to ProcessResultInterface would be
// a breaking change. Either hold off until 2.x, or make
// a new interface containing a method that takes the extra parameter.
if ($processor instanceof ProcessResultInterface) {
$processed = $processor->process($result, $args, $annotationData);
}
if (is_callable($processor)) {
$processed = $processor($result, $args, $annotationData);
}
if (isset($processed)) {
return $processed;
}
return $result;
}
protected function callDeterminer($determiner, $result)
{
if ($determiner instanceof StatusDeterminerInterface) {
return $determiner->determineStatusCode($result);
}
if (is_callable($determiner)) {
return $determiner($result);
}
}
protected function callExtractor($extractor, $result)
{
if ($extractor instanceof ExtractOutputInterface) {
return $extractor->extractOutput($result);
}
if (is_callable($extractor)) {
return $extractor($result);
}
}
/**
* @param ConsoleCommandEvent $event
*/
public function callCommandEventHooks(ConsoleCommandEvent $event)
{
/* @var Command $command */
$command = $event->getCommand();
$names = [$command->getName()];
$commandEventHooks = $this->getCommandEvents($names);
foreach ($commandEventHooks as $commandEvent) {
if (is_callable($commandEvent)) {
$commandEvent($event);
}
}
}
/**
* @{@inheritdoc}
*/
public static function getSubscribedEvents()
{
return [ConsoleEvents::COMMAND => 'callCommandEventHooks'];
}
}
PK r?V~u# # " src/Hooks/AlterResultInterface.phpnu W+A reflection = new \ReflectionMethod($classNameOrInstance, $methodName);
$this->methodName = $methodName;
$this->otherAnnotations = new AnnotationData();
// Set up a default name for the command from the method name.
// This can be overridden via @command or @name annotations.
$this->name = $this->convertName($this->reflection->name);
$this->options = new DefaultsWithDescriptions($this->determineOptionsFromParameters(), false);
$this->arguments = $this->determineAgumentClassifications();
// Remember the name of the last parameter, if it holds the options.
// We will use this information to ignore @param annotations for the options.
if (!empty($this->options)) {
$this->optionParamName = $this->lastParameterName();
}
}
/**
* Recover the method name provided to the constructor.
*
* @return string
*/
public function getMethodName()
{
return $this->methodName;
}
/**
* Return the primary name for this command.
*
* @return string
*/
public function getName()
{
$this->parseDocBlock();
return $this->name;
}
/**
* Set the primary name for this command.
*
* @param string $name
*/
public function setName($name)
{
$this->name = $name;
}
public function getReturnType()
{
$this->parseDocBlock();
return $this->returnType;
}
public function setReturnType($returnType)
{
$this->returnType = $returnType;
}
/**
* Get any annotations included in the docblock comment for the
* implementation method of this command that are not already
* handled by the primary methods of this class.
*
* @return AnnotationData
*/
public function getAnnotations()
{
$this->parseDocBlock();
return $this->otherAnnotations;
}
/**
* Return a specific named annotation for this command.
*
* @param string $annotation The name of the annotation.
* @return string
*/
public function getAnnotation($annotation)
{
// hasAnnotation parses the docblock
if (!$this->hasAnnotation($annotation)) {
return null;
}
return $this->otherAnnotations[$annotation];
}
/**
* Check to see if the specified annotation exists for this command.
*
* @param string $annotation The name of the annotation.
* @return boolean
*/
public function hasAnnotation($annotation)
{
$this->parseDocBlock();
return isset($this->otherAnnotations[$annotation]);
}
/**
* Save any tag that we do not explicitly recognize in the
* 'otherAnnotations' map.
*/
public function addOtherAnnotation($name, $content)
{
$this->otherAnnotations[$name] = $content;
}
/**
* Get the synopsis of the command (~first line).
*
* @return string
*/
public function getDescription()
{
$this->parseDocBlock();
return $this->description;
}
/**
* Set the command description.
*
* @param string $description The description to set.
*/
public function setDescription($description)
{
$this->description = $description;
}
/**
* Get the help text of the command (the description)
*/
public function getHelp()
{
$this->parseDocBlock();
return $this->help;
}
/**
* Set the help text for this command.
*
* @param string $help The help text.
*/
public function setHelp($help)
{
$this->help = $help;
}
/**
* Return the list of aliases for this command.
* @return string[]
*/
public function getAliases()
{
$this->parseDocBlock();
return $this->aliases;
}
/**
* Set aliases that can be used in place of the command's primary name.
*
* @param string|string[] $aliases
*/
public function setAliases($aliases)
{
if (is_string($aliases)) {
$aliases = explode(',', static::convertListToCommaSeparated($aliases));
}
$this->aliases = array_filter($aliases);
}
/**
* Return the examples for this command. This is @usage instead of
* @example because the later is defined by the phpdoc standard to
* be example method calls.
*
* @return string[]
*/
public function getExampleUsages()
{
$this->parseDocBlock();
return $this->exampleUsage;
}
/**
* Add an example usage for this command.
*
* @param string $usage An example of the command, including the command
* name and all of its example arguments and options.
* @param string $description An explanation of what the example does.
*/
public function setExampleUsage($usage, $description)
{
$this->exampleUsage[$usage] = $description;
}
/**
* Return the list of refleaction parameters.
*
* @return ReflectionParameter[]
*/
public function getParameters()
{
return $this->reflection->getParameters();
}
/**
* Descriptions of commandline arguements for this command.
*
* @return DefaultsWithDescriptions
*/
public function arguments()
{
return $this->arguments;
}
/**
* Descriptions of commandline options for this command.
*
* @return DefaultsWithDescriptions
*/
public function options()
{
return $this->options;
}
/**
* Return the name of the last parameter if it holds the options.
*/
public function optionParamName()
{
return $this->optionParamName;
}
/**
* An option might have a name such as 'silent|s'. In this
* instance, we will allow the @option or @default tag to
* reference the option only by name (e.g. 'silent' or 's'
* instead of 'silent|s').
*
* @param string $optionName
* @return string
*/
public function findMatchingOption($optionName)
{
// Exit fast if there's an exact match
if ($this->options->exists($optionName)) {
return $optionName;
}
$existingOptionName = $this->findExistingOption($optionName);
if (isset($existingOptionName)) {
return $existingOptionName;
}
return $this->findOptionAmongAlternatives($optionName);
}
/**
* @param string $optionName
* @return string
*/
protected function findOptionAmongAlternatives($optionName)
{
// Check the other direction: if the annotation contains @silent|s
// and the options array has 'silent|s'.
$checkMatching = explode('|', $optionName);
if (count($checkMatching) > 1) {
foreach ($checkMatching as $checkName) {
if ($this->options->exists($checkName)) {
$this->options->rename($checkName, $optionName);
return $optionName;
}
}
}
return $optionName;
}
/**
* @param string $optionName
* @return string|null
*/
protected function findExistingOption($optionName)
{
// Check to see if we can find the option name in an existing option,
// e.g. if the options array has 'silent|s' => false, and the annotation
// is @silent.
foreach ($this->options()->getValues() as $name => $default) {
if (in_array($optionName, explode('|', $name))) {
return $name;
}
}
}
/**
* Examine the parameters of the method for this command, and
* build a list of commandline arguements for them.
*
* @return array
*/
protected function determineAgumentClassifications()
{
$result = new DefaultsWithDescriptions();
$params = $this->reflection->getParameters();
$optionsFromParameters = $this->determineOptionsFromParameters();
if (!empty($optionsFromParameters)) {
array_pop($params);
}
foreach ($params as $param) {
$this->addParameterToResult($result, $param);
}
return $result;
}
/**
* Examine the provided parameter, and determine whether it
* is a parameter that will be filled in with a positional
* commandline argument.
*/
protected function addParameterToResult($result, $param)
{
// Commandline arguments must be strings, so ignore any
// parameter that is typehinted to any non-primative class.
if ($param->getClass() != null) {
return;
}
$result->add($param->name);
if ($param->isDefaultValueAvailable()) {
$defaultValue = $param->getDefaultValue();
if (!$this->isAssoc($defaultValue)) {
$result->setDefaultValue($param->name, $defaultValue);
}
} elseif ($param->isArray()) {
$result->setDefaultValue($param->name, []);
}
}
/**
* Examine the parameters of the method for this command, and determine
* the disposition of the options from them.
*
* @return array
*/
protected function determineOptionsFromParameters()
{
$params = $this->reflection->getParameters();
if (empty($params)) {
return [];
}
$param = end($params);
if (!$param->isDefaultValueAvailable()) {
return [];
}
if (!$this->isAssoc($param->getDefaultValue())) {
return [];
}
return $param->getDefaultValue();
}
protected function lastParameterName()
{
$params = $this->reflection->getParameters();
$param = end($params);
if (!$param) {
return '';
}
return $param->name;
}
/**
* Helper; determine if an array is associative or not. An array
* is not associative if its keys are numeric, and numbered sequentially
* from zero. All other arrays are considered to be associative.
*
* @param arrau $arr The array
* @return boolean
*/
protected function isAssoc($arr)
{
if (!is_array($arr)) {
return false;
}
return array_keys($arr) !== range(0, count($arr) - 1);
}
/**
* Convert from a method name to the corresponding command name. A
* method 'fooBar' will become 'foo:bar', and 'fooBarBazBoz' will
* become 'foo:bar-baz-boz'.
*
* @param string $camel method name.
* @return string
*/
protected function convertName($camel)
{
$splitter="-";
$camel=preg_replace('/(?!^)[[:upper:]][[:lower:]]/', '$0', preg_replace('/(?!^)[[:upper:]]+/', $splitter.'$0', $camel));
$camel = preg_replace("/$splitter/", ':', $camel, 1);
return strtolower($camel);
}
/**
* Parse the docBlock comment for this command, and set the
* fields of this class with the data thereby obtained.
*/
protected function parseDocBlock()
{
if (!$this->docBlockIsParsed) {
// The parse function will insert data from the provided method
// into this object, using our accessors.
CommandDocBlockParserFactory::parse($this, $this->reflection);
$this->docBlockIsParsed = true;
}
}
/**
* Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c',
* convert the data into the last of these forms.
*/
protected static function convertListToCommaSeparated($text)
{
return preg_replace('#[ \t\n\r,]+#', ',', $text);
}
}
PK r?V3T T ' src/Parser/DefaultsWithDescriptions.phpnu W+A values = $values;
$this->hasDefault = [];
$this->descriptions = [];
$this->defaultDefault = $defaultDefault;
}
/**
* Return just the key : default values mapping
*
* @return array
*/
public function getValues()
{
return $this->values;
}
/**
* Check to see whether the speicifed key exists in the collection.
*
* @param string $key
* @return boolean
*/
public function exists($key)
{
return array_key_exists($key, $this->values);
}
/**
* Get the value of one entry.
*
* @param string $key The key of the item.
* @return string
*/
public function get($key)
{
if (array_key_exists($key, $this->values)) {
return $this->values[$key];
}
return $this->defaultDefault;
}
/**
* Get the description of one entry.
*
* @param string $key The key of the item.
* @return string
*/
public function getDescription($key)
{
if (array_key_exists($key, $this->descriptions)) {
return $this->descriptions[$key];
}
return '';
}
/**
* Add another argument to this command.
*
* @param string $key Name of the argument.
* @param string $description Help text for the argument.
* @param mixed $defaultValue The default value for the argument.
*/
public function add($key, $description = '', $defaultValue = null)
{
if (!$this->exists($key) || isset($defaultValue)) {
$this->values[$key] = isset($defaultValue) ? $defaultValue : $this->defaultDefault;
}
unset($this->descriptions[$key]);
if (!empty($description)) {
$this->descriptions[$key] = $description;
}
}
/**
* Change the default value of an entry.
*
* @param string $key
* @param mixed $defaultValue
*/
public function setDefaultValue($key, $defaultValue)
{
$this->values[$key] = $defaultValue;
$this->hasDefault[$key] = true;
}
/**
* Check to see if the named argument definitively has a default value.
*
* @param string $key
* @return bool
*/
public function hasDefault($key)
{
return array_key_exists($key, $this->hasDefault);
}
/**
* Remove an entry
*
* @param string $key The entry to remove
*/
public function clear($key)
{
unset($this->values[$key]);
unset($this->descriptions[$key]);
}
/**
* Rename an existing option to something else.
*/
public function rename($oldName, $newName)
{
$this->add($newName, $this->getDescription($oldName), $this->get($oldName));
$this->clear($oldName);
}
}
PK r?V)zs . src/Parser/Internal/CommandDocBlockParser2.phpnu W+A reflection->getDocComment();
$phpdoc = new DocBlock($docblockComment);
// First set the description (synopsis) and help.
$this->commandInfo->setDescription((string)$phpdoc->getShortDescription());
$this->commandInfo->setHelp((string)$phpdoc->getLongDescription());
$this->processAllTags($phpdoc);
}
protected function getTagContents($tag)
{
return $tag->getContent();
}
/**
* Store the data from a @arg annotation in our argument descriptions.
*/
protected function processArgumentTag($tag)
{
$this->addOptionOrArgumentTag($tag, $this->commandInfo->arguments());
}
/**
* Store the data from a @param annotation in our argument descriptions.
*/
protected function processParamTag($tag)
{
if (!$tag instanceof ParamTag) {
return;
}
return parent::processParamTag($tag);
}
/**
* Store the data from a @return annotation in our argument descriptions.
*/
protected function processReturnTag($tag)
{
if (!$tag instanceof ReturnTag) {
return;
}
$this->commandInfo->setReturnType($tag->getType());
}
}
PK r?Vp'ʠ 4 src/Parser/Internal/CommandDocBlockParserFactory.phpnu W+A parse();
}
private static function create(CommandInfo $commandInfo, \ReflectionMethod $reflection)
{
if (static::hasReflectionDocBlock3()) {
return new CommandDocBlockParser3($commandInfo, $reflection);
}
return new CommandDocBlockParser2($commandInfo, $reflection);
}
private static function hasReflectionDocBlock3()
{
return class_exists('phpDocumentor\Reflection\DocBlockFactory') && class_exists('phpDocumentor\Reflection\Types\ContextFactory');
}
}
PK r?Vr . src/Parser/Internal/CommandDocBlockParser3.phpnu W+A reflection->getDocComment();
if (empty($docComment)) {
return;
}
$phpdoc = $this->createDocBlock();
// First set the description (synopsis) and help.
$this->commandInfo->setDescription((string)$phpdoc->getSummary());
$this->commandInfo->setHelp((string)$phpdoc->getDescription());
$this->processAllTags($phpdoc);
}
public function createDocBlock()
{
$docBlockFactory = \phpDocumentor\Reflection\DocBlockFactory::createInstance();
$contextFactory = new \phpDocumentor\Reflection\Types\ContextFactory();
return $docBlockFactory->create(
$this->reflection,
$contextFactory->createFromReflector($this->reflection)
);
}
protected function getTagContents($tag)
{
return (string)$tag;
}
/**
* Store the data from a @param annotation in our argument descriptions.
*/
protected function processParamTag($tag)
{
if (!$tag instanceof Param) {
return;
}
return parent::processParamTag($tag);
}
/**
* Store the data from a @return annotation in our argument descriptions.
*/
protected function processReturnTag($tag)
{
if (!$tag instanceof Return_) {
return;
}
// If there is a spurrious trailing space on the return type, remove it.
$this->commandInfo->setReturnType(trim($this->getTagContents($tag)));
}
}
PK r?V{V 5 src/Parser/Internal/AbstractCommandDocBlockParser.phpnu W+A 'processCommandTag',
'name' => 'processCommandTag',
'arg' => 'processArgumentTag',
'param' => 'processParamTag',
'return' => 'processReturnTag',
'option' => 'processOptionTag',
'default' => 'processDefaultTag',
'aliases' => 'processAliases',
'usage' => 'processUsageTag',
'description' => 'processAlternateDescriptionTag',
'desc' => 'processAlternateDescriptionTag',
];
public function __construct(CommandInfo $commandInfo, \ReflectionMethod $reflection)
{
$this->commandInfo = $commandInfo;
$this->reflection = $reflection;
}
protected function processAllTags($phpdoc)
{
// Iterate over all of the tags, and process them as necessary.
foreach ($phpdoc->getTags() as $tag) {
$processFn = [$this, 'processGenericTag'];
if (array_key_exists($tag->getName(), $this->tagProcessors)) {
$processFn = [$this, $this->tagProcessors[$tag->getName()]];
}
$processFn($tag);
}
}
abstract protected function getTagContents($tag);
/**
* Parse the docBlock comment for this command, and set the
* fields of this class with the data thereby obtained.
*/
abstract public function parse();
/**
* Save any tag that we do not explicitly recognize in the
* 'otherAnnotations' map.
*/
protected function processGenericTag($tag)
{
$this->commandInfo->addOtherAnnotation($tag->getName(), $this->getTagContents($tag));
}
/**
* Set the name of the command from a @command or @name annotation.
*/
protected function processCommandTag($tag)
{
$this->commandInfo->setName($this->getTagContents($tag));
// We also store the name in the 'other annotations' so that is is
// possible to determine if the method had a @command annotation.
$this->processGenericTag($tag);
}
/**
* The @description and @desc annotations may be used in
* place of the synopsis (which we call 'description').
* This is discouraged.
*
* @deprecated
*/
protected function processAlternateDescriptionTag($tag)
{
$this->commandInfo->setDescription($this->getTagContents($tag));
}
/**
* Store the data from a @arg annotation in our argument descriptions.
*/
protected function processArgumentTag($tag)
{
$this->addOptionOrArgumentTag($tag, $this->commandInfo->arguments());
}
/**
* Store the data from an @option annotation in our option descriptions.
*/
protected function processOptionTag($tag)
{
$this->addOptionOrArgumentTag($tag, $this->commandInfo->options());
}
protected function addOptionOrArgumentTag($tag, DefaultsWithDescriptions $set)
{
if (!$this->pregMatchNameAndDescription((string)$tag->getDescription(), $match)) {
return;
}
$variableName = $this->commandInfo->findMatchingOption($match['name']);
$desc = $match['description'];
$description = static::removeLineBreaks($desc);
$set->add($variableName, $description);
}
/**
* Store the data from a @default annotation in our argument or option store,
* as appropriate.
*/
protected function processDefaultTag($tag)
{
if (!$this->pregMatchNameAndDescription((string)$tag->getDescription(), $match)) {
return;
}
$variableName = $match['name'];
$defaultValue = $this->interpretDefaultValue($match['description']);
if ($this->commandInfo->arguments()->exists($variableName)) {
$this->commandInfo->arguments()->setDefaultValue($variableName, $defaultValue);
return;
}
$variableName = $this->commandInfo->findMatchingOption($variableName);
if ($this->commandInfo->options()->exists($variableName)) {
$this->commandInfo->options()->setDefaultValue($variableName, $defaultValue);
}
}
/**
* Store the data from a @usage annotation in our example usage list.
*/
protected function processUsageTag($tag)
{
$lines = explode("\n", $this->getTagContents($tag));
$usage = array_shift($lines);
$description = static::removeLineBreaks(implode("\n", $lines));
$this->commandInfo->setExampleUsage($usage, $description);
}
/**
* Process the comma-separated list of aliases
*/
protected function processAliases($tag)
{
$this->commandInfo->setAliases((string)$tag->getDescription());
}
/**
* Store the data from a @param annotation in our argument descriptions.
*/
protected function processParamTag($tag)
{
$variableName = $tag->getVariableName();
$variableName = str_replace('$', '', $variableName);
$description = static::removeLineBreaks((string)$tag->getDescription());
if ($variableName == $this->commandInfo->optionParamName()) {
return;
}
$this->commandInfo->arguments()->add($variableName, $description);
}
/**
* Store the data from a @return annotation in our argument descriptions.
*/
abstract protected function processReturnTag($tag);
protected function interpretDefaultValue($defaultValue)
{
$defaults = [
'null' => null,
'true' => true,
'false' => false,
"''" => '',
'[]' => [],
];
foreach ($defaults as $defaultName => $defaultTypedValue) {
if ($defaultValue == $defaultName) {
return $defaultTypedValue;
}
}
return $defaultValue;
}
/**
* Given a docblock description in the form "$variable description",
* return the variable name and description via the 'match' parameter.
*/
protected function pregMatchNameAndDescription($source, &$match)
{
$nameRegEx = '\\$(?P[^ \t]+)[ \t]+';
$descriptionRegEx = '(?P.*)';
$optionRegEx = "/{$nameRegEx}{$descriptionRegEx}/s";
return preg_match($optionRegEx, $source, $match);
}
/**
* Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c',
* convert the data into the last of these forms.
*/
protected static function convertListToCommaSeparated($text)
{
return preg_replace('#[ \t\n\r,]+#', ',', $text);
}
/**
* Take a multiline description and convert it into a single
* long unbroken line.
*/
protected static function removeLineBreaks($text)
{
return trim(preg_replace('#[ \t\n\r]+#', ' ', $text));
}
}
PK r?V? ? src/OutputDataInterface.phpnu W+A getArrayCopy());
}
}
PK r?V3VK K phpunit.xml.distnu W+A PK r?Vƻ
.editorconfignu W+A PK r?V1 1 LICENSEnu W+A PK r?V;8 8 . CONTRIBUTING.mdnu W+A PK r?V~ .travis.ymlnu W+A PK r?V4 4
composer.jsonnu W+A PK r?V3 CHANGELOG.mdnu W+A PK r?V ," ," tests/testFullStack.phpnu W+A PK r?V֊?P P % {<