Passed
Push — master ( bb2359...2e375e )
by
unknown
14:26
created

src/Runner.php (1 issue)

Severity
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace PhpTaskman\Core;
6
7
use Composer\Autoload\ClassLoader;
8
use Consolidation\AnnotatedCommand\AnnotatedCommand;
9
use League\Container\ContainerAwareTrait;
10
use League\Container\Inflector\Inflector;
11
use PhpTaskman\Core\Robo\Plugin\Commands\YamlCommands;
12
use PhpTaskman\CoreTasks\Plugin\Task\YamlTask;
13
use Robo\Application;
14
use Robo\Collection\CollectionBuilder;
15
use Robo\Common\ConfigAwareTrait;
16
use Robo\Contract\BuilderAwareInterface;
17
use Robo\Tasks;
18
use Symfony\Component\Console\Input\ArgvInput;
19
use Symfony\Component\Console\Input\InputArgument;
20
use Symfony\Component\Console\Input\InputInterface;
21
use Symfony\Component\Console\Input\InputOption;
22
use Symfony\Component\Console\Output\ConsoleOutput;
23
use Symfony\Component\Console\Output\OutputInterface;
24
25
/**
26
 * Class Runner.
27
 */
28
final class Runner
29
{
30
    use ConfigAwareTrait;
31
    use ContainerAwareTrait;
32
33
    /**
34
     * @var Application
35
     */
36
    private $application;
37
38
    /**
39
     * @var \Composer\Autoload\ClassLoader
40
     */
41
    private $classLoader;
42
43
    /**
44
     * @var InputInterface
45
     */
46
    private $input;
47
48
    /**
49
     * @var OutputInterface
50
     */
51
    private $output;
52
53
    /**
54
     * @var \Robo\Runner
55
     */
56
    private $runner;
57
58
    /**
59
     * @var string
60
     */
61
    private $workingDir;
0 ignored issues
show
The private property $workingDir is not used, and could be removed.
Loading history...
62
63
    /**
64
     * Runner constructor.
65
     *
66
     * @param InputInterface $input
67
     * @param OutputInterface $output
68
     * @param ClassLoader $classLoader
69
     *
70
     * @throws \Exception
71
     */
72
    public function __construct(
73
        InputInterface $input = null,
74
        OutputInterface $output = null,
75
        ClassLoader $classLoader = null
76
    ) {
77
        $this->input = $input ?? new ArgvInput();
78
        $this->output = $output ?? new ConsoleOutput();
79
        $this->classLoader = $classLoader ?? new ClassLoader();
80
81
        $this->config = Taskman::createConfiguration(
82
            []
83
        );
84
85
        $this->application = Taskman::createDefaultApplication();
86
87
        $this->container = Taskman::createContainer(
88
            $this->input,
89
            $this->output,
90
            $this->application,
91
            $this->config,
92
            $this->classLoader
93
        );
94
95
        $this->runner = Taskman::createDefaultRunner($this->container);
96
    }
97
98
    /**
99
     * @param mixed $args
100
     *
101
     * @throws \ReflectionException
102
     *
103
     * @return int
104
     */
105
    public function run($args)
106
    {
107
        // Register command classes.
108
        $this->runner->registerCommandClasses($this->application, [YamlCommands::class]);
109
110
        // Register commands defined in task.yml file.
111
        $this->registerDynamicCommands($this->application);
112
113
        // Register tasks
114
        $this->registerDynamicTasks($this->application);
115
116
        // Register global options.
117
        $this->registerGlobalCommandOptions($this->application);
118
119
        // Run command.
120
        return $this->runner->run($this->input, $this->output, $this->application);
121
    }
122
123
    /**
124
     * @param bool $hasDefault
125
     * @param mixed $defaultValue
126
     *
127
     * @return int
128
     */
129
    protected function getCommandArgumentMode(bool $hasDefault, $defaultValue)
130
    {
131
        if (!$hasDefault) {
132
            return InputArgument::REQUIRED;
133
        }
134
135
        if (\is_array($defaultValue)) {
136
            return InputArgument::IS_ARRAY;
137
        }
138
139
        return InputArgument::OPTIONAL;
140
    }
141
142
    /**
143
     * @param \Consolidation\AnnotatedCommand\AnnotatedCommand $command
144
     * @param array $commandDefinition
145
     */
146
    private function addOptions(AnnotatedCommand $command, array $commandDefinition): void
147
    {
148
        // This command doesn't define any option.
149
        if (empty($commandDefinition['options'])) {
150
            return;
151
        }
152
153
        $defaults = array_fill_keys(['shortcut', 'mode', 'description', 'default'], null);
154
155
        foreach ($commandDefinition['options'] as $optionName => $optionDefinition) {
156
            $optionDefinition += $defaults;
157
            $command->addOption(
158
                '--' . $optionName,
159
                $optionDefinition['shortcut'],
160
                $optionDefinition['mode'],
161
                $optionDefinition['description'],
162
                $optionDefinition['default']
163
            );
164
        }
165
    }
166
167
    /**
168
     * @param \Robo\Application $application
169
     */
170
    private function registerDynamicCommands(Application $application): void
171
    {
172
        $commandDefinitions = $this->getConfig()->get('commands', null);
173
174
        if (null === $commandDefinitions) {
175
            return;
176
        }
177
178
        foreach ($commandDefinitions as $name => $commandDefinition) {
179
            /** @var \PhpTaskman\Core\Robo\Plugin\Commands\YamlCommands $commandClass */
180
            $commandClass = $this->container->get(YamlCommands::class . 'Commands');
181
182
            /** @var \Consolidation\AnnotatedCommand\AnnotatedCommandFactory $commandFactory */
183
            $commandFactory = $this->container->get('commandFactory');
184
185
            $commandInfo = $commandFactory->createCommandInfo($commandClass, 'runTasks');
186
187
            $commandDefinition += ['options' => []];
188
189
            foreach ($commandDefinition['options'] as &$option) {
190
                if (isset($option['mode'])) {
191
                    continue;
192
                }
193
194
                $option['mode'] = $this->getCommandArgumentMode(
195
                    isset($option['default']),
196
                    $option['default'] ?? null
197
                );
198
            }
199
200
            $command = $commandFactory->createCommand($commandInfo, $commandClass)->setName($name);
201
202
            // Override default description.
203
            if (isset($commandDefinition['description'])) {
204
                $command->setDescription($commandDefinition['description']);
205
            }
206
            // Override default help.
207
            if (isset($commandDefinition['help'])) {
208
                $command->setHelp($commandDefinition['help']);
209
            }
210
211
            // Dynamic commands may define their own options.
212
            $this->addOptions($command, $commandDefinition);
213
214
            $tasks = $commandDefinition['tasks'] ?? $commandDefinition;
215
216
            // Append also options of subsequent tasks.
217
            foreach ($tasks as $taskEntry) {
218
                if (!\is_array($taskEntry)) {
219
                    continue;
220
                }
221
222
                if (!isset($taskEntry['task'])) {
223
                    continue;
224
                }
225
226
                if ('run' !== $taskEntry['task']) {
227
                    continue;
228
                }
229
230
                if (empty($taskEntry['command'])) {
231
                    continue;
232
                }
233
234
                // This is a 'run' task.
235
                if (!empty($customCommands[$taskEntry['command']])) {
236
                    // Add the options of another custom command.
237
                    $this->addOptions($command, $customCommands[$taskEntry['command']]);
238
                } else {
239
                    // Add the options of an already registered command.
240
                    if ($this->application->has($taskEntry['command'])) {
241
                        $subCommand = $this->application->get($taskEntry['command']);
242
                        $command->addOptions($subCommand->getDefinition()->getOptions());
243
                    }
244
                }
245
            }
246
247
            $application->add($command);
248
        }
249
    }
250
251
    /**
252
     * @param \Robo\Application $application
253
     *
254
     * @throws \ReflectionException
255
     */
256
    private function registerDynamicTasks(Application $application): void
257
    {
258
        $classes = Taskman::discoverTasksClasses('Plugin');
259
260
        /** @var \ReflectionClass[] $tasks */
261
        $tasks = [];
262
263
        foreach ($classes as $className) {
264
            $class = new \ReflectionClass($className);
265
266
            if (!$class->isInstantiable()) {
267
                continue;
268
            }
269
270
            $tasks[] = $class;
271
        }
272
273
        $builder = CollectionBuilder::create($this->container, new Tasks());
274
275
        $inflector = $this->container->inflector(BuilderAwareInterface::class);
276
277
        if ($inflector instanceof Inflector) {
278
            $inflector->invokeMethod('setBuilder', [$builder]);
279
        }
280
281
        // Register custom Task classes.
282
        foreach ($tasks as $taskReflectionClass) {
283
            $this->container->add(
284
                'task.' . $taskReflectionClass->getConstant('NAME'),
285
                $taskReflectionClass->getName()
286
            );
287
        }
288
289
        // Register custom YAML tasks.
290
        $customTasks = $this->getConfig()->get('tasks', null);
291
292
        if (null === $customTasks) {
293
            return;
294
        }
295
296
        foreach ($customTasks as $name => $tasks) {
297
            $this->container->add(
298
                'task.' . $name,
299
                YamlTask::class
300
            )->withArgument($tasks);
301
        }
302
    }
303
304
    /**
305
     * Register the global commands options.
306
     *
307
     * @param \Robo\Application $application
308
     */
309
    private function registerGlobalCommandOptions(Application $application): void
310
    {
311
        $globalOptions = $this->config->get('globals.options', null);
312
313
        if (null === $globalOptions) {
314
            return;
315
        }
316
317
        $config = $this->getConfig();
318
319
        foreach ($globalOptions as $option => $optionDefinition) {
320
            $optionMachineName = 'options.' . ($optionDefinition['config'] ?? $option);
321
322
            $optionDefinition += [
323
                'default' => null,
324
            ];
325
326
            $optionDefinition['default'] = $this->input->getParameterOption(
327
                '--' . $option,
328
                $optionDefinition['default']
329
            );
330
331
            // Special handling for the working-dir option.
332
            if ('working-dir' === $option) {
333
                if (null === $optionDefinition['default']) {
334
                    $optionDefinition['default'] = getcwd();
335
                }
336
337
                $optionDefinition['default'] = realpath($optionDefinition['default']);
338
            }
339
340
            $config->set($optionMachineName, $optionDefinition['default']);
341
342
            $optionDefinition += [
343
                'mode' => InputOption::VALUE_OPTIONAL,
344
                'description' => '',
345
                'shortcut' => [],
346
            ];
347
348
            $optionDefinition['shortcut'] = (array) $optionDefinition['shortcut'];
349
350
            $application
351
                ->getDefinition()
352
                ->addOption(
353
                    new InputOption(
354
                        '--' . $option,
355
                        $optionDefinition['shortcut'],
356
                        $optionDefinition['mode'],
357
                        $optionDefinition['description'],
358
                        $optionDefinition['default']
359
                    )
360
                );
361
        }
362
    }
363
}
364