Completed
Push — master ( 0bfae6...7513f1 )
by Bernhard
04:21
created

BindCommandHandler::handleAdd()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 31
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 4

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 31
ccs 17
cts 17
cp 1
rs 8.5806
cc 4
eloc 21
nc 4
nop 1
crap 4
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\Binding\ClassBinding;
18
use Puli\Discovery\Binding\ResourceBinding;
19
use Puli\Manager\Api\Discovery\BindingDescriptor;
20
use Puli\Manager\Api\Discovery\BindingState;
21
use Puli\Manager\Api\Discovery\DiscoveryManager;
22
use Puli\Manager\Api\Package\PackageCollection;
23
use Puli\Manager\Api\Package\RootPackage;
24
use RuntimeException;
25
use Webmozart\Console\Api\Args\Args;
26
use Webmozart\Console\Api\IO\IO;
27
use Webmozart\Console\UI\Component\Table;
28
use Webmozart\Expression\Expr;
29
use Webmozart\PathUtil\Path;
30
31
/**
32
 * Handles the "bind" command.
33
 *
34
 * @since  1.0
35
 *
36
 * @author Bernhard Schussek <[email protected]>
37
 */
38
class BindCommandHandler
39
{
40
    /**
41
     * @var DiscoveryManager
42
     */
43
    private $discoveryManager;
44
45
    /**
46
     * @var PackageCollection
47
     */
48
    private $packages;
49
50
    /**
51
     * @var string
52
     */
53
    private $currentPath = '/';
54
55
    /**
56
     * Creates the handler.
57
     *
58
     * @param DiscoveryManager  $discoveryManager The discovery manager.
59
     * @param PackageCollection $packages         The loaded packages.
60
     */
61 39
    public function __construct(DiscoveryManager $discoveryManager, PackageCollection $packages)
62
    {
63 39
        $this->discoveryManager = $discoveryManager;
64 39
        $this->packages = $packages;
65 39
    }
66
67
    /**
68
     * Handles the "bind --list" command.
69
     *
70
     * @param Args $args The console arguments.
71
     * @param IO   $io   The I/O.
72
     *
73
     * @return int The status code.
74
     */
75 14
    public function handleList(Args $args, IO $io)
76
    {
77 14
        $packageNames = ArgsUtil::getPackageNames($args, $this->packages);
78 14
        $bindingStates = $this->getBindingStates($args);
79
80 14
        $printBindingState = count($bindingStates) > 1;
81 14
        $printPackageName = count($packageNames) > 1;
82 14
        $printHeaders = $printBindingState || $printPackageName;
83 14
        $indentation = $printBindingState && $printPackageName ? 8
84 14
            : ($printBindingState || $printPackageName ? 4 : 0);
85
86 14
        foreach ($bindingStates as $bindingState) {
87 14
            $bindingStatePrinted = !$printBindingState;
88
89 14
            foreach ($packageNames as $packageName) {
90 14
                $expr = Expr::method('getContainingPackage', Expr::method('getName', Expr::same($packageName)))
91 14
                    ->andMethod('getState', Expr::same($bindingState));
92
93 14
                $descriptors = $this->discoveryManager->findBindingDescriptors($expr);
94
95 14
                if (empty($descriptors)) {
96 1
                    continue;
97
                }
98
99 14
                if (!$bindingStatePrinted) {
100 7
                    $this->printBindingStateHeader($io, $bindingState);
101 7
                    $bindingStatePrinted = true;
102
                }
103
104 14 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 10
                    $prefix = $printBindingState ? '    ' : '';
106 10
                    $io->writeLine(sprintf('%sPackage: %s', $prefix, $packageName));
107 10
                    $io->writeLine('');
108
                }
109
110 14
                $this->printBindingTable($io, $descriptors, $indentation, BindingState::ENABLED === $bindingState);
111
112 14
                if ($printHeaders) {
113 14
                    $io->writeLine('');
114
                }
115
            }
116
        }
117
118 14
        return 0;
119
    }
