Failed Conditions
Pull Request — master (#40)
by Tobias
06:48
created

TypeCommandHandler::detectBindingClass()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3.072

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 9
ccs 4
cts 5
cp 0.8
rs 9.6666
cc 3
eloc 5
nc 2
nop 1
crap 3.072
1
<?php
2
3
/*
4
 * This file is part of the puli/cli package.
5
 *
6
 * (c) Bernhard Schussek <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Puli\Cli\Handler;
13
14
use Puli\Cli\Style\PuliTableStyle;
15
use Puli\Cli\Util\ArgsUtil;
16
use Puli\Cli\Util\StringUtil;
17
use Puli\Discovery\Api\Type\BindingParameter;
18
use Puli\Discovery\Api\Type\BindingType;
19
use Puli\Manager\Api\Discovery\BindingTypeDescriptor;
20
use Puli\Manager\Api\Discovery\BindingTypeState;
21
use Puli\Manager\Api\Discovery\DiscoveryManager;
22
use Puli\Manager\Api\Package\PackageCollection;
23
use RuntimeException;
24
use Webmozart\Console\Api\Args\Args;
25
use Webmozart\Console\Api\IO\IO;
26
use Webmozart\Console\UI\Component\Table;
27
use Webmozart\Expression\Expr;
28
29
/**
30
 * Handles the "puli type" command.
31
 *
32
 * @since  1.0
33
 *
34
 * @author Bernhard Schussek <[email protected]>
35
 */
36
class TypeCommandHandler
37
{
38
    /**
39
     * @var DiscoveryManager
40
     */
41
    private $discoveryManager;
42
43
    /**
44
     * @var PackageCollection
45
     */
46
    private $packages;
47
48
    /**
49
     * Creates the handler.
50
     *
51
     * @param DiscoveryManager  $discoveryManager The discovery manager.
52
     * @param PackageCollection $packages         The loaded packages.
53
     */
54 25
    public function __construct(DiscoveryManager $discoveryManager, PackageCollection $packages)
55
    {
56 25
        $this->discoveryManager = $discoveryManager;
57 25
        $this->packages = $packages;
58 25
    }
59
60
    /**
61
     * Handles the "puli type --list" command.
62
     *
63
     * @param Args $args The console arguments.
64
     * @param IO   $io   The I/O.
65
     *
66
     * @return int The status code.
67
     */
68 11
    public function handleList(Args $args, IO $io)
69
    {
70 11
        $packageNames = ArgsUtil::getPackageNames($args, $this->packages);
71 11
        $states = $this->getBindingTypeStates($args);
72
73 11
        $printStates = count($states) > 1;
74 11
        $printPackageName = count($packageNames) > 1;
75 11
        $printHeaders = $printStates || $printPackageName;
76 11
        $printTypeAdvice = true;
77 11
        $printBindAdvice = false;
78 11
        $indentation = $printStates && $printPackageName ? 8
79 11
            : ($printStates || $printPackageName ? 4 : 0);
80
81 11
        foreach ($states as $state) {
82 11
            $statePrinted = !$printStates;
83
84 11
            foreach ($packageNames as $packageName) {
85 11
                $expr = Expr::method('getContainingPackage', Expr::method('getName', Expr::same($packageName)))
86 11
                    ->andMethod('getState', Expr::same($state));
87
88 11
                $bindingTypes = $this->discoveryManager->findTypeDescriptors($expr);
89
90 11
                if (!$bindingTypes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $bindingTypes of type Puli\Manager\Api\Discovery\BindingTypeDescriptor[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
91 1
                    continue;
92
                }
93
94 10
                $printTypeAdvice = false;
95
96 10
                if (!$statePrinted) {
97 6
                    $this->printBindingTypeState($io, $state);
98 6
                    $statePrinted = true;
99
100
                    // Only print the advice if at least one type was printed
101 6
                    $printBindAdvice = true;
102
                }
103
104 10 View Code Duplication
                if ($printPackageName) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
105 6
                    $prefix = $printStates ? '    ' : '';
106 6
                    $io->writeLine("{$prefix}Package: $packageName");
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $prefix instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $packageName instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
107 6
                    $io->writeLine('');
108
                }
109
110 10
                $styleTag = BindingTypeState::ENABLED === $state ? null : 'bad';
111
112 10
                $this->printTypeTable($io, $bindingTypes, $styleTag, $indentation);
113
114 10
                if ($printHeaders) {
115 11
                    $io->writeLine('');
116
                }
117
            }
118
        }
119
120 11
        if ($printTypeAdvice) {
121 1
            $io->writeLine('No types defined. Use "puli type --define <name>" to define a type.');
122
        }
123 11
        if ($printBindAdvice) {
124 6
            $io->writeLine('Use "puli bind <resource> <type>" to bind a resource to a type.');
125
        }
126
127 11
        return 0;
128
    }
129
130
    /**
131
     * Handles the "puli type --define" command.
132
     *
133
     * @param Args $args The console arguments.
134
     *
135
     * @return int The status code.
136
     */
137 6
    public function handleDefine(Args $args)
138
    {
139 6
        $flags = $args->isOptionSet('force') ? DiscoveryManager::OVERRIDE : 0;
140 6
        $bindingParams = array();
141 6
        $paramDescriptions = array();
142
143 6
        $this->parseParamDescriptions($args, $paramDescriptions);
144 6
        $this->parseParams($args, $bindingParams);
145
146 6
        $name = $args->getArgument('name');
147 6
        $this->discoveryManager->addRootTypeDescriptor(new BindingTypeDescriptor(
148 6
            new BindingType($name, $this->detectBindingClass($name), $bindingParams),
149 6
            $args->getOption('description'),
150
            $paramDescriptions
151
        ), $flags);
152
153 6
        return 0;
154
    }
155
156
    /**
157
     * Handles the "puli type --update" command.
158
     *
159
     * @param Args $args The console arguments.
160
     *
161
     * @return int The status code.
162
     */
163 6
    public function handleUpdate(Args $args)
164
    {
165 6
        $name = $args->getArgument('name');
166 6
        $descriptorToUpdate = $this->discoveryManager->getRootTypeDescriptor($name);
167 6
        $bindingParams = $descriptorToUpdate->getType()->getParameters();
168 6
        $description = $descriptorToUpdate->getDescription();
169 6
        $paramDescriptions = $descriptorToUpdate->getParameterDescriptions();
170
171 6
        $this->parseParamDescriptions($args, $paramDescriptions);
172 6
        $this->parseParams($args, $bindingParams);
173 6
        $this->parseUnsetParams($args, $bindingParams, $paramDescriptions);
174
175 6
        if ($args->isOptionSet('description')) {
176 1
            $description = $args->getOption('description');
177
        }
178
179 6
        $updatedDescriptor = new BindingTypeDescriptor(
180 6
            new BindingType($name, $this->detectBindingClass($name), $bindingParams),
181
            $description,
182
            $paramDescriptions
183
        );
184
185 6
        if ($this->typesEqual($descriptorToUpdate, $updatedDescriptor)) {
186 1
            throw new RuntimeException('Nothing to update.');
187
        }
188
189 5
        $this->discoveryManager->addRootTypeDescriptor($updatedDescriptor, DiscoveryManager::OVERRIDE);
190
191 5
        return 0;
192
    }
193
194
    /**
195
     * Handles the "puli type --delete" command.
196
     *
197
     * @param Args $args The console arguments.
198
     *
199
     * @return int The status code.
200
     */
201 2
    public function handleDelete(Args $args)
202
    {
203 2
        $typeName = $args->getArgument('name');
204
205 2
        if (!$this->discoveryManager->hasRootTypeDescriptor($typeName)) {
206 1
            throw new RuntimeException(sprintf(
207 1
                'The type "%s" does not exist in the package "%s".',
208
                $typeName,
209 1
                $this->packages->getRootPackageName()
210
            ));
211
        }
212
213 1
        $this->discoveryManager->removeRootTypeDescriptor($typeName);
214
215 1
        return 0;
216
    }
217
218
    /**
219
     * Returns the binding type states selected in the console arguments.
220
     *
221
     * @param Args $args The console arguments.
222
     *
223
     * @return int[] A list of {@link BindingTypeState} constants.
224
     */
225 11
    private function getBindingTypeStates(Args $args)
226
    {
227 11
        $states = array();
228
229 11
        if ($args->isOptionSet('enabled')) {
230 4
            $states[] = BindingTypeState::ENABLED;
231
        }
232
233 11
        if ($args->isOptionSet('duplicate')) {
234 2
            $states[] = BindingTypeState::DUPLICATE;
235
        }
236
237 11
        return $states ?: BindingTypeState::all();
238
    }
239
240
    /**
241
     * Prints the binding types in a table.
242
     *
243
     * @param IO                      $io          The I/O.
244
     * @param BindingTypeDescriptor[] $descriptors The type descriptors to print.
245
     * @param string                  $styleTag    The tag used to style the output
0 ignored issues
show
Documentation introduced by
Should the type for parameter $styleTag not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
246
     * @param int                     $indentation The number of spaces to indent.
247
     */
248 10
    private function printTypeTable(IO $io, array $descriptors, $styleTag = null, $indentation = 0)
249
    {
250 10
        $table = new Table(PuliTableStyle::borderless());
251
252 10
        $table->setHeaderRow(array('Type', 'Description', 'Parameters'));
253
254 10
        $paramTag = $styleTag ?: 'c1';
255 10
        $typeTag = $styleTag ?: 'u';
256
257 10
        foreach ($descriptors as $descriptor) {
258 10
            $type = $descriptor->getType();
259 10
            $parameters = array();
260
261 10
            foreach ($type->getParameters() as $parameter) {
262 6
                $paramString = $parameter->isRequired()
263 6
                    ? $parameter->getName()
264 6
                    : $parameter->getName().'='.StringUtil::formatValue($parameter->getDefaultValue());
265
266 6
                $parameters[$parameter->getName()] = "<$paramTag>$paramString</$paramTag>";
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $paramTag instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $paramString instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
267
            }
268
269 10
            $description = $descriptor->getDescription();
270
271 10
            if ($styleTag) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $styleTag of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
272 7
                $description = "<$styleTag>$description</$styleTag>";
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $styleTag instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $description instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
273
            }
274
275 10
            ksort($parameters);
276
277 10
            $table->addRow(array(
278 10
                "<$typeTag>".$descriptor->getTypeName()."</$typeTag>",
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $typeTag instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
279 10
                $description,
280 10
                implode("\n", $parameters),
281
            ));
282
        }
283
284 10
        $table->render($io, $indentation);
285 10
    }
286
287
    /**
288
     * Prints the heading for a binding type state.
289
     *
290
     * @param IO  $io        The I/O.
291
     * @param int $typeState The {@link BindingTypeState} constant.
292
     */
293 6
    private function printBindingTypeState(IO $io, $typeState)
294
    {
295
        switch ($typeState) {
296 6
            case BindingTypeState::ENABLED:
297 6
                $io->writeLine('The following binding types are currently enabled:');
298 6
                $io->writeLine('');
299
300 6
                return;
301 6
            case BindingTypeState::DUPLICATE:
302 6
                $io->writeLine('The following types have duplicate definitions and are disabled:');
303 6
                $io->writeLine('');
304
305 6
                return;
306
        }
307
    }
308
309 12 View Code Duplication
    private function parseParamDescriptions(Args $args, array &$paramDescriptions)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
310
    {
311 12
        foreach ($args->getOption('param-description') as $paramDescription) {
312 2
            $pos = strpos($paramDescription, '=');
313
314 2
            if (false === $pos) {
315
                throw new RuntimeException(sprintf(
316
                    'The "--param-description" option expects a parameter in '.
317
                    'the form "key=value". Got: "%s"',
318
                    $paramDescription
319
                ));
320
            }
321
322 2
            $key = substr($paramDescription, 0, $pos);
323 2
            $paramDescriptions[$key] = StringUtil::parseValue(substr($paramDescription, $pos + 1));
324
        }
325 12
    }
326
327 12
    private function parseParams(Args $args, array &$bindingParams)
328
    {
329 12 View Code Duplication
        foreach ($args->getOption('param') as $parameter) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
330
            // Optional parameter with default value
331 5
            if (false !== ($pos = strpos($parameter, '='))) {
332 2
                $key = substr($parameter, 0, $pos);
333
334 2
                $bindingParams[$key] = new BindingParameter(
335
                    $key,
336 2
                    BindingParameter::OPTIONAL,
337 2
                    StringUtil::parseValue(substr($parameter, $pos + 1))
338
                );
339
340 2
                continue;
341
            }
342
343
            // Required parameter
344 3
            $bindingParams[$parameter] = new BindingParameter(
345
                $parameter,
346 3
                BindingParameter::REQUIRED,
347 3
                null
348
            );
349
        }
350 12
    }
