Failed Conditions
Push — master ( d49c03...6343d0 )
by Bernhard
05:03
created

ModuleFileInstallerManager   C

Complexity

Total Complexity 74

Size/Duplication

Total Lines 487
Duplicated Lines 16.63 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 81
loc 487
wmc 74
lcom 1
cbo 11
ccs 192
cts 192
cp 1
rs 5.5244

24 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
B addRootInstallerDescriptor() 6 33 6
C removeRootInstallerDescriptor() 7 33 7
B removeRootInstallerDescriptors() 0 24 4
A clearRootInstallerDescriptors() 0 4 1
A getRootInstallerDescriptor() 0 10 2
A getRootInstallerDescriptors() 0 6 1
A findRootInstallerDescriptors() 14 14 3
A hasRootInstallerDescriptor() 0 6 1
A hasRootInstallerDescriptors() 16 16 4
A getInstallerDescriptor() 0 10 2
A getInstallerDescriptors() 0 6 1
A findInstallerDescriptors() 14 14 3
A hasInstallerDescriptor() 0 6 1
A hasInstallerDescriptors() 16 16 4
A assertInstallersLoaded() 0 16 4
A persistInstallersData() 0 14 3
C loadInstallers() 8 43 7
A dataToInstaller() 0 15 3
A dataToParameters() 0 10 2
B dataToParameter() 0 11 5
A installerToData() 0 16 3
A parametersToData() 0 10 2
A parameterToData() 0 18 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ModuleFileInstallerManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ModuleFileInstallerManager, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the puli/manager 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\Manager\Installer;
13
14
use Exception;
15
use Puli\Manager\Api\Installer\InstallerDescriptor;
16
use Puli\Manager\Api\Installer\InstallerManager;
17
use Puli\Manager\Api\Installer\InstallerParameter;
18
use Puli\Manager\Api\Installer\NoSuchInstallerException;
19
use Puli\Manager\Api\Module\Module;
20
use Puli\Manager\Api\Module\ModuleCollection;
21
use Puli\Manager\Api\Module\RootModule;
22
use Puli\Manager\Api\Module\RootModuleFileManager;
23
use RuntimeException;
24
use stdClass;
25
use Webmozart\Expression\Expr;
26
use Webmozart\Expression\Expression;
27
use Webmozart\Json\JsonValidator;
28
use Webmozart\Json\ValidationFailedException;
29
30
/**
31
 * An installer manager that stores the installers in the module file.
32
 *
33
 * @since  1.0
34
 *
35
 * @author Bernhard Schussek <[email protected]>
36
 */
