Completed
Push — develop ( 64ad43...c5a4cf )
by Neomerx
03:58
created

CommandsCommand::execute()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 2
dl 0
loc 19
ccs 13
cts 13
cp 1
crap 3
rs 9.6333
c 0
b 0
f 0
1
<?php namespace Limoncello\Commands;
2
3
/**
4
 * Copyright 2015-2018 [email protected]
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
use Composer\Command\BaseCommand;
20
use Limoncello\Commands\Traits\CacheFilePathTrait;
21
use Limoncello\Commands\Traits\CommandSerializationTrait;
22
use Limoncello\Commands\Traits\CommandTrait;
23
use Limoncello\Contracts\Application\ApplicationConfigurationInterface as S;
24
use Limoncello\Contracts\Application\CacheSettingsProviderInterface;
25
use Limoncello\Contracts\Commands\CommandInterface;
26
use Limoncello\Contracts\Commands\CommandStorageInterface;
27
use Limoncello\Contracts\Commands\IoInterface;
28
use Limoncello\Contracts\FileSystem\FileSystemInterface;
29
use Psr\Container\ContainerExceptionInterface;
30
use Psr\Container\ContainerInterface;
31
use Psr\Container\NotFoundExceptionInterface;
32
use ReflectionException;
33
use Symfony\Component\Console\Input\InputArgument;
34
use Symfony\Component\Console\Input\InputInterface;
35
use Symfony\Component\Console\Output\OutputInterface;
36
37
/**
38
 * This is a special command which is immediately available from composer. The main purpose of it is to
39
 * load command list from user application and generate a special cache file with the list. On the next
40
 * composer run the list would be loaded into composer and all the commands would be available.
41
 *
42
 * Also it provides such a nice feature as generation of an empty/template command for the developer.
43
 *
44
 * @package Limoncello\Commands
45
 *
46
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
47
 */