120
121
    /**
122
     * Handles the "bind <query> <type>" command.
123
     *
124
     * @param Args $args The console arguments.
125
     *
126
     * @return int The status code.
127
     */
128 8
    public function handleAdd(Args $args)
129
    {
130 8
        $flags = $args->isOptionSet('force')
131 1
            ? DiscoveryManager::OVERRIDE | DiscoveryManager::IGNORE_TYPE_NOT_FOUND
132 1
                | DiscoveryManager::IGNORE_TYPE_NOT_ENABLED
133 8
            : 0;
134
135 8
        $bindingParams = array();
136 8
        $artifact = $args->getArgument('artifact');
137
138 8
        $this->parseParams($args, $bindingParams);
139
140 7
        if (false !== strpos($artifact, '\\') || $args->isOptionSet('class')) {
141 2
            $binding = new ClassBinding(
142
                $artifact,
143 2
                $args->getArgument('type'),
144
                $bindingParams
145
            );
146
        } else {
147 5
            $binding = new ResourceBinding(
148 5
                Path::makeAbsolute($artifact, $this->currentPath),
149 5
                $args->getArgument('type'),
150
                $bindingParams,
151 5
                $args->getOption('language')
152
            );
153
        }
154
155 7
        $this->discoveryManager->addRootBindingDescriptor(new BindingDescriptor($binding), $flags);
156
157 7
        return 0;
158
    }
159
160
    /**
161
     * Handles the "bind --update <uuid>" command.
162
     *
163
     * @param Args $args The console arguments.
164
     *
165
     * @return int The status code.
166
     */
167 7
    public function handleUpdate(Args $args)
168
    {
169 7
        $flags = $args->isOptionSet('force')
170 1
            ? DiscoveryManager::OVERRIDE | DiscoveryManager::IGNORE_TYPE_NOT_FOUND
171 1
                | DiscoveryManager::IGNORE_TYPE_NOT_ENABLED
172 7
            : DiscoveryManager::OVERRIDE;
173
174 7
        $descriptorToUpdate = $this->getBindingByUuidPrefix($args->getArgument('uuid'));
175 7
        $bindingToUpdate = $descriptorToUpdate->getBinding();
176
177 7
        if (!$descriptorToUpdate->getContainingPackage() instanceof RootPackage) {
178 1
            throw new RuntimeException(sprintf(
179 1
                'Can only update bindings in the package "%s".',
180 1
                $this->packages->getRootPackageName()
181
            ));
182
        }
183
184 6
        if ($bindingToUpdate instanceof ResourceBinding) {
185 5
            $updatedBinding = $this->getUpdatedResourceBinding($args, $bindingToUpdate);
186
        } elseif ($bindingToUpdate instanceof ClassBinding) {
187 1
            $updatedBinding = $this->getUpdatedClassBinding($args, $bindingToUpdate);
188
        } else {
189
            throw new RuntimeException(sprintf(
190
                'Cannot update bindings of type %s.',
191
                get_class($bindingToUpdate)
192
            ));
193
        }
194
195 6
        $updatedDescriptor = new BindingDescriptor($updatedBinding);
196
197 6
        if ($this->bindingsEqual($descriptorToUpdate, $updatedDescriptor)) {
198 1
            throw new RuntimeException('Nothing to update.');
199
        }
200
201 5
        $this->discoveryManager->addRootBindingDescriptor($updatedDescriptor, $flags);
202
203 5
        return 0;
204
    }
205
206
    /**
207
     * Handles the "bind --delete" command.
208
     *
209
     * @param Args $args The console arguments.
210
     *
211
     * @return int The status code.
212
     */
