Completed
Push — master ( 47d5e6...daebf4 )
by Greg
11s
created

AnnotatedCommandFactory::commandProcessor()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
namespace Consolidation\AnnotatedCommand;
3
4
use Consolidation\AnnotatedCommand\Hooks\HookManager;
5
use Consolidation\AnnotatedCommand\Parser\CommandInfo;
6
use Consolidation\OutputFormatters\Options\FormatterOptions;
7
use Symfony\Component\Console\Command\Command;
8
use Symfony\Component\Console\Input\InputInterface;
9
use Symfony\Component\Console\Output\OutputInterface;
10
11
/**
12
 * The AnnotatedCommandFactory creates commands for your application.
13
 * Use with a Dependency Injection Container and the CommandFactory.
14
 * Alternately, use the CommandFileDiscovery to find commandfiles, and
15
 * then use AnnotatedCommandFactory::createCommandsFromClass() to create
16
 * commands.  See the README for more information.
17
 *
18
 * @package Consolidation\AnnotatedCommand
19
 */
20
class AnnotatedCommandFactory implements AutomaticOptionsProviderInterface
21
{
22
    /** var CommandProcessor */
23
    protected $commandProcessor;
24
25
    /** var CommandCreationListenerInterface[] */
26
    protected $listeners = [];
27
28
    /** var AutomaticOptionsProvider[] */
29
30
    protected $automaticOptionsProviderList = [];
31
32
    /** var boolean */
33
    protected $includeAllPublicMethods = true;
34
35
    public function __construct()
36
    {
37
        $this->commandProcessor = new CommandProcessor(new HookManager());
38
        $this->addAutomaticOptionProvider($this);
39
    }
40
41
    public function setCommandProcessor(CommandProcessor $commandProcessor)
42
    {
43
        $this->commandProcessor = $commandProcessor;
44
    }
45
46
    /**
47
     * @return CommandProcessor
48
     */
49
    public function commandProcessor()
50
    {
51
        return $this->commandProcessor;
52
    }
53
54
    /**
55
     * Set the 'include all public methods flag'. If true (the default), then
56
     * every public method of each commandFile will be used to create commands.
57
     * If it is false, then only those public methods annotated with @command
58
     * or @name (deprecated) will be used to create commands.
59
     */
60
    public function setIncludeAllPublicMethods($includeAllPublicMethods)
61
    {
62
        $this->includeAllPublicMethods = $includeAllPublicMethods;
63
    }
64
65
    public function getIncludeAllPublicMethods()
66
    {
67
        return $this->includeAllPublicMethods;
68
    }
69
70
    /**
71
     * @return HookManager
72
     */
73
    public function hookManager()
74
    {
75
        return $this->commandProcessor()->hookManager();
76
    }
77
78
    /**
79
     * Add a listener that is notified immediately before the command
80
     * factory creates commands from a commandFile instance.  This
81
     * listener can use this opportunity to do more setup for the commandFile,
82
     * and so on.
83
     *
84
     * @param CommandCreationListenerInterface $listener
85
     */
86
    public function addListener(CommandCreationListenerInterface $listener)
87
    {
88
        $this->listeners[] = $listener;
89
    }
90
91
    /**
92
     * Call all command creation listeners
93
     *
94
     * @param object $commandFileInstance
95
     */
96
    protected function notify($commandFileInstance)
97
    {
98
        foreach ($this->listeners as $listener) {
99
            $listener->notifyCommandFileAdded($commandFileInstance);
100
        }
101
    }
102
103
    public function addAutomaticOptionProvider(AutomaticOptionsProviderInterface $optionsProvider)
104
    {
105
        $this->automaticOptionsProviderList[] = $optionsProvider;
106
    }
107
108
    public function createCommandsFromClass($commandFileInstance, $includeAllPublicMethods = null)
109
    {
110
        // Deprecated: avoid using the $includeAllPublicMethods in favor of the setIncludeAllPublicMethods() accessor.
111
        if (!isset($includeAllPublicMethods)) {
112
            $includeAllPublicMethods = $this->getIncludeAllPublicMethods();
113
        }
114
        $this->notify($commandFileInstance);
115
        $commandInfoList = $this->getCommandInfoListFromClass($commandFileInstance);
116
        $this->registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance);
117
        return $this->createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods);
118
    }
119
120
    public function getCommandInfoListFromClass($classNameOrInstance)
121
    {
122
        $commandInfoList = [];
123
124
        // Ignore special functions, such as __construct and __call, and
125
        // accessor methods such as getFoo and setFoo, while allowing
126
        // set or setup.
127
        $commandMethodNames = array_filter(
128
            get_class_methods($classNameOrInstance) ?: [],
129
            function ($m) {
130
                return !preg_match('#^(_|get[A-Z]|set[A-Z])#', $m);
131
            }
132
        );
133
134
        foreach ($commandMethodNames as $commandMethodName) {
135
            $commandInfoList[] = new CommandInfo($classNameOrInstance, $commandMethodName);
136
        }
137
138
        return $commandInfoList;
139
    }
140
141
    public function createCommandInfo($classNameOrInstance, $commandMethodName)
142
    {
143
        return new CommandInfo($classNameOrInstance, $commandMethodName);
144
    }
145
146
    public function createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods = null)
