├── .editorconfig ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ac ├── ac.php ├── auth.json ├── composer.json ├── composer.lock ├── config.json ├── dependencies.yml ├── infection.json.dist ├── phpunit.xml.dist └── src ├── AnnotatedCommand.php ├── AnnotatedCommandFactory.php ├── AnnotationData.php ├── Attributes ├── Argument.php ├── Command.php ├── Complete.php ├── DefaultFields.php ├── DefaultTableFields.php ├── FieldLabels.php ├── FilterDefaultField.php ├── Help.php ├── Hook.php ├── HookSelector.php ├── Misc.php ├── Option.php ├── Topics.php └── Usage.php ├── Cache ├── CacheWrapper.php ├── NullCache.php └── SimpleCacheInterface.php ├── CommandCreationListener.php ├── CommandCreationListenerInterface.php ├── CommandData.php ├── CommandError.php ├── CommandFileDiscovery.php ├── CommandInfoAltererInterface.php ├── CommandProcessor.php ├── CommandResult.php ├── Events ├── CustomEventAwareInterface.php └── CustomEventAwareTrait.php ├── ExitCodeInterface.php ├── Help ├── HelpCommand.php ├── HelpDocument.php ├── HelpDocumentAlter.php └── HelpDocumentBuilder.php ├── Hooks ├── AlterResultInterface.php ├── Dispatchers │ ├── CommandEventHookDispatcher.php │ ├── ExtracterHookDispatcher.php │ ├── HookDispatcher.php │ ├── InitializeHookDispatcher.php │ ├── InteractHookDispatcher.php │ ├── OptionsHookDispatcher.php │ ├── ProcessResultHookDispatcher.php │ ├── ReplaceCommandHookDispatcher.php │ ├── StatusDeterminerHookDispatcher.php │ └── ValidateHookDispatcher.php ├── ExtractOutputInterface.php ├── HookManager.php ├── InitializeHookInterface.php ├── InteractorInterface.php ├── OptionHookInterface.php ├── ProcessResultInterface.php ├── StatusDeterminerInterface.php └── ValidatorInterface.php ├── Input ├── StdinAwareInterface.php ├── StdinAwareTrait.php └── StdinHandler.php ├── Options ├── AlterOptionsCommandEvent.php ├── AutomaticOptionsProviderInterface.php ├── PrepareFormatter.php └── PrepareTerminalWidthOption.php ├── Output └── OutputAwareInterface.php ├── OutputDataInterface.php ├── ParameterInjection.php ├── ParameterInjector.php ├── Parser ├── CommandInfo.php ├── CommandInfoDeserializer.php ├── CommandInfoSerializer.php ├── DefaultsWithDescriptions.php └── Internal │ ├── AttributesDocBlockParser.php │ ├── BespokeDocBlockParser.php │ ├── CommandDocBlockParserFactory.php │ ├── CsvUtils.php │ ├── DefaultValueFromString.php │ ├── DocBlockUtils.php │ ├── DocblockTag.php │ ├── FullyQualifiedClassCache.php │ └── TagFactory.php ├── ResultWriter.php ├── State.php └── State ├── SavableState.php ├── State.php └── StateHelper.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [**.php] 13 | indent_style = space 14 | indent_size = 4 15 | 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ### 4.10.1 - 13 Dec 2024 4 | 5 | - Support PHP 8.4 6 | 7 | ### 4.10.0 - 5 Apr 2024 8 | 9 | - Support Symfony 7 10 | 11 | ### 4.9.2 - 26 Dec 2023 12 | 13 | - Avoid infinite loop in \Consolidation\AnnotatedCommand\Parser\CommandInfo::getName (#306) 14 | 15 | ### 4.9.1, 4.9.0 - 19 May 2023 16 | 17 | - Inject command metadata: make AnnotationData and FormatterOptions available (#301) 18 | - Only call setHidden() if value is true. (#299) 19 | 20 | ### 4.8.2 - 11 March 2023 21 | 22 | - Fix bug with simple options with dashes in their names. (#295) 23 | - Add new public ->addOption() method (#294) 24 | - Attributes - Topic needs path (#293) 25 | - More robust hook attribute, and misc (#291) 26 | 27 | ### 4.8.0, 4.8.1 - 28 February 2023 28 | 29 | - Add HookSelector attribute and adjust Hook attribute accordingly (#290) 30 | - Allow for just one param to be passed in Help and Topics Attributes (#289) 31 | 32 | ### 4.7.1 - 6 December 2022 33 | 34 | - PHP 8.2 fixes 35 | 36 | ### 4.7.0 - 22 November 2022 37 | 38 | - Allow command help and descriptions to be defined via docblock comments (#281) 39 | - Fix hidden via Attribute (#283) 40 | - Fix command completion (#282) 41 | 42 | ### 4.6.1 - 9 November 2022 43 | 44 | - Use Attributes to set suggestedValues for arg/option completion (#275) 45 | - Default value and therefore data type of parameter-defined cli options was being dropped (#280) 46 | - Make nullable properties in Attributes actually nullable (#278) 47 | - Only ignore commands in parent classes if flag is set (reverts backwards-compatibility break) (#277) 48 | 49 | ### 4.6.0 - 30 October 2022 50 | 51 | - Add support for command completion (#274) 52 | 53 | ### 4.5.7 - 20 October 2022 54 | 55 | - Stop loading commands from inherited classes (#273) 56 | 57 | ### 4.5.6 - 22 June 2022 58 | 59 | - PHP 8.2 compatibility: dynamic properties are deprecated (#271) 60 | 61 | ### 4.5.5 - 26 April 2022 62 | 63 | - No functional change; new release to fix false positives in b/c check caused by lockfile problem. 64 | 65 | ### 4.5.4 - 5 April 2022 66 | 67 | - Allow psr/log ^3 68 | 69 | ### 4.5.3 - 1 April 2022 70 | 71 | - Check the type of the reflection object before attempting to call isBuiltin(). (#265) 72 | 73 | ### 4.5.2 - 20 February 2022 74 | 75 | - Do not pass null to Symfony Command methods (#262) 76 | - CommandResult inheritance (#260) 77 | 78 | ### 4.5.1 - 29 December 2021 79 | 80 | - PHP 8.1 81 | 82 | ### 4.5.0 - 27 December 2021 83 | 84 | - Symfony 6 / Symfony 5.2 compatibility 85 | - Make addUsageOrExample() public 86 | 87 | ### 4.4.0 - 29 September 2021 88 | 89 | - Add support for providing command information via php8 Attributes. (#239) 90 | 91 | ### 4.3.3 - 26 September 2021 92 | 93 | - Back out change from 4.3.2. Will restore in 4.4.0, but with a switch that defaults to "off" (backwards-compatible). 94 | 95 | ### 4.3.2 - 19 September 2021 96 | 97 | - Less parsing by ignoring Traits and IO.php (for Drush) (#237) 98 | 99 | ### 4.3.1 - 29 August 2021 100 | 101 | - Fix bc break in 4.3.0. (#232) 102 | 103 | ### 4.3.0 - 27 August 2021 104 | 105 | - Allow options to be passed in as regular method parameters. (#224) 106 | 107 | ### 4.2.4 - 10 December 2020 108 | 109 | - PHP 8 110 | 111 | ### 4.2.3 - 3 October 2020 112 | 113 | - Add ability to ignore methods using regular expressions. (#212) 114 | 115 | ### 4.2.2 - 30 September 2020 116 | 117 | - PHP 8 / Composer 2 support (#210) 118 | - Add @ignored-command annotation. (#211) 119 | - Address deprecation of ReflectionType::getClass() (#209) 120 | 121 | ### 4.2.1 - 30 August 2020 122 | 123 | - Give command handlers the ability to save and restore their state (#208) 124 | - Do not inject $input and $output into the command instance unless it supports saving and restoring state. 125 | 126 | ### 4.2.0 - 27 August 2020 127 | 128 | DEPRECATED RELEASE. Do not use. 129 | 130 | - Inject $input and $output into the command instance if it is set up to receive them. (#207) 131 | 132 | ### 4.1.1 - 27 May 2020 133 | 134 | - Fix bugs with Symfony 5. (#204) 135 | 136 | ### 4.1.0 - 6 Feb 2020 137 | 138 | - Test with PHP 7.4. 139 | 140 | ### 4.0.0 - 29 Oct 2019 141 | 142 | - Compatible with the 2.x branch, but removes support for old PHP versions and requires Symfony 4. 143 | 144 | ### 2.12.0 - 8 Mar 2019 145 | 146 | - Allow annotated args and options to specify their default values in their descriptions. (#186) 147 | 148 | ### 2.11.2 - 1 Feb 2019 149 | 150 | - Fix handling of old caches from 2.11.1 that introduced upgrade errors. 151 | 152 | ### 2.11.1 - 31 Jan 2019 153 | 154 | - Cache injected classes (#182) 155 | 156 | ### 2.11.0 - 27 Jan 2019 157 | 158 | - Make injection of InputInterface / OutputInterface general-purpose (#179) 159 | 160 | ### 2.10.2 - 20 Dec 2018 161 | 162 | - Fix commands that have a @param annotation for their InputInterface/OutputInterface params (#176) 163 | 164 | ### 2.10.1 - 13 Dec 2018 165 | 166 | - Add stdin handler convenience class 167 | - Add setter to AnnotationData to suppliment existing array acces 168 | - Update to Composer Test Scenarios 3 169 | 170 | ### 2.10.0 - 14 Nov 2018 171 | 172 | - Add a new data type, CommandResult (#167) 173 | 174 | ### 2.9.0 & 2.9.1 - 19 Sept 2018 175 | 176 | - Improve commandfile discovery for extensions installed via Composer. (#156) 177 | 178 | ### 2.8.5 - 18 Aug 2018 179 | 180 | - Add dependencies.yml for dependencies.io 181 | - Fix warning in AnnotatedCommandFactory when getCommandInfoListFromCache called with null. 182 | 183 | ### 2.8.4 - 25 May 2018 184 | 185 | - Use g1a/composer-test-scenarios for better PHP version matrix testing. 186 | 187 | ### 2.8.3 - 23 Feb 2018 188 | 189 | - BUGFIX: Do not shift off the command name unless it is there. (#139) 190 | - Use test scenarios to test multiple versions of Symfony. (#136, #137) 191 | 192 | ### 2.8.2 - 29 Nov 2017 193 | 194 | - Allow Symfony 4 components. 195 | 196 | ### 2.8.1 - 16 Oct 2017 197 | 198 | - Add hook methods to allow Symfony command events to be added directly to the hook manager, givig better control of hook order. (#131) 199 | 200 | ### 2.8.0 - 13 Oct 2017 201 | 202 | - Remove phpdocumentor/reflection-docblock in favor of using a bespoke parser (#130) 203 | 204 | ### 2.7.0 - 18 Sept 2017 205 | 206 | - Add support for options with a default value of 'true' (#119) 207 | - BUGFIX: Improve handling of options with optional values, which previously was not working correctly. (#118) 208 | 209 | ### 2.6.1 - 18 Sep 2017 210 | 211 | - Reverts to contents of the 2.4.13 release. 212 | 213 | ### 2.5.0 & 2.5.1 - 17 Sep 2017 214 | 215 | - BACKED OUT. These releases accidentally introduced breaking changes. 216 | 217 | ### 2.4.13 - 28 Aug 2017 218 | 219 | - Add a followLinks() method (#108) 220 | 221 | ### 2.4.12 - 24 Aug 2017 222 | 223 | - BUGFIX: Allow annotated commands to directly use InputInterface and OutputInterface (#106) 224 | 225 | ### 2.4.11 - 27 July 2017 226 | 227 | - Back out #102: do not change behavior of word wrap based on STDOUT redirection. 228 | 229 | ### 2.4.10 - 21 July 2017 230 | 231 | - Add a method CommandProcessor::setPassExceptions() to allow applicationsto prevent the command processor from catching exceptions thrown by command methods and hooks. (#103) 232 | 233 | ### 2.4.9 - 20 Jul 2017 234 | 235 | - Automatically disable wordwrap when the terminal is not connected to STDOUT (#102) 236 | 237 | ### 2.4.8 - 3 Apr 2017 238 | 239 | - Allow multiple annotations with the same key. These are returned as a csv, or, alternately, can be accessed as an array via the new accessor. 240 | - Unprotect two methods for benefit of Drush help. (#99) 241 | - BUGFIX: Remove symfony/console pin (#100) 242 | 243 | ### 2.4.7 & 2.4.6 - 17 Mar 2017 244 | 245 | - Avoid wrapping help text (#93) 246 | - Pin symfony/console to version < 3.2.5 (#94) 247 | - Add getExampleUsages() to AnnotatedCommand. (#92) 248 | 249 | ### 2.4.5 - 28 Feb 2017 250 | 251 | - Ensure that placeholder entries are written into the commandfile cache. (#86) 252 | 253 | ### 2.4.4 - 27 Feb 2017 254 | 255 | - BUGFIX: Avoid rewriting the command cache unless something has changed. 256 | - BUGFIX: Ensure that the default value of options are correctly cached. 257 | 258 | ### 2.4.2 - 24 Feb 2017 259 | 260 | - Add SimpleCacheInterface as a documentation interface (not enforced). 261 | 262 | ### 2.4.1 - 20 Feb 2017 263 | 264 | - Support array options: multiple options on the commandline may be passed in to options array as an array of values. 265 | - Add php 7.1 to the test matrix. 266 | 267 | ### 2.4.0 - 3 Feb 2017 268 | 269 | - Automatically rebuild cached commandfile data when commandfile changes. 270 | - Provide path to command file in AnnotationData objects. 271 | - Bugfix: Add dynamic options when user runs '--help my:command' (previously, only 'help my:command' worked). 272 | - Bugfix: Include description of last parameter in help (was omitted if no options present) 273 | - Add Windows testing with Appveyor 274 | 275 | 276 | ### 2.3.0 - 19 Jan 2017 277 | 278 | - Add a command info cache to improve performance of applications with many commands 279 | - Bugfix: Allow trailing backslashes in namespaces in CommandFileDiscovery 280 | - Bugfix: Rename @topic to @topics 281 | 282 | 283 | ### 2.2.0 - 23 November 2016 284 | 285 | - Support custom events 286 | - Add xml and json output for replacement help command. Text / html format for replacement help command not available yet. 287 | 288 | 289 | ### 2.1.0 - 14 November 2016 290 | 291 | - Add support for output formatter wordwrapping 292 | - Fix version requirement for output-formatters in composer.json 293 | - Use output-formatters ~3 294 | - Move php_codesniffer back to require-dev (moved to require by mistake) 295 | 296 | 297 | ### 2.0.0 - 30 September 2016 298 | 299 | - **Breaking** 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. 300 | - **Breaking** The interfaces ValidatorInterface, ProcessResultInterface and AlterResultInterface have been updated to be passed a CommandData object, which contains an Input and Output object, plus the AnnotationData. 301 | - **Breaking** The Symfony Command Event hook has been renamed to COMMAND_EVENT. There is a new COMMAND hook that behaves like the existing Drush command hook (i.e. the post-command event is called after the primary command method runs). 302 | - Add an accessor function AnnotatedCommandFactory::setIncludeAllPublicMethods() to control whether all public methods of a command class, or only those with a @command annotation will be treated as commands. Default remains to treat all public methods as commands. The parameters to AnnotatedCommandFactory::createCommandsFromClass() and AnnotatedCommandFactory::createCommandsFromClassInfo() still behave the same way, but are deprecated. If omitted, the value set by the accessor will be used. 303 | - @option and @usage annotations provided with @hook methods will be added to the help text of the command they hook. This should be done if a hook needs to add a new option, e.g. to control the behavior of the hook. 304 | - @option annotations can now be either `@option type $name description`, or just `@option name description`. 305 | - `@hook option` can be used to programatically add options to a command. 306 | - A CommandInfoAltererInterface can be added via AnnotatedCommandFactory::addCommandInfoAlterer(); it will be given the opportunity to adjust every CommandInfo object parsed from a command file prior to the creation of commands. 307 | - AnnotatedCommandFactory::setIncludeAllPublicMethods(false) may be used to require methods to be annotated with @commnad in order to be considered commands. This is in preference to the existing parameters of various command-creation methods of AnnotatedCommandFactory, which are now all deprecated in favor of this setter function. 308 | - If a --field option is given, it will also force the output format to 'string'. 309 | - Setter methods more consistently return $this. 310 | - Removed PassThroughArgsInput. This class was unnecessary. 311 | 312 | 313 | ### 1.4.0 - 13 September 2016 314 | 315 | - Add basic annotation hook capability, to allow hook functions to be attached to commands with arbitrary annotations. 316 | 317 | 318 | ### 1.3.0 - 8 September 2016 319 | 320 | - 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. 321 | 322 | 323 | ### 1.2.0 - 2 August 2016 324 | 325 | - Support both the 2.x and 3.x versions of phpdocumentor/reflection-docblock. 326 | - Support php 5.4. 327 | - **Bug** Do not allow an @param docblock comment for the options to override the meaning of the options. 328 | 329 | 330 | ### 1.1.0 - 6 July 2016 331 | 332 | - Introduce AnnotatedCommandFactory::createSelectedCommandsFromClassInfo() method. 333 | 334 | 335 | ### 1.0.0 - 20 May 2016 336 | 337 | - First stable release. 338 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Consolidation 2 | 3 | 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. 4 | 5 | Here are some of the guidelines you should follow to make the most of your efforts: 6 | 7 | ## Code Style Guidelines 8 | 9 | Consolidation adheres to the [PSR-2 Coding Style Guide](http://www.php-fig.org/psr/psr-2/) for PHP code. 10 | 11 | ## Pull Request Guidelines 12 | 13 | Every pull request is run through: 14 | 15 | - phpcs -n --standard=PSR2 src 16 | - phpunit 17 | - [Scrutinizer](https://scrutinizer-ci.com/g/consolidation/annotated-command/) 18 | 19 | It is easy to run the unit tests and code sniffer locally; just run: 20 | 21 | - composer cs 22 | 23 | To run the code beautifier, which will fix many of the problems reported by phpcs: 24 | 25 | - composer cbf 26 | 27 | These two commands (`composer cs` and `composer cbf`) are defined in the `scripts` section of [composer.json](composer.json). 28 | 29 | 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 need to be addressed. 30 | 31 | Pull requests must pass phpcs and phpunit in order to be merged; ideally, new functionality will also include new unit tests. 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2020 Consolidation Org Developers 4 | 5 | 6 | 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: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | 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. 11 | 12 | DEPENDENCY LICENSES: 13 | 14 | Name Version License 15 | consolidation/output-formatters 4.1.1 MIT 16 | dflydev/dot-access-data v1.1.0 MIT 17 | psr/container 1.0.0 MIT 18 | psr/log 1.1.3 MIT 19 | symfony/console v4.4.10 MIT 20 | symfony/event-dispatcher v4.4.10 MIT 21 | symfony/event-dispatcher-contracts v1.1.7 MIT 22 | symfony/finder v4.4.10 MIT 23 | symfony/polyfill-mbstring v1.17.0 MIT 24 | symfony/polyfill-php73 v1.17.0 MIT 25 | symfony/polyfill-php80 v1.17.0 MIT 26 | symfony/service-contracts v1.1.8 MIT -------------------------------------------------------------------------------- /ac: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | setIncludeAllPublicMethods(true); 13 | $commandFactory->commandProcessor()->setFormatterManager(new \Consolidation\OutputFormatters\FormatterManager()); 14 | $commandList = $commandFactory->createCommandsFromClass($myCommandClassInstance); 15 | $application = new \Symfony\Component\Console\Application('ac'); 16 | foreach ($commandList as $command) { 17 | $application->add($command); 18 | } 19 | $application->run(); 20 | -------------------------------------------------------------------------------- /auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "bitbucket-oauth": {}, 3 | "github-oauth": {}, 4 | "gitlab-oauth": {}, 5 | "gitlab-token": {}, 6 | "http-basic": {}, 7 | "bearer": {} 8 | } 9 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "consolidation/annotated-command", 3 | "description": "Initialize Symfony Console commands from annotated command class methods.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Greg Anderson", 8 | "email": "greg.1.anderson@greenknowe.org" 9 | } 10 | ], 11 | "autoload":{ 12 | "psr-4":{ 13 | "Consolidation\\AnnotatedCommand\\": "src" 14 | } 15 | }, 16 | "autoload-dev": { 17 | "psr-4": { 18 | "Consolidation\\TestUtils\\": "tests/src" 19 | } 20 | }, 21 | "require": { 22 | "php": ">=7.1.3", 23 | "consolidation/output-formatters": "^4.3.1", 24 | "psr/log": "^1 || ^2 || ^3", 25 | "symfony/console": "^4.4.8 || ^5 || ^6 || ^7", 26 | "symfony/event-dispatcher": "^4.4.8 || ^5 || ^6 || ^7", 27 | "symfony/finder": "^4.4.8 || ^5 || ^6 || ^7" 28 | }, 29 | "require-dev": { 30 | "composer-runtime-api": "^2.0", 31 | "phpunit/phpunit": "^7.5.20 || ^8 || ^9", 32 | "squizlabs/php_codesniffer": "^3", 33 | "yoast/phpunit-polyfills": "^0.2.0" 34 | }, 35 | "config": { 36 | "optimize-autoloader": true, 37 | "sort-packages": true, 38 | "platform": { 39 | "php": "8.2.17" 40 | } 41 | }, 42 | "scripts": { 43 | "cs": "phpcs --standard=PSR2 -n src", 44 | "cbf": "phpcbf --standard=PSR2 -n src", 45 | "unit": "SHELL_INTERACTIVE=true phpunit --colors=always", 46 | "lint": [ 47 | "find src -name '*.php' -and ! -path 'src/Attributes/*' -print0 | xargs -0 -n1 php -l", 48 | "find tests/src -name '*.php' -and ! -name 'ExampleAttributesCommandFile.php' -print0 | xargs -0 -n1 php -l" 49 | ], 50 | "test": [ 51 | "@lint", 52 | "@unit", 53 | "@cs" 54 | ] 55 | }, 56 | "extra": { 57 | "branch-alias": { 58 | "dev-main": "4.x-dev" 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": {} 3 | } 4 | -------------------------------------------------------------------------------- /dependencies.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | dependencies: 3 | - type: php 4 | path: / 5 | settings: 6 | composer_options: "" 7 | manifest_updates: 8 | filters: 9 | - name: ".*" 10 | versions: "L.Y.Y" 11 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 10, 3 | "source": { 4 | "directories": [ 5 | "src" 6 | ] 7 | }, 8 | "logs": { 9 | "text": "infection-log.txt" 10 | } 11 | } -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | src 17 | 18 | src/Attributes 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/AnnotatedCommand.php: -------------------------------------------------------------------------------- 1 | getName(); 62 | } 63 | } 64 | parent::__construct($name); 65 | if ($commandInfo && $commandInfo->hasAnnotation('command')) { 66 | $this->setCommandInfo($commandInfo); 67 | $this->setCommandOptions($commandInfo); 68 | } 69 | } 70 | 71 | public function setCommandCallback($commandCallback) 72 | { 73 | $this->commandCallback = $commandCallback; 74 | return $this; 75 | } 76 | 77 | public function setCompletionCallback($completionCallback) 78 | { 79 | $this->completionCallback = $completionCallback; 80 | return $this; 81 | } 82 | 83 | public function setCommandProcessor($commandProcessor) 84 | { 85 | $this->commandProcessor = $commandProcessor; 86 | return $this; 87 | } 88 | 89 | public function commandProcessor() 90 | { 91 | // If someone is using an AnnotatedCommand, and is NOT getting 92 | // it from an AnnotatedCommandFactory OR not correctly injecting 93 | // a command processor via setCommandProcessor() (ideally via the 94 | // DI container), then we'll just give each annotated command its 95 | // own command processor. This is not ideal; preferably, there would 96 | // only be one instance of the command processor in the application. 97 | if (!isset($this->commandProcessor)) { 98 | $this->commandProcessor = new CommandProcessor(new HookManager()); 99 | } 100 | return $this->commandProcessor; 101 | } 102 | 103 | public function getReturnType() 104 | { 105 | return $this->returnType; 106 | } 107 | 108 | public function setReturnType($returnType) 109 | { 110 | $this->returnType = $returnType; 111 | return $this; 112 | } 113 | 114 | public function getAnnotationData() 115 | { 116 | return $this->annotationData; 117 | } 118 | 119 | public function setAnnotationData($annotationData) 120 | { 121 | $this->annotationData = $annotationData; 122 | return $this; 123 | } 124 | 125 | public function getTopics() 126 | { 127 | return $this->topics; 128 | } 129 | 130 | public function setTopics($topics) 131 | { 132 | $this->topics = $topics; 133 | return $this; 134 | } 135 | 136 | public function setCommandInfo($commandInfo) 137 | { 138 | $this->setDescription($commandInfo->getDescription() ?: ''); 139 | $this->setHelp($commandInfo->getHelp() ?: ''); 140 | $this->setAliases($commandInfo->getAliases()); 141 | $this->setAnnotationData($commandInfo->getAnnotations()); 142 | $this->setTopics($commandInfo->getTopics()); 143 | foreach ($commandInfo->getExampleUsages() as $usage => $description) { 144 | $this->addUsageOrExample($usage, $description); 145 | } 146 | $this->setCommandArguments($commandInfo); 147 | $this->setReturnType($commandInfo->getReturnType()); 148 | // Hidden commands available since Symfony 3.2 149 | // http://symfony.com/doc/current/console/hide_commands.html 150 | if (method_exists($this, 'setHidden')) { 151 | $this->setHidden($commandInfo->getHidden()); 152 | } 153 | $this->parameterMap = $commandInfo->getParameterMap(); 154 | return $this; 155 | } 156 | 157 | public function getExampleUsages() 158 | { 159 | return $this->examples; 160 | } 161 | 162 | public function addUsageOrExample($usage, $description) 163 | { 164 | $this->addUsage($usage); 165 | if (!empty($description)) { 166 | $this->examples[$usage] = $description; 167 | } 168 | } 169 | 170 | public function getCompletionCallback() 171 | { 172 | return $this->completionCallback; 173 | } 174 | 175 | public function helpAlter(\DomDocument $originalDom) 176 | { 177 | return HelpDocumentBuilder::alter($originalDom, $this); 178 | } 179 | 180 | protected function setCommandArguments($commandInfo) 181 | { 182 | $this->injectedClasses = $commandInfo->getInjectedClasses(); 183 | $this->setCommandArgumentsFromParameters($commandInfo); 184 | return $this; 185 | } 186 | 187 | protected function setCommandArgumentsFromParameters($commandInfo) 188 | { 189 | $args = $commandInfo->arguments()->getValues(); 190 | foreach ($args as $name => $defaultValue) { 191 | $description = $commandInfo->arguments()->getDescription($name); 192 | $hasDefault = $commandInfo->arguments()->hasDefault($name); 193 | $parameterMode = $this->getCommandArgumentMode($hasDefault, $defaultValue); 194 | $suggestedValues = $commandInfo->arguments()->getSuggestedValues($name); 195 | $this->addArgument($name, $parameterMode, $description, $defaultValue, $suggestedValues); 196 | } 197 | return $this; 198 | } 199 | 200 | protected function getCommandArgumentMode($hasDefault, $defaultValue) 201 | { 202 | if (!$hasDefault) { 203 | return InputArgument::REQUIRED; 204 | } 205 | if (is_array($defaultValue)) { 206 | return InputArgument::IS_ARRAY; 207 | } 208 | return InputArgument::OPTIONAL; 209 | } 210 | 211 | public function setCommandOptions($commandInfo, $automaticOptions = []) 212 | { 213 | $inputOptions = $commandInfo->inputOptions(); 214 | 215 | $this->addOptions($inputOptions + $automaticOptions, $automaticOptions); 216 | return $this; 217 | } 218 | 219 | public function addOptions($inputOptions, $automaticOptions = []) 220 | { 221 | foreach ($inputOptions as $name => $inputOption) { 222 | $description = $inputOption->getDescription(); 223 | 224 | if (empty($description) && isset($automaticOptions[$name])) { 225 | // Unfortunately, Console forces us too construct a new InputOption to set a description. 226 | $description = $automaticOptions[$name]->getDescription(); 227 | $this->addInputOption($inputOption, $description); 228 | } else { 229 | if ($native = $this->getNativeDefinition()) { 230 | $native->addOption($inputOption); 231 | } 232 | $this->getDefinition()->addOption($inputOption); 233 | } 234 | } 235 | } 236 | 237 | private function addInputOption($inputOption, $description = null) 238 | { 239 | $default = $inputOption->getDefault(); 240 | // Recover the 'mode' value, because Symfony is stubborn 241 | $mode = 0; 242 | if ($inputOption->isValueRequired()) { 243 | $mode |= InputOption::VALUE_REQUIRED; 244 | } 245 | if ($inputOption->isValueOptional()) { 246 | $mode |= InputOption::VALUE_OPTIONAL; 247 | } 248 | if ($inputOption->isArray()) { 249 | $mode |= InputOption::VALUE_IS_ARRAY; 250 | } 251 | if (!$mode) { 252 | $mode = InputOption::VALUE_NONE; 253 | $default = null; 254 | } 255 | 256 | $suggestedValues = []; 257 | // Symfony 6.1+ feature https://symfony.com/blog/new-in-symfony-6-1-improved-console-autocompletion#completion-values-in-input-definitions 258 | if (property_exists($inputOption, 'suggestedValues')) { 259 | // Alas, Symfony provides no accessor. 260 | $class = new \ReflectionClass($inputOption); 261 | $property = $class->getProperty('suggestedValues'); 262 | $property->setAccessible(true); 263 | $suggestedValues = $property->getValue($inputOption); 264 | } 265 | $this->addOption( 266 | $inputOption->getName(), 267 | $inputOption->getShortcut(), 268 | $mode, 269 | $description ?? $inputOption->getDescription(), 270 | $default, 271 | $suggestedValues 272 | ); 273 | } 274 | 275 | /** 276 | * @deprecated since 4.5.0 277 | */ 278 | protected static function inputOptionSetDescription($inputOption, $description) 279 | { 280 | @\trigger_error( 281 | 'Since consolidation/annotated-command 4.5: ' . 282 | 'AnnotatedCommand::inputOptionSetDescription method is deprecated and will be removed in 5.0', 283 | \E_USER_DEPRECATED 284 | ); 285 | // Recover the 'mode' value, because Symfony is stubborn 286 | $mode = 0; 287 | if ($inputOption->isValueRequired()) { 288 | $mode |= InputOption::VALUE_REQUIRED; 289 | } 290 | if ($inputOption->isValueOptional()) { 291 | $mode |= InputOption::VALUE_OPTIONAL; 292 | } 293 | if ($inputOption->isArray()) { 294 | $mode |= InputOption::VALUE_IS_ARRAY; 295 | } 296 | if (!$mode) { 297 | $mode = InputOption::VALUE_NONE; 298 | } 299 | 300 | $inputOption = new InputOption( 301 | $inputOption->getName(), 302 | $inputOption->getShortcut(), 303 | $mode, 304 | $description, 305 | $inputOption->getDefault() 306 | ); 307 | return $inputOption; 308 | } 309 | 310 | /** 311 | * Returns all of the hook names that may be called for this command. 312 | * 313 | * @return array 314 | */ 315 | public function getNames() 316 | { 317 | return HookManager::getNames($this, $this->commandCallback); 318 | } 319 | 320 | /** 321 | * Add any options to this command that are defined by hook implementations 322 | */ 323 | public function optionsHook() 324 | { 325 | $this->commandProcessor()->optionsHook( 326 | $this, 327 | $this->getNames(), 328 | $this->annotationData 329 | ); 330 | } 331 | 332 | /** 333 | * Route a completion request to the specified Callable if available. 334 | */ 335 | public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void 336 | { 337 | parent::complete($input, $suggestions); 338 | if (is_callable($this->completionCallback)) { 339 | call_user_func($this->completionCallback, $input, $suggestions); 340 | } 341 | } 342 | 343 | public function optionsHookForHookAnnotations($commandInfoList) 344 | { 345 | foreach ($commandInfoList as $commandInfo) { 346 | $inputOptions = $commandInfo->inputOptions(); 347 | $this->addOptions($inputOptions); 348 | foreach ($commandInfo->getExampleUsages() as $usage => $description) { 349 | if (!in_array($usage, $this->getUsages())) { 350 | $this->addUsageOrExample($usage, $description); 351 | } 352 | } 353 | } 354 | } 355 | 356 | /** 357 | * {@inheritdoc} 358 | */ 359 | protected function interact(InputInterface $input, OutputInterface $output) 360 | { 361 | $state = $this->injectIntoCommandfileInstance($input, $output); 362 | $this->commandProcessor()->interact( 363 | $input, 364 | $output, 365 | $this->getNames(), 366 | $this->annotationData 367 | ); 368 | $state->restore(); 369 | } 370 | 371 | protected function initialize(InputInterface $input, OutputInterface $output) 372 | { 373 | $state = $this->injectIntoCommandfileInstance($input, $output); 374 | // Allow the hook manager a chance to provide configuration values, 375 | // if there are any registered hooks to do that. 376 | $this->commandProcessor()->initializeHook($input, $this->getNames(), $this->annotationData); 377 | $state->restore(); 378 | } 379 | 380 | /** 381 | * {@inheritdoc} 382 | */ 383 | protected function execute(InputInterface $input, OutputInterface $output): int 384 | { 385 | $state = $this->injectIntoCommandfileInstance($input, $output); 386 | // Validate, run, process, alter, handle results. 387 | $result = $this->commandProcessor()->process( 388 | $output, 389 | $this->getNames(), 390 | $this->commandCallback, 391 | $this->createCommandData($input, $output) 392 | ); 393 | $state->restore(); 394 | return $result; 395 | } 396 | 397 | /** 398 | * This function is available for use by a class that may 399 | * wish to extend this class rather than use annotations to 400 | * define commands. Using this technique does allow for the 401 | * use of annotations to define hooks. 402 | */ 403 | public function processResults(InputInterface $input, OutputInterface $output, $results) 404 | { 405 | $state = $this->injectIntoCommandfileInstance($input, $output); 406 | $commandData = $this->createCommandData($input, $output); 407 | $commandProcessor = $this->commandProcessor(); 408 | $names = $this->getNames(); 409 | $results = $commandProcessor->processResults( 410 | $names, 411 | $results, 412 | $commandData 413 | ); 414 | $status = $commandProcessor->handleResults( 415 | $output, 416 | $names, 417 | $results, 418 | $commandData 419 | ); 420 | $state->restore(); 421 | return $status; 422 | } 423 | 424 | protected function createCommandData(InputInterface $input, OutputInterface $output) 425 | { 426 | $commandData = new CommandData( 427 | $this->annotationData, 428 | $input, 429 | $output, 430 | $this->parameterMap 431 | ); 432 | 433 | $formatterOptions = new FormatterOptions($commandData->annotationData()->getArrayCopy(), $commandData->input()->getOptions()); 434 | $commandData->setFormatterOptions($formatterOptions); 435 | 436 | // Fetch any classes (e.g. InputInterface / OutputInterface) that 437 | // this command's callback wants passed as a parameter and inject 438 | // it into the command data. 439 | $this->commandProcessor()->injectIntoCommandData($commandData, $this->injectedClasses); 440 | 441 | // Allow the commandData to cache the list of options with 442 | // special default values ('null' and 'true'), as these will 443 | // need special handling. @see CommandData::options(). 444 | $commandData->cacheSpecialDefaults($this->getDefinition()); 445 | 446 | return $commandData; 447 | } 448 | 449 | /** 450 | * Inject $input and $output into the command instance if it is set up to receive them. 451 | * 452 | * @param callable $commandCallback 453 | * @param CommandData $commandData 454 | * @return State 455 | */ 456 | public function injectIntoCommandfileInstance(InputInterface $input, OutputInterface $output) 457 | { 458 | return StateHelper::injectIntoCallbackObject($this->commandCallback, $input, $output); 459 | } 460 | } 461 | -------------------------------------------------------------------------------- /src/AnnotationData.php: -------------------------------------------------------------------------------- 1 | has($key) ? CsvUtils::toString($this[$key]) : $default; 11 | } 12 | 13 | public function getList($key, $default = []) 14 | { 15 | return $this->has($key) ? CsvUtils::toList($this[$key]) : $default; 16 | } 17 | 18 | public function has($key) 19 | { 20 | return isset($this[$key]); 21 | } 22 | 23 | public function keys() 24 | { 25 | return array_keys($this->getArrayCopy()); 26 | } 27 | 28 | public function set($key, $value = '') 29 | { 30 | $this->offsetSet($key, $value); 31 | return $this; 32 | } 33 | 34 | #[\ReturnTypeWillChange] 35 | public function append($key, $value = '') 36 | { 37 | $data = $this->offsetGet($key); 38 | if (is_array($data)) { 39 | $this->offsetSet($key, array_merge($data, $value)); 40 | } elseif (is_scalar($data)) { 41 | $this->offsetSet($key, $data . $value); 42 | } 43 | return $this; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Attributes/Argument.php: -------------------------------------------------------------------------------- 1 | newInstance(); 29 | $commandInfo->addArgumentDescription($instance->name, $instance->description, $instance->suggestedValues); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Attributes/Command.php: -------------------------------------------------------------------------------- 1 | getArguments(); 26 | $commandInfo->setName($args['name']); 27 | $commandInfo->addAnnotation('command', $args['name']); 28 | $commandInfo->setAliases($args['aliases'] ?? []); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Attributes/Complete.php: -------------------------------------------------------------------------------- 1 | getArguments(); 23 | $commandInfo->addAnnotation('complete', $args['method_name_or_callable']); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Attributes/DefaultFields.php: -------------------------------------------------------------------------------- 1 | getArguments(); 23 | $commandInfo->addAnnotation('default-fields', $args['fields']); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Attributes/DefaultTableFields.php: -------------------------------------------------------------------------------- 1 | getArguments(); 23 | $commandInfo->addAnnotation('default-table-fields', $args['fields']); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Attributes/FieldLabels.php: -------------------------------------------------------------------------------- 1 | getArguments(); 23 | $commandInfo->addAnnotation('field-labels', $args['labels']); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Attributes/FilterDefaultField.php: -------------------------------------------------------------------------------- 1 | getArguments(); 23 | $commandInfo->addAnnotation('filter-default-field', $args['field']); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Attributes/Help.php: -------------------------------------------------------------------------------- 1 | newInstance(); 29 | if ($instance->description) { 30 | $commandInfo->setDescription($instance->description); 31 | } 32 | if ($instance->synopsis) { 33 | $commandInfo->setHelp($instance->synopsis); 34 | } 35 | if ($instance->hidden) { 36 | $commandInfo->setHidden($instance->hidden); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Attributes/Hook.php: -------------------------------------------------------------------------------- 1 | newInstance(); 31 | if ($instance->selector && $instance->target) { 32 | throw new \Exception('Selector and Target may not be sent in the same Hook attribute.'); 33 | } 34 | $value = null; 35 | if ($instance->selector) { 36 | if (strpos($instance->selector, '@') !== false) { 37 | throw new \Exception('Selector may not contain an \'@\''); 38 | } 39 | $value = '@' . $instance->selector; 40 | } elseif ($instance->target) { 41 | $value = $instance->target; 42 | } 43 | $commandInfo->setName($value); 44 | $commandInfo->addAnnotation('hook', $instance->type . ' ' . $value); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Attributes/HookSelector.php: -------------------------------------------------------------------------------- 1 | newInstance(); 26 | $commandInfo->addAnnotation($instance->name, $instance->value); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Attributes/Misc.php: -------------------------------------------------------------------------------- 1 | getArguments(); 26 | $commandInfo->AddAnnotation(key($args['data']), current($args['data'])); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Attributes/Option.php: -------------------------------------------------------------------------------- 1 | newInstance(); 29 | $commandInfo->addOptionDescription($instance->name, $instance->description, $instance->suggestedValues); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Attributes/Topics.php: -------------------------------------------------------------------------------- 1 | newInstance(); 29 | $commandInfo->addAnnotation('topics', $instance->topics); 30 | if ($instance->isTopic || $instance->path) { 31 | $commandInfo->addAnnotation('topic', $instance->path ?? true); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Attributes/Usage.php: -------------------------------------------------------------------------------- 1 | getArguments(); 26 | $commandInfo->setExampleUsage($args['name'], @$args['description']); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Cache/CacheWrapper.php: -------------------------------------------------------------------------------- 1 | dataStore = $dataStore; 14 | } 15 | 16 | /** 17 | * Test for an entry from the cache 18 | * @param string $key 19 | * @return boolean 20 | */ 21 | public function has($key) 22 | { 23 | if (method_exists($this->dataStore, 'has')) { 24 | return $this->dataStore->has($key); 25 | } 26 | $test = $this->dataStore->get($key); 27 | return !empty($test); 28 | } 29 | 30 | /** 31 | * Get an entry from the cache 32 | * @param string $key 33 | * @return array 34 | */ 35 | public function get($key) 36 | { 37 | return (array) $this->dataStore->get($key); 38 | } 39 | 40 | /** 41 | * Store an entry in the cache 42 | * @param string $key 43 | * @param array $data 44 | */ 45 | public function set($key, $data) 46 | { 47 | $this->dataStore->set($key, $data); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Cache/NullCache.php: -------------------------------------------------------------------------------- 1 | listener = $listener; 19 | } 20 | 21 | public function notifyCommandFileAdded($command) 22 | { 23 | call_user_func($this->listener, $command); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/CommandCreationListenerInterface.php: -------------------------------------------------------------------------------- 1 | annotationData = $annotationData; 35 | $this->input = $input; 36 | $this->output = $output; 37 | $this->includeOptionsInArgs = true; 38 | $this->parameterMap = $parameterMap; 39 | } 40 | 41 | /** 42 | * For internal use only; inject an instance to be passed back 43 | * to the command callback as a parameter. 44 | */ 45 | public function injectInstance($injectedInstance) 46 | { 47 | array_unshift($this->injectedInstances, $injectedInstance); 48 | return $this; 49 | } 50 | 51 | /** 52 | * Provide a reference to the instances that will be added to the 53 | * beginning of the parameter list when the command callback is invoked. 54 | */ 55 | public function injectedInstances() 56 | { 57 | return $this->injectedInstances; 58 | } 59 | 60 | /** 61 | * For backwards-compatibility mode only: disable addition of 62 | * options on the end of the arguments list. 63 | */ 64 | public function setIncludeOptionsInArgs($includeOptionsInArgs) 65 | { 66 | $this->includeOptionsInArgs = $includeOptionsInArgs; 67 | return $this; 68 | } 69 | 70 | public function annotationData() 71 | { 72 | return $this->annotationData; 73 | } 74 | 75 | public function formatterOptions() 76 | { 77 | return $this->formatterOptions; 78 | } 79 | 80 | public function setFormatterOptions($formatterOptions) 81 | { 82 | $this->formatterOptions = $formatterOptions; 83 | } 84 | 85 | public function input() 86 | { 87 | return $this->input; 88 | } 89 | 90 | public function output() 91 | { 92 | return $this->output; 93 | } 94 | 95 | public function arguments() 96 | { 97 | return $this->input->getArguments(); 98 | } 99 | 100 | public function options() 101 | { 102 | // We cannot tell the difference between '--foo' (an option without 103 | // a value) and the absence of '--foo' when the option has an optional 104 | // value, and the current value of the option is 'null' using only 105 | // the public methods of InputInterface. We'll try to figure out 106 | // which is which by other means here. 107 | $options = $this->getAdjustedOptions(); 108 | 109 | // Make two conversions here: 110 | // --foo=0 wil convert $value from '0' to 'false' for binary options. 111 | // --foo with $value of 'true' will be forced to 'false' if --no-foo exists. 112 | foreach ($options as $option => $value) { 113 | if ($this->shouldConvertOptionToFalse($options, $option, $value)) { 114 | $options[$option] = false; 115 | } 116 | } 117 | 118 | return $options; 119 | } 120 | 121 | /** 122 | * Use 'hasParameterOption()' to attempt to disambiguate option states. 123 | */ 124 | protected function getAdjustedOptions() 125 | { 126 | $options = $this->input->getOptions(); 127 | 128 | // If Input isn't an ArgvInput, then return the options as-is. 129 | if (!$this->input instanceof ArgvInput) { 130 | return $options; 131 | } 132 | 133 | // If we have an ArgvInput, then we can determine if options 134 | // are missing from the command line. If the option value is 135 | // missing from $input, then we will keep the value `null`. 136 | // If it is present, but has no explicit value, then change it its 137 | // value to `true`. 138 | foreach ($options as $option => $value) { 139 | if (($value === null) && ($this->input->hasParameterOption("--$option"))) { 140 | $options[$option] = true; 141 | } 142 | } 143 | 144 | return $options; 145 | } 146 | 147 | protected function shouldConvertOptionToFalse($options, $option, $value) 148 | { 149 | // If the value is 'true' (e.g. the option is '--foo'), then convert 150 | // it to false if there is also an option '--no-foo'. n.b. if the 151 | // commandline has '--foo=bar' then $value will not be 'true', and 152 | // --no-foo will be ignored. 153 | if ($value === true) { 154 | // Check if the --no-* option exists. Note that none of the other 155 | // alteration apply in the $value == true case, so we can exit early here. 156 | $negation_key = 'no-' . $option; 157 | return array_key_exists($negation_key, $options) && $options[$negation_key]; 158 | } 159 | 160 | // If the option is '--foo=0', convert the '0' to 'false' when appropriate. 161 | if ($value !== '0') { 162 | return false; 163 | } 164 | 165 | // The '--foo=0' convertion is only applicable when the default value 166 | // is not in the special defaults list. i.e. you get a literal '0' 167 | // when your default is a string. 168 | return in_array($option, $this->specialDefaults); 169 | } 170 | 171 | public function cacheSpecialDefaults($definition) 172 | { 173 | foreach ($definition->getOptions() as $option => $inputOption) { 174 | $defaultValue = $inputOption->getDefault(); 175 | if (($defaultValue === null) || ($defaultValue === true)) { 176 | $this->specialDefaults[] = $option; 177 | } 178 | } 179 | } 180 | 181 | public function getArgsWithoutAppName() 182 | { 183 | $args = $this->arguments(); 184 | 185 | // When called via the Application, the first argument 186 | // will be the command name. The Application alters the 187 | // input definition to match, adding a 'command' argument 188 | // to the beginning. 189 | if ($this->input->hasArgument('command')) { 190 | array_shift($args); 191 | } 192 | 193 | return $args; 194 | } 195 | 196 | public function getArgsAndOptions() 197 | { 198 | // Get passthrough args, and add the options on the end. 199 | $args = $this->getArgsWithoutAppName(); 200 | 201 | // If this command has a mix of named arguments and options in its 202 | // parameter list, then use the parameter map to insert the options 203 | // into the correct spot in the parameters list. 204 | if (!empty($this->parameterMap)) { 205 | $mappedArgs = []; 206 | foreach ($this->parameterMap as $name => $mappedName) { 207 | if ($mappedName) { 208 | $mappedArgs[$name] = $this->input->getOption($mappedName); 209 | } else { 210 | $mappedArgs[$name] = array_shift($args); 211 | } 212 | } 213 | $args = $mappedArgs; 214 | } 215 | 216 | if ($this->includeOptionsInArgs) { 217 | $args['options'] = $this->options(); 218 | } 219 | 220 | return $args; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/CommandError.php: -------------------------------------------------------------------------------- 1 | message = $message; 18 | // Ensure the exit code is non-zero. The exit code may have 19 | // come from an exception, and those often default to zero if 20 | // a specific value is not provided. 21 | $this->exitCode = $exitCode == 0 ? 1 : $exitCode; 22 | } 23 | public function getExitCode() 24 | { 25 | return $this->exitCode; 26 | } 27 | 28 | public function getOutputData() 29 | { 30 | return $this->message; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/CommandFileDiscovery.php: -------------------------------------------------------------------------------- 1 | discoverNamespaced($moduleList, '\Drupal'); 26 | * 27 | * To discover global commands: 28 | * 29 | * $commandFiles = $discovery->discover($drupalRoot, '\Drupal'); 30 | * 31 | * WARNING: 32 | * 33 | * This class is deprecated. Commandfile discovery is complicated, and does 34 | * not work from within phar files. It is recommended to instead use a static 35 | * list of command classes as shown in https://github.com/g1a/starter/blob/master/example 36 | * 37 | * For a better alternative when implementing a plugin mechanism, see 38 | * https://robo.li/extending/#register-command-files-via-psr-4-autoloading 39 | */ 40 | class CommandFileDiscovery 41 | { 42 | /** @var string[] */ 43 | protected $excludeList; 44 | /** @var string[] */ 45 | protected $searchLocations; 46 | /** @var string */ 47 | protected $searchPattern = '*Commands.php'; 48 | /** @var boolean */ 49 | protected $includeFilesAtBase = true; 50 | /** @var integer */ 51 | protected $searchDepth = 2; 52 | /** @var bool */ 53 | protected $followLinks = false; 54 | /** @var string[] */ 55 | protected $strippedNamespaces; 56 | 57 | public function __construct() 58 | { 59 | $this->excludeList = ['Exclude']; 60 | $this->searchLocations = [ 61 | 'Command', 62 | 'CliTools', // TODO: Maybe remove 63 | ]; 64 | } 65 | 66 | /** 67 | * Specify whether to search for files at the base directory 68 | * ($directoryList parameter to discover and discoverNamespaced 69 | * methods), or only in the directories listed in the search paths. 70 | * 71 | * @param boolean $includeFilesAtBase 72 | */ 73 | public function setIncludeFilesAtBase($includeFilesAtBase) 74 | { 75 | $this->includeFilesAtBase = $includeFilesAtBase; 76 | return $this; 77 | } 78 | 79 | /** 80 | * Set the list of excludes to add to the finder, replacing 81 | * whatever was there before. 82 | * 83 | * @param array $excludeList The list of directory names to skip when 84 | * searching for command files. 85 | */ 86 | public function setExcludeList($excludeList) 87 | { 88 | $this->excludeList = $excludeList; 89 | return $this; 90 | } 91 | 92 | /** 93 | * Add one more location to the exclude list. 94 | * 95 | * @param string $exclude One directory name to skip when searching 96 | * for command files. 97 | */ 98 | public function addExclude($exclude) 99 | { 100 | $this->excludeList[] = $exclude; 101 | return $this; 102 | } 103 | 104 | /** 105 | * Set the search depth. By default, fills immediately in the 106 | * base directory are searched, plus all of the search locations 107 | * to this specified depth. If the search locations is set to 108 | * an empty array, then the base directory is searched to this 109 | * depth. 110 | */ 111 | public function setSearchDepth($searchDepth) 112 | { 113 | $this->searchDepth = $searchDepth; 114 | return $this; 115 | } 116 | 117 | /** 118 | * Specify that the discovery object should follow symlinks. By 119 | * default, symlinks are not followed. 120 | */ 121 | public function followLinks($followLinks = true) 122 | { 123 | $this->followLinks = $followLinks; 124 | return $this; 125 | } 126 | 127 | /** 128 | * Set the list of search locations to examine in each directory where 129 | * command files may be found. This replaces whatever was there before. 130 | * 131 | * @param array $searchLocations The list of locations to search for command files. 132 | */ 133 | public function setSearchLocations($searchLocations) 134 | { 135 | $this->searchLocations = $searchLocations; 136 | return $this; 137 | } 138 | 139 | /** 140 | * Set a particular namespace part to ignore. This is useful in plugin 141 | * mechanisms where the plugin is placed by Composer. 142 | * 143 | * For example, Drush extensions are placed in `./drush/Commands`. 144 | * If the Composer installer path is `"drush/Commands/contrib/{$name}": ["type:drupal-drush"]`, 145 | * then Composer will place the command files in `drush/Commands/contrib`. 146 | * The namespace should not be any different in this instance than if 147 | * the extension were placed in `drush/Commands`, though, so Drush therefore 148 | * calls `ignoreNamespacePart('contrib', 'Commands')`. This causes the 149 | * `contrib` component to be removed from the namespace if it follows 150 | * the namespace `Commands`. If the '$base' parameter is not specified, then 151 | * the ignored portion of the namespace may appear anywhere in the path. 152 | */ 153 | public function ignoreNamespacePart($ignore, $base = '') 154 | { 155 | $replacementPart = '\\'; 156 | if (!empty($base)) { 157 | $replacementPart .= $base . '\\'; 158 | } 159 | $ignoredPart = $replacementPart . $ignore . '\\'; 160 | $this->strippedNamespaces[$ignoredPart] = $replacementPart; 161 | 162 | return $this; 163 | } 164 | 165 | /** 166 | * Add one more location to the search location list. 167 | * 168 | * @param string $location One more relative path to search 169 | * for command files. 170 | */ 171 | public function addSearchLocation($location) 172 | { 173 | $this->searchLocations[] = $location; 174 | return $this; 175 | } 176 | 177 | /** 178 | * Specify the pattern / regex used by the finder to search for 179 | * command files. 180 | */ 181 | public function setSearchPattern($searchPattern) 182 | { 183 | $this->searchPattern = $searchPattern; 184 | return $this; 185 | } 186 | 187 | /** 188 | * Given a list of directories, e.g. Drupal modules like: 189 | * 190 | * core/modules/block 191 | * core/modules/dblog 192 | * modules/default_content 193 | * 194 | * Discover command files in any of these locations. 195 | * 196 | * @param string|string[] $directoryList Places to search for commands. 197 | * 198 | * @return array 199 | */ 200 | public function discoverNamespaced($directoryList, $baseNamespace = '') 201 | { 202 | return $this->discover($this->convertToNamespacedList((array)$directoryList), $baseNamespace); 203 | } 204 | 205 | /** 206 | * Given a simple list containing paths to directories, where 207 | * the last component of the path should appear in the namespace, 208 | * after the base namespace, this function will return an 209 | * associative array mapping the path's basename (e.g. the module 210 | * name) to the directory path. 211 | * 212 | * Module names must be unique. 213 | * 214 | * @param string[] $directoryList A list of module locations 215 | * 216 | * @return array 217 | */ 218 | public function convertToNamespacedList($directoryList) 219 | { 220 | $namespacedArray = []; 221 | foreach ((array)$directoryList as $directory) { 222 | $namespacedArray[basename($directory)] = $directory; 223 | } 224 | return $namespacedArray; 225 | } 226 | 227 | /** 228 | * Search for command files in the specified locations. This is the function that 229 | * should be used for all locations that are NOT modules of a framework. 230 | * 231 | * @param string|string[] $directoryList Places to search for commands. 232 | * @return array 233 | */ 234 | public function discover($directoryList, $baseNamespace = '') 235 | { 236 | $commandFiles = []; 237 | foreach ((array)$directoryList as $key => $directory) { 238 | $itemsNamespace = $this->joinNamespace([$baseNamespace, $key]); 239 | $commandFiles = array_merge( 240 | $commandFiles, 241 | $this->discoverCommandFiles($directory, $itemsNamespace), 242 | $this->discoverCommandFiles("$directory/src", $itemsNamespace) 243 | ); 244 | } 245 | return $this->fixNamespaces($commandFiles); 246 | } 247 | 248 | /** 249 | * fixNamespaces will alter the namespaces in the commandFiles 250 | * result to remove the Composer placement directory, if any. 251 | */ 252 | protected function fixNamespaces($commandFiles) 253 | { 254 | // Do nothing unless the client told us to remove some namespace components. 255 | if (empty($this->strippedNamespaces)) { 256 | return $commandFiles; 257 | } 258 | 259 | // Strip out any part of the namespace the client did not want. 260 | // @see CommandFileDiscovery::ignoreNamespacePart 261 | return array_map( 262 | function ($fqcn) { 263 | return str_replace( 264 | array_keys($this->strippedNamespaces), 265 | array_values($this->strippedNamespaces), 266 | $fqcn 267 | ); 268 | }, 269 | $commandFiles 270 | ); 271 | } 272 | 273 | /** 274 | * Search for command files in specific locations within a single directory. 275 | * 276 | * In each location, we will accept only a few places where command files 277 | * can be found. This will reduce the need to search through many unrelated 278 | * files. 279 | * 280 | * The default search locations include: 281 | * 282 | * . 283 | * CliTools 284 | * src/CliTools 285 | * 286 | * The pattern we will look for is any file whose name ends in 'Commands.php'. 287 | * A list of paths to found files will be returned. 288 | */ 289 | protected function discoverCommandFiles($directory, $baseNamespace) 290 | { 291 | $commandFiles = []; 292 | // In the search location itself, we will search for command files 293 | // immediately inside the directory only. 294 | if ($this->includeFilesAtBase) { 295 | $commandFiles = $this->discoverCommandFilesInLocation( 296 | $directory, 297 | $this->getBaseDirectorySearchDepth(), 298 | $baseNamespace 299 | ); 300 | } 301 | 302 | // In the other search locations, 303 | foreach ($this->searchLocations as $location) { 304 | $itemsNamespace = $this->joinNamespace([$baseNamespace, $location]); 305 | $commandFiles = array_merge( 306 | $commandFiles, 307 | $this->discoverCommandFilesInLocation( 308 | "$directory/$location", 309 | $this->getSearchDepth(), 310 | $itemsNamespace 311 | ) 312 | ); 313 | } 314 | return $commandFiles; 315 | } 316 | 317 | /** 318 | * Return a Finder search depth appropriate for our selected search depth. 319 | * 320 | * @return string 321 | */ 322 | protected function getSearchDepth() 323 | { 324 | return $this->searchDepth <= 0 ? '== 0' : '<= ' . $this->searchDepth; 325 | } 326 | 327 | /** 328 | * Return a Finder search depth for the base directory. If the 329 | * searchLocations array has been populated, then we will only search 330 | * for files immediately inside the base directory; no traversal into 331 | * deeper directories will be done, as that would conflict with the 332 | * specification provided by the search locations. If there is no 333 | * search location, then we will search to whatever depth was specified 334 | * by the client. 335 | * 336 | * @return string 337 | */ 338 | protected function getBaseDirectorySearchDepth() 339 | { 340 | if (!empty($this->searchLocations)) { 341 | return '== 0'; 342 | } 343 | return $this->getSearchDepth(); 344 | } 345 | 346 | /** 347 | * Search for command files in just one particular location. Returns 348 | * an associative array mapping from the pathname of the file to the 349 | * classname that it contains. The pathname may be ignored if the search 350 | * location is included in the autoloader. 351 | * 352 | * @param string $directory The location to search 353 | * @param string $depth How deep to search (e.g. '== 0' or '< 2') 354 | * @param string $baseNamespace Namespace to prepend to each classname 355 | * 356 | * @return array 357 | */ 358 | protected function discoverCommandFilesInLocation($directory, $depth, $baseNamespace) 359 | { 360 | if (!is_dir($directory)) { 361 | return []; 362 | } 363 | $finder = $this->createFinder($directory, $depth); 364 | 365 | $commands = []; 366 | foreach ($finder as $file) { 367 | $relativePathName = $file->getRelativePathname(); 368 | $relativeNamespaceAndClassname = str_replace( 369 | ['/', '-', '.php'], 370 | ['\\', '_', ''], 371 | $relativePathName 372 | ); 373 | $classname = $this->joinNamespace([$baseNamespace, $relativeNamespaceAndClassname]); 374 | $commandFilePath = $this->joinPaths([$directory, $relativePathName]); 375 | $commands[$commandFilePath] = $classname; 376 | } 377 | 378 | return $commands; 379 | } 380 | 381 | /** 382 | * Create a Finder object for use in searching a particular directory 383 | * location. 384 | * 385 | * @param string $directory The location to search 386 | * @param string $depth The depth limitation 387 | * 388 | * @return Finder 389 | */ 390 | protected function createFinder($directory, $depth) 391 | { 392 | $finder = new Finder(); 393 | $finder->files() 394 | ->name($this->searchPattern) 395 | ->in($directory) 396 | ->depth($depth); 397 | 398 | foreach ($this->excludeList as $item) { 399 | $finder->exclude($item); 400 | } 401 | 402 | if ($this->followLinks) { 403 | $finder->followLinks(); 404 | } 405 | 406 | return $finder; 407 | } 408 | 409 | /** 410 | * Combine the items of the provied array into a backslash-separated 411 | * namespace string. Empty and numeric items are omitted. 412 | * 413 | * @param array $namespaceParts List of components of a namespace 414 | * 415 | * @return string 416 | */ 417 | protected function joinNamespace(array $namespaceParts) 418 | { 419 | return $this->joinParts( 420 | '\\', 421 | $namespaceParts, 422 | function ($item) { 423 | return !is_numeric($item) && !empty($item); 424 | } 425 | ); 426 | } 427 | 428 | /** 429 | * Combine the items of the provied array into a slash-separated 430 | * pathname. Empty items are omitted. 431 | * 432 | * @param array $pathParts List of components of a path 433 | * 434 | * @return string 435 | */ 436 | protected function joinPaths(array $pathParts) 437 | { 438 | $path = $this->joinParts( 439 | '/', 440 | $pathParts, 441 | function ($item) { 442 | return !empty($item); 443 | } 444 | ); 445 | return str_replace(DIRECTORY_SEPARATOR, '/', $path); 446 | } 447 | 448 | /** 449 | * Simple wrapper around implode and array_filter. 450 | * 451 | * @param string $delimiter 452 | * @param array $parts 453 | * @param callable $filterFunction 454 | */ 455 | protected function joinParts($delimiter, $parts, $filterFunction) 456 | { 457 | $parts = array_map( 458 | function ($item) use ($delimiter) { 459 | return rtrim($item, $delimiter); 460 | }, 461 | $parts 462 | ); 463 | return implode( 464 | $delimiter, 465 | array_filter($parts, $filterFunction) 466 | ); 467 | } 468 | } 469 | -------------------------------------------------------------------------------- /src/CommandInfoAltererInterface.php: -------------------------------------------------------------------------------- 1 | hookManager = $hookManager; 50 | } 51 | 52 | /** 53 | * Return the hook manager 54 | * @return HookManager 55 | */ 56 | public function hookManager() 57 | { 58 | return $this->hookManager; 59 | } 60 | 61 | public function resultWriter() 62 | { 63 | if (!$this->resultWriter) { 64 | $this->setResultWriter(new ResultWriter()); 65 | } 66 | return $this->resultWriter; 67 | } 68 | 69 | public function setResultWriter($resultWriter) 70 | { 71 | $this->resultWriter = $resultWriter; 72 | } 73 | 74 | public function parameterInjection() 75 | { 76 | if (!$this->parameterInjection) { 77 | $this->setParameterInjection(new ParameterInjection()); 78 | } 79 | return $this->parameterInjection; 80 | } 81 | 82 | public function setParameterInjection($parameterInjection) 83 | { 84 | $this->parameterInjection = $parameterInjection; 85 | } 86 | 87 | public function addPrepareFormatter(PrepareFormatter $preparer) 88 | { 89 | $this->prepareOptionsList[] = $preparer; 90 | } 91 | 92 | public function setFormatterManager(FormatterManager $formatterManager) 93 | { 94 | $this->formatterManager = $formatterManager; 95 | $this->resultWriter()->setFormatterManager($formatterManager); 96 | return $this; 97 | } 98 | 99 | public function setDisplayErrorFunction(callable $fn) 100 | { 101 | $this->resultWriter()->setDisplayErrorFunction($fn); 102 | } 103 | 104 | /** 105 | * Set a mode to make the annotated command library re-throw 106 | * any exception that it catches while processing a command. 107 | * 108 | * The default behavior in the current (2.x) branch is to catch 109 | * the exception and replace it with a CommandError object that 110 | * may be processed by the normal output processing passthrough. 111 | * 112 | * In the 3.x branch, exceptions will never be caught; they will 113 | * be passed through, as if setPassExceptions(true) were called. 114 | * This is the recommended behavior. 115 | */ 116 | public function setPassExceptions($passExceptions) 117 | { 118 | $this->passExceptions = $passExceptions; 119 | return $this; 120 | } 121 | 122 | public function commandErrorForException(\Exception $e) 123 | { 124 | if ($this->passExceptions) { 125 | throw $e; 126 | } 127 | return new CommandError($e->getMessage(), $e->getCode()); 128 | } 129 | 130 | /** 131 | * Return the formatter manager 132 | * @return FormatterManager 133 | */ 134 | public function formatterManager() 135 | { 136 | return $this->formatterManager; 137 | } 138 | 139 | public function initializeHook( 140 | InputInterface $input, 141 | $names, 142 | AnnotationData $annotationData 143 | ) { 144 | $initializeDispatcher = new InitializeHookDispatcher($this->hookManager(), $names); 145 | return $initializeDispatcher->initialize($input, $annotationData); 146 | } 147 | 148 | public function optionsHook( 149 | AnnotatedCommand $command, 150 | $names, 151 | AnnotationData $annotationData 152 | ) { 153 | $optionsDispatcher = new OptionsHookDispatcher($this->hookManager(), $names); 154 | $optionsDispatcher->getOptions($command, $annotationData); 155 | } 156 | 157 | public function interact( 158 | InputInterface $input, 159 | OutputInterface $output, 160 | $names, 161 | AnnotationData $annotationData 162 | ) { 163 | $interactDispatcher = new InteractHookDispatcher($this->hookManager(), $names); 164 | return $interactDispatcher->interact($input, $output, $annotationData); 165 | } 166 | 167 | public function process( 168 | OutputInterface $output, 169 | $names, 170 | $commandCallback, 171 | CommandData $commandData 172 | ) { 173 | $result = []; 174 | try { 175 | $result = $this->validateRunAndAlter( 176 | $names, 177 | $commandCallback, 178 | $commandData 179 | ); 180 | return $this->handleResults($output, $names, $result, $commandData); 181 | } catch (\Exception $e) { 182 | $result = $this->commandErrorForException($e); 183 | return $this->handleResults($output, $names, $result, $commandData); 184 | } 185 | } 186 | 187 | public function validateRunAndAlter( 188 | $names, 189 | $commandCallback, 190 | CommandData $commandData 191 | ) { 192 | // Validators return any object to signal a validation error; 193 | // if the return an array, it replaces the arguments. 194 | $validateDispatcher = new ValidateHookDispatcher($this->hookManager(), $names); 195 | $validated = $validateDispatcher->validate($commandData); 196 | if (is_object($validated)) { 197 | return $validated; 198 | } 199 | 200 | // Once we have validated the options, update the formatter options. 201 | $this->updateFormatterOptions($commandData); 202 | 203 | $replaceDispatcher = new ReplaceCommandHookDispatcher($this->hookManager(), $names); 204 | if ($this->logger) { 205 | $replaceDispatcher->setLogger($this->logger); 206 | } 207 | if ($replaceDispatcher->hasReplaceCommandHook()) { 208 | $commandCallback = $replaceDispatcher->getReplacementCommand($commandData); 209 | } 210 | 211 | // Run the command, alter the results, and then handle output and status 212 | $result = $this->runCommandCallback($commandCallback, $commandData); 213 | return $this->processResults($names, $result, $commandData); 214 | } 215 | 216 | public function processResults($names, $result, CommandData $commandData) 217 | { 218 | $processDispatcher = new ProcessResultHookDispatcher($this->hookManager(), $names); 219 | return $processDispatcher->process($result, $commandData); 220 | } 221 | 222 | /** 223 | * Update the FormatterOptions object with validated command options. 224 | * Also runs the perparers. 225 | * 226 | * @param CommandData $commandData 227 | * @return FormatterOptions 228 | */ 229 | protected function updateFormatterOptions($commandData) 230 | { 231 | // Update formatter options, in case anything was changed in a hook 232 | $formatterOptions = $commandData->formatterOptions(); 233 | $formatterOptions->setConfigurationData($commandData->annotationData()->getArrayCopy()); 234 | $formatterOptions->setOptions($commandData->input()->getOptions()); 235 | 236 | // Run any prepare function, e.g. to prepare a format object to run 237 | foreach ($this->prepareOptionsList as $preparer) { 238 | $preparer->prepare($commandData, $formatterOptions); 239 | } 240 | 241 | // Call 'overrideOptions' in advance of calling the command, to 242 | // allow the format options to be as close as possible to their 243 | // final state. 'overrideOptions' will be called again right before 244 | // rendering, and may be altered, e.g. if the override depends on 245 | // the data in the command output. 246 | $format = $formatterOptions->getFormat(); 247 | if ($format) { 248 | $placeholderStructuredOutput = []; 249 | try { 250 | $formatter = $this->formatterManager->getFormatter($format); 251 | $this->formatterManager->overrideOptions($formatter, $placeholderStructuredOutput, $formatterOptions); 252 | } catch (\Exception $e) { 253 | } 254 | } 255 | } 256 | 257 | /** 258 | * Handle the result output and status code calculation. 259 | */ 260 | public function handleResults(OutputInterface $output, $names, $result, CommandData $commandData) 261 | { 262 | $statusCodeDispatcher = new StatusDeterminerHookDispatcher($this->hookManager(), $names); 263 | $extractDispatcher = new ExtracterHookDispatcher($this->hookManager(), $names); 264 | 265 | return $this->resultWriter()->handle($output, $result, $commandData, $statusCodeDispatcher, $extractDispatcher); 266 | } 267 | 268 | /** 269 | * Run the main command callback 270 | */ 271 | protected function runCommandCallback($commandCallback, CommandData $commandData) 272 | { 273 | $result = false; 274 | try { 275 | $args = $this->parameterInjection()->args($commandData); 276 | $result = call_user_func_array($commandCallback, array_values($args)); 277 | } catch (\Exception $e) { 278 | $result = $this->commandErrorForException($e); 279 | } 280 | return $result; 281 | } 282 | 283 | public function injectIntoCommandData($commandData, $injectedClasses) 284 | { 285 | $this->parameterInjection()->injectIntoCommandData($commandData, $injectedClasses); 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/CommandResult.php: -------------------------------------------------------------------------------- 1 | data = $data; 39 | $this->exitCode = $exitCode; 40 | } 41 | 42 | public static function exitCode($exitCode) 43 | { 44 | return new static(null, $exitCode); 45 | } 46 | 47 | public static function data($data) 48 | { 49 | return new static($data); 50 | } 51 | 52 | public static function dataWithExitCode($data, $exitCode) 53 | { 54 | return new static($data, $exitCode); 55 | } 56 | 57 | public function getExitCode() 58 | { 59 | return $this->exitCode; 60 | } 61 | 62 | public function getOutputData() 63 | { 64 | return $this->data; 65 | } 66 | 67 | public function setOutputData($data) 68 | { 69 | $this->data = $data; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Events/CustomEventAwareInterface.php: -------------------------------------------------------------------------------- 1 | hookManager = $hookManager; 17 | } 18 | 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function getCustomEventHandlers($eventName) 23 | { 24 | if (!$this->hookManager) { 25 | return []; 26 | } 27 | return $this->hookManager->getHook($eventName, HookManager::ON_EVENT); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ExitCodeInterface.php: -------------------------------------------------------------------------------- 1 | application = $application; 20 | } 21 | 22 | public function getApplication() 23 | { 24 | return $this->application; 25 | } 26 | 27 | /** 28 | * Run the help command 29 | * 30 | * @command my-help 31 | * @return \Consolidation\AnnotatedCommand\Help\HelpDocument 32 | */ 33 | public function help($commandName = 'help') 34 | { 35 | $command = $this->getApplication()->find($commandName); 36 | 37 | $helpDocument = $this->getHelpDocument($command); 38 | return $helpDocument; 39 | } 40 | 41 | /** 42 | * Create a help document. 43 | */ 44 | protected function getHelpDocument($command) 45 | { 46 | return new HelpDocument($command); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Help/HelpDocument.php: -------------------------------------------------------------------------------- 1 | generateBaseHelpDom($command); 23 | $dom = $this->alterHelpDocument($command, $dom); 24 | 25 | $this->command = $command; 26 | $this->dom = $dom; 27 | } 28 | 29 | /** 30 | * Convert data into a \DomDocument. 31 | * 32 | * @return \DomDocument 33 | */ 34 | public function getDomData() 35 | { 36 | return $this->dom; 37 | } 38 | 39 | /** 40 | * Create the base help DOM prior to alteration by the Command object. 41 | * @param Command $command 42 | * @return \DomDocument 43 | */ 44 | protected function generateBaseHelpDom(Command $command) 45 | { 46 | // Use Symfony to generate xml text. If other formats are 47 | // requested, convert from xml to the desired form. 48 | $descriptor = new XmlDescriptor(); 49 | return $descriptor->getCommandDocument($command); 50 | } 51 | 52 | /** 53 | * Alter the DOM document per the command object 54 | * @param Command $command 55 | * @param \DomDocument $dom 56 | * @return \DomDocument 57 | */ 58 | protected function alterHelpDocument(Command $command, \DomDocument $dom) 59 | { 60 | if ($command instanceof HelpDocumentAlter) { 61 | $dom = $command->helpAlter($dom); 62 | } 63 | return $dom; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Help/HelpDocumentAlter.php: -------------------------------------------------------------------------------- 1 | appendChild($commandXML = $dom->createElement('command')); 16 | $commandXML->setAttribute('id', $command->getName()); 17 | $commandXML->setAttribute('name', $command->getName()); 18 | 19 | // Get the original element and its top-level elements. 20 | $originalCommandXML = static::getSingleElementByTagName($dom, $originalDom, 'command'); 21 | $originalUsagesXML = static::getSingleElementByTagName($dom, $originalCommandXML, 'usages'); 22 | $originalDescriptionXML = static::getSingleElementByTagName($dom, $originalCommandXML, 'description'); 23 | $originalHelpXML = static::getSingleElementByTagName($dom, $originalCommandXML, 'help'); 24 | $originalArgumentsXML = static::getSingleElementByTagName($dom, $originalCommandXML, 'arguments'); 25 | $originalOptionsXML = static::getSingleElementByTagName($dom, $originalCommandXML, 'options'); 26 | 27 | // Keep only the first of the elements 28 | $newUsagesXML = $dom->createElement('usages'); 29 | $firstUsageXML = static::getSingleElementByTagName($dom, $originalUsagesXML, 'usage'); 30 | $newUsagesXML->appendChild($firstUsageXML); 31 | 32 | // Create our own elements 33 | $newExamplesXML = $dom->createElement('examples'); 34 | foreach ($command->getExampleUsages() as $usage => $description) { 35 | $newExamplesXML->appendChild($exampleXML = $dom->createElement('example')); 36 | $exampleXML->appendChild($usageXML = $dom->createElement('usage', $usage)); 37 | $exampleXML->appendChild($descriptionXML = $dom->createElement('description', $description)); 38 | } 39 | 40 | // Create our own elements 41 | $newAliasesXML = $dom->createElement('aliases'); 42 | foreach ($command->getAliases() as $alias) { 43 | $newAliasesXML->appendChild($dom->createElement('alias', $alias)); 44 | } 45 | 46 | // Create our own elements 47 | $newTopicsXML = $dom->createElement('topics'); 48 | foreach ($command->getTopics() as $topic) { 49 | $newTopicsXML->appendChild($topicXML = $dom->createElement('topic', $topic)); 50 | } 51 | 52 | // Place the different elements into the element in the desired order 53 | $commandXML->appendChild($newUsagesXML); 54 | $commandXML->appendChild($newExamplesXML); 55 | $commandXML->appendChild($originalDescriptionXML); 56 | $commandXML->appendChild($originalArgumentsXML); 57 | $commandXML->appendChild($originalOptionsXML); 58 | $commandXML->appendChild($originalHelpXML); 59 | $commandXML->appendChild($newAliasesXML); 60 | $commandXML->appendChild($newTopicsXML); 61 | 62 | return $dom; 63 | } 64 | 65 | 66 | protected static function getSingleElementByTagName($dom, $parent, $tagName) 67 | { 68 | // There should always be exactly one '' element. 69 | $elements = $parent->getElementsByTagName($tagName); 70 | $result = $elements->item(0); 71 | 72 | $result = $dom->importNode($result, true); 73 | 74 | return $result; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Hooks/AlterResultInterface.php: -------------------------------------------------------------------------------- 1 | getInput(); 26 | $output = $event->getOutput(); 27 | 28 | $hooks = [ 29 | HookManager::PRE_COMMAND_EVENT, 30 | HookManager::COMMAND_EVENT, 31 | HookManager::POST_COMMAND_EVENT 32 | ]; 33 | $commandEventHooks = $this->getHooks($hooks); 34 | foreach ($commandEventHooks as $commandEvent) { 35 | if ($commandEvent instanceof EventDispatcherInterface) { 36 | $commandEvent->dispatch($event, ConsoleEvents::COMMAND); 37 | } 38 | if (is_callable($commandEvent)) { 39 | $state = StateHelper::injectIntoCallbackObject($commandEvent, $input, $output); 40 | $commandEvent($event); 41 | $state->restore(); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Hooks/Dispatchers/ExtracterHookDispatcher.php: -------------------------------------------------------------------------------- 1 | getOutputData(); 22 | } 23 | 24 | $hooks = [ 25 | HookManager::EXTRACT_OUTPUT, 26 | ]; 27 | $extractors = $this->getHooks($hooks); 28 | foreach ($extractors as $extractor) { 29 | $structuredOutput = $this->callExtractor($extractor, $result); 30 | if (isset($structuredOutput)) { 31 | return $structuredOutput; 32 | } 33 | } 34 | 35 | return $result; 36 | } 37 | 38 | protected function callExtractor($extractor, $result) 39 | { 40 | if ($extractor instanceof ExtractOutputInterface) { 41 | return $extractor->extractOutput($result); 42 | } 43 | if (is_callable($extractor)) { 44 | return $extractor($result); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Hooks/Dispatchers/HookDispatcher.php: -------------------------------------------------------------------------------- 1 | hookManager = $hookManager; 20 | $this->names = $names; 21 | } 22 | 23 | public function getHooks($hooks, $annotationData = null) 24 | { 25 | return $this->hookManager->getHooks($this->names, $hooks, $annotationData); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Hooks/Dispatchers/InitializeHookDispatcher.php: -------------------------------------------------------------------------------- 1 | getHooks($hooks, $annotationData); 28 | foreach ($providers as $provider) { 29 | $this->callInitializeHook($provider, $input, $annotationData); 30 | } 31 | } 32 | 33 | protected function callInitializeHook($provider, $input, AnnotationData $annotationData) 34 | { 35 | $state = StateHelper::injectIntoCallbackObject($provider, $input); 36 | $result = $this->doInitializeHook($provider, $input, $annotationData); 37 | $state->restore(); 38 | return $result; 39 | } 40 | 41 | private function doInitializeHook($provider, $input, AnnotationData $annotationData) 42 | { 43 | if ($provider instanceof InitializeHookInterface) { 44 | return $provider->initialize($input, $annotationData); 45 | } 46 | if (is_callable($provider)) { 47 | return $provider($input, $annotationData); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Hooks/Dispatchers/InteractHookDispatcher.php: -------------------------------------------------------------------------------- 1 | getHooks($hooks, $annotationData); 29 | foreach ($interactors as $interactor) { 30 | $this->callInteractor($interactor, $input, $output, $annotationData); 31 | } 32 | } 33 | 34 | protected function callInteractor($interactor, $input, $output, AnnotationData $annotationData) 35 | { 36 | $state = StateHelper::injectIntoCallbackObject($interactor, $input, $output); 37 | $result = $this->doInteractor($interactor, $input, $output, $annotationData); 38 | $state->restore(); 39 | return $result; 40 | } 41 | 42 | private function doInteractor($interactor, $input, $output, AnnotationData $annotationData) 43 | { 44 | if ($interactor instanceof InteractorInterface) { 45 | return $interactor->interact($input, $output, $annotationData); 46 | } 47 | if (is_callable($interactor)) { 48 | return $interactor($input, $output, $annotationData); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Hooks/Dispatchers/OptionsHookDispatcher.php: -------------------------------------------------------------------------------- 1 | getHooks($hooks, $annotationData); 26 | foreach ($optionHooks as $optionHook) { 27 | $this->callOptionHook($optionHook, $command, $annotationData); 28 | } 29 | $commandInfoList = $this->hookManager->getHookOptionsForCommand($command); 30 | if ($command instanceof AnnotatedCommand) { 31 | $command->optionsHookForHookAnnotations($commandInfoList); 32 | } 33 | } 34 | 35 | protected function callOptionHook($optionHook, $command, AnnotationData $annotationData) 36 | { 37 | if ($optionHook instanceof OptionHookInterface) { 38 | return $optionHook->getOptions($command, $annotationData); 39 | } 40 | if (is_callable($optionHook)) { 41 | return $optionHook($command, $annotationData); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Hooks/Dispatchers/ProcessResultHookDispatcher.php: -------------------------------------------------------------------------------- 1 | getHooks($hooks, $commandData->annotationData()); 34 | foreach ($processors as $processor) { 35 | $result = $this->callProcessor($processor, $result, $commandData); 36 | } 37 | 38 | return $result; 39 | } 40 | 41 | protected function callProcessor($processor, $result, CommandData $commandData) 42 | { 43 | $state = StateHelper::injectIntoCallbackObject($processor, $commandData->input(), $commandData->output()); 44 | $result = $this->doProcessor($processor, $result, $commandData); 45 | $state->restore(); 46 | return $result; 47 | } 48 | 49 | private function doProcessor($processor, $result, CommandData $commandData) 50 | { 51 | $processed = null; 52 | if ($processor instanceof ProcessResultInterface) { 53 | $processed = $processor->process($result, $commandData); 54 | } 55 | if (is_callable($processor)) { 56 | $processed = $processor($result, $commandData); 57 | } 58 | if (isset($processed)) { 59 | return $processed; 60 | } 61 | return $result; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Hooks/Dispatchers/ReplaceCommandHookDispatcher.php: -------------------------------------------------------------------------------- 1 | getReplaceCommandHooks()); 24 | } 25 | 26 | /** 27 | * @return \callable[] 28 | */ 29 | public function getReplaceCommandHooks() 30 | { 31 | $hooks = [ 32 | HookManager::REPLACE_COMMAND_HOOK, 33 | ]; 34 | $replaceCommandHooks = $this->getHooks($hooks); 35 | 36 | return $replaceCommandHooks; 37 | } 38 | 39 | /** 40 | * @param \Consolidation\AnnotatedCommand\CommandData $commandData 41 | * 42 | * @return callable 43 | */ 44 | public function getReplacementCommand(CommandData $commandData) 45 | { 46 | $replaceCommandHooks = $this->getReplaceCommandHooks(); 47 | 48 | // We only take the first hook implementation of "replace-command" as the replacement. Commands shouldn't have 49 | // more than one replacement. 50 | $replacementCommand = reset($replaceCommandHooks); 51 | 52 | if ($this->logger && count($replaceCommandHooks) > 1) { 53 | $command_name = $commandData->annotationData()->get('command', 'unknown'); 54 | $message = "Multiple implementations of the \"replace - command\" hook exist for the \"$command_name\" command.\n"; 55 | foreach ($replaceCommandHooks as $replaceCommandHook) { 56 | $class = get_class($replaceCommandHook[0]); 57 | $method = $replaceCommandHook[1]; 58 | $hook_name = "$class->$method"; 59 | $message .= " - $hook_name\n"; 60 | } 61 | $this->logger->warning($message); 62 | } 63 | 64 | return $replacementCommand; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Hooks/Dispatchers/StatusDeterminerHookDispatcher.php: -------------------------------------------------------------------------------- 1 | getExitCode(); 25 | } 26 | 27 | $hooks = [ 28 | HookManager::STATUS_DETERMINER, 29 | ]; 30 | // If the result does not implement ExitCodeInterface, 31 | // then we'll see if there is a determiner that can 32 | // extract a status code from the result. 33 | $determiners = $this->getHooks($hooks); 34 | foreach ($determiners as $determiner) { 35 | $status = $this->callDeterminer($determiner, $result); 36 | if (isset($status)) { 37 | return $status; 38 | } 39 | } 40 | } 41 | 42 | protected function callDeterminer($determiner, $result) 43 | { 44 | if ($determiner instanceof StatusDeterminerInterface) { 45 | return $determiner->determineStatusCode($result); 46 | } 47 | if (is_callable($determiner)) { 48 | return $determiner($result); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Hooks/Dispatchers/ValidateHookDispatcher.php: -------------------------------------------------------------------------------- 1 | getHooks($hooks, $commandData->annotationData()); 28 | foreach ($validators as $validator) { 29 | $validated = $this->callValidator($validator, $commandData); 30 | if ($validated === false) { 31 | return new CommandError(); 32 | } 33 | if (is_object($validated)) { 34 | return $validated; 35 | } 36 | } 37 | } 38 | 39 | protected function callValidator($validator, CommandData $commandData) 40 | { 41 | $state = StateHelper::injectIntoCallbackObject($validator, $commandData->input(), $commandData->output()); 42 | $result = $this->doValidator($validator, $commandData); 43 | $state->restore(); 44 | return $result; 45 | } 46 | 47 | private function doValidator($validator, CommandData $commandData) 48 | { 49 | if ($validator instanceof ValidatorInterface) { 50 | return $validator->validate($commandData); 51 | } 52 | if (is_callable($validator)) { 53 | return $validator($commandData); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Hooks/ExtractOutputInterface.php: -------------------------------------------------------------------------------- 1 | hooks; 66 | } 67 | 68 | /** 69 | * Add a hook 70 | * 71 | * @param mixed $callback The callback function to call 72 | * @param string $hook The name of the hook to add 73 | * @param string $name The name of the command to hook 74 | * ('*' for all) 75 | */ 76 | public function add(callable $callback, $hook, $name = '*') 77 | { 78 | if (empty($name)) { 79 | $name = static::getClassNameFromCallback($callback); 80 | } 81 | $this->hooks[$name][$hook][] = $callback; 82 | return $this; 83 | } 84 | 85 | public function recordHookOptions($commandInfo, $name) 86 | { 87 | $this->hookOptions[$name][] = $commandInfo; 88 | return $this; 89 | } 90 | 91 | public static function getNames($command, $callback) 92 | { 93 | return array_filter( 94 | array_merge( 95 | static::getNamesUsingCommands($command), 96 | [static::getClassNameFromCallback($callback)] 97 | ) 98 | ); 99 | } 100 | 101 | protected static function getNamesUsingCommands($command) 102 | { 103 | return array_merge( 104 | [$command->getName()], 105 | $command->getAliases() 106 | ); 107 | } 108 | 109 | /** 110 | * If a command hook does not specify any particular command 111 | * name that it should be attached to, then it will be applied 112 | * to every command that is defined in the same class as the hook. 113 | * This is controlled by using the namespace + class name of 114 | * the implementing class of the callback hook. 115 | */ 116 | protected static function getClassNameFromCallback($callback) 117 | { 118 | if (!is_array($callback)) { 119 | return ''; 120 | } 121 | $reflectionClass = new \ReflectionClass($callback[0]); 122 | return $reflectionClass->getName(); 123 | } 124 | 125 | /** 126 | * Add a replace command hook 127 | * 128 | * @param type ReplaceCommandHookInterface $provider 129 | * @param type string $command_name The name of the command to replace 130 | */ 131 | public function addReplaceCommandHook(ReplaceCommandHookInterface $replaceCommandHook, $name) 132 | { 133 | $this->hooks[$name][self::REPLACE_COMMAND_HOOK][] = $replaceCommandHook; 134 | return $this; 135 | } 136 | 137 | public function addPreCommandEventDispatcher(EventDispatcherInterface $eventDispatcher, $name = '*') 138 | { 139 | $this->hooks[$name][self::PRE_COMMAND_EVENT][] = $eventDispatcher; 140 | return $this; 141 | } 142 | 143 | public function addCommandEventDispatcher(EventDispatcherInterface $eventDispatcher, $name = '*') 144 | { 145 | $this->hooks[$name][self::COMMAND_EVENT][] = $eventDispatcher; 146 | return $this; 147 | } 148 | 149 | public function addPostCommandEventDispatcher(EventDispatcherInterface $eventDispatcher, $name = '*') 150 | { 151 | $this->hooks[$name][self::POST_COMMAND_EVENT][] = $eventDispatcher; 152 | return $this; 153 | } 154 | 155 | public function addCommandEvent(EventSubscriberInterface $eventSubscriber) 156 | { 157 | // Wrap the event subscriber in a dispatcher and add it 158 | $dispatcher = new EventDispatcher(); 159 | $dispatcher->addSubscriber($eventSubscriber); 160 | return $this->addCommandEventDispatcher($dispatcher); 161 | } 162 | 163 | /** 164 | * Add an configuration provider hook 165 | * 166 | * @param type InitializeHookInterface $provider 167 | * @param type $name The name of the command to hook 168 | * ('*' for all) 169 | */ 170 | public function addInitializeHook(InitializeHookInterface $initializeHook, $name = '*') 171 | { 172 | $this->hooks[$name][self::INITIALIZE][] = $initializeHook; 173 | return $this; 174 | } 175 | 176 | /** 177 | * Add an option hook 178 | * 179 | * @param type ValidatorInterface $validator 180 | * @param type $name The name of the command to hook 181 | * ('*' for all) 182 | */ 183 | public function addOptionHook(OptionHookInterface $interactor, $name = '*') 184 | { 185 | $this->hooks[$name][self::INTERACT][] = $interactor; 186 | return $this; 187 | } 188 | 189 | /** 190 | * Add an interact hook 191 | * 192 | * @param type ValidatorInterface $validator 193 | * @param type $name The name of the command to hook 194 | * ('*' for all) 195 | */ 196 | public function addInteractor(InteractorInterface $interactor, $name = '*') 197 | { 198 | $this->hooks[$name][self::INTERACT][] = $interactor; 199 | return $this; 200 | } 201 | 202 | /** 203 | * Add a pre-validator hook 204 | * 205 | * @param type ValidatorInterface $validator 206 | * @param type $name The name of the command to hook 207 | * ('*' for all) 208 | */ 209 | public function addPreValidator(ValidatorInterface $validator, $name = '*') 210 | { 211 | $this->hooks[$name][self::PRE_ARGUMENT_VALIDATOR][] = $validator; 212 | return $this; 213 | } 214 | 215 | /** 216 | * Add a validator hook 217 | * 218 | * @param type ValidatorInterface $validator 219 | * @param type $name The name of the command to hook 220 | * ('*' for all) 221 | */ 222 | public function addValidator(ValidatorInterface $validator, $name = '*') 223 | { 224 | $this->hooks[$name][self::ARGUMENT_VALIDATOR][] = $validator; 225 | return $this; 226 | } 227 | 228 | /** 229 | * Add a pre-command hook. This is the same as a validator hook, except 230 | * that it will run after all of the post-validator hooks. 231 | * 232 | * @param type ValidatorInterface $preCommand 233 | * @param type $name The name of the command to hook 234 | * ('*' for all) 235 | */ 236 | public function addPreCommandHook(ValidatorInterface $preCommand, $name = '*') 237 | { 238 | $this->hooks[$name][self::PRE_COMMAND_HOOK][] = $preCommand; 239 | return $this; 240 | } 241 | 242 | /** 243 | * Add a post-command hook. This is the same as a pre-process hook, 244 | * except that it will run before the first pre-process hook. 245 | * 246 | * @param type ProcessResultInterface $postCommand 247 | * @param type $name The name of the command to hook 248 | * ('*' for all) 249 | */ 250 | public function addPostCommandHook(ProcessResultInterface $postCommand, $name = '*') 251 | { 252 | $this->hooks[$name][self::POST_COMMAND_HOOK][] = $postCommand; 253 | return $this; 254 | } 255 | 256 | /** 257 | * Add a result processor. 258 | * 259 | * @param type ProcessResultInterface $resultProcessor 260 | * @param type $name The name of the command to hook 261 | * ('*' for all) 262 | */ 263 | public function addResultProcessor(ProcessResultInterface $resultProcessor, $name = '*') 264 | { 265 | $this->hooks[$name][self::PROCESS_RESULT][] = $resultProcessor; 266 | return $this; 267 | } 268 | 269 | /** 270 | * Add a result alterer. After a result is processed 271 | * by a result processor, an alter hook may be used 272 | * to convert the result from one form to another. 273 | * 274 | * @param type AlterResultInterface $resultAlterer 275 | * @param type $name The name of the command to hook 276 | * ('*' for all) 277 | */ 278 | public function addAlterResult(AlterResultInterface $resultAlterer, $name = '*') 279 | { 280 | $this->hooks[$name][self::ALTER_RESULT][] = $resultAlterer; 281 | return $this; 282 | } 283 | 284 | /** 285 | * Add a status determiner. Usually, a command should return 286 | * an integer on error, or a result object on success (which 287 | * implies a status code of zero). If a result contains the 288 | * status code in some other field, then a status determiner 289 | * can be used to call the appropriate accessor method to 290 | * determine the status code. This is usually not necessary, 291 | * though; a command that fails may return a CommandError 292 | * object, which contains a status code and a result message 293 | * to display. 294 | * @see CommandError::getExitCode() 295 | * 296 | * @param type StatusDeterminerInterface $statusDeterminer 297 | * @param type $name The name of the command to hook 298 | * ('*' for all) 299 | */ 300 | public function addStatusDeterminer(StatusDeterminerInterface $statusDeterminer, $name = '*') 301 | { 302 | $this->hooks[$name][self::STATUS_DETERMINER][] = $statusDeterminer; 303 | return $this; 304 | } 305 | 306 | /** 307 | * Add an output extractor. If a command returns an object 308 | * object, by default it is passed directly to the output 309 | * formatter (if in use) for rendering. If the result object 310 | * contains more information than just the data to render, though, 311 | * then an output extractor can be used to call the appopriate 312 | * accessor method of the result object to get the data to 313 | * rendered. This is usually not necessary, though; it is preferable 314 | * to have complex result objects implement the OutputDataInterface. 315 | * @see OutputDataInterface::getOutputData() 316 | * 317 | * @param type ExtractOutputInterface $outputExtractor 318 | * @param type $name The name of the command to hook 319 | * ('*' for all) 320 | */ 321 | public function addOutputExtractor(ExtractOutputInterface $outputExtractor, $name = '*') 322 | { 323 | $this->hooks[$name][self::EXTRACT_OUTPUT][] = $outputExtractor; 324 | return $this; 325 | } 326 | 327 | public function getHookOptionsForCommand($command) 328 | { 329 | $names = $this->addWildcardHooksToNames($command->getNames(), $command->getAnnotationData()); 330 | return $this->getHookOptions($names); 331 | } 332 | 333 | /** 334 | * @return CommandInfo[] 335 | */ 336 | public function getHookOptions($names) 337 | { 338 | $result = []; 339 | foreach ($names as $name) { 340 | if (isset($this->hookOptions[$name])) { 341 | $result = array_merge($result, $this->hookOptions[$name]); 342 | } 343 | } 344 | return $result; 345 | } 346 | 347 | /** 348 | * Get a set of hooks with the provided name(s). Include the 349 | * pre- and post- hooks, and also include the global hooks ('*') 350 | * in addition to the named hooks provided. 351 | * 352 | * @param string|array $names The name of the function being hooked. 353 | * @param string[] $hooks A list of hooks (e.g. [HookManager::ALTER_RESULT]) 354 | * 355 | * @return callable[] 356 | */ 357 | public function getHooks($names, $hooks, $annotationData = null) 358 | { 359 | return $this->get($this->addWildcardHooksToNames($names, $annotationData), $hooks); 360 | } 361 | 362 | protected function addWildcardHooksToNames($names, $annotationData = null) 363 | { 364 | $names = array_merge( 365 | (array)$names, 366 | ($annotationData == null) ? [] : array_map(function ($item) { 367 | return "@$item"; 368 | }, $annotationData->keys()) 369 | ); 370 | $names[] = '*'; 371 | return array_unique($names); 372 | } 373 | 374 | /** 375 | * Get a set of hooks with the provided name(s). 376 | * 377 | * @param string|array $names The name of the function being hooked. 378 | * @param string[] $hooks The list of hook names (e.g. [HookManager::ALTER_RESULT]) 379 | * 380 | * @return callable[] 381 | */ 382 | public function get($names, $hooks) 383 | { 384 | $result = []; 385 | foreach ((array)$hooks as $hook) { 386 | foreach ((array)$names as $name) { 387 | $result = array_merge($result, $this->getHook($name, $hook)); 388 | } 389 | } 390 | return $result; 391 | } 392 | 393 | /** 394 | * Get a single named hook. 395 | * 396 | * @param string $name The name of the hooked method 397 | * @param string $hook The specific hook name (e.g. alter) 398 | * 399 | * @return callable[] 400 | */ 401 | public function getHook($name, $hook) 402 | { 403 | if (isset($this->hooks[$name][$hook])) { 404 | return $this->hooks[$name][$hook]; 405 | } 406 | return []; 407 | } 408 | 409 | /** 410 | * Call the command event hooks. 411 | * 412 | * TODO: This should be moved to CommandEventHookDispatcher, which 413 | * should become the class that implements EventSubscriberInterface. 414 | * This change would break all clients, though, so postpone until next 415 | * major release. 416 | * 417 | * @param ConsoleCommandEvent $event 418 | */ 419 | public function callCommandEventHooks(ConsoleCommandEvent $event) 420 | { 421 | /* @var Command $command */ 422 | $command = $event->getCommand(); 423 | $dispatcher = new CommandEventHookDispatcher($this, [$command->getName()]); 424 | $dispatcher->callCommandEventHooks($event); 425 | } 426 | 427 | /** 428 | * @{@inheritdoc} 429 | */ 430 | public static function getSubscribedEvents() 431 | { 432 | return [ConsoleEvents::COMMAND => 'callCommandEventHooks']; 433 | } 434 | } 435 | -------------------------------------------------------------------------------- /src/Hooks/InitializeHookInterface.php: -------------------------------------------------------------------------------- 1 | stdinHandler = $stdin; 18 | } 19 | 20 | /** 21 | * @inheritdoc 22 | */ 23 | public function stdin() 24 | { 25 | if (!$this->stdinHandler) { 26 | $this->stdinHandler = new StdinHandler(); 27 | } 28 | return $this->stdinHandler; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Input/StdinHandler.php: -------------------------------------------------------------------------------- 1 | stdin()->contents()); 25 | * } 26 | * } 27 | * 28 | * Command that reads from stdin or file via an option: 29 | * 30 | * /** 31 | * * @command cat 32 | * * @param string $file 33 | * * @default $file - 34 | * * / 35 | * public function cat(InputInterface $input) 36 | * { 37 | * $data = $this->stdin()->select($input, 'file')->contents(); 38 | * } 39 | * 40 | * Command that reads from stdin or file via an option: 41 | * 42 | * /** 43 | * * @command cat 44 | * * @option string $file 45 | * * @default $file - 46 | * * / 47 | * public function cat(InputInterface $input) 48 | * { 49 | * $data = $this->stdin()->select($input, 'file')->contents(); 50 | * } 51 | * 52 | * It is also possible to inject the selected stream into the input object, 53 | * e.g. if you want the contents of the source file to be fed to any Question 54 | * helper et. al. that the $input object is used with. 55 | * 56 | * /** 57 | * * @command example 58 | * * @option string $file 59 | * * @default $file - 60 | * * / 61 | * public function example(InputInterface $input) 62 | * { 63 | * $this->stdin()->setStream($input, 'file'); 64 | * } 65 | * 66 | * 67 | * Inject an alternate source for standard input in tests. Presumes that 68 | * the object under test gets a reference to the StdinHandler via dependency 69 | * injection from the container. 70 | * 71 | * $container->get('stdinHandler')->redirect($pathToTestStdinFileFixture); 72 | * 73 | * You may also inject your stdin file fixture stream into the $input object 74 | * as usual, and then use it with 'select()' or 'setStream()' as shown above. 75 | * 76 | * Finally, this class may also be used in absence of a dependency injection 77 | * container by using the static 'selectStream()' method: 78 | * 79 | * /** 80 | * * @command example 81 | * * @option string $file 82 | * * @default $file - 83 | * * / 84 | * public function example(InputInterface $input) 85 | * { 86 | * $data = StdinHandler::selectStream($input, 'file')->contents(); 87 | * } 88 | * 89 | * To test a method that uses this technique, simply inject your stdin 90 | * fixture into the $input object in your test: 91 | * 92 | * $input->setStream(fopen($pathToFixture, 'r')); 93 | */ 94 | class StdinHandler 95 | { 96 | protected $path; 97 | protected $stream; 98 | 99 | public static function selectStream(InputInterface $input, $optionOrArg) 100 | { 101 | $handler = new self(); 102 | 103 | return $handler->setStream($input, $optionOrArg); 104 | } 105 | 106 | /** 107 | * hasPath returns 'true' if the stdin handler has a path to a file. 108 | * 109 | * @return bool 110 | */ 111 | public function hasPath() 112 | { 113 | // Once the stream has been opened, we mask the existence of the path. 114 | return !$this->hasStream() && !empty($this->path); 115 | } 116 | 117 | /** 118 | * hasStream returns 'true' if the stdin handler has opened a stream. 119 | * 120 | * @return bool 121 | */ 122 | public function hasStream() 123 | { 124 | return !empty($this->stream); 125 | } 126 | 127 | /** 128 | * path returns the path to any file that was set as a redirection 129 | * source, or `php://stdin` if none have been. 130 | * 131 | * @return string 132 | */ 133 | public function path() 134 | { 135 | return $this->path ?: 'php://stdin'; 136 | } 137 | 138 | /** 139 | * close closes the input stream if it was opened. 140 | */ 141 | public function close() 142 | { 143 | if ($this->hasStream()) { 144 | fclose($this->stream); 145 | $this->stream = null; 146 | } 147 | return $this; 148 | } 149 | 150 | /** 151 | * redirect specifies a path to a file that should serve as the 152 | * source to read from. If the input path is '-' or empty, 153 | * then output will be taken from php://stdin (or whichever source 154 | * was provided via the 'redirect' method). 155 | * 156 | * @return $this 157 | */ 158 | public function redirect($path) 159 | { 160 | if ($this->pathProvided($path)) { 161 | $this->path = $path; 162 | } 163 | 164 | return $this; 165 | } 166 | 167 | /** 168 | * select chooses the source of the input stream based on whether or 169 | * not the user provided the specified option or argument on the commandline. 170 | * Stdin is selected if there is no user selection. 171 | * 172 | * @param InputInterface $input 173 | * @param string $optionOrArg 174 | * @return $this 175 | */ 176 | public function select(InputInterface $input, $optionOrArg) 177 | { 178 | $this->redirect($this->getOptionOrArg($input, $optionOrArg)); 179 | if (!$this->hasPath() && ($input instanceof StreamableInputInterface)) { 180 | $this->stream = $input->getStream(); 181 | } 182 | 183 | return $this; 184 | } 185 | 186 | /** 187 | * getStream opens and returns the stdin stream (or redirect file). 188 | */ 189 | public function getStream() 190 | { 191 | if (!$this->hasStream()) { 192 | $this->stream = fopen($this->path(), 'r'); 193 | } 194 | return $this->stream; 195 | } 196 | 197 | /** 198 | * setStream functions like 'select', and also sets up the $input 199 | * object to read from the selected input stream e.g. when used 200 | * with a question helper. 201 | */ 202 | public function setStream(InputInterface $input, $optionOrArg) 203 | { 204 | $this->select($input, $optionOrArg); 205 | if ($input instanceof StreamableInputInterface) { 206 | $stream = $this->getStream(); 207 | $input->setStream($stream); 208 | } 209 | return $this; 210 | } 211 | 212 | /** 213 | * contents reads the entire contents of the standard input stream. 214 | * 215 | * @return string 216 | */ 217 | public function contents() 218 | { 219 | // Optimization: use file_get_contents if we have a path to a file 220 | // and the stream has not been opened yet. 221 | if (!$this->hasStream()) { 222 | return file_get_contents($this->path()); 223 | } 224 | $stream = $this->getStream(); 225 | stream_set_blocking($stream, false); // TODO: We did this in backend invoke. Necessary here? 226 | $contents = stream_get_contents($stream); 227 | $this->close(); 228 | 229 | return $contents; 230 | } 231 | 232 | /** 233 | * Returns 'true' if a path was specfied, and that path was not '-'. 234 | */ 235 | protected function pathProvided($path) 236 | { 237 | return !empty($path) && ($path != '-'); 238 | } 239 | 240 | protected function getOptionOrArg(InputInterface $input, $optionOrArg) 241 | { 242 | if ($input->hasOption($optionOrArg)) { 243 | return $input->getOption($optionOrArg); 244 | } 245 | return $input->getArgument($optionOrArg); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/Options/AlterOptionsCommandEvent.php: -------------------------------------------------------------------------------- 1 | application = $application; 40 | } 41 | 42 | /** 43 | * @param ConsoleCommandEvent $event 44 | */ 45 | public function alterCommandOptions(ConsoleCommandEvent $event) 46 | { 47 | /* @var Command $command */ 48 | $command = $event->getCommand(); 49 | $input = $event->getInput(); 50 | if ($command->getName() == 'help') { 51 | // Symfony 3.x prepares $input for us; Symfony 2.x, on the other 52 | // hand, passes it in prior to binding with the command definition, 53 | // so we have to go to a little extra work. It may be inadvisable 54 | // to do these steps for commands other than 'help'. 55 | if (!$input->hasArgument('command_name')) { 56 | $command->ignoreValidationErrors(); 57 | $command->mergeApplicationDefinition(); 58 | $input->bind($command->getDefinition()); 59 | } 60 | 61 | // Symfony Console helpfully swaps 'command_name' and 'command' 62 | // depending on whether the user entered `help foo` or `--help foo`. 63 | // One of these is always `help`, and the other is the command we 64 | // are actually interested in. 65 | $nameOfCommandToDescribe = $event->getInput()->getArgument('command_name'); 66 | if ($nameOfCommandToDescribe == 'help') { 67 | $nameOfCommandToDescribe = $event->getInput()->getArgument('command'); 68 | } 69 | $commandToDescribe = $this->application->find($nameOfCommandToDescribe); 70 | $this->findAndAddHookOptions($commandToDescribe); 71 | } else { 72 | $this->findAndAddHookOptions($command); 73 | } 74 | } 75 | 76 | public function findAndAddHookOptions($command) 77 | { 78 | if (!$command instanceof AnnotatedCommand) { 79 | return; 80 | } 81 | $command->optionsHook(); 82 | } 83 | 84 | 85 | /** 86 | * @{@inheritdoc} 87 | */ 88 | public static function getSubscribedEvents() 89 | { 90 | return [ConsoleEvents::COMMAND => 'alterCommandOptions']; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Options/AutomaticOptionsProviderInterface.php: -------------------------------------------------------------------------------- 1 | defaultWidth = $defaultWidth; 30 | } 31 | 32 | public function setApplication(Application $application) 33 | { 34 | $this->application = $application; 35 | } 36 | 37 | public function setTerminal($terminal) 38 | { 39 | $this->terminal = $terminal; 40 | } 41 | 42 | public function getTerminal() 43 | { 44 | if (!$this->terminal && class_exists('\Symfony\Component\Console\Terminal')) { 45 | $this->terminal = new \Symfony\Component\Console\Terminal(); 46 | } 47 | return $this->terminal; 48 | } 49 | 50 | public function enableWrap($shouldWrap) 51 | { 52 | $this->shouldWrap = $shouldWrap; 53 | } 54 | 55 | public function prepare(CommandData $commandData, FormatterOptions $options) 56 | { 57 | $width = $this->getTerminalWidth(); 58 | if (!$width) { 59 | $width = $this->defaultWidth; 60 | } 61 | 62 | // Enforce minimum and maximum widths 63 | $width = min($width, $this->getMaxWidth($commandData)); 64 | $width = max($width, $this->getMinWidth($commandData)); 65 | 66 | $options->setWidth($width); 67 | } 68 | 69 | protected function getTerminalWidth() 70 | { 71 | // Don't wrap if wrapping has been disabled. 72 | if (!$this->shouldWrap) { 73 | return 0; 74 | } 75 | 76 | $terminal = $this->getTerminal(); 77 | if ($terminal) { 78 | return $terminal->getWidth(); 79 | } 80 | 81 | return $this->getTerminalWidthViaApplication(); 82 | } 83 | 84 | protected function getTerminalWidthViaApplication() 85 | { 86 | if (!$this->application) { 87 | return 0; 88 | } 89 | $dimensions = $this->application->getTerminalDimensions(); 90 | if ($dimensions[0] == null) { 91 | return 0; 92 | } 93 | 94 | return $dimensions[0]; 95 | } 96 | 97 | protected function getMaxWidth(CommandData $commandData) 98 | { 99 | return $this->maxWidth; 100 | } 101 | 102 | protected function getMinWidth(CommandData $commandData) 103 | { 104 | return $this->minWidth; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Output/OutputAwareInterface.php: -------------------------------------------------------------------------------- 1 | register('Symfony\Component\Console\Input\InputInterface', $this); 15 | $this->register('Symfony\Component\Console\Output\OutputInterface', $this); 16 | $this->register('Consolidation\AnnotatedCommand\AnnotationData', $this); 17 | $this->register('Consolidation\OutputFormatters\Options\FormatterOptions', $this); 18 | } 19 | 20 | public function register($interfaceName, ParameterInjector $injector) 21 | { 22 | $this->injectors[$interfaceName] = $injector; 23 | } 24 | 25 | public function args($commandData) 26 | { 27 | return array_merge( 28 | $commandData->injectedInstances(), 29 | $commandData->getArgsAndOptions() 30 | ); 31 | } 32 | 33 | public function injectIntoCommandData($commandData, $injectedClasses) 34 | { 35 | foreach ($injectedClasses as $injectedClass) { 36 | $injectedInstance = $this->getInstanceToInject($commandData, $injectedClass); 37 | $commandData->injectInstance($injectedInstance); 38 | } 39 | } 40 | 41 | protected function getInstanceToInject(CommandData $commandData, $interfaceName) 42 | { 43 | if (!isset($this->injectors[$interfaceName])) { 44 | return null; 45 | } 46 | 47 | return $this->injectors[$interfaceName]->get($commandData, $interfaceName); 48 | } 49 | 50 | public function get(CommandData $commandData, $interfaceName) 51 | { 52 | switch ($interfaceName) { 53 | case 'Symfony\Component\Console\Input\InputInterface': 54 | return $commandData->input(); 55 | case 'Symfony\Component\Console\Output\OutputInterface': 56 | return $commandData->output(); 57 | case 'Consolidation\AnnotatedCommand\AnnotationData': 58 | return $commandData->annotationData(); 59 | case 'Consolidation\OutputFormatters\Options\FormatterOptions': 60 | return $commandData->formatterOptions(); 61 | } 62 | 63 | return null; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/ParameterInjector.php: -------------------------------------------------------------------------------- 1 | 0) && 32 | ($cache['schema'] == CommandInfo::SERIALIZATION_SCHEMA_VERSION) && 33 | self::cachedMethodExists($cache); 34 | } 35 | 36 | public function constructFromCache(CommandInfo $commandInfo, $info_array) 37 | { 38 | $info_array += $this->defaultSerializationData(); 39 | 40 | $commandInfo 41 | ->setName($info_array['name']) 42 | ->replaceRawAnnotations($info_array['annotations']) 43 | ->setAliases($info_array['aliases']) 44 | ->setHelp($info_array['help']) 45 | ->setDescription($info_array['description']) 46 | ->replaceExampleUsages($info_array['example_usages']) 47 | ->setReturnType($info_array['return_type']) 48 | ->setInjectedClasses($info_array['injected_classes']) 49 | ; 50 | 51 | $this->constructDefaultsWithDescriptions($commandInfo->arguments(), (array)$info_array['arguments']); 52 | $this->constructDefaultsWithDescriptions($commandInfo->options(), (array)$info_array['options']); 53 | } 54 | 55 | protected function constructDefaultsWithDescriptions(DefaultsWithDescriptions $defaults, $data) 56 | { 57 | foreach ($data as $key => $info) { 58 | $info = (array)$info; 59 | $defaults->add($key, $info['description']); 60 | if (array_key_exists('default', $info)) { 61 | $defaults->setDefaultValue($key, $info['default']); 62 | } 63 | } 64 | } 65 | 66 | 67 | /** 68 | * Default data. Everything should be provided during serialization; 69 | * this is just as a fallback for unusual circumstances. 70 | * @return array 71 | */ 72 | protected function defaultSerializationData() 73 | { 74 | return [ 75 | 'name' => '', 76 | 'description' => '', 77 | 'help' => '', 78 | 'aliases' => [], 79 | 'annotations' => [], 80 | 'example_usages' => [], 81 | 'return_type' => [], 82 | 'parameters' => [], 83 | 'arguments' => [], 84 | 'options' => [], 85 | 'injected_classes' => [], 86 | 'mtime' => 0, 87 | ]; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Parser/CommandInfoSerializer.php: -------------------------------------------------------------------------------- 1 | getAnnotations(); 17 | $path = $allAnnotations['_path']; 18 | $className = $allAnnotations['_classname']; 19 | 20 | // Include the minimum information for command info (including placeholder records) 21 | $info = [ 22 | 'schema' => CommandInfo::SERIALIZATION_SCHEMA_VERSION, 23 | 'class' => $className, 24 | 'method_name' => $commandInfo->getMethodName(), 25 | 'mtime' => filemtime($path), 26 | 'injected_classes' => [], 27 | ]; 28 | 29 | // If this is a valid method / hook, then add more information. 30 | if ($commandInfo->valid()) { 31 | $info += [ 32 | 'name' => $commandInfo->getName(), 33 | 'description' => $commandInfo->getDescription(), 34 | 'help' => $commandInfo->getHelp(), 35 | 'aliases' => $commandInfo->getAliases(), 36 | 'annotations' => $commandInfo->getRawAnnotations()->getArrayCopy(), 37 | 'example_usages' => $commandInfo->getExampleUsages(), 38 | 'return_type' => $commandInfo->getReturnType(), 39 | ]; 40 | $info['arguments'] = $this->serializeDefaultsWithDescriptions($commandInfo->arguments()); 41 | $info['options'] = $this->serializeDefaultsWithDescriptions($commandInfo->options()); 42 | $info['injected_classes'] = $commandInfo->getInjectedClasses(); 43 | } 44 | 45 | return $info; 46 | } 47 | 48 | protected function serializeDefaultsWithDescriptions(DefaultsWithDescriptions $defaults) 49 | { 50 | $result = []; 51 | foreach ($defaults->getValues() as $key => $val) { 52 | $result[$key] = [ 53 | 'description' => $defaults->getDescription($key), 54 | ]; 55 | if ($defaults->hasDefault($key)) { 56 | $result[$key]['default'] = $val; 57 | } 58 | } 59 | return $result; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Parser/DefaultsWithDescriptions.php: -------------------------------------------------------------------------------- 1 | values = $values; 40 | $this->hasDefault = array_filter($this->values, function ($value) { 41 | return isset($value); 42 | }); 43 | $this->descriptions = []; 44 | $this->suggestedValues = []; 45 | $this->defaultDefault = $defaultDefault; 46 | } 47 | 48 | /** 49 | * Return just the key : default values mapping 50 | * 51 | * @return array 52 | */ 53 | public function getValues() 54 | { 55 | return $this->values; 56 | } 57 | 58 | /** 59 | * Return true if this set of options is empty 60 | * 61 | * @return 62 | */ 63 | public function isEmpty() 64 | { 65 | return empty($this->values); 66 | } 67 | 68 | /** 69 | * Check to see whether the speicifed key exists in the collection. 70 | * 71 | * @param string $key 72 | * @return boolean 73 | */ 74 | public function exists($key) 75 | { 76 | return array_key_exists($key, $this->values); 77 | } 78 | 79 | /** 80 | * Get the value of one entry. 81 | * 82 | * @param string $key The key of the item. 83 | * @return string 84 | */ 85 | public function get($key) 86 | { 87 | if (array_key_exists($key, $this->values)) { 88 | return $this->values[$key]; 89 | } 90 | return $this->defaultDefault; 91 | } 92 | 93 | /** 94 | * Remove a matching entry, if it exists. 95 | * 96 | * @param string $key The key of the value to remove 97 | * @return string The value of the removed item, or empty 98 | */ 99 | public function removeMatching($key) 100 | { 101 | $key = $this->approximatelyMatchingKey($key); 102 | if (!$key) { 103 | return ''; 104 | } 105 | $result = $this->values[$key]; 106 | unset($this->values[$key]); 107 | return $result; 108 | } 109 | 110 | public function approximatelyMatchingKey($key) 111 | { 112 | $key = $this->simplifyKey($key); 113 | foreach ($this->values as $k => $v) { 114 | if ($key === $this->simplifyKey($k)) { 115 | return $k; 116 | } 117 | } 118 | return ''; 119 | } 120 | 121 | protected function simplifyKey($key) 122 | { 123 | return strtolower(preg_replace('#[-_]#', '', $key)); 124 | } 125 | 126 | /** 127 | * Get the description of one entry. 128 | * 129 | * @param string $key The key of the item. 130 | * @return string 131 | */ 132 | public function getDescription($key) 133 | { 134 | if (array_key_exists($key, $this->descriptions)) { 135 | return $this->descriptions[$key]; 136 | } 137 | return ''; 138 | } 139 | 140 | /** 141 | * Get the suggested values for an item. 142 | * 143 | * @param string $key The key of the item. 144 | * @return array|\Closure 145 | */ 146 | public function getSuggestedValues($key) 147 | { 148 | if (array_key_exists($key, $this->suggestedValues)) { 149 | return $this->suggestedValues[$key]; 150 | } 151 | return []; 152 | } 153 | 154 | /** 155 | * Add another argument to this command. 156 | * 157 | * @param string $key Name of the argument. 158 | * @param string $description Help text for the argument. 159 | * @param mixed $defaultValue The default value for the argument. 160 | * @param array|\Closure $suggestions Possible values for the argument or option. 161 | */ 162 | public function add($key, $description = '', $defaultValue = null, $suggestedValues = []) 163 | { 164 | if (!$this->exists($key) || isset($defaultValue)) { 165 | $this->values[$key] = isset($defaultValue) ? $defaultValue : $this->defaultDefault; 166 | } 167 | unset($this->descriptions[$key]); 168 | if (!empty($description)) { 169 | $this->descriptions[$key] = $description; 170 | } 171 | unset($this->suggestedValues[$key]); 172 | if (!empty($suggestedValues)) { 173 | $this->suggestedValues[$key] = $suggestedValues; 174 | } 175 | } 176 | 177 | /** 178 | * Change the default value of an entry. 179 | * 180 | * @param string $key 181 | * @param mixed $defaultValue 182 | */ 183 | public function setDefaultValue($key, $defaultValue) 184 | { 185 | $this->values[$key] = $defaultValue; 186 | $this->hasDefault[$key] = true; 187 | return $this; 188 | } 189 | 190 | /** 191 | * Check to see if the named argument definitively has a default value. 192 | * 193 | * @param string $key 194 | * @return bool 195 | */ 196 | public function hasDefault($key) 197 | { 198 | return array_key_exists($key, $this->hasDefault); 199 | } 200 | 201 | /** 202 | * Remove an entry 203 | * 204 | * @param string $key The entry to remove 205 | */ 206 | public function clear($key) 207 | { 208 | unset($this->values[$key]); 209 | unset($this->descriptions[$key]); 210 | unset($this->suggestedValues[$key]); 211 | } 212 | 213 | /** 214 | * Rename an existing option to something else. 215 | */ 216 | public function rename($oldName, $newName) 217 | { 218 | $this->add($newName, $this->getDescription($oldName), $this->get($oldName)); 219 | $this->clear($oldName); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/Parser/Internal/AttributesDocBlockParser.php: -------------------------------------------------------------------------------- 1 | commandInfo = $commandInfo; 20 | $this->reflection = $reflection; 21 | // @todo Unused. Lets just remove from this class? 22 | $this->fqcnCache = $fqcnCache ?: new FullyQualifiedClassCache(); 23 | } 24 | 25 | /** 26 | * Call the handle method of each attribute, which alters the CommandInfo object. 27 | */ 28 | public function parse() 29 | { 30 | $attributes = $this->reflection->getAttributes(); 31 | foreach ($attributes as $attribute) { 32 | if (method_exists($attribute->getName(), 'handle')) { 33 | call_user_func([$attribute->getName(), 'handle'], $attribute, $this->commandInfo); 34 | } 35 | } 36 | 37 | // If 'CLI\Help' is not defined, then get the help from the docblock comment 38 | if (!$this->commandInfo->hasHelp()) { 39 | $doc = $this->reflection->getDocComment(); 40 | $doc = DocBlockUtils::stripLeadingCommentCharacters($doc); 41 | 42 | $lines = explode("\n", $doc); 43 | 44 | // Everything up to the first blank line goes in the description. 45 | $description = array_shift($lines); 46 | while (DocBlockUtils::nextLineIsNotEmpty($lines)) { 47 | $description .= ' ' . array_shift($lines); 48 | } 49 | 50 | // Everything else goes in the help, up to the first @annotation 51 | // (e.g. @param) 52 | $help = ''; 53 | foreach ($lines as $line) { 54 | if (preg_match('#^[ \t]*@#', $line)) { 55 | break; 56 | } 57 | $help .= $line . PHP_EOL; 58 | } 59 | 60 | $this->commandInfo->setDescription($description); 61 | $this->commandInfo->setHelp(trim($help)); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Parser/Internal/BespokeDocBlockParser.php: -------------------------------------------------------------------------------- 1 | 'processCommandTag', 24 | 'name' => 'processCommandTag', 25 | 'arg' => 'processArgumentTag', 26 | 'param' => 'processParamTag', 27 | 'return' => 'processReturnTag', 28 | 'option' => 'processOptionTag', 29 | 'default' => 'processDefaultTag', 30 | 'aliases' => 'processAliases', 31 | 'usage' => 'processUsageTag', 32 | 'description' => 'processAlternateDescriptionTag', 33 | 'desc' => 'processAlternateDescriptionTag', 34 | ]; 35 | 36 | public function __construct(CommandInfo $commandInfo, \ReflectionMethod $reflection, $fqcnCache = null) 37 | { 38 | $this->commandInfo = $commandInfo; 39 | $this->reflection = $reflection; 40 | $this->fqcnCache = $fqcnCache ?: new FullyQualifiedClassCache(); 41 | } 42 | 43 | /** 44 | * Parse the docBlock comment for this command, and set the 45 | * fields of this class with the data thereby obtained. 46 | */ 47 | public function parse() 48 | { 49 | $doc = $this->reflection->getDocComment(); 50 | $this->parseDocBlock($doc); 51 | } 52 | 53 | /** 54 | * Save any tag that we do not explicitly recognize in the 55 | * 'otherAnnotations' map. 56 | */ 57 | protected function processGenericTag($tag) 58 | { 59 | $this->commandInfo->addAnnotation($tag->getTag(), $tag->getContent()); 60 | } 61 | 62 | /** 63 | * Set the name of the command from a @command or @name annotation. 64 | */ 65 | protected function processCommandTag($tag) 66 | { 67 | if (!$tag->hasWordAndDescription($matches)) { 68 | throw new \Exception('Could not determine command name from tag ' . (string)$tag); 69 | } 70 | $commandName = $matches['word']; 71 | $this->commandInfo->setName($commandName); 72 | // We also store the name in the 'other annotations' so that is is 73 | // possible to determine if the method had a @command annotation. 74 | $this->commandInfo->addAnnotation($tag->getTag(), $commandName); 75 | } 76 | 77 | /** 78 | * The @description and @desc annotations may be used in 79 | * place of the synopsis (which we call 'description'). 80 | * This is discouraged. 81 | * 82 | * @deprecated 83 | */ 84 | protected function processAlternateDescriptionTag($tag) 85 | { 86 | $this->commandInfo->setDescription($tag->getContent()); 87 | } 88 | 89 | /** 90 | * Store the data from a @param annotation in our argument descriptions. 91 | */ 92 | protected function processParamTag($tag) 93 | { 94 | if ($tag->hasTypeVariableAndDescription($matches)) { 95 | if ($this->ignoredParamType($matches['type'])) { 96 | return; 97 | } 98 | } 99 | return $this->processArgumentTag($tag); 100 | } 101 | 102 | protected function ignoredParamType($paramType) 103 | { 104 | // TODO: We should really only allow a couple of types here, 105 | // e.g. 'string', 'array', 'bool'. Blacklist things we do not 106 | // want for now to avoid breaking commands with weird types. 107 | // Fix in the next major version. 108 | // 109 | // This works: 110 | // return !in_array($paramType, ['string', 'array', 'integer', 'bool']); 111 | return preg_match('#(InputInterface|OutputInterface)$#', $paramType); 112 | } 113 | 114 | /** 115 | * Store the data from a @arg annotation in our argument descriptions. 116 | */ 117 | protected function processArgumentTag($tag) 118 | { 119 | if (!$tag->hasVariable($matches)) { 120 | throw new \Exception('Could not determine argument name from tag ' . (string)$tag); 121 | } 122 | if ($matches['variable'] == $this->optionParamName()) { 123 | return; 124 | } 125 | $this->commandInfo->addArgumentDescription($matches['variable'], static::removeLineBreaks($matches['description'])); 126 | } 127 | 128 | /** 129 | * Store the data from an @option annotation in our option descriptions. 130 | */ 131 | protected function processOptionTag($tag) 132 | { 133 | if (!$tag->hasVariable($matches)) { 134 | throw new \Exception('Could not determine option name from tag ' . (string)$tag); 135 | } 136 | $this->commandInfo->addOptionDescription($matches['variable'], static::removeLineBreaks($matches['description'])); 137 | } 138 | 139 | // @deprecated No longer called, only here for backwards compatibility (no clients should use "internal" classes anyway) 140 | protected function addOptionOrArgumentTag($tag, DefaultsWithDescriptions $set, $name, $description) 141 | { 142 | $variableName = $this->commandInfo->findMatchingOption($name); 143 | $description = static::removeLineBreaks($description); 144 | list($description, $defaultValue) = $this->splitOutDefault($description); 145 | $set->add($variableName, $description); 146 | if ($defaultValue !== null) { 147 | $set->setDefaultValue($variableName, $defaultValue); 148 | } 149 | } 150 | 151 | // @deprecated No longer called, only here for backwards compatibility (no clients should use "internal" classes anyway) 152 | protected function splitOutDefault($description) 153 | { 154 | if (!preg_match('#(.*)(Default: *)(.*)#', trim($description), $matches)) { 155 | return [$description, null]; 156 | } 157 | 158 | return [trim($matches[1]), $this->interpretDefaultValue(trim($matches[3]))]; 159 | } 160 | 161 | /** 162 | * Store the data from a @default annotation in our argument or option store, 163 | * as appropriate. 164 | */ 165 | protected function processDefaultTag($tag) 166 | { 167 | if (!$tag->hasVariable($matches)) { 168 | throw new \Exception('Could not determine parameter name for default value from tag ' . (string)$tag); 169 | } 170 | $variableName = $matches['variable']; 171 | $defaultValue = DefaultValueFromString::fromString($matches['description'])->value(); 172 | if ($this->commandInfo->arguments()->exists($variableName)) { 173 | $this->commandInfo->arguments()->setDefaultValue($variableName, $defaultValue); 174 | return; 175 | } 176 | $variableName = $this->commandInfo->findMatchingOption($variableName); 177 | if ($this->commandInfo->options()->exists($variableName)) { 178 | $this->commandInfo->options()->setDefaultValue($variableName, $defaultValue); 179 | } 180 | } 181 | 182 | /** 183 | * Store the data from a @usage annotation in our example usage list. 184 | */ 185 | protected function processUsageTag($tag) 186 | { 187 | $lines = explode("\n", $tag->getContent()); 188 | $usage = trim(array_shift($lines)); 189 | $description = static::removeLineBreaks(implode("\n", array_map(function ($line) { 190 | return trim($line); 191 | }, $lines))); 192 | 193 | $this->commandInfo->setExampleUsage($usage, $description); 194 | } 195 | 196 | /** 197 | * Process the comma-separated list of aliases 198 | */ 199 | protected function processAliases($tag) 200 | { 201 | $this->commandInfo->setAliases((string)$tag->getContent()); 202 | } 203 | 204 | /** 205 | * Store the data from a @return annotation in our argument descriptions. 206 | */ 207 | protected function processReturnTag($tag) 208 | { 209 | // The return type might be a variable -- '$this'. It will 210 | // usually be a type, like RowsOfFields, or \Namespace\RowsOfFields. 211 | if (!$tag->hasVariableAndDescription($matches)) { 212 | throw new \Exception('Could not determine return type from tag ' . (string)$tag); 213 | } 214 | // Look at namespace and `use` statments to make returnType a fqdn 215 | $returnType = $matches['variable']; 216 | $returnType = $this->findFullyQualifiedClass($returnType); 217 | $this->commandInfo->setReturnType($returnType); 218 | } 219 | 220 | protected function findFullyQualifiedClass($className) 221 | { 222 | if (strpos($className, '\\') !== false) { 223 | return $className; 224 | } 225 | 226 | return $this->fqcnCache->qualify($this->reflection->getFileName(), $className); 227 | } 228 | 229 | 230 | private function parseDocBlock($doc) 231 | { 232 | // Remove the leading /** and the trailing */ 233 | $doc = DocBlockUtils::stripLeadingCommentCharacters($doc); 234 | 235 | // Nothing left? Exit. 236 | if (empty($doc)) { 237 | return; 238 | } 239 | 240 | $tagFactory = new TagFactory(); 241 | $lines = []; 242 | 243 | foreach (explode("\n", $doc) as $row) { 244 | // Remove trailing whitespace and leading space + '*'s 245 | $row = rtrim($row); 246 | 247 | if (!$tagFactory->parseLine($row)) { 248 | $lines[] = $row; 249 | } 250 | } 251 | 252 | $this->processDescriptionAndHelp($lines); 253 | $this->processAllTags($tagFactory->getTags()); 254 | } 255 | 256 | protected function processDescriptionAndHelp($lines) 257 | { 258 | // Trim all of the lines individually. 259 | $lines = 260 | array_map( 261 | function ($line) { 262 | return trim($line); 263 | }, 264 | $lines 265 | ); 266 | 267 | // Everything up to the first blank line goes in the description. 268 | $description = array_shift($lines); 269 | while (static::nextLineIsNotEmpty($lines)) { 270 | $description .= ' ' . array_shift($lines); 271 | } 272 | 273 | // Everything else goes in the help. 274 | $help = trim(implode("\n", $lines)); 275 | 276 | $this->commandInfo->setDescription($description); 277 | $this->commandInfo->setHelp($help); 278 | } 279 | 280 | protected function nextLineIsNotEmpty($lines) 281 | { 282 | return DocBlockUtils::nextLineIsNotEmpty($lines); 283 | } 284 | 285 | protected function processAllTags($tags) 286 | { 287 | // Iterate over all of the tags, and process them as necessary. 288 | foreach ($tags as $tag) { 289 | $processFn = [$this, 'processGenericTag']; 290 | if (array_key_exists($tag->getTag(), $this->tagProcessors)) { 291 | $processFn = [$this, $this->tagProcessors[$tag->getTag()]]; 292 | } 293 | $processFn($tag); 294 | } 295 | } 296 | 297 | protected function lastParameterName() 298 | { 299 | $params = $this->commandInfo->getParameters(); 300 | $param = end($params); 301 | if (!$param) { 302 | return ''; 303 | } 304 | return $param->name; 305 | } 306 | 307 | /** 308 | * Return the name of the last parameter if it holds the options. 309 | */ 310 | public function optionParamName() 311 | { 312 | // Remember the name of the last parameter, if it holds the options. 313 | // We will use this information to ignore @param annotations for the options. 314 | if (!isset($this->optionParamName)) { 315 | $this->optionParamName = ''; 316 | $options = $this->commandInfo->options(); 317 | if (!$options->isEmpty()) { 318 | $this->optionParamName = $this->lastParameterName(); 319 | } 320 | } 321 | 322 | return $this->optionParamName; 323 | } 324 | 325 | // @deprecated No longer called, only here for backwards compatibility (no clients should use "internal" classes anyway) 326 | protected function interpretDefaultValue($defaultValue) 327 | { 328 | $defaults = [ 329 | 'null' => null, 330 | 'true' => true, 331 | 'false' => false, 332 | "''" => '', 333 | '[]' => [], 334 | ]; 335 | foreach ($defaults as $defaultName => $defaultTypedValue) { 336 | if ($defaultValue == $defaultName) { 337 | return $defaultTypedValue; 338 | } 339 | } 340 | return $defaultValue; 341 | } 342 | 343 | /** 344 | * Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c', 345 | * convert the data into the last of these forms. 346 | */ 347 | protected static function convertListToCommaSeparated($text) 348 | { 349 | return preg_replace('#[ \t\n\r,]+#', ',', $text); 350 | } 351 | 352 | /** 353 | * Take a multiline description and convert it into a single 354 | * long unbroken line. 355 | */ 356 | protected static function removeLineBreaks($text) 357 | { 358 | return trim(preg_replace('#[ \t\n\r]+#', ' ', $text)); 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /src/Parser/Internal/CommandDocBlockParserFactory.php: -------------------------------------------------------------------------------- 1 | parse(); 15 | } 16 | 17 | private static function create(CommandInfo $commandInfo, \ReflectionMethod $reflection) 18 | { 19 | if (in_array('getAttributes', get_class_methods($reflection))) { 20 | $attributes = $reflection->getAttributes(); 21 | } 22 | if (empty($attributes)) { 23 | return new BespokeDocBlockParser($commandInfo, $reflection); 24 | } else { 25 | return new AttributesDocBlockParser($commandInfo, $reflection); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Parser/Internal/CsvUtils.php: -------------------------------------------------------------------------------- 1 | value = $value; 14 | } 15 | 16 | public static function fromString($defaultValue) 17 | { 18 | $defaults = [ 19 | 'null' => null, 20 | 'true' => true, 21 | 'false' => false, 22 | "''" => '', 23 | '[]' => [], 24 | ]; 25 | if (array_key_exists($defaultValue, $defaults)) { 26 | $defaultValue = $defaults[$defaultValue]; 27 | } 28 | return new self($defaultValue); 29 | } 30 | 31 | public function value() 32 | { 33 | return $this->value; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Parser/Internal/DocBlockUtils.php: -------------------------------------------------------------------------------- 1 | [^\s$]+)[\s]*'; 22 | const VARIABLE_REGEX = '\\$(?P[^\s$]+)[\s]*'; 23 | const VARIABLE_OR_WORD_REGEX = '\\$?(?P[^\s$]+)[\s]*'; 24 | const TYPE_REGEX = '(?P[^\s$]+)[\s]*'; 25 | const WORD_REGEX = '(?P[^\s$]+)[\s]*'; 26 | const DESCRIPTION_REGEX = '(?P.*)'; 27 | const IS_TAG_REGEX = '/^[*\s]*@/'; 28 | 29 | /** 30 | * Check if the provided string begins with a tag 31 | * @param string $subject 32 | * @return bool 33 | */ 34 | public static function isTag($subject) 35 | { 36 | return preg_match(self::IS_TAG_REGEX, $subject); 37 | } 38 | 39 | /** 40 | * Use a regular expression to separate the tag from the content. 41 | * 42 | * @param string $subject 43 | * @param string[] &$matches Sets $matches['tag'] and $matches['description'] 44 | * @return bool 45 | */ 46 | public static function splitTagAndContent($subject, &$matches) 47 | { 48 | $regex = '/' . self::TAG_REGEX . self::DESCRIPTION_REGEX . '/s'; 49 | return preg_match($regex, $subject, $matches); 50 | } 51 | 52 | /** 53 | * DockblockTag constructor 54 | */ 55 | public function __construct($tag, $content = null) 56 | { 57 | $this->tag = $tag; 58 | $this->content = $content; 59 | } 60 | 61 | /** 62 | * Add more content onto a tag during parsing. 63 | */ 64 | public function appendContent($line) 65 | { 66 | $this->content .= "\n$line"; 67 | } 68 | 69 | /** 70 | * Return the tag - e.g. "@foo description" returns 'foo' 71 | * 72 | * @return string 73 | */ 74 | public function getTag() 75 | { 76 | return $this->tag; 77 | } 78 | 79 | /** 80 | * Return the content portion of the tag - e.g. "@foo bar baz boz" returns 81 | * "bar baz boz" 82 | * 83 | * @return string 84 | */ 85 | public function getContent() 86 | { 87 | return $this->content; 88 | } 89 | 90 | /** 91 | * Convert tag back into a string. 92 | */ 93 | public function __toString() 94 | { 95 | return '@' . $this->getTag() . ' ' . $this->getContent(); 96 | } 97 | 98 | /** 99 | * Determine if tag is one of: 100 | * - "@tag variable description" 101 | * - "@tag $variable description" 102 | * - "@tag type $variable description" 103 | * 104 | * @param string $subject 105 | * @param string[] &$matches Sets $matches['variable'] and 106 | * $matches['description']; might set $matches['type']. 107 | * @return bool 108 | */ 109 | public function hasVariable(&$matches) 110 | { 111 | return 112 | $this->hasTypeVariableAndDescription($matches) || 113 | $this->hasVariableAndDescription($matches); 114 | } 115 | 116 | /** 117 | * Determine if tag is "@tag $variable description" 118 | * @param string $subject 119 | * @param string[] &$matches Sets $matches['variable'] and 120 | * $matches['description'] 121 | * @return bool 122 | */ 123 | public function hasVariableAndDescription(&$matches) 124 | { 125 | $regex = '/^\s*' . self::VARIABLE_OR_WORD_REGEX . self::DESCRIPTION_REGEX . '/s'; 126 | return preg_match($regex, $this->getContent(), $matches); 127 | } 128 | 129 | /** 130 | * Determine if tag is "@tag type $variable description" 131 | * 132 | * @param string $subject 133 | * @param string[] &$matches Sets $matches['variable'], 134 | * $matches['description'] and $matches['type']. 135 | * @return bool 136 | */ 137 | public function hasTypeVariableAndDescription(&$matches) 138 | { 139 | $regex = '/^\s*' . self::TYPE_REGEX . self::VARIABLE_REGEX . self::DESCRIPTION_REGEX . '/s'; 140 | return preg_match($regex, $this->getContent(), $matches); 141 | } 142 | 143 | /** 144 | * Determine if tag is "@tag word description" 145 | * @param string $subject 146 | * @param string[] &$matches Sets $matches['word'] and 147 | * $matches['description'] 148 | * @return bool 149 | */ 150 | public function hasWordAndDescription(&$matches) 151 | { 152 | $regex = '/^\s*' . self::WORD_REGEX . self::DESCRIPTION_REGEX . '/s'; 153 | return preg_match($regex, $this->getContent(), $matches); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Parser/Internal/FullyQualifiedClassCache.php: -------------------------------------------------------------------------------- 1 | primeCache($filename, $className); 12 | return $this->cached($filename, $className); 13 | } 14 | 15 | protected function cached($filename, $className) 16 | { 17 | return isset($this->classCache[$filename][$className]) ? $this->classCache[$filename][$className] : $className; 18 | } 19 | 20 | protected function primeCache($filename, $className) 21 | { 22 | // If the cache has already been primed, do no further work 23 | if (isset($this->namespaceCache[$filename])) { 24 | return false; 25 | } 26 | 27 | $handle = fopen($filename, "r"); 28 | if (!$handle) { 29 | return false; 30 | } 31 | 32 | $namespaceName = $this->primeNamespaceCache($filename, $handle); 33 | $this->primeUseCache($filename, $handle); 34 | 35 | // If there is no 'use' statement for the className, then 36 | // generate an effective classname from the namespace 37 | if (!isset($this->classCache[$filename][$className])) { 38 | $this->classCache[$filename][$className] = $namespaceName . '\\' . $className; 39 | } 40 | 41 | fclose($handle); 42 | } 43 | 44 | protected function primeNamespaceCache($filename, $handle) 45 | { 46 | $namespaceName = $this->readNamespace($handle); 47 | if (!$namespaceName) { 48 | return false; 49 | } 50 | $this->namespaceCache[$filename] = $namespaceName; 51 | return $namespaceName; 52 | } 53 | 54 | protected function primeUseCache($filename, $handle) 55 | { 56 | $usedClasses = $this->readUseStatements($handle); 57 | if (empty($usedClasses)) { 58 | return false; 59 | } 60 | $this->classCache[$filename] = $usedClasses; 61 | } 62 | 63 | protected function readNamespace($handle) 64 | { 65 | $namespaceRegex = '#^\s*namespace\s+#'; 66 | $line = $this->readNextRelevantLine($handle); 67 | if (!$line || !preg_match($namespaceRegex, $line)) { 68 | return false; 69 | } 70 | 71 | $namespaceName = preg_replace($namespaceRegex, '', $line); 72 | $namespaceName = rtrim($namespaceName, ';'); 73 | return $namespaceName; 74 | } 75 | 76 | protected function readUseStatements($handle) 77 | { 78 | $useRegex = '#^\s*use\s+#'; 79 | $result = []; 80 | while (true) { 81 | $line = $this->readNextRelevantLine($handle); 82 | if (!$line || !preg_match($useRegex, $line)) { 83 | return $result; 84 | } 85 | $usedClass = preg_replace($useRegex, '', $line); 86 | $usedClass = rtrim($usedClass, ';'); 87 | $unqualifiedClass = preg_replace('#.*\\\\#', '', $usedClass); 88 | // If this is an aliased class, 'use \Foo\Bar as Baz', then adjust 89 | if (strpos($usedClass, ' as ')) { 90 | $unqualifiedClass = preg_replace('#.*\sas\s+#', '', $usedClass); 91 | $usedClass = preg_replace('#[a-zA-Z0-9]+\s+as\s+#', '', $usedClass); 92 | } 93 | $result[$unqualifiedClass] = $usedClass; 94 | } 95 | } 96 | 97 | protected function readNextRelevantLine($handle) 98 | { 99 | while (($line = fgets($handle)) !== false) { 100 | if (preg_match('#^\s*\w#', $line)) { 101 | return trim($line); 102 | } 103 | } 104 | return false; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Parser/Internal/TagFactory.php: -------------------------------------------------------------------------------- 1 | current = null; 21 | $this->tags = []; 22 | } 23 | 24 | public function parseLine($line) 25 | { 26 | if (DocblockTag::isTag($line)) { 27 | return $this->createTag($line); 28 | } 29 | if (empty($line)) { 30 | return $this->storeCurrentTag(); 31 | } 32 | return $this->accumulateContent($line); 33 | } 34 | 35 | public function getTags() 36 | { 37 | $this->storeCurrentTag(); 38 | return $this->tags; 39 | } 40 | 41 | protected function createTag($line) 42 | { 43 | DocblockTag::splitTagAndContent($line, $matches); 44 | $this->storeCurrentTag(); 45 | $this->current = new DocblockTag($matches['tag'], $matches['description']); 46 | return true; 47 | } 48 | 49 | protected function storeCurrentTag() 50 | { 51 | if (!$this->current) { 52 | return false; 53 | } 54 | $this->tags[] = $this->current; 55 | $this->current = false; 56 | return true; 57 | } 58 | 59 | protected function accumulateContent($line) 60 | { 61 | if (!$this->current) { 62 | return false; 63 | } 64 | $this->current->appendContent($line); 65 | return true; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/ResultWriter.php: -------------------------------------------------------------------------------- 1 | formatterManager = $formatterManager; 36 | return $this; 37 | } 38 | 39 | /** 40 | * Return the formatter manager 41 | * @return FormatterManager 42 | */ 43 | public function formatterManager() 44 | { 45 | return $this->formatterManager; 46 | } 47 | 48 | public function setDisplayErrorFunction(callable $fn) 49 | { 50 | $this->displayErrorFunction = $fn; 51 | return $this; 52 | } 53 | 54 | /** 55 | * Handle the result output and status code calculation. 56 | */ 57 | public function handle(OutputInterface $output, $result, CommandData $commandData, $statusCodeDispatcher = null, $extractDispatcher = null) 58 | { 59 | // A little messy, for backwards compatibility: if the result implements 60 | // ExitCodeInterface, then use that as the exit code. If a status code 61 | // dispatcher returns a non-zero result, then we will never print a 62 | // result. 63 | $status = null; 64 | if ($result instanceof ExitCodeInterface) { 65 | $status = $result->getExitCode(); 66 | } elseif (isset($statusCodeDispatcher)) { 67 | $status = $statusCodeDispatcher->determineStatusCode($result); 68 | if (isset($status) && ($status != 0)) { 69 | return $status; 70 | } 71 | } 72 | // If the result is an integer and no separate status code was provided, then use the result as the status and do no output. 73 | if (is_integer($result) && !isset($status)) { 74 | return $result; 75 | } 76 | $status = $this->interpretStatusCode($status); 77 | 78 | // Get the structured output, the output stream and the formatter 79 | $structuredOutput = $result; 80 | if (isset($extractDispatcher)) { 81 | $structuredOutput = $extractDispatcher->extractOutput($result); 82 | } 83 | if (($status != 0) && is_string($structuredOutput)) { 84 | $output = $this->chooseOutputStream($output, $status); 85 | return $this->writeErrorMessage($output, $status, $structuredOutput, $result); 86 | } 87 | if ($this->dataCanBeFormatted($structuredOutput) && isset($this->formatterManager)) { 88 | return $this->writeUsingFormatter($output, $structuredOutput, $commandData, $status); 89 | } 90 | return $this->writeCommandOutput($output, $structuredOutput, $status); 91 | } 92 | 93 | protected function dataCanBeFormatted($structuredOutput) 94 | { 95 | if (!isset($this->formatterManager)) { 96 | return false; 97 | } 98 | return 99 | is_object($structuredOutput) || 100 | is_array($structuredOutput); 101 | } 102 | 103 | /** 104 | * Determine the formatter that should be used to render 105 | * output. 106 | * 107 | * If the user specified a format via the --format option, 108 | * then always return that. Otherwise, return the default 109 | * format, unless --pipe was specified, in which case 110 | * return the default pipe format, format-pipe. 111 | * 112 | * n.b. --pipe is a handy option introduced in Drush 2 113 | * (or perhaps even Drush 1) that indicates that the command 114 | * should select the output format that is most appropriate 115 | * for use in scripts (e.g. to pipe to another command). 116 | * 117 | * @return string 118 | */ 119 | protected function getFormat(FormatterOptions $options) 120 | { 121 | // In Symfony Console, there is no way for us to differentiate 122 | // between the user specifying '--format=table', and the user 123 | // not specifying --format when the default value is 'table'. 124 | // Therefore, we must make --field always override --format; it 125 | // cannot become the default value for --format. 126 | if ($options->get('field')) { 127 | return 'string'; 128 | } 129 | $defaults = []; 130 | if ($options->get('pipe')) { 131 | return $options->get('pipe-format', [], 'tsv'); 132 | } 133 | return $options->getFormat($defaults); 134 | } 135 | 136 | /** 137 | * Determine whether we should use stdout or stderr. 138 | */ 139 | protected function chooseOutputStream(OutputInterface $output, $status) 140 | { 141 | // If the status code indicates an error, then print the 142 | // result to stderr rather than stdout 143 | if ($status && ($output instanceof ConsoleOutputInterface)) { 144 | return $output->getErrorOutput(); 145 | } 146 | return $output; 147 | } 148 | 149 | /** 150 | * Call the formatter to output the provided data. 151 | */ 152 | protected function writeUsingFormatter(OutputInterface $output, $structuredOutput, CommandData $commandData, $status = 0) 153 | { 154 | $formatterOptions = $commandData->formatterOptions(); 155 | $format = $this->getFormat($formatterOptions); 156 | $this->formatterManager->write( 157 | $output, 158 | $format, 159 | $structuredOutput, 160 | $formatterOptions 161 | ); 162 | return $status; 163 | } 164 | 165 | /** 166 | * Description 167 | * @param OutputInterface $output 168 | * @param int $status 169 | * @param string $structuredOutput 170 | * @param mixed $originalResult 171 | * @return type 172 | */ 173 | protected function writeErrorMessage($output, $status, $structuredOutput, $originalResult) 174 | { 175 | if (isset($this->displayErrorFunction)) { 176 | call_user_func($this->displayErrorFunction, $output, $structuredOutput, $status, $originalResult); 177 | } else { 178 | $this->writeCommandOutput($output, $structuredOutput); 179 | } 180 | return $status; 181 | } 182 | 183 | /** 184 | * If the result object is a string, then print it. 185 | */ 186 | protected function writeCommandOutput( 187 | OutputInterface $output, 188 | $structuredOutput, 189 | $status = 0 190 | ) { 191 | // If there is no formatter, we will print strings, 192 | // but can do no more than that. 193 | if (is_string($structuredOutput)) { 194 | $output->writeln($structuredOutput); 195 | } 196 | return $status; 197 | } 198 | 199 | /** 200 | * If a status code was set, then return it; otherwise, 201 | * presume success. 202 | */ 203 | protected function interpretStatusCode($status) 204 | { 205 | if (isset($status)) { 206 | return $status; 207 | } 208 | return 0; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/State.php: -------------------------------------------------------------------------------- 1 | currentState(); 44 | 45 | if ($target instanceof InputAwareInterface) { 46 | $target->setInput($input); 47 | } 48 | if (isset($output) && $target instanceof OutputAwareInterface) { 49 | $target->setOutput($output); 50 | } 51 | 52 | return $state; 53 | } 54 | 55 | /** 56 | * If the command callback is a method of an object, return the object. 57 | * 58 | * @param Callable|object $callback 59 | * @return object|bool 60 | */ 61 | protected static function recoverCallbackObject($callback) 62 | { 63 | if (is_object($callback)) { 64 | return $callback; 65 | } 66 | 67 | if (!is_array($callback)) { 68 | return false; 69 | } 70 | 71 | if (!is_object($callback[0])) { 72 | return false; 73 | } 74 | 75 | return $callback[0]; 76 | } 77 | } 78 | --------------------------------------------------------------------------------