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 }