213 4 View Code Duplication
    public function handleDelete(Args $args)
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...
214
    {
215 4
        $bindingToRemove = $this->getBindingByUuidPrefix($args->getArgument('uuid'));
216
217 2
        if (!$bindingToRemove->getContainingPackage() instanceof RootPackage) {
218 1
            throw new RuntimeException(sprintf(
219 1
                'Can only delete bindings from the package "%s".',
220 1
                $this->packages->getRootPackageName()
221
            ));
222
        }
223
224 1
        $this->discoveryManager->removeRootBindingDescriptor($bindingToRemove->getUuid());
225
226 1
        return 0;
227
    }
228
229
    /**
230
     * Handles the "bind --enable" command.
231
     *
232
     * @param Args $args The console arguments.
233
     *
234
     * @return int The status code.
235
     */
236 3 View Code Duplication
    public function handleEnable(Args $args)
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...
237
    {
238 3
        $bindingToEnable = $this->getBindingByUuidPrefix($args->getArgument('uuid'));
239
240 2
        if ($bindingToEnable->getContainingPackage() instanceof RootPackage) {
241 1
            throw new RuntimeException(sprintf(
242 1
                'Cannot enable bindings in the package "%s".',
243 1
                $bindingToEnable->getContainingPackage()->getName()
244
            ));
245
        }
246
247 1
        $this->discoveryManager->enableBindingDescriptor($bindingToEnable->getUuid());
248
249 1
        return 0;
250
    }
251
252
    /**
253
     * Handles the "bind --disable" command.
254
     *
255
     * @param Args $args The console arguments.
256
     *
257
     * @return int The status code.
258
     */
259 3 View Code Duplication
    public function handleDisable(Args $args)
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...
260
    {
261 3
        $bindingToDisable = $this->getBindingByUuidPrefix($args->getArgument('uuid'));
262
263 2
        if ($bindingToDisable->getContainingPackage() instanceof RootPackage) {
264 1
            throw new RuntimeException(sprintf(
265 1
                'Cannot disable bindings in the package "%s".',
266 1
                $bindingToDisable->getContainingPackage()->getName()
267
            ));
268
        }
269
270 1
        $this->discoveryManager->disableBindingDescriptor($bindingToDisable->getUuid());
271
272 1
        return 0;
273
    }
274
275
    /**
276
     * Returns the binding states selected in the console arguments.
277
     *
278
     * @param Args $args The console arguments.
279
     *
280
     * @return int[] The selected {@link BindingState} constants.
0 ignored issues
show
Documentation introduced by
Should the return type not be array<integer|string>?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
281
     */
282 14
    private function getBindingStates(Args $args)
283
    {
284
        $states = array(
285 14
            BindingState::ENABLED => 'enabled',
286 14
            BindingState::DISABLED => 'disabled',
287 14
            BindingState::TYPE_NOT_FOUND => 'type-not-found',
288 14
            BindingState::TYPE_NOT_ENABLED => 'type-not-enabled',
289 14
            BindingState::INVALID => 'invalid',
290
        );
291
292 14
        $states = array_filter($states, function ($option) use ($args) {
293 14
            return $args->isOptionSet($option);
294 14
        });
295
296 14
        return array_keys($states) ?: BindingState::all();
297
    }
298
299
    /**
300
     * Prints a list of binding descriptors.
301
     *
302
     * @param IO                  $io          The I/O.
303
     * @param BindingDescriptor[] $descriptors The binding descriptors.
304
     * @param int                 $indentation The number of spaces to indent.
305
     * @param bool                $enabled     Whether the binding descriptors
306
     *                                         are enabled. If not, the output
307
     *                                         is printed in red.
308
     */
309 14
    private function printBindingTable(IO $io, array $descriptors, $indentation = 0, $enabled = true)