48
class CommandsCommand extends BaseCommand
49
{
50
    use CommandTrait, CommandSerializationTrait, CacheFilePathTrait;
51
52
    /**
53
     * Command name.
54
     */
55
    const NAME = 'l:commands';
56
57
    /** Argument name */
58
    const ARG_ACTION = 'action';
59
60
    /** Command action */
61
    const ACTION_CONNECT = 'connect';
62
63
    /** Command action */
64
    const ACTION_CREATE = 'create';
65
66
    /** Argument name */
67
    const ARG_CLASS = 'class';
68
69
    /**
70
     * Constructor.
71
     */
72 9
    public function __construct()
73
    {
74 9
        parent::__construct(static::NAME);
75
    }
76
77
    /**
78
     * @inheritdoc
79
     */
80 9
    public function configure()
81
    {
82 9
        parent::configure();
83
84 9
        $connect    = static::ACTION_CONNECT;
85 9
        $create     = static::ACTION_CREATE;
86 9
        $actionDesc = "Required action such as `$connect` to find and connect commands from application and plugins " .
87 9
            "or `$create` to create an empty command template.";
88
89 9
        $classDesc = "Required valid class name in commands' namespace for action `$create`.";
90
91
        $this
92 9
            ->setDescription('Manages commands executed from composer.')
93 9
            ->setHelp('This command connects plugin and user-defined commands and creates new commands.')
94 9
            ->setDefinition([
95 9
                new InputArgument(static::ARG_ACTION, InputArgument::REQUIRED, $actionDesc),
96 9
                new InputArgument(static::ARG_CLASS, InputArgument::OPTIONAL, $classDesc),
97
            ]);
98
    }
99
100
101
    /** @noinspection PhpMissingParentCallCommonInspection
102
     * @inheritdoc
103
     *
104
     * @throws ReflectionException
105
     */
106 8
    public function execute(InputInterface $input, OutputInterface $output)
107
    {
108 8
        $inOut     = $this->wrapIo($input, $output);
109 8
        $container = $this->createContainer($this->getComposer(), static::NAME);
0 ignored issues
show
Bug introduced by
It seems like $this->getComposer() can be null; however, createContainer() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
Unused Code introduced by
The call to CommandsCommand::createContainer() has too many arguments starting with static::NAME.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
110
111 8
        $argAction = static::ARG_ACTION;
112 8
        $action    = $inOut->getArgument($argAction);
113
        switch ($action) {
114 8
            case static::ACTION_CONNECT:
115 3
                $this->executeConnect($container, $inOut);
116 3
                break;
117 5
            case static::ACTION_CREATE:
118 4
                $this->executeCreate($container, $inOut);
119 4
                break;
120
            default:
121 1
                $inOut->writeError("Unknown value `$action` for argument `$argAction`." . PHP_EOL);
122 1
                break;
123
        }
124
    }
125
126
    /**
127
     * @param ContainerInterface $container
128
     * @param IoInterface        $inOut
129
     *
130
     * @return void
131
     *
132
     * @throws ContainerExceptionInterface
133
     * @throws NotFoundExceptionInterface
134
     */
135 3
    private function executeConnect(ContainerInterface $container, IoInterface $inOut): void
136
    {
137 3
        assert($container->has(CommandStorageInterface::class));
138
        /** @var CommandStorageInterface $commandStorage */
139 3
        $commandStorage = $container->get(CommandStorageInterface::class);
140
141 3
        $commandClasses = [];
142 3
        foreach ($commandStorage->getAll() as $commandClass) {
143 3
            if (class_exists($commandClass) === false ||
144 3
                array_key_exists(CommandInterface::class, class_implements($commandClass)) === false
145
            ) {
146 1
                $inOut->writeWarning("Class `$commandClass` either do not exist or not a command class." . PHP_EOL);
147 1
                continue;
148
            }
149
150 2
            $inOut->writeInfo("Found command class `$commandClass`." . PHP_EOL, IoInterface::VERBOSITY_VERBOSE);
151
152 2
            $commandClasses[] = $this->commandClassToArray($commandClass);
153
        }
154
155 3
        if (empty($commandClasses) === false) {
156 2
            $now           = date(DATE_RFC2822);
157 2
            $data          = var_export($commandClasses, true);
158
            $content       = <<<EOT
159
<?php
160
161
// THIS FILE IS AUTO GENERATED. DO NOT EDIT IT MANUALLY.
162 2
// Generated at: $now
163
164 2
    return $data;
165
166
EOT;
167 2
            $cacheFilePath = $this->getCommandsCacheFilePath($this->getComposer());
0 ignored issues
show
Bug introduced by
It seems like $this->getComposer() can be null; however, getCommandsCacheFilePath() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
168 2
            if (empty($cacheFilePath) === true) {
169 1
                $inOut->writeError("Commands cache file path is not set. Check your `Application` settings." . PHP_EOL);
170
171 1
                return;
172
            }
173
174 1
            $this->getFileSystem($container)->write($cacheFilePath, $content);
175
176 1
            $inOut->writeInfo('Commands connected.' . PHP_EOL);
177
178 1
            return;
179
        }
180
181 1
        $inOut->writeWarning('No commands found.' . PHP_EOL);
182
    }
183
184
    /**
185
     * @param ContainerInterface $container
186
     * @param IoInterface        $inOut
187
     *
188
     * @return void
189
     *
190
     * @throws ContainerExceptionInterface
191
     * @throws NotFoundExceptionInterface
192
     */
193 4
    private function executeCreate(ContainerInterface $container, IoInterface $inOut): void
194
    {
195 4
        $argClass = static::ARG_CLASS;
196 4
        if ($inOut->hasArgument($argClass) === false) {
197 1
            $inOut->writeError("Argument `$argClass` is not provided." . PHP_EOL);
198
199 1
            return;
200
        }
201 3
        $class = $inOut->getArgument($argClass);
202
203 3
        $fileSystem     = $this->getFileSystem($container);
204 3
        $commandsFolder = $this->getCommandsFolder($container);
205 3
        if (empty($commandsFolder) === true || $fileSystem->isFolder($commandsFolder) === false) {
206 1
            $inOut->writeError(
207 1
                "Commands folder `$commandsFolder` is not valid. Check your `Application` settings." . PHP_EOL
208
            );
209
210 1
            return;
211
        }
212
213 2
        $classPath = $commandsFolder . DIRECTORY_SEPARATOR . $class . '.php';
214 2
        if (ctype_alpha($class) === false ||
215 2
            $fileSystem->exists($classPath) === true
216
        ) {
217 1
            $inOut->writeError(
218 1
                "Class name `$class` does not look valid for a command. " .
219 1
                'Can you please choose another one?' . PHP_EOL
220
            );
221
222 1
            return;
223
        }
224
225
        $replace = function (string $template, iterable $parameters): string {
226 1
            $result = $template;
227 1
            foreach ($parameters as $key => $value) {
228 1
                $result = str_replace($key, $value, $result);
229
            }
230
231 1
            return $result;
232 1
        };
233
234 1
        $templateContent = $fileSystem->read(__DIR__ . DIRECTORY_SEPARATOR . 'SampleCommand.txt');
235 1
        $fileSystem->write($classPath, $replace($templateContent, [
236 1
            '{CLASS_NAME}'   => $class,
237 1
            '{COMMAND_NAME}' => strtolower($class),
238 1
            '{TO_DO}'        => 'TODO',
239
        ]));
240
    }
241
242
    /**
243
     * @param ContainerInterface $container
244
     *
245
     * @return string
246
     *
247
     * @throws ContainerExceptionInterface
248
     * @throws NotFoundExceptionInterface
249
     */
250 3
    private function getCommandsFolder(ContainerInterface $container): string
251
    {
252 3
        assert($container->has(CacheSettingsProviderInterface::class));
253
254
        /** @var CacheSettingsProviderInterface $provider */
255 3
        $provider  = $container->get(CacheSettingsProviderInterface::class);
256 3
        $appConfig = $provider->getApplicationConfiguration();
257 3
        $folder    = $appConfig[S::KEY_COMMANDS_FOLDER];
258
259 3
        return $folder;
260
    }
261
262
    /**
263
     * @param ContainerInterface $container
264
     *
265
     * @return FileSystemInterface
266
     *
267
     * @throws ContainerExceptionInterface
268
     * @throws NotFoundExceptionInterface
269
     */
270 4
    private function getFileSystem(ContainerInterface $container): FileSystemInterface
271
    {
272 4
        assert($container->has(FileSystemInterface::class));
273
274
        /** @var FileSystemInterface $fileSystem */
275 4
        $fileSystem = $container->get(FileSystemInterface::class);
276
277 4
        return $fileSystem;
278
    }
279
}
280