37
class ModuleFileInstallerManager implements InstallerManager
38
{
39
    /**
40
     * The extra key that stores the installer data.
41
     */
42
    const INSTALLERS_KEY = 'installers';
43
44
    /**
45
     * @var array
46
     */
47
    private static $builtinInstallers = array(
48
        'copy' => array(
49
            'class' => 'Puli\Manager\Installer\CopyInstaller',
50
            'description' => 'Copies assets to a target directory',
51
        ),
52
        'symlink' => array(
53
            'class' => 'Puli\Manager\Installer\SymlinkInstaller',
54
            'description' => 'Creates asset symlinks in a target directory',
55
            'parameters' => array(
56
                'relative' => array(
57
                    'default' => true,
58
                    'description' => 'Whether to create relative or absolute links',
59
                ),
60
            ),
61
        ),
62
    );
63
64
    /**
65
     * @var RootModuleFileManager
66
     */
67
    private $rootModuleFileManager;
68
69
    /**
70
     * @var ModuleCollection
71
     */
72
    private $modules;
73
74
    /**
75
     * @var RootModule
76
     */
77
    private $rootModule;
78
79
    /**
80
     * @var InstallerDescriptor[]
81
     */
82
    private $installerDescriptors;
83
84
    /**
85
     * @var InstallerDescriptor[]
86
     */
87
    private $rootInstallerDescriptors;
88
89 82
    public function __construct(RootModuleFileManager $rootModuleFileManager, ModuleCollection $modules)
90
    {
91 82
        $this->rootModuleFileManager = $rootModuleFileManager;
92 82
        $this->modules = $modules;
93 82
        $this->rootModule = $modules->getRootModule();
94 82
    }
95
96
    /**
97
     * {@inheritdoc}
98
     */
99 14
    public function addRootInstallerDescriptor(InstallerDescriptor $descriptor)
100
    {
101 14
        $this->assertInstallersLoaded();
102
103 14
        $name = $descriptor->getName();
104
105 14
        $previouslySetInRoot = isset($this->rootInstallerDescriptors[$name]);
106 14
        $previousInstaller = $previouslySetInRoot ? $this->rootInstallerDescriptors[$name] : null;
107
108 14 View Code Duplication
        if (isset($this->installerDescriptors[$name]) && !$previouslySetInRoot) {
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...
109 4
            throw new RuntimeException(sprintf(
110 4
                'An installer with the name "%s" exists already.',
111
                $name
112
            ));
113
        }
114
115
        try {
116 10
            $this->installerDescriptors[$name] = $descriptor;
117 10
            $this->rootInstallerDescriptors[$name] = $descriptor;
118
119 10
            $this->persistInstallersData();
120 4
        } catch (Exception $e) {
121 4
            if ($previouslySetInRoot) {
122 2
                $this->installerDescriptors[$name] = $previousInstaller;
123 2
                $this->rootInstallerDescriptors[$name] = $previousInstaller;
124
            } else {
125 2
                unset($this->installerDescriptors[$name]);
126 2
                unset($this->rootInstallerDescriptors[$name]);
127
            }
128
129 4
            throw $e;
130
        }
131 6
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136 12
    public function removeRootInstallerDescriptor($name)
137
    {
138 12
        $this->assertInstallersLoaded();
139
140 12
        $previouslySetInRoot = isset($this->rootInstallerDescriptors[$name]);
141 12
        $previousInstaller = $previouslySetInRoot ? $this->rootInstallerDescriptors[$name] : null;
142
143 12 View Code Duplication
        if (isset($this->installerDescriptors[$name]) && !$previouslySetInRoot) {
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...
144 4
            throw new RuntimeException(sprintf(
145
                'Cannot remove installer "%s": Can only remove installers '.
146 4
                'configured in the root module.',
147
                $name
148
            ));
149
        }
150
151 8
        if (!$previouslySetInRoot) {
152 2
            return;
153
        }
154
155
        try {
156 6
            unset($this->installerDescriptors[$name]);
157 6
            unset($this->rootInstallerDescriptors[$name]);
158
159 6
            $this->persistInstallersData();
160 2
        } catch (Exception $e) {
161 2
            if ($previouslySetInRoot) {
162 2
                $this->installerDescriptors[$name] = $previousInstaller;
163 2
                $this->rootInstallerDescriptors[$name] = $previousInstaller;
164
            }
165
166 2
            throw $e;
167
        }
168 4
    }
169
170
    /**
171
     * {@inheritdoc}
172
     */
173 6
    public function removeRootInstallerDescriptors(Expression $expr)
174
    {
175 6
        $this->assertInstallersLoaded();
176
177 6
        $previousInstallers = $this->rootInstallerDescriptors;
178 6
        $previousRootInstallers = $this->rootInstallerDescriptors;
179
180
        try {
181
            // Only remove root installers
182 6
            foreach ($previousRootInstallers as $installer) {
183 6
                if ($expr->evaluate($installer)) {
184 6
                    unset($this->installerDescriptors[$installer->getName()]);
185 6
                    unset($this->rootInstallerDescriptors[$installer->getName()]);
186
                }
187
            }
188
189 6
            $this->persistInstallersData();
190 2
        } catch (Exception $e) {
191 2
            $this->installerDescriptors = $previousInstallers;
192 2
            $this->rootInstallerDescriptors = $previousRootInstallers;
193
194 2
            throw $e;
195
        }
196 4
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201 2
    public function clearRootInstallerDescriptors()
202
    {
203 2
        $this->removeRootInstallerDescriptors(Expr::true());
204 2
    }
205
206
    /**
207
     * {@inheritdoc}
208
     */
209 8
    public function getRootInstallerDescriptor($name)
210
    {
211 8
        $this->assertInstallersLoaded();
212
213 8
        if (!isset($this->rootInstallerDescriptors[$name])) {
214 6
            throw NoSuchInstallerException::forInstallerNameAndModuleName($name, $this->rootModule->getName());
215
        }
216
217 2
        return $this->rootInstallerDescriptors[$name];
218
    }
219
220
    /**
221
     * {@inheritdoc}
222
     */
223 2
    public function getRootInstallerDescriptors()
224
    {
225 2
        $this->assertInstallersLoaded();
226
227 2
        return $this->rootInstallerDescriptors;
228
    }
229
230
    /**
231
     * {@inheritdoc}
232
     */
233 2 View Code Duplication
    public function findRootInstallerDescriptors(Expression $expr)
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...
234
    {
235 2
        $this->assertInstallersLoaded();
236
237 2
        $installers = array();
238
239 2
        foreach ($this->rootInstallerDescriptors as $installer) {
240 2
            if ($expr->evaluate($installer)) {
241 2
                $installers[] = $installer;
242
            }
243
        }
244
245 2
        return $installers;
246
    }
247
248
    /**
249
     * {@inheritdoc}
250
     */
251 2
    public function hasRootInstallerDescriptor($name)
252
    {
253 2
        $this->assertInstallersLoaded();
254
255 2
        return isset($this->rootInstallerDescriptors[$name]);
256
    }
257
258
    /**
259
     * {@inheritdoc}
260
     */
261 4 View Code Duplication
    public function hasRootInstallerDescriptors(Expression $expr = null)
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...
262
    {
263 4
        $this->assertInstallersLoaded();
264
265 4
        if (!$expr) {
266 4
            return count($this->rootInstallerDescriptors) > 0;
267
        }
268
269 2
        foreach ($this->rootInstallerDescriptors as $installer) {
270 2
            if ($expr->evaluate($installer)) {
271 2
                return true;
272
            }
273
        }
274
275 2
        return false;
276
    }
277
278
    /**
279
     * {@inheritdoc}
280
     */
281 24
    public function getInstallerDescriptor($name)
282
    {
283 24
        $this->assertInstallersLoaded();
284
285 22
        if (!isset($this->installerDescriptors[$name])) {
286 2
            throw NoSuchInstallerException::forInstallerName($name);
287
        }
288
289 20
        return $this->installerDescriptors[$name];
290
    }
291
292
    /**
293
     * {@inheritdoc}
294
     */
295 9
    public function getInstallerDescriptors()
296
    {
297 9
        $this->assertInstallersLoaded();
298
299 9
        return $this->installerDescriptors;
300
    }
301
302
    /**
303
     * {@inheritdoc}
304
     */
305 2 View Code Duplication
    public function findInstallerDescriptors(Expression $expr)
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...
306
    {
307 2
        $this->assertInstallersLoaded();
308
309 2
        $installers = array();
310
311 2
        foreach ($this->installerDescriptors as $installer) {
312 2
            if ($expr->evaluate($installer)) {
313 2
                $installers[] = $installer;
314
            }
315
        }
316
317 2
        return $installers;
318
    }
319
320
    /**
321
     * {@inheritdoc}
322
     */
323 14
    public function hasInstallerDescriptor($name)
324
    {
325 14
        $this->assertInstallersLoaded();
326
327 14
        return isset($this->installerDescriptors[$name]);
328
    }
329
330
    /**
331
     * {@inheritdoc}
332
     */
333 2 View Code Duplication
    public function hasInstallerDescriptors(Expression $expr = null)
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...
334
    {
335 2
        $this->assertInstallersLoaded();
336
337 2
        if (!$expr) {
338 2
            return count($this->installerDescriptors) > 0;
339
        }
340
341 2
        foreach ($this->installerDescriptors as $installer) {
342 2
            if ($expr->evaluate($installer)) {
343 2
                return true;
344
            }
345
        }
346
347 2
        return false;
348
    }
349
350 68
    private function assertInstallersLoaded()
351
    {
352 68
        if (null !== $this->installerDescriptors) {
353 38
            return;
354
        }
355
356 68
        $this->installerDescriptors = array();
357
358 68
        foreach ($this->modules as $module) {
359 68
            if ($this->rootModule !== $module) {
360 68
                $this->loadInstallers($module);
361
            }
362
        }
363
364 66
        $this->loadInstallers($this->rootModule);
365 66
    }
366
367 22
    private function persistInstallersData()
368
    {
369 22
        $data = array();
370
371 22
        foreach ($this->rootInstallerDescriptors as $installerName => $installer) {
372 16
            $data[$installerName] = $this->installerToData($installer);
373
        }
374
375 22
        if ($data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type array 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...
376 16
            $this->rootModuleFileManager->setExtraKey(self::INSTALLERS_KEY, (object) $data);
377
        } else {
378 6
            $this->rootModuleFileManager->removeExtraKey(self::INSTALLERS_KEY);
379
        }
380 14
    }
381
382 68
    private function loadInstallers(Module $module)
383
    {
384 68
        foreach (self::$builtinInstallers as $name => $installerData) {
385 68
            $installer = $this->dataToInstaller($name, (object) $installerData);
386
387 68
            $this->installerDescriptors[$name] = $installer;
388
        }
389
390 68
        $moduleFile = $module->getModuleFile();
391
392 68
        if (null === $moduleFile) {
393 66
            return;
394
        }
395
396 68
        $moduleName = $module->getName();
397 68
        $installersData = $moduleFile->getExtraKey(self::INSTALLERS_KEY);
398
399 68
        if (!$installersData) {
400 66
            return;
401
        }
402
403 54
        $jsonValidator = new JsonValidator();
404 54
        $errors = $jsonValidator->validate($installersData, __DIR__.'/../../res/schema/installers-schema-1.0.json');
405
406 54 View Code Duplication
        if (count($errors) > 0) {
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...
407 2
            throw new ValidationFailedException(sprintf(
408 2
                "The extra key \"%s\" of module \"%s\" is invalid:\n%s",
409 2
                self::INSTALLERS_KEY,
410
                $moduleName,
411 2
                implode("\n", $errors)
412
            ));
413
        }
414
415 52
        foreach ($installersData as $name => $installerData) {
416 52
            $installer = $this->dataToInstaller($name, $installerData);
417
418 52
            $this->installerDescriptors[$name] = $installer;
419
420 52
            if ($module instanceof RootModule) {
421 52
                $this->rootInstallerDescriptors[$name] = $installer;
422
            }
423
        }
424 52
    }
425
426 68
    private function dataToInstaller($installerName, stdClass $installerData)
427
    {
428 68
        $parameters = array();
429
430 68
        if (isset($installerData->parameters)) {
431 68
            $parameters = $this->dataToParameters((object) $installerData->parameters);
432
        }
433
434 68
        return new InstallerDescriptor(
435
            $installerName,
436 68
            $installerData->class,
437 68
            isset($installerData->description) ? $installerData->description : null,
438
            $parameters
439
        );
440
    }
441
442 68
    private function dataToParameters(stdClass $parametersData)
443
    {
444 68
        $parameters = array();
445
446 68
        foreach ($parametersData as $parameterName => $parameterData) {
0 ignored issues
show
Bug introduced by
The expression $parametersData of type object<stdClass> is not traversable.
Loading history...
447 68
            $parameters[$parameterName] = $this->dataToParameter($parameterName, (object) $parameterData);
448
        }
449
450 68
        return $parameters;
451
    }
452
453 68
    private function dataToParameter($parameterName, stdClass $parameterData)
454
    {
455 68
        return new InstallerParameter(
456
            $parameterName,
457 68
            isset($parameterData->required) && $parameterData->required
458 2
                ? InstallerParameter::REQUIRED
459 68
                : InstallerParameter::OPTIONAL,
460 68
            isset($parameterData->default) ? $parameterData->default : null,
461 68
            isset($parameterData->description) ? $parameterData->description : null
462
        );
463
    }
464
465
    /**
466
     * Extracting an object containing the data from an installer descriptor.
467
     *
468
     * @param InstallerDescriptor $installer The installer descriptor.
469
     *
470
     * @return stdClass
471
     */
472 16
    private function installerToData(InstallerDescriptor $installer)
473
    {
474
        $data = (object) array(
475 16
            'class' => $installer->getClassName(),
476
        );
477
478 16
        if ($installer->getDescription()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $installer->getDescription() 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...
479 2
            $data->description = $installer->getDescription();
480
        }
481
482 16
        if ($installer->getParameters()) {
483 2
            $data->parameters = $this->parametersToData($installer->getParameters());
484
        }
485
486 16
        return $data;
487
    }
488
489
    /**
490
     * @param InstallerParameter[] $parameters
491
     *
492
     * @return array
493
     */
494 2
    private function parametersToData(array $parameters)
495
    {
496 2
        $data = array();
497
498 2
        foreach ($parameters as $parameter) {
499 2
            $data[$parameter->getName()] = $this->parameterToData($parameter);
500
        }
501
502 2
        return (object) $data;
503
    }
504
505 2
    private function parameterToData(InstallerParameter $parameter)
506
    {
507 2
        $data = new stdClass();
508
509 2
        if ($parameter->isRequired()) {
510 2
            $data->required = true;
511
        }
512
513 2
        if (null !== $default = $parameter->getDefaultValue()) {
514 2
            $data->default = $default;
515
        }
516
517 2
        if ($description = $parameter->getDescription()) {
518 2
            $data->description = $description;
519
        }
520
521 2
        return $data;
522
    }
523
}
524