310
    {
311 14
        $table = new Table(PuliTableStyle::borderless());
312
313 14
        $table->setHeaderRow(array('UUID', 'Glob', 'Type'));
314
315 14
        $paramTag = $enabled ? 'c1' : 'bad';
316 14
        $artifactTag = $enabled ? 'c1' : 'bad';
317 14
        $typeTag = $enabled ? 'u' : 'bad';
318
319 14
        foreach ($descriptors as $descriptor) {
320 14
            $parameters = array();
321 14
            $binding = $descriptor->getBinding();
322
323 14
            foreach ($binding->getParameterValues() as $parameterName => $parameterValue) {
324 1
                $parameters[] = $parameterName.'='.StringUtil::formatValue($parameterValue);
325
            }
326
327 14
            $uuid = substr($descriptor->getUuid(), 0, 6);
328
329 14
            if (!$enabled) {
330 10
                $uuid = sprintf('<bad>%s</bad>', $uuid);
331
            }
332
333 14
            $paramString = '';
334
335 14
            if (!empty($parameters)) {
336
                // \xc2\xa0 is a non-breaking space
337 1
                $paramString = sprintf(
338 1
                    ' <%s>(%s)</%s>',
339
                    $paramTag,
340 1
                    implode(",\xc2\xa0", $parameters),
341
                    $paramTag
342
                );
343
            }
344
345 14
            if ($binding instanceof ResourceBinding) {
346 14
                $artifact = $binding->getQuery();
347
            } elseif ($binding instanceof ClassBinding) {
348 6
                $artifact = StringUtil::getShortClassName($binding->getClassName());
349
            } else {
350
                continue;
351
            }
352
353 14
            $typeString = StringUtil::getShortClassName($binding->getTypeName());
354
355 14
            $table->addRow(array(
356 14
                $uuid,
357 14
                sprintf('<%s>%s</%s>', $artifactTag, $artifact, $artifactTag),
358 14
                sprintf('<%s>%s</%s>%s', $typeTag, $typeString, $typeTag, $paramString),
359
            ));
360
        }
361
362 14
        $table->render($io, $indentation);
363 14
    }
364
365
    /**
366
     * Prints the header for a binding state.
367
     *
368
     * @param IO  $io           The I/O.
369
     * @param int $bindingState The {@link BindingState} constant.
370
     */
371 7
    private function printBindingStateHeader(IO $io, $bindingState)
372
    {
373
        switch ($bindingState) {
374 7
            case BindingState::ENABLED:
375 7
                $io->writeLine('The following bindings are currently enabled:');
376 7
                $io->writeLine('');
377
378 7
                return;
379 6
            case BindingState::DISABLED:
380 6
                $io->writeLine('The following bindings are disabled:');
381 6
                $io->writeLine(' (use "puli bind --enable <uuid>" to enable)');
382 6
                $io->writeLine('');
383
384 6
                return;
385 5
            case BindingState::TYPE_NOT_FOUND:
386 5
                $io->writeLine('The types of the following bindings could not be found:');
387 5
                $io->writeLine(' (install or create their type definitions to enable)');
388 5
                $io->writeLine('');
389
390 5
                return;
391 5
            case BindingState::TYPE_NOT_ENABLED:
392 5
                $io->writeLine('The types of the following bindings are not enabled:');
393 5
                $io->writeLine(' (remove the duplicate type definitions to enable)');
394 5
                $io->writeLine('');
395
396 5
                return;
397 5
            case BindingState::INVALID:
398 5
                $io->writeLine('The following bindings have invalid parameters:');
399 5
                $io->writeLine(' (remove the binding and add again with correct parameters)');
400 5
                $io->writeLine('');
401
402 5
                return;
403
        }
404
    }
405
406 14
    private function parseParams(Args $args, array &$bindingParams)
407
    {
408 14
        foreach ($args->getOption('param') as $parameter) {
409 4
            $pos = strpos($parameter, '=');
410
411 4
            if (false === $pos) {
412 1
                throw new RuntimeException(sprintf(
413
                    'The "--param" option expects a parameter in the form '.
414 1
                    '"key=value". Got: "%s"',
415
                    $parameter
416
                ));
417
            }
418
419 3
            $key = substr($parameter, 0, $pos);
420 3
            $value = StringUtil::parseValue(substr($parameter, $pos + 1));
421
422 3
            $bindingParams[$key] = $value;
423
        }
424 13
    }
