Completed
Pull Request — master (#205)
by Anton
02:45
created

ConsoleDispatcher::getCommands()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 3
nop 1
dl 0
loc 18
rs 9.2
c 0
b 0
f 0
1
<?php
2
/**
3
 * spiral
4
 *
5
 * @author    Wolfy-J
6
 */
7
8
namespace Spiral\Console;
9
10
use Spiral\Console\Configs\ConsoleConfig;
11
use Spiral\Console\Exceptions\ConsoleException;
12
use Spiral\Console\Logging\DebugHandler;
13
use Spiral\Core\Component;
14
use Spiral\Core\Container;
15
use Spiral\Core\Container\SingletonInterface;
16
use Spiral\Core\ContainerInterface;
17
use Spiral\Core\Core;
18
use Spiral\Core\DispatcherInterface;
19
use Spiral\Core\MemoryInterface;
20
use Spiral\Core\NullMemory;
21
use Spiral\Debug\LogManager;
22
use Spiral\Debug\SnapshotInterface;
23
use Symfony\Component\Console\Application as ConsoleApplication;
24
use Symfony\Component\Console\Input\ArgvInput;
25
use Symfony\Component\Console\Input\ArrayInput;
26
use Symfony\Component\Console\Input\InputInterface;
27
use Symfony\Component\Console\Output\BufferedOutput;
28
use Symfony\Component\Console\Output\ConsoleOutput;
29
use Symfony\Component\Console\Output\OutputInterface;
30
31
/**
32
 * Used as application dispatcher in console mode. Can execute automatically locate and execute
33
 * every available Symfony command.
34
 */
35
class ConsoleDispatcher extends Component implements SingletonInterface, DispatcherInterface
36
{
37
    /**
38
     * Undefined response code for command (errors). See below.
39
     */
40
    const CODE_UNDEFINED = 102;
41
42
    /**
43
     * @var ConsoleApplication
44
     */
45
    private $application = null;
46
47
    /**
48
     * Active console output.
49
     *
50
     * @var ConsoleOutput
51
     */
52
    private $output = null;
53
54
    /**
55
     * @var ConsoleConfig
56
     */
57
    protected $config;
58
59
    /**
60
     * @invisible
61
     * @var ContainerInterface
62
     */
63
    protected $container;
64
65
    /**
66
     * @invisible
67
     * @var MemoryInterface
68
     */
69
    protected $memory;
70
71
    /**
72
     * @invisible
73
     * @var LocatorInterface
74
     */
75
    protected $locator;
76
77
    /**
78
     * @param ConsoleConfig           $config
79
     * @param ContainerInterface|null $container
80
     * @param MemoryInterface|null    $memory
81
     * @param LocatorInterface|null   $locator
82
     */
83
    public function __construct(
84
        ConsoleConfig $config,
85
        ContainerInterface $container = null,
86
        MemoryInterface $memory = null,
87
        LocatorInterface $locator = null
88
    ) {
89
        $this->config = $config;
90
        $this->container = $container ?? new Container();
0 ignored issues
show
Documentation Bug introduced by
It seems like $container ?? new \Spiral\Core\Container() can also be of type object<Spiral\Core\Container>. However, the property $container is declared as type object<Spiral\Core\ContainerInterface>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
91
        $this->memory = $memory ?? new NullMemory();
92
        $this->locator = $locator ?? new NullLocator();
93
    }
94
95
    /**
96
     * {@inheritdoc}
97
     *
98
     * @param InputInterface  $input
0 ignored issues
show
Bug introduced by
There is no parameter named $input. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
99
     * @param OutputInterface $output
0 ignored issues
show
Bug introduced by
There is no parameter named $output. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
100
     */
101
    public function start()
102
    {
103
        //Let's keep output reference to render exceptions
104
        $input = new ArgvInput();
105
        $this->output = new ConsoleOutput();
106
        $this->configureIO($input, $this->output);
107
108
        $this->runScoped(function () use ($input) {
109
            $this->consoleApplication()->run($input, $this->output);
110
        }, $this->output);
111
    }
112
113
    /**
114
     * Execute console command by it's name. Attention, this method will automatically set debug
115
     * handler which will display log messages into console when verbosity is ON, hovewer, already
116
     * existed Logger instances would not be affected.
117
     *
118
     * @param string|null          $command Default command when null.
119
     * @param array|InputInterface $input
120
     * @param OutputInterface      $output
121
     *
122
     * @return CommandOutput
123
     *
124
     * @throws ConsoleException
125
     */
126
    public function run(
127
        string $command = null,
128
        $input = [],
129
        OutputInterface $output = null
130
    ): CommandOutput {
131
        if (is_array($input)) {
132
            $input = new ArrayInput($input + compact('command'));
133
        }
134
135
        $output = $output ?? new BufferedOutput();
136
137
        $code = $this->runScoped(function () use ($input, $output, $command) {
138
            return $this->consoleApplication()->find($command)->run($input, $output);
139
        }, $output);
140
141
        return new CommandOutput($code ?? self::CODE_UNDEFINED, $output);
142
    }
143
144
    /**
145
     * Get or create instance of ConsoleApplication.
146
     *
147
     * @return ConsoleApplication
148
     */
149
    public function consoleApplication()
150
    {
151
        if (!empty($this->application)) {
152
            //Already initiated
153
            return $this->application;
154
        }
155
156
        $commands = [];
157
        foreach ($this->getCommands() as $command) {
158
            $commands[] = $this->container->get($command);
159
        }
160
161
        $this->application = new ConsoleApplication('Spiral Framework', Core::VERSION);
162
        $this->application->setCatchExceptions(false);
163
        $this->application->addCommands($commands);
164
165
        return $this->application;
166
    }
167
168
    /**
169
     * Locate every available Symfony command using Tokenizer.
170
     *
171
     * @param bool $reset Ignore cache.
172
     *
173
     * @return array
174
     */
175
    public function getCommands(bool $reset = false): array
176
    {
177
        $commands = (array)$this->memory->loadData('commands');
178
        if (!empty($commands) && !$reset) {
179
            //Reading from cache
180
            return $commands + $this->config->userCommands();
181
        }
182
183
        if ($this->config->locateCommands()) {
184
            //Automatically locate commands
185
            $commands = $this->locator->locateCommands();
186
        }
187
188
        //Warming up cache
189
        $this->memory->saveData('commands', $commands);
190
191
        return $commands + $this->config->userCommands();
192
    }
193
194
    /**
195
     * {@inheritdoc}
196
     *
197
     * @param OutputInterface $output
198
     */
199
    public function handleSnapshot(SnapshotInterface $snapshot, OutputInterface $output = null)
200
    {
201
        $output = $output ?? $this->output;
202
        if ($output == null) {
203
            // unable to handle
204
            return;
205
        }
206
207
        if ($output instanceof ConsoleOutput) {
208
            $output = $output->getErrorOutput();
209
        }
210
211
        $output->writeln('', OutputInterface::VERBOSITY_QUIET);
212
        $this->container->get(ErrorWriter::class)->renderException(
213
            $output,
214
            $snapshot->getException()
215
        );
216
    }
217
218
    /**
219
     * Run method in console IoC scope.
220
     *
221
     * @param \Closure                                          $closure
222
     * @param \Symfony\Component\Console\Output\OutputInterface $output
223
     *
224
     * @return mixed
225
     */
226
    private function runScoped(\Closure $closure, OutputInterface $output)
227
    {
228
        //Each command are executed in a specific environment
229
        $scope = self::staticContainer($this->container);
230
231
        //This handler will allow us to enable verbosity mode
232
        $debugHandler = $this->container->get(LogManager::class)->debugHandler(
233
            new DebugHandler($output)
234
        );
235
236
        try {
237
            return $closure->call($this);
238
        } finally {
239
            //Restore default debug handler and container scope
240
            $this->container->get(LogManager::class)->debugHandler($debugHandler);
241
            self::staticContainer($scope);
242
        }
243
    }
244
245
    /**
246
     * Reduced version of Symfony::configureIO.
247
     *
248
     * @source Symfony\Component\Console::configureIO()
249
     *
250
     * @param InputInterface  $input
251
     * @param OutputInterface $output
252
     */
253
    private function configureIO(InputInterface $input, OutputInterface $output)
254
    {
255
        if (true === $input->hasParameterOption(['--ansi'], true)) {
256
            $output->setDecorated(true);
257
        } elseif (true === $input->hasParameterOption(['--no-ansi'], true)) {
258
            $output->setDecorated(false);
259
        }
260
261
        if (true === $input->hasParameterOption(['--quiet', '-q'], true)) {
262
            $output->setVerbosity(OutputInterface::VERBOSITY_QUIET);
263
264
            return;
265
        }
266
267
        if (
268
            $input->hasParameterOption('-vvv', true)
269
            || $input->hasParameterOption('--verbose=3', true)
270
            || 3 === $input->getParameterOption('--verbose', false, true)
271
        ) {
272
            $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG);
273
        } elseif (
274
            $input->hasParameterOption('-vv', true)
275
            || $input->hasParameterOption('--verbose=2', true)
276
            || 2 === $input->getParameterOption('--verbose', false, true)
277
        ) {
278
            $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE);
279
        } elseif (
280
            $input->hasParameterOption('-v', true)
281
            || $input->hasParameterOption('--verbose=1', true)
282
            || $input->hasParameterOption('--verbose', true)
283
            || $input->getParameterOption('--verbose', false, true)
284
        ) {
285
            $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
286
        }
287
    }
288
}