Completed
Push — master ( c1a557...09df77 )
by Pol
03:01
created

Runner::createConfiguration()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 15
ccs 0
cts 9
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 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\Services\Composer;
12
use phptaskman\core as Taskman;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, phptaskman\core\Taskman. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
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
24
/**
25
 * Class Runner.
26
 */
27
final class Runner
28
{
29
    use ConfigAwareTrait;
30
    use ContainerAwareTrait;
31
32
    public const APPLICATION_NAME = 'Taskman';
33
34
    /**
35
     * @var Application
36
     */
37
    private $application;
38
39
    /**
40
     * @var \Composer\Autoload\ClassLoader
41
     */
42
    private $classLoader;
43
44
    /**
45
     * @var array
46
     */
47
    private $defaultCommandClasses = [
48
        YamlCommands::class,
49
    ];
50
51
    /**
52
     * @var InputInterface
53
     */
54
    private $input;
55
56
    /**
57
     * @var OutputInterface
58
     */
59
    private $output;
60
61
    /**
62
     * @var \Robo\Runner
63
     */
64
    private $runner;
65
66
    /**
67
     * @var string
68
     */
69
    private $workingDir;
70
71
    /**
72
     * Runner constructor.
73
     *
74
     * @param InputInterface $input
75
     * @param OutputInterface $output
76
     * @param ClassLoader $classLoader
77
     */
78
    public function __construct(
79
        InputInterface $input = null,
80
        OutputInterface $output = null,
81
        ClassLoader $classLoader = null
82
    ) {
83
        $this->input = $input ?? new ArgvInput();
84
        $this->output = $output ?? new ConsoleOutput();
85
        $this->classLoader = $classLoader ?? new ClassLoader();
86
87
        $this->workingDir = $this->getWorkingDir($this->input);
88
        \chdir($this->workingDir);
89
90
        $this->config = $this->createConfiguration();
91
92
        $this->application = $this->createApplication();
93
        $this->application->setAutoExit(false);
94
95
        $this->container = $this->createContainer(
96
            $this->input,
97
            $this->output,
98
            $this->application,
99
            $this->config,
100
            $this->classLoader
101
        );
102
103
        $this->runner = (new \Robo\Runner())
104
            ->setRelativePluginNamespace('Robo\Plugin')
105
            ->setContainer($this->container);
106
    }
107
108
    /**
109
     * @param mixed $args
110
     *
111
     * @return int
112
     */
113
    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

113
    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...
114
    {
115
        // Register command classes.
116
        $this->runner->registerCommandClasses($this->application, $this->defaultCommandClasses);
117
118
        // Register commands defined in task.yml file.
119
        $this->registerDynamicCommands($this->application);
120
121
        // Run command.
122
        return $this->runner->run($this->input, $this->output, $this->application);
123
    }
124
125
    /**
126
     * @param \Consolidation\AnnotatedCommand\AnnotatedCommand $command
127
     * @param array $commandDefinition
128
     */
129
    private function addOptions(AnnotatedCommand $command, array $commandDefinition)
130
    {
131
        // This command doesn't define any option.
132
        if (empty($commandDefinition['options'])) {
133
            return;
134
        }
135
136
        $defaults = \array_fill_keys(['shortcut', 'mode', 'description', 'default'], null);
137
        foreach ($commandDefinition['options'] as $optionName => $optionDefinition) {
138
            $optionDefinition += $defaults;
139
            $command->addOption(
140
                '--' . $optionName,
141
                $optionDefinition['shortcut'],
142
                $optionDefinition['mode'],
143
                $optionDefinition['description'],
144
                $optionDefinition['default']
145
            );
146
        }
147
    }
148
149
    /**
150
     * Create application.
151
     *
152
     * @return \Robo\Application
153
     */
154
    private function createApplication()
155
    {
156
        $application = new Application(self::APPLICATION_NAME, 'UNKNOWN');
157
158
        $application
159
            ->getDefinition()
160
            ->addOption(
161
                new InputOption(
162
                    '--working-dir',
163
                    null,
164
                    InputOption::VALUE_REQUIRED,
165
                    'Working directory, defaults to current working directory.',
166
                    $this->workingDir
167
                )
168
            );
169
170
        return $application;
171
    }
172
173
    /**
174
     * Create default configuration.
175
     *
176
     * @return Config
177
     */
178
    private function createConfiguration()
179
    {
180
        // Create a default configuration.
181
        $config = Robo::createConfiguration([]);
182
183
        // Set the variable working_dir.
184
        $config->set('taskman.working_dir', \realpath($this->workingDir));
185
186
        // Load the configuration.
187
        Robo::loadConfiguration(
188
            Taskman\Config\Config::findFilesToIncludeInConfiguration($this->workingDir),
189
            $config
190
        );
191
192
        return $config;
193
    }
194
195
    /**
196
     * Create and configure container.
197
     *
198
     * @param \Symfony\Component\Console\Input\InputInterface $input
199
     * @param \Symfony\Component\Console\Output\OutputInterface $output
200
     * @param \Robo\Application $application
201
     * @param \Robo\Config\Config $config
202
     * @param \Composer\Autoload\ClassLoader $classLoader
203
     *
204
     * @return \League\Container\Container|\League\Container\ContainerInterface
205
     */
206
    private function createContainer(
207
        InputInterface $input,
208
        OutputInterface $output,
209
        Application $application,
210
        Config $config,
211
        ClassLoader $classLoader
212
    ) {
213
        $container = Robo::createDefaultContainer($input, $output, $application, $config, $classLoader);
214
        $container->get('commandFactory')->setIncludeAllPublicMethods(false);
215
        $container->share('taskman.composer', Composer::class)->withArgument($this->workingDir);
216
217
        // Add service inflectors.
218
        if (null !== $service = $container->inflector(ComposerAwareInterface::class)) {
219
            $service->invokeMethod('setComposer', ['taskman.composer']);
220
        }
221
222
        return $container;
223
    }
224
225
    /**
226
     * @param string $command
227
     *
228
     * @throws \InvalidArgumentException
229
     *
230
     * @return array
231
     */
232
    private function getTasks($command)
233
    {
234
        $commands = $this->getConfig()->get('commands', []);
235
236
        if (!isset($commands[$command])) {
237
            throw new \InvalidArgumentException("Custom command '${command}' not defined.");
238
        }
239
240
        return !empty($commands[$command]['tasks']) ? $commands[$command]['tasks'] : $commands[$command];
241
    }
242
243
    /**
244
     * @param \Symfony\Component\Console\Input\InputInterface $input
245
     *
246
     * @return mixed
247
     */
248
    private function getWorkingDir(InputInterface $input)
249
    {
250
        return $input->getParameterOption('--working-dir', \getcwd());
251
    }
252
253
    /**
254
     * @param \Robo\Application $application
255
     */
256
    private function registerDynamicCommands(Application $application)
257
    {
258
        $customCommands = $this->getConfig()->get('commands', []);
259
        foreach ($customCommands as $name => $commandDefinition) {
260
            /** @var \Consolidation\AnnotatedCommand\AnnotatedCommandFactory $commandFactory */
261
            $commandFileName = YamlCommands::class . 'Commands';
262
            $commandClass = $this->container->get($commandFileName);
263
            $commandFactory = $this->container->get('commandFactory');
264
            $commandInfo = $commandFactory->createCommandInfo($commandClass, 'runTasks');
265
            $command = $commandFactory->createCommand($commandInfo, $commandClass)->setName($name);
266
267
            // Dynamic commands may define their own options.
268
            $this->addOptions($command, $commandDefinition);
269
270
            // Append also options of subsequent tasks.
271
            foreach ($this->getTasks($name) as $taskEntry) {
272
                if (!\is_array($taskEntry)) {
273
                    continue;
274
                }
275
276
                if (!isset($taskEntry['task'])) {
277
                    continue;
278
                }
279
280
                if ('run' !== $taskEntry['task']) {
281
                    continue;
282
                }
283
284
                if (empty($taskEntry['command'])) {
285
                    continue;
286
                }
287
288
                // This is a 'run' task.
289
                if (!empty($customCommands[$taskEntry['command']])) {
290
                    // Add the options of another custom command.
291
                    $this->addOptions($command, $customCommands[$taskEntry['command']]);
292
                } else {
293
                    // Add the options of an already registered command.
294
                    if ($this->application->has($taskEntry['command'])) {
295
                        $subCommand = $this->application->get($taskEntry['command']);
296
                        $command->addOptions($subCommand->getDefinition()->getOptions());
297
                    }
298
                }
299
            }
300
301
            $application->add($command);
302
        }
303
    }
304
}
305