Passed
Push — master ( 13fa34...dc56c0 )
by Pol
10:27
created

Runner   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 356
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 32
eloc 136
dl 0
loc 356
ccs 0
cts 176
cp 0
rs 9.84
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A run() 0 10 1
A __construct() 0 28 1
A createApplication() 0 17 1
A addOptions() 0 16 3
B createConfiguration() 0 71 7
A getTasks() 0 9 3
A getWorkingDir() 0 3 1
B registerDynamicCommands() 0 46 9
A getLocalConfigurationFilepath() 0 15 4
A createContainer() 0 17 2
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 phptaskman\core\Contract\ComposerAwareInterface;
10
use phptaskman\core\Robo\Plugin\Commands\YamlCommands;
11
use phptaskman\core\Config\YamlRecursivePathsFinder;
12
use phptaskman\core\Services\Composer;
13
use League\Container\ContainerAwareTrait;
14
use Robo\Application;
15
use Robo\Common\ConfigAwareTrait;
16
use Robo\Config\Config;
17
use Robo\Robo;
18
use Symfony\Component\Console\Input\ArgvInput;
19
use Symfony\Component\Console\Input\InputInterface;
20
use Symfony\Component\Console\Input\InputOption;
21
use Symfony\Component\Console\Output\ConsoleOutput;
22
use Symfony\Component\Console\Output\OutputInterface;
23
use Symfony\Component\Finder\Finder;
24
25
/**
26
 * Class Runner.
27
 */