147
    {
148
        // Deprecated: avoid using the $includeAllPublicMethods in favor of the setIncludeAllPublicMethods() accessor.
149
        if (!isset($includeAllPublicMethods)) {
150
            $includeAllPublicMethods = $this->getIncludeAllPublicMethods();
151
        }
152
        return $this->createSelectedCommandsFromClassInfo(
153
            $commandInfoList,
154
            $commandFileInstance,
155
            function ($commandInfo) use ($includeAllPublicMethods) {
156
                return $includeAllPublicMethods || static::isCommandMethod($commandInfo);
157
            }
158
        );
159
    }
160
161
    public function createSelectedCommandsFromClassInfo($commandInfoList, $commandFileInstance, callable $commandSelector)
162
    {
163
        $commandList = [];
164
165
        foreach ($commandInfoList as $commandInfo) {
166
            if ($commandSelector($commandInfo)) {
167
                $command = $this->createCommand($commandInfo, $commandFileInstance);
168
                $commandList[] = $command;
169
            }
170
        }
171
172
        return $commandList;
173
    }
174
175
    public static function isCommandMethod($commandInfo)
176
    {
177
        if ($commandInfo->hasAnnotation('hook')) {
178
            return false;
179
        }
180
        return $commandInfo->hasAnnotation('command');
181
    }
182
183
    public function registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance)
184
    {
185
        foreach ($commandInfoList as $commandInfo) {
186
            if ($commandInfo->hasAnnotation('hook')) {
187
                $this->registerCommandHook($commandInfo, $commandFileInstance);
188
            }
189
        }
190
    }
191
192
    /**
193
     * Register a command hook given the CommandInfo for a method.
194
     *
195
     * The hook format is:
196
     *
197
     *   @hook type name type
198
     *
199
     * For example, the pre-validate hook for the core:init command is:
200
     *
201
     *   @hook pre-validate core:init
202
     *
203
     * If no command name is provided, then this hook will affect every
204
     * command that is defined in the same file.
205
     *
206
     * If no hook is provided, then we will presume that ALTER_RESULT
207
     * is intended.
208
     *
209
     * @param CommandInfo $commandInfo Information about the command hook method.
210
     * @param object $commandFileInstance An instance of the CommandFile class.
211
     */
212
    public function registerCommandHook(CommandInfo $commandInfo, $commandFileInstance)
213
    {
214
        // Ignore if the command info has no @hook
215
        if (!$commandInfo->hasAnnotation('hook')) {
216
            return;
217
        }
218
        $hookData = $commandInfo->getAnnotation('hook');
219
        $hook = $this->getNthWord($hookData, 0, HookManager::ALTER_RESULT);
220
        $commandName = $this->getNthWord($hookData, 1);
221
222
        // Register the hook
223
        $callback = [$commandFileInstance, $commandInfo->getMethodName()];
224
        $this->commandProcessor()->hookManager()->add($callback, $hook, $commandName);
225
    }
226
227
    protected function getNthWord($string, $n, $default = '', $delimiter = ' ')
228
    {
229
        $words = explode($delimiter, $string);
230
        if (!empty($words[$n])) {
231
            return $words[$n];
232
        }
233
        return $default;
234
    }
235
236
    public function createCommand(CommandInfo $commandInfo, $commandFileInstance)
237
    {
238
        $command = new AnnotatedCommand($commandInfo->getName());
239
        $commandCallback = [$commandFileInstance, $commandInfo->getMethodName()];
240
        $command->setCommandCallback($commandCallback);
241
        $command->setCommandProcessor($this->commandProcessor);
242
        $command->setCommandInfo($commandInfo);
243
        $automaticOptions = $this->callAutomaticOptionsProviders($commandInfo);
244
        $command->setCommandOptions($commandInfo, $automaticOptions);
245
        // Annotation commands are never bootstrap-aware, but for completeness
246
        // we will notify on every created command, as some clients may wish to
247
        // use this notification for some other purpose.
248
        $this->notify($command);
249
        return $command;
250
    }
251
252
    /**
253
     * Get the options that are implied by annotations, e.g. @fields implies
254
     * that there should be a --fields and a --format option.
255
     *
256
     * @return InputOption[]
257
     */
258
    public function callAutomaticOptionsProviders(CommandInfo $commandInfo)
259
    {
260
        $automaticOptions = [];
261
        foreach ($this->automaticOptionsProviderList as $automaticOptionsProvider) {
262
            $automaticOptions += $automaticOptionsProvider->automaticOptions($commandInfo);
263
        }
264
        return $automaticOptions;
265
    }
266
267
    /**
268
     * Get the options that are implied by annotations, e.g. @fields implies
269
     * that there should be a --fields and a --format option.
270
     *
271
     * @return InputOption[]
272
     */
273
    public function automaticOptions(CommandInfo $commandInfo)
274
    {
275
        $automaticOptions = [];
276
        $formatManager = $this->commandProcessor()->formatterManager();
277
        if ($formatManager) {
278
            $annotationData = $commandInfo->getAnnotationsForCommand()->getArrayCopy();
279
            $formatterOptions = new FormatterOptions($annotationData);
280
            $dataType = $commandInfo->getReturnType();
281
            $automaticOptions = $formatManager->automaticOptions($formatterOptions, $dataType);
282
        }
283
        return $automaticOptions;
284
    }
285
}
286