├── .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 |
--------------------------------------------------------------------------------