425
426 6
    private function unsetParams(Args $args, array &$bindingParams)
427
    {
428 6
        foreach ($args->getOption('unset-param') as $parameter) {
429 1
            unset($bindingParams[$parameter]);
430
        }
431 6
    }
432
433
    /**
434
     * @param string $uuidPrefix
435
     *
436
     * @return BindingDescriptor
437
     */
438 17 View Code Duplication
    private function getBindingByUuidPrefix($uuidPrefix)
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...
439
    {
440 17
        $expr = Expr::method('getUuid', Expr::startsWith($uuidPrefix));
441 17
        $descriptors = $this->discoveryManager->findBindingDescriptors($expr);
442
443 17
        if (0 === count($descriptors)) {
444 3
            throw new RuntimeException(sprintf('The binding "%s" does not exist.', $uuidPrefix));
445
        }
446
447 14
        if (count($descriptors) > 1) {
448 1
            throw new RuntimeException(sprintf(
449 1
                'More than one binding matches the UUID prefix "%s".',
450
                $uuidPrefix
451
            ));
452
        }
453
454 13
        return reset($descriptors);
455
    }
456
457 6
    private function bindingsEqual(BindingDescriptor $descriptor1, BindingDescriptor $descriptor2)
458
    {
459 6
        return $descriptor1->getBinding() == $descriptor2->getBinding();
460
    }
461
462
    /**
463
     * @param Args            $args
464
     * @param ResourceBinding $bindingToUpdate
465
     *
466
     * @return ResourceBinding
467
     */
468 5
    private function getUpdatedResourceBinding(Args $args, ResourceBinding $bindingToUpdate)
469
    {
470 5
        $query = $bindingToUpdate->getQuery();
471 5
        $typeName = $bindingToUpdate->getTypeName();
472 5
        $language = $bindingToUpdate->getLanguage();
473 5
        $bindingParams = $bindingToUpdate->getParameterValues();
474
475 5
        if ($args->isOptionSet('query')) {
476 3
            $query = $args->getOption('query');
477
        }
478
479 5
        if ($args->isOptionSet('type')) {
480 1
            $typeName = $args->getOption('type');
481
        }
482
483 5
        if ($args->isOptionSet('language')) {
484 1
            $language = $args->getOption('language');
485
        }
486
487 5
        $this->parseParams($args, $bindingParams);
488 5
        $this->unsetParams($args, $bindingParams);
489
490 5
        return new ResourceBinding(
491 5
            Path::makeAbsolute($query, $this->currentPath),
492
            $typeName,
493
            $bindingParams,
494
            $language,
495 5
            $bindingToUpdate->getUuid()
496
        );
497
    }
498
499
    /**
500
     * @param Args         $args
501
     * @param ClassBinding $bindingToUpdate
502
     *
503
     * @return ClassBinding
504
     */
505 1
    private function getUpdatedClassBinding(Args $args, ClassBinding $bindingToUpdate)
506
    {
507 1
        $className = $bindingToUpdate->getClassName();
508 1
        $typeName = $bindingToUpdate->getTypeName();
509 1
        $bindingParams = $bindingToUpdate->getParameterValues();
510
511 1
        if ($args->isOptionSet('class')) {
512 1
            $className = $args->getOption('class');
513
        }
514
515 1
        if ($args->isOptionSet('type')) {
516 1
            $typeName = $args->getOption('type');
517
        }
518
519 1
        $this->parseParams($args, $bindingParams);
520 1
        $this->unsetParams($args, $bindingParams);
521
522 1
        return new ClassBinding(
523
            $className,
524
            $typeName,
525
            $bindingParams,
526 1
            $bindingToUpdate->getUuid()
527
        );
528
    }
529
}
530