351
352 6
    private function parseUnsetParams(Args $args, array &$bindingParams, array &$paramDescriptions)
353
    {
354 6
        foreach ($args->getOption('unset-param') as $parameterName) {
355 1
            unset($bindingParams[$parameterName]);
356 1
            unset($paramDescriptions[$parameterName]);
357
        }
358 6
    }
359
360 6
    private function typesEqual(BindingTypeDescriptor $descriptor1, BindingTypeDescriptor $descriptor2)
361
    {
362 6
        return $descriptor1->getTypeName() === $descriptor2->getTypeName() &&
363 6
            $descriptor1->getDescription() === $descriptor2->getDescription() &&
364 6
            $descriptor1->getParameterDescriptions() === $descriptor2->getParameterDescriptions() &&
365 6
            $descriptor1->getType()->getParameters() === $descriptor2->getType()->getParameters();
366
    }
367
368
    /**
369
     * Identify what type of binding class $name is.
370
     *
371
     * @param string $name
372
     */
373 12
    private function detectBindingClass($name)
374
    {
375 12
        $bindingClass = 'Puli\Repository\Discovery\ResourceBinding';
376 12
        if (class_exists($name) || interface_exists($name)) {
377
            $bindingClass = 'Puli\Discovery\Binding\ClassBinding';
378
        }
379
380 12
        return $bindingClass;
381
    }
382
}
383