Completed
Push — master ( 8d7108...7d2cce )
by Neomerx
11:09
created

CommandsCommand::executeCreate()   C

Complexity

Conditions 7
Paths 4

Size

Total Lines 48
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 7

Importance

Changes 0
Metric Value
dl 0
loc 48
ccs 30
cts 30
cp 1
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 29
nc 4
nop 2
crap 7
1
<?php namespace Limoncello\Commands;
2
3
/**
4
 * Copyright 2015-2017 [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 Symfony\Component\Console\Input\InputArgument;
33
use Symfony\Component\Console\Input\InputInterface;
34
use Symfony\Component\Console\Output\OutputInterface;
35
36
/**
37
 * This is a special command which is immediately available from composer. The main purpose of it is to
38
 * load command list from user application and generate a special cache file with the list. On the next
39
 * composer run the list would be loaded into composer and all the commands would be available.
40
 *
41
 * Also it provides such a nice feature as generation of an empty/template command for the developer.
42
 *
43
 * @package Limoncello\Commands
44
 */
45
class CommandsCommand extends BaseCommand
46
{
47
    use CommandTrait, CommandSerializationTrait, CacheFilePathTrait;
48
49
    /**
50
     * Command name.
51
     */
52
    const NAME = 'l:commands';
53
54
    /** Argument name */
55
    const ARG_ACTION = 'action';
56
57
    /** Command action */
58
    const ACTION_CONNECT = 'connect';
59
60
    /** Command action */
61
    const ACTION_CREATE = 'create';
62
63
    /** Argument name */
64
    const ARG_CLASS = 'class';
65
66
    /**
67
     * Constructor.
68
     */
69 9
    public function __construct()
70
    {
71 9
        parent::__construct(static::NAME);
72
    }
73
74
    /**
75
     * @inheritdoc
76
     */
77 9
    public function configure()
78
    {
79 9
        parent::configure();
80
81 9
        $connect    = static::ACTION_CONNECT;
82 9
        $create     = static::ACTION_CREATE;
83 9
        $actionDesc = "Required action such as `$connect` to find and connect commands from application and plugins " .
84 9
            "or `$create` to create an empty command template.";
85
86 9
        $classDesc = "Required valid class name in commands' namespace for action `$create`.";
87
88
        $this
89 9
            ->setDescription('Manages commands executed from composer.')
90 9
            ->setHelp('This command connects plugin and user-defined commands and creates new commands.')
91 9
            ->setDefinition([
92 9
                new InputArgument(static::ARG_ACTION, InputArgument::REQUIRED, $actionDesc),
93 9
                new InputArgument(static::ARG_CLASS, InputArgument::OPTIONAL, $classDesc),
94
            ]);
95
    }
96
97
98
    /** @noinspection PhpMissingParentCallCommonInspection
99
     * @inheritdoc
100
     */
101 8
    public function execute(InputInterface $input, OutputInterface $output)
102
    {
103 8
        $inOut     = $this->wrapIo($input, $output);
104 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...
105
106 8
        $argAction = static::ARG_ACTION;
107 8
        $action    = $inOut->getArgument($argAction);
108
        switch ($action) {
109 8
            case static::ACTION_CONNECT:
110 3
                $this->executeConnect($container, $inOut);
111 3
                break;
112 5
            case static::ACTION_CREATE:
113 4
                $this->executeCreate($container, $inOut);
114 4
                break;
115
            default:
116 1
                $inOut->writeError("Unknown value `$action` for argument `$argAction`." . PHP_EOL);
117 1
                break;
118
        }
119
    }
120
121
    /**
122
     * @param ContainerInterface $container
123
     * @param IoInterface        $inOut
124
     *
125
     * @return void
126
     *
127
     * @throws ContainerExceptionInterface
128
     * @throws NotFoundExceptionInterface
129
     */
130 3
    private function executeConnect(ContainerInterface $container, IoInterface $inOut): void
131
    {
132 3
        assert($container->has(CommandStorageInterface::class));
133
        /** @var CommandStorageInterface $commandStorage */
134 3
        $commandStorage = $container->get(CommandStorageInterface::class);
135
136 3
        $commandClasses = [];
137 3
        foreach ($commandStorage->getAll() as $commandClass) {
138 3
            if (class_exists($commandClass) === false ||
139 3
                array_key_exists(CommandInterface::class, class_implements($commandClass)) === false
140
            ) {
141 1
                $inOut->writeWarning("Class `$commandClass` either do not exist or not a command class." . PHP_EOL);
142 1
                continue;
143
            }
144
145 2
            $inOut->writeInfo("Found command class `$commandClass`." . PHP_EOL, IoInterface::VERBOSITY_VERBOSE);
146
147 2
            $commandClasses[] = $this->commandClassToArray($commandClass);
148
        }
149
150 3
        if (empty($commandClasses) === false) {
151 2
            $now           = date(DATE_RFC2822);
152 2
            $data          = var_export($commandClasses, true);
153
            $content       = <<<EOT
154
<?php
155
156
// THIS FILE IS AUTO GENERATED. DO NOT EDIT IT MANUALLY.
157 2
// Generated at: $now
158
159 2
    return $data;
160
161
EOT;
162 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...
163 2
            if (empty($cacheFilePath) === true) {
164 1
                $inOut->writeError("Commands cache file path is not set. Check your `Application` settings." . PHP_EOL);
165
166 1
                return;
167
            }
168
169 1
            $this->getFileSystem($container)->write($cacheFilePath, $content);
170
171 1
            $inOut->writeInfo('Commands connected.' . PHP_EOL);
172
173 1
            return;
174
        }
175
176 1
        $inOut->writeWarning('No commands found.' . PHP_EOL);
177
    }
178
179
    /**
180
     * @param ContainerInterface $container
181
     * @param IoInterface        $inOut
182
     *
183
     * @return void
184
     *
185
     * @throws ContainerExceptionInterface
186
     * @throws NotFoundExceptionInterface
187
     */
188 4
    private function executeCreate(ContainerInterface $container, IoInterface $inOut): void
189
    {
190 4
        $argClass = static::ARG_CLASS;
191 4
        if ($inOut->hasArgument($argClass) === false) {
192 1
            $inOut->writeError("Argument `$argClass` is not provided." . PHP_EOL);
193
194 1
            return;
195
        }
196 3
        $class = $inOut->getArgument($argClass);
197
198 3
        $fileSystem     = $this->getFileSystem($container);
199 3
        $commandsFolder = $this->getCommandsFolder($container);
200 3
        if (empty($commandsFolder) === true || $fileSystem->isFolder($commandsFolder) === false) {
201 1
            $inOut->writeError(
202 1
                "Commands folder `$commandsFolder` is not valid. Check your `Application` settings." . PHP_EOL
203
            );
204
205 1
            return;
206
        }
207
208 2
        $classPath = $commandsFolder . DIRECTORY_SEPARATOR . $class . '.php';
209 2
        if (ctype_alpha($class) === false ||
210 2
            $fileSystem->exists($classPath) === true
211
        ) {
212 1
            $inOut->writeError(
213 1
                "Class name `$class` does not look valid for a command. " .
214 1
                'Can you please choose another one?' . PHP_EOL
215
            );
216
217 1
            return;
218
        }
219
220 1
        $replace = function (string $template, iterable $parameters): string {
221 1
            $result = $template;
222 1
            foreach ($parameters as $key => $value) {
223 1
                $result = str_replace($key, $value, $result);
224
            }
225
226 1
            return $result;
227 1
        };
228
229 1
        $templateContent = $fileSystem->read(__DIR__ . DIRECTORY_SEPARATOR . 'SampleCommand.txt');
230 1
        $fileSystem->write($classPath, $replace($templateContent, [
231 1
            '{CLASS_NAME}'   => $class,
232 1
            '{COMMAND_NAME}' => strtolower($class),
233 1
            '{TO_DO}'        => 'TODO',
234
        ]));
235
    }
236
237
    /**
238
     * @param ContainerInterface $container
239
     *
240
     * @return string
241
     *
242
     * @throws ContainerExceptionInterface
243
     * @throws NotFoundExceptionInterface
244
     */
245 3
    private function getCommandsFolder(ContainerInterface $container): string
246
    {
247 3
        assert($container->has(CacheSettingsProviderInterface::class));
248
249
        /** @var CacheSettingsProviderInterface $provider */
250 3
        $provider  = $container->get(CacheSettingsProviderInterface::class);
251 3
        $appConfig = $provider->getApplicationConfiguration();
252 3
        $folder    = $appConfig[S::KEY_COMMANDS_FOLDER];
253
254 3
        return $folder;
255
    }
256
257
    /**
258
     * @param ContainerInterface $container
259
     *
260
     * @return FileSystemInterface
261
     *
262
     * @throws ContainerExceptionInterface
263
     * @throws NotFoundExceptionInterface
264
     */
265 4
    private function getFileSystem(ContainerInterface $container): FileSystemInterface
266
    {
267 4
        assert($container->has(FileSystemInterface::class));
268
269
        /** @var FileSystemInterface $fileSystem */
270 4
        $fileSystem = $container->get(FileSystemInterface::class);
271
272 4
        return $fileSystem;
273
    }
274
}
275