28
final class Runner
29
{
30
    use ConfigAwareTrait;
31
    use ContainerAwareTrait;
32
33
    public const APPLICATION_NAME = 'Taskman';
34
35
    /**
36
     * @var Application
37
     */
38
    private $application;
39
40
    /**
41
     * @var \Composer\Autoload\ClassLoader
42
     */
43
    private $classLoader;
44
45
    /**
46
     * @var array
47
     */
48
    private $defaultCommandClasses = [
49
        YamlCommands::class,
50
    ];
51
52
    /**
53
     * @var InputInterface
54
     */
55
    private $input;
56
57
    /**
58
     * @var OutputInterface
59
     */
60
    private $output;
61
62
    /**
63
     * @var \Robo\Runner
64
     */
65
    private $runner;
66
67
    /**
68
     * @var string
69
     */
70
    private $workingDir;
71
72
    /**
73
     * Runner constructor.
74
     *
75
     * @param InputInterface $input
76
     * @param OutputInterface $output
77
     * @param ClassLoader $classLoader
78
     */
79
    public function __construct(
80
        InputInterface $input = null,
81
        OutputInterface $output = null,
82
        ClassLoader $classLoader = null
83
    ) {
84
        $this->input = $input ?? new ArgvInput();
85
        $this->output = $output ?? new ConsoleOutput();
86
        $this->classLoader = $classLoader ?? new ClassLoader();
87
88
        $this->workingDir = $this->getWorkingDir($this->input);
89
        \chdir($this->workingDir);
90
91
        $this->config = $this->createConfiguration();
92
93
        $this->application = $this->createApplication();
94
        $this->application->setAutoExit(false);
95
96
        $this->container = $this->createContainer(
97
            $this->input,
98
            $this->output,
99
            $this->application,
100
            $this->config,
101
            $this->classLoader
102
        );
103
104
        $this->runner = (new \Robo\Runner())
105
            ->setRelativePluginNamespace('Robo\Plugin')
106
            ->setContainer($this->container);
107
    }
108
109
    /**
110
     * @param mixed $args
111
     *
112
     * @return int
113
     */
114
    public function run($args)
0 ignored issues
show
Unused Code introduced by
The parameter $args is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

114
    public function run(/** @scrutinizer ignore-unused */ $args)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
115
    {
116
        // Register command classes.
117
        $this->runner->registerCommandClasses($this->application, $this->defaultCommandClasses);
118
119
        // Register commands defined in task.yml file.
120
        $this->registerDynamicCommands($this->application);
121
122
        // Run command.
123
        return $this->runner->run($this->input, $this->output, $this->application);
124
    }
125
126
    /**
127
     * @param \Consolidation\AnnotatedCommand\AnnotatedCommand $command
128
     * @param array $commandDefinition
129
     */
130
    private function addOptions(AnnotatedCommand $command, array $commandDefinition)
131
    {
132
        // This command doesn't define any option.
133
        if (empty($commandDefinition['options'])) {
134
            return;
135
        }
136
137
        $defaults = \array_fill_keys(['shortcut', 'mode', 'description', 'default'], null);
138
        foreach ($commandDefinition['options'] as $optionName => $optionDefinition) {
139
            $optionDefinition += $defaults;
140
            $command->addOption(
141
                '--' . $optionName,
142
                $optionDefinition['shortcut'],
143
                $optionDefinition['mode'],
144
                $optionDefinition['description'],
145
                $optionDefinition['default']
146
            );
147
        }
148
    }
149
150
    /**
151
     * Create application.
152
     *
153
     * @return \Robo\Application
154
     */
155
    private function createApplication()
156
    {
157
        $application = new Application(self::APPLICATION_NAME, 'UNKNOWN');
158
159
        $application
160
            ->getDefinition()
161
            ->addOption(
162
                new InputOption(
163
                    '--working-dir',
164
                    null,
165
                    InputOption::VALUE_REQUIRED,
166
                    'Working directory, defaults to current working directory.',
167
                    $this->workingDir
168
                )
169
            );
170
171
        return $application;
172
    }
173
174
    /**
175
     * Create default configuration.
176
     *
177
     * @return Config
178
     */
179
    private function createConfiguration()
180
    {
181
        // Load default paths.
182
        $filesystemPaths = [
183
            __DIR__ . '/../config/default.yml',
184
            __DIR__ . '/../default.yml',
185
            'taskman.yml.dist',
186
            'taskman.yml',
187
            \getcwd() . '/taskman.yml.dist',
188
            \getcwd() . '/taskman.yml',
189
            $this->getLocalConfigurationFilepath(),
190
        ];
191
192
        // Get the vendor-bin property from the composer.json.
193
        $composer = Taskman::getComposerFromDirectory(getcwd());
194
        $vendorDir = $composer->getConfig('vendor-dir') ?? \getcwd() . '/vendor';
195
196
        // Load extra section from original composer.json file.
197
        // Sometimes the vendor-dir can on a complete different location.
198
        $extra = $composer->getExtra() + ['taskman' => []];
199
        $extra['taskman'] += ['files' => []];
200
201
        foreach ($extra['taskman']['files'] as $file) {
202
            $filesystemPaths[$file] = $file;
203
        }
204
205
        // Load files listed in extra section of each composer.json files.
206
        $finder = (new Finder())
207
            ->files()
208
            ->in(\dirname(\realpath($vendorDir)))
209
            ->name('composer.json');
210
211
        foreach ($finder as $file) {
212
            $composer = Taskman::getComposerFromDirectory(
213
                \dirname($file->getRealPath())
214
            );
215
216
            $extra = $composer->getExtra();
217
218
            if (empty($extra)) {
219
                continue;
220
            }
221
222
            $extra += ['taskman' => []];
223
            $extra['taskman'] += ['files' => []];
224
225
            foreach ($extra['taskman']['files'] as $commandFile) {
226
                if (file_exists($commandFile)) {
227
                    $filesystemPaths[] = $commandFile;
228
                    continue;
229
                }
230
231
                $commandFile = \dirname($file->getRealPath()) . '/' . $commandFile;
232
233
                if (file_exists($commandFile)) {
234
                    $filesystemPaths[] = $commandFile;
235
                    continue;
236
                }
237
            }
238
        }
239
240
        // Resolve imports if any.
241
        $paths = (new YamlRecursivePathsFinder($filesystemPaths))
242
            ->getAllPaths();
243
244
        $config = new Config();
245
        $config->set('taskman.working_dir', \realpath($this->workingDir));
246
247
        Robo::loadConfiguration($paths, $config);
248
249
        return $config;
250
    }
251
252
    /**
253
     * Create and configure container.
254
     *
255
     * @param \Symfony\Component\Console\Input\InputInterface $input
256
     * @param \Symfony\Component\Console\Output\OutputInterface $output
257
     * @param \Robo\Application $application
258
     * @param \Robo\Config\Config $config
259
     * @param \Composer\Autoload\ClassLoader $classLoader
260
     *
261
     * @return \League\Container\Container|\League\Container\ContainerInterface
262
     */
263
    private function createContainer(
264
        InputInterface $input,
265
        OutputInterface $output,
266
        Application $application,
267
        Config $config,
268
        ClassLoader $classLoader
269
    ) {
270
        $container = Robo::createDefaultContainer($input, $output, $application, $config, $classLoader);
271
        $container->get('commandFactory')->setIncludeAllPublicMethods(false);
272
        $container->share('taskman.composer', Composer::class)->withArgument($this->workingDir);
273
274
        // Add service inflectors.
275
        if (null !== $service = $container->inflector(ComposerAwareInterface::class)) {
276
            $service->invokeMethod('setComposer', ['taskman.composer']);
277
        }
278
279
        return $container;
280
    }
281
282
    /**
283
     * Get the local configuration filepath.
284
     *
285
     * @param string $configuration_file
286
     *   The default filepath.
287
     *
288
     * @return null|string
289
     *   The local configuration file path, or null if it doesn't exist.
290
     */
291
    private function getLocalConfigurationFilepath($configuration_file = 'phptaskman/taskman.yml')
292
    {
293
        if ($config = \getenv('PHPTASKMAN_CONFIG')) {
294
            return $config;
295
        }
296
297
        if ($config = \getenv('XDG_CONFIG_HOME')) {
298
            return $config . '/' . $configuration_file;
299
        }
300
301
        if ($home = \getenv('HOME')) {
0 ignored issues
show
Unused Code introduced by
The assignment to $home is dead and can be removed.
Loading history...
302
            return \getenv('HOME') . '/.config/' . $configuration_file;
303
        }
304
305
        return null;
306
    }
307
    /**
308
     * @param string $command
309
     *
310
     * @throws \InvalidArgumentException
311
     *
312
     * @return array
313
     */
314
    private function getTasks($command)
315
    {
316
        $commands = $this->getConfig()->get('commands', []);
317
318
        if (!isset($commands[$command])) {
319
            throw new \InvalidArgumentException("Custom command '${command}' not defined.");
320
        }
321
322
        return !empty($commands[$command]['tasks']) ? $commands[$command]['tasks'] : $commands[$command];
323
    }
324
325
    /**
326
     * @param \Symfony\Component\Console\Input\InputInterface $input
327
     *
328
     * @return mixed
329
     */
330
    private function getWorkingDir(InputInterface $input)
331
    {
332
        return $input->getParameterOption('--working-dir', \getcwd());
333
    }
334
335
    /**
336
     * @param \Robo\Application $application
337
     */
338
    private function registerDynamicCommands(Application $application)
339
    {
340
        $customCommands = $this->getConfig()->get('commands', []);
341
        foreach ($customCommands as $name => $commandDefinition) {
342
            /** @var \Consolidation\AnnotatedCommand\AnnotatedCommandFactory $commandFactory */
343
            $commandFileName = YamlCommands::class . 'Commands';
344
            $commandClass = $this->container->get($commandFileName);
345
            $commandFactory = $this->container->get('commandFactory');
346
            $commandInfo = $commandFactory->createCommandInfo($commandClass, 'runTasks');
347
            $command = $commandFactory->createCommand($commandInfo, $commandClass)->setName($name);
348
349
            // Dynamic commands may define their own options.
350
            $this->addOptions($command, $commandDefinition);
351
352
            // Append also options of subsequent tasks.
353
            foreach ($this->getTasks($name) as $taskEntry) {
354
                if (!\is_array($taskEntry)) {
355
                    continue;
356
                }
357
358
                if (!isset($taskEntry['task'])) {
359
                    continue;
360
                }
361
362
                if ('run' !== $taskEntry['task']) {
363
                    continue;
364
                }
365
366
                if (empty($taskEntry['command'])) {
367
                    continue;
368
                }
369
370
                // This is a 'run' task.
371
                if (!empty($customCommands[$taskEntry['command']])) {
372
                    // Add the options of another custom command.
373
                    $this->addOptions($command, $customCommands[$taskEntry['command']]);
374
                } else {
375
                    // Add the options of an already registered command.
376
                    if ($this->application->has($taskEntry['command'])) {
377
                        $subCommand = $this->application->get($taskEntry['command']);
378
                        $command->addOptions($subCommand->getDefinition()->getOptions());
379
                    }
380
                }
381
            }
382
383
            $application->add($command);
384
        }
385
    }
386
}
387