Mercurial > hg > isophonics-drupal-site
comparison vendor/consolidation/annotated-command/src/AnnotatedCommandFactory.php @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | 129ea1e6d783 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4c8ae668cc8c |
---|---|
1 <?php | |
2 namespace Consolidation\AnnotatedCommand; | |
3 | |
4 use Consolidation\AnnotatedCommand\Cache\CacheWrapper; | |
5 use Consolidation\AnnotatedCommand\Cache\NullCache; | |
6 use Consolidation\AnnotatedCommand\Cache\SimpleCacheInterface; | |
7 use Consolidation\AnnotatedCommand\Hooks\HookManager; | |
8 use Consolidation\AnnotatedCommand\Options\AutomaticOptionsProviderInterface; | |
9 use Consolidation\AnnotatedCommand\Parser\CommandInfo; | |
10 use Consolidation\AnnotatedCommand\Parser\CommandInfoDeserializer; | |
11 use Consolidation\AnnotatedCommand\Parser\CommandInfoSerializer; | |
12 use Consolidation\OutputFormatters\Options\FormatterOptions; | |
13 use Symfony\Component\Console\Command\Command; | |
14 use Symfony\Component\Console\Input\InputInterface; | |
15 use Symfony\Component\Console\Output\OutputInterface; | |
16 | |
17 /** | |
18 * The AnnotatedCommandFactory creates commands for your application. | |
19 * Use with a Dependency Injection Container and the CommandFactory. | |
20 * Alternately, use the CommandFileDiscovery to find commandfiles, and | |
21 * then use AnnotatedCommandFactory::createCommandsFromClass() to create | |
22 * commands. See the README for more information. | |
23 * | |
24 * @package Consolidation\AnnotatedCommand | |
25 */ | |
26 class AnnotatedCommandFactory implements AutomaticOptionsProviderInterface | |
27 { | |
28 /** var CommandProcessor */ | |
29 protected $commandProcessor; | |
30 | |
31 /** var CommandCreationListenerInterface[] */ | |
32 protected $listeners = []; | |
33 | |
34 /** var AutomaticOptionsProvider[] */ | |
35 protected $automaticOptionsProviderList = []; | |
36 | |
37 /** var boolean */ | |
38 protected $includeAllPublicMethods = true; | |
39 | |
40 /** var CommandInfoAltererInterface */ | |
41 protected $commandInfoAlterers = []; | |
42 | |
43 /** var SimpleCacheInterface */ | |
44 protected $dataStore; | |
45 | |
46 public function __construct() | |
47 { | |
48 $this->dataStore = new NullCache(); | |
49 $this->commandProcessor = new CommandProcessor(new HookManager()); | |
50 $this->addAutomaticOptionProvider($this); | |
51 } | |
52 | |
53 public function setCommandProcessor(CommandProcessor $commandProcessor) | |
54 { | |
55 $this->commandProcessor = $commandProcessor; | |
56 return $this; | |
57 } | |
58 | |
59 /** | |
60 * @return CommandProcessor | |
61 */ | |
62 public function commandProcessor() | |
63 { | |
64 return $this->commandProcessor; | |
65 } | |
66 | |
67 /** | |
68 * Set the 'include all public methods flag'. If true (the default), then | |
69 * every public method of each commandFile will be used to create commands. | |
70 * If it is false, then only those public methods annotated with @command | |
71 * or @name (deprecated) will be used to create commands. | |
72 */ | |
73 public function setIncludeAllPublicMethods($includeAllPublicMethods) | |
74 { | |
75 $this->includeAllPublicMethods = $includeAllPublicMethods; | |
76 return $this; | |
77 } | |
78 | |
79 public function getIncludeAllPublicMethods() | |
80 { | |
81 return $this->includeAllPublicMethods; | |
82 } | |
83 | |
84 /** | |
85 * @return HookManager | |
86 */ | |
87 public function hookManager() | |
88 { | |
89 return $this->commandProcessor()->hookManager(); | |
90 } | |
91 | |
92 /** | |
93 * Add a listener that is notified immediately before the command | |
94 * factory creates commands from a commandFile instance. This | |
95 * listener can use this opportunity to do more setup for the commandFile, | |
96 * and so on. | |
97 * | |
98 * @param CommandCreationListenerInterface $listener | |
99 */ | |
100 public function addListener(CommandCreationListenerInterface $listener) | |
101 { | |
102 $this->listeners[] = $listener; | |
103 return $this; | |
104 } | |
105 | |
106 /** | |
107 * Add a listener that's just a simple 'callable'. | |
108 * @param callable $listener | |
109 */ | |
110 public function addListernerCallback(callable $listener) | |
111 { | |
112 $this->addListener(new CommandCreationListener($listener)); | |
113 return $this; | |
114 } | |
115 | |
116 /** | |
117 * Call all command creation listeners | |
118 * | |
119 * @param object $commandFileInstance | |
120 */ | |
121 protected function notify($commandFileInstance) | |
122 { | |
123 foreach ($this->listeners as $listener) { | |
124 $listener->notifyCommandFileAdded($commandFileInstance); | |
125 } | |
126 } | |
127 | |
128 public function addAutomaticOptionProvider(AutomaticOptionsProviderInterface $optionsProvider) | |
129 { | |
130 $this->automaticOptionsProviderList[] = $optionsProvider; | |
131 } | |
132 | |
133 public function addCommandInfoAlterer(CommandInfoAltererInterface $alterer) | |
134 { | |
135 $this->commandInfoAlterers[] = $alterer; | |
136 } | |
137 | |
138 /** | |
139 * n.b. This registers all hooks from the commandfile instance as a side-effect. | |
140 */ | |
141 public function createCommandsFromClass($commandFileInstance, $includeAllPublicMethods = null) | |
142 { | |
143 // Deprecated: avoid using the $includeAllPublicMethods in favor of the setIncludeAllPublicMethods() accessor. | |
144 if (!isset($includeAllPublicMethods)) { | |
145 $includeAllPublicMethods = $this->getIncludeAllPublicMethods(); | |
146 } | |
147 $this->notify($commandFileInstance); | |
148 $commandInfoList = $this->getCommandInfoListFromClass($commandFileInstance); | |
149 $this->registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance); | |
150 return $this->createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods); | |
151 } | |
152 | |
153 public function getCommandInfoListFromClass($commandFileInstance) | |
154 { | |
155 $cachedCommandInfoList = $this->getCommandInfoListFromCache($commandFileInstance); | |
156 $commandInfoList = $this->createCommandInfoListFromClass($commandFileInstance, $cachedCommandInfoList); | |
157 if (!empty($commandInfoList)) { | |
158 $cachedCommandInfoList = array_merge($commandInfoList, $cachedCommandInfoList); | |
159 $this->storeCommandInfoListInCache($commandFileInstance, $cachedCommandInfoList); | |
160 } | |
161 return $cachedCommandInfoList; | |
162 } | |
163 | |
164 protected function storeCommandInfoListInCache($commandFileInstance, $commandInfoList) | |
165 { | |
166 if (!$this->hasDataStore()) { | |
167 return; | |
168 } | |
169 $cache_data = []; | |
170 $serializer = new CommandInfoSerializer(); | |
171 foreach ($commandInfoList as $i => $commandInfo) { | |
172 $cache_data[$i] = $serializer->serialize($commandInfo); | |
173 } | |
174 $className = get_class($commandFileInstance); | |
175 $this->getDataStore()->set($className, $cache_data); | |
176 } | |
177 | |
178 /** | |
179 * Get the command info list from the cache | |
180 * | |
181 * @param mixed $commandFileInstance | |
182 * @return array | |
183 */ | |
184 protected function getCommandInfoListFromCache($commandFileInstance) | |
185 { | |
186 $commandInfoList = []; | |
187 $className = get_class($commandFileInstance); | |
188 if (!$this->getDataStore()->has($className)) { | |
189 return []; | |
190 } | |
191 $deserializer = new CommandInfoDeserializer(); | |
192 | |
193 $cache_data = $this->getDataStore()->get($className); | |
194 foreach ($cache_data as $i => $data) { | |
195 if (CommandInfoDeserializer::isValidSerializedData((array)$data)) { | |
196 $commandInfoList[$i] = $deserializer->deserialize((array)$data); | |
197 } | |
198 } | |
199 return $commandInfoList; | |
200 } | |
201 | |
202 /** | |
203 * Check to see if this factory has a cache datastore. | |
204 * @return boolean | |
205 */ | |
206 public function hasDataStore() | |
207 { | |
208 return !($this->dataStore instanceof NullCache); | |
209 } | |
210 | |
211 /** | |
212 * Set a cache datastore for this factory. Any object with 'set' and | |
213 * 'get' methods is acceptable. The key is the classname being cached, | |
214 * and the value is a nested associative array of strings. | |
215 * | |
216 * TODO: Typehint this to SimpleCacheInterface | |
217 * | |
218 * This is not done currently to allow clients to use a generic cache | |
219 * store that does not itself depend on the annotated-command library. | |
220 * | |
221 * @param Mixed $dataStore | |
222 * @return type | |
223 */ | |
224 public function setDataStore($dataStore) | |
225 { | |
226 if (!($dataStore instanceof SimpleCacheInterface)) { | |
227 $dataStore = new CacheWrapper($dataStore); | |
228 } | |
229 $this->dataStore = $dataStore; | |
230 return $this; | |
231 } | |
232 | |
233 /** | |
234 * Get the data store attached to this factory. | |
235 */ | |
236 public function getDataStore() | |
237 { | |
238 return $this->dataStore; | |
239 } | |
240 | |
241 protected function createCommandInfoListFromClass($classNameOrInstance, $cachedCommandInfoList) | |
242 { | |
243 $commandInfoList = []; | |
244 | |
245 // Ignore special functions, such as __construct and __call, which | |
246 // can never be commands. | |
247 $commandMethodNames = array_filter( | |
248 get_class_methods($classNameOrInstance) ?: [], | |
249 function ($m) use ($classNameOrInstance) { | |
250 $reflectionMethod = new \ReflectionMethod($classNameOrInstance, $m); | |
251 return !$reflectionMethod->isStatic() && !preg_match('#^_#', $m); | |
252 } | |
253 ); | |
254 | |
255 foreach ($commandMethodNames as $commandMethodName) { | |
256 if (!array_key_exists($commandMethodName, $cachedCommandInfoList)) { | |
257 $commandInfo = CommandInfo::create($classNameOrInstance, $commandMethodName); | |
258 if (!static::isCommandOrHookMethod($commandInfo, $this->getIncludeAllPublicMethods())) { | |
259 $commandInfo->invalidate(); | |
260 } | |
261 $commandInfoList[$commandMethodName] = $commandInfo; | |
262 } | |
263 } | |
264 | |
265 return $commandInfoList; | |
266 } | |
267 | |
268 public function createCommandInfo($classNameOrInstance, $commandMethodName) | |
269 { | |
270 return CommandInfo::create($classNameOrInstance, $commandMethodName); | |
271 } | |
272 | |
273 public function createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods = null) | |
274 { | |
275 // Deprecated: avoid using the $includeAllPublicMethods in favor of the setIncludeAllPublicMethods() accessor. | |
276 if (!isset($includeAllPublicMethods)) { | |
277 $includeAllPublicMethods = $this->getIncludeAllPublicMethods(); | |
278 } | |
279 return $this->createSelectedCommandsFromClassInfo( | |
280 $commandInfoList, | |
281 $commandFileInstance, | |
282 function ($commandInfo) use ($includeAllPublicMethods) { | |
283 return static::isCommandMethod($commandInfo, $includeAllPublicMethods); | |
284 } | |
285 ); | |
286 } | |
287 | |
288 public function createSelectedCommandsFromClassInfo($commandInfoList, $commandFileInstance, callable $commandSelector) | |
289 { | |
290 $commandInfoList = $this->filterCommandInfoList($commandInfoList, $commandSelector); | |
291 return array_map( | |
292 function ($commandInfo) use ($commandFileInstance) { | |
293 return $this->createCommand($commandInfo, $commandFileInstance); | |
294 }, | |
295 $commandInfoList | |
296 ); | |
297 } | |
298 | |
299 protected function filterCommandInfoList($commandInfoList, callable $commandSelector) | |
300 { | |
301 return array_filter($commandInfoList, $commandSelector); | |
302 } | |
303 | |
304 public static function isCommandOrHookMethod($commandInfo, $includeAllPublicMethods) | |
305 { | |
306 return static::isHookMethod($commandInfo) || static::isCommandMethod($commandInfo, $includeAllPublicMethods); | |
307 } | |
308 | |
309 public static function isHookMethod($commandInfo) | |
310 { | |
311 return $commandInfo->hasAnnotation('hook'); | |
312 } | |
313 | |
314 public static function isCommandMethod($commandInfo, $includeAllPublicMethods) | |
315 { | |
316 // Ignore everything labeled @hook | |
317 if (static::isHookMethod($commandInfo)) { | |
318 return false; | |
319 } | |
320 // Include everything labeled @command | |
321 if ($commandInfo->hasAnnotation('command')) { | |
322 return true; | |
323 } | |
324 // Skip anything that has a missing or invalid name. | |
325 $commandName = $commandInfo->getName(); | |
326 if (empty($commandName) || preg_match('#[^a-zA-Z0-9:_-]#', $commandName)) { | |
327 return false; | |
328 } | |
329 // Skip anything named like an accessor ('get' or 'set') | |
330 if (preg_match('#^(get[A-Z]|set[A-Z])#', $commandInfo->getMethodName())) { | |
331 return false; | |
332 } | |
333 | |
334 // Default to the setting of 'include all public methods'. | |
335 return $includeAllPublicMethods; | |
336 } | |
337 | |
338 public function registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance) | |
339 { | |
340 foreach ($commandInfoList as $commandInfo) { | |
341 if (static::isHookMethod($commandInfo)) { | |
342 $this->registerCommandHook($commandInfo, $commandFileInstance); | |
343 } | |
344 } | |
345 } | |
346 | |
347 /** | |
348 * Register a command hook given the CommandInfo for a method. | |
349 * | |
350 * The hook format is: | |
351 * | |
352 * @hook type name type | |
353 * | |
354 * For example, the pre-validate hook for the core:init command is: | |
355 * | |
356 * @hook pre-validate core:init | |
357 * | |
358 * If no command name is provided, then this hook will affect every | |
359 * command that is defined in the same file. | |
360 * | |
361 * If no hook is provided, then we will presume that ALTER_RESULT | |
362 * is intended. | |
363 * | |
364 * @param CommandInfo $commandInfo Information about the command hook method. | |
365 * @param object $commandFileInstance An instance of the CommandFile class. | |
366 */ | |
367 public function registerCommandHook(CommandInfo $commandInfo, $commandFileInstance) | |
368 { | |
369 // Ignore if the command info has no @hook | |
370 if (!static::isHookMethod($commandInfo)) { | |
371 return; | |
372 } | |
373 $hookData = $commandInfo->getAnnotation('hook'); | |
374 $hook = $this->getNthWord($hookData, 0, HookManager::ALTER_RESULT); | |
375 $commandName = $this->getNthWord($hookData, 1); | |
376 | |
377 // Register the hook | |
378 $callback = [$commandFileInstance, $commandInfo->getMethodName()]; | |
379 $this->commandProcessor()->hookManager()->add($callback, $hook, $commandName); | |
380 | |
381 // If the hook has options, then also register the commandInfo | |
382 // with the hook manager, so that we can add options and such to | |
383 // the commands they hook. | |
384 if (!$commandInfo->options()->isEmpty()) { | |
385 $this->commandProcessor()->hookManager()->recordHookOptions($commandInfo, $commandName); | |
386 } | |
387 } | |
388 | |
389 protected function getNthWord($string, $n, $default = '', $delimiter = ' ') | |
390 { | |
391 $words = explode($delimiter, $string); | |
392 if (!empty($words[$n])) { | |
393 return $words[$n]; | |
394 } | |
395 return $default; | |
396 } | |
397 | |
398 public function createCommand(CommandInfo $commandInfo, $commandFileInstance) | |
399 { | |
400 $this->alterCommandInfo($commandInfo, $commandFileInstance); | |
401 $command = new AnnotatedCommand($commandInfo->getName()); | |
402 $commandCallback = [$commandFileInstance, $commandInfo->getMethodName()]; | |
403 $command->setCommandCallback($commandCallback); | |
404 $command->setCommandProcessor($this->commandProcessor); | |
405 $command->setCommandInfo($commandInfo); | |
406 $automaticOptions = $this->callAutomaticOptionsProviders($commandInfo); | |
407 $command->setCommandOptions($commandInfo, $automaticOptions); | |
408 // Annotation commands are never bootstrap-aware, but for completeness | |
409 // we will notify on every created command, as some clients may wish to | |
410 // use this notification for some other purpose. | |
411 $this->notify($command); | |
412 return $command; | |
413 } | |
414 | |
415 /** | |
416 * Give plugins an opportunity to update the commandInfo | |
417 */ | |
418 public function alterCommandInfo(CommandInfo $commandInfo, $commandFileInstance) | |
419 { | |
420 foreach ($this->commandInfoAlterers as $alterer) { | |
421 $alterer->alterCommandInfo($commandInfo, $commandFileInstance); | |
422 } | |
423 } | |
424 | |
425 /** | |
426 * Get the options that are implied by annotations, e.g. @fields implies | |
427 * that there should be a --fields and a --format option. | |
428 * | |
429 * @return InputOption[] | |
430 */ | |
431 public function callAutomaticOptionsProviders(CommandInfo $commandInfo) | |
432 { | |
433 $automaticOptions = []; | |
434 foreach ($this->automaticOptionsProviderList as $automaticOptionsProvider) { | |
435 $automaticOptions += $automaticOptionsProvider->automaticOptions($commandInfo); | |
436 } | |
437 return $automaticOptions; | |
438 } | |
439 | |
440 /** | |
441 * Get the options that are implied by annotations, e.g. @fields implies | |
442 * that there should be a --fields and a --format option. | |
443 * | |
444 * @return InputOption[] | |
445 */ | |
446 public function automaticOptions(CommandInfo $commandInfo) | |
447 { | |
448 $automaticOptions = []; | |
449 $formatManager = $this->commandProcessor()->formatterManager(); | |
450 if ($formatManager) { | |
451 $annotationData = $commandInfo->getAnnotations()->getArrayCopy(); | |
452 $formatterOptions = new FormatterOptions($annotationData); | |
453 $dataType = $commandInfo->getReturnType(); | |
454 $automaticOptions = $formatManager->automaticOptions($formatterOptions, $dataType); | |
455 } | |
456 return $automaticOptions; | |
457 } | |
458 } |