ModuleManagerImpl   F
last analyzed

Complexity

Total Complexity 55

Size/Duplication

Total Lines 434
Duplicated Lines 3.69 %

Coupling/Cohesion

Components 1
Dependencies 17

Test Coverage

Coverage 97.7%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 55
c 3
b 0
f 0
lcom 1
cbo 17
dl 16
loc 434
ccs 170
cts 174
cp 0.977
rs 3.5483

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
B installModule() 0 56 8
A renameModule() 0 18 4
A removeModule() 0 23 3
B removeModules() 0 27 5
A clearModules() 0 4 1
A getModule() 0 8 1
A getRootModule() 0 6 1
A getModules() 0 7 1
A findModules() 0 14 3
A hasModule() 0 8 1
A hasModules() 16 16 4
A getContext() 0 4 1
A loadModules() 0 9 2
B loadModule() 0 20 6
A loadModuleFile() 0 20 4
A assertModulesLoaded() 0 6 2
A assertNoLoadErrors() 0 9 2
A renameRootModule() 0 17 2
B renameNonRootModule() 0 31 3

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 ModuleManagerImpl 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 ModuleManagerImpl, 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\Module;
13
14
use Exception;
15
use Puli\Manager\Api\Context\ProjectContext;
16
use Puli\Manager\Api\Environment;
17
use Puli\Manager\Api\FileNotFoundException;
18
use Puli\Manager\Api\InvalidConfigException;
19
use Puli\Manager\Api\Module\DependencyFile;
20
use Puli\Manager\Api\Module\InstallInfo;
21
use Puli\Manager\Api\Module\Module;
22
use Puli\Manager\Api\Module\ModuleFile;
23
use Puli\Manager\Api\Module\ModuleList;
24
use Puli\Manager\Api\Module\ModuleManager;
25
use Puli\Manager\Api\Module\NameConflictException;
26
use Puli\Manager\Api\Module\RootModule;
27
use Puli\Manager\Api\Module\RootModuleFile;
28
use Puli\Manager\Api\Module\UnsupportedVersionException;
29
use Puli\Manager\Api\NoDirectoryException;
30
use Puli\Manager\Assert\Assert;
31
use Puli\Manager\Json\JsonStorage;
32
use Webmozart\Expression\Expr;
33
use Webmozart\Expression\Expression;
34
use Webmozart\PathUtil\Path;
35
36
/**
37
 * Manages the module repository of a Puli project.
38
 *
39
 * @since  1.0
40
 *
41
 * @author Bernhard Schussek <[email protected]>
42
 */
43
class ModuleManagerImpl implements ModuleManager
44
{
45
    /**
46
     * @var ProjectContext
47
     */
48
    private $context;
49
50
    /**
51
     * @var string
52
     */
53
    private $rootDir;
54
55
    /**
56
     * @var RootModuleFile
57
     */
58
    private $rootModuleFile;
59
60
    /**
61
     * @var JsonStorage
62
     */
63
    private $jsonStorage;
64
65
    /**
66
     * @var ModuleList
67
     */
68
    private $modules;
69
70
    /**
71
     * @var DependencyFile[]
72
     */
73
    private $dependencyFilesByInstallerName = array();
0 ignored issues
show
Unused Code introduced by
The property $dependencyFilesByInstallerName is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
74
75
    /**
76
     * Loads the module repository for a given project.
77
     *
78
     * @param ProjectContext $context     The project context.
79
     * @param JsonStorage    $jsonStorage The module file storage.
80
     *
81
     * @throws FileNotFoundException  If the install path of a module not exist.
82
     * @throws NoDirectoryException   If the install path of a module points to a file.
83
     * @throws InvalidConfigException If a configuration file contains invalid configuration.
84
     * @throws NameConflictException  If a module has the same name as another loaded module.
85
     */
86 53
    public function __construct(ProjectContext $context, JsonStorage $jsonStorage)
87
    {
88 53
        $this->context = $context;
89 53
        $this->rootDir = $context->getRootDirectory();
90 53
        $this->rootModuleFile = $context->getRootModuleFile();
91 53
        $this->jsonStorage = $jsonStorage;
92 53
    }
93
94
    /**
95
     * {@inheritdoc}
96
     */
97 12
    public function installModule($installPath, $name = null, $installerName = InstallInfo::DEFAULT_INSTALLER_NAME, $env = Environment::PROD)
98
    {
99 12
        Assert::string($installPath, 'The install path must be a string. Got: %s');
100 12
        Assert::string($installerName, 'The installer name must be a string. Got: %s');
101 12
        Assert::oneOf($env, Environment::all(), 'The environment must be one of: %2$s. Got: %s');
102 12
        Assert::nullOrModuleName($name);
103
104 11
        $this->assertModulesLoaded();
105
106 11
        $installPath = Path::makeAbsolute($installPath, $this->rootDir);
107
108 11
        foreach ($this->modules as $module) {
109 11
            if ($installPath === $module->getInstallPath()) {
110 11
                return;
111
            }
112
        }
113
114 10
        if (null === $name && $moduleFile = $this->loadModuleFile($installPath)) {
115
            // Read the name from the module file
116 6
            $name = $moduleFile->getModuleName();
117
        }
118
119 8
        if (null === $name) {
120 1
            throw new InvalidConfigException(sprintf(
121
                'Could not find a name for the module at %s. The name should '.
122
                'either be passed to the installer or be set in the "name" '.
123 1
                'property of %s.',
124
                $installPath,
125 1
                $installPath.'/puli.json'
126
            ));
127
        }
128
129 7
        if ($this->modules->contains($name)) {
130 1
            throw NameConflictException::forName($name);
131
        }
132
133 6
        $relInstallPath = Path::makeRelative($installPath, $this->rootDir);
134 6
        $installInfo = new InstallInfo($name, $relInstallPath);
135 6
        $installInfo->setInstallerName($installerName);
136 6
        $installInfo->setEnvironment($env);
137
138 6
        $module = $this->loadModule($installInfo);
139
140 6
        $this->assertNoLoadErrors($module);
141 5
        $this->rootModuleFile->addInstallInfo($installInfo);
142
143
        try {
144 5
            $this->jsonStorage->saveRootModuleFile($this->rootModuleFile);
145
        } catch (Exception $e) {
146
            $this->rootModuleFile->removeInstallInfo($name);
147
148
            throw $e;
149
        }
150
151 5
        $this->modules->add($module);
152 5
    }
153
154
    /**
155
     * {@inheritdoc}
156
     */
157 8
    public function renameModule($name, $newName)
158
    {
159 8
        $module = $this->getModule($name);
160
161 8
        if ($name === $newName) {
162 2
            return;
163
        }
164
165 6
        if ($this->modules->contains($newName)) {
166 2
            throw NameConflictException::forName($newName);
167
        }
168
169 4
        if ($module instanceof RootModule) {
170 2
            $this->renameRootModule($module, $newName);
171
        } else {
172 2
            $this->renameNonRootModule($module, $newName);
173
        }
174 2
    }
175
176
    /**
177
     * {@inheritdoc}
178
     */
179 4
    public function removeModule($name)
180
    {
181
        // Only check that this is a string. The error message "not found" is
182
        // more helpful than e.g. "module name must contain /".
183 4
        Assert::string($name, 'The module name must be a string. Got: %s');
184
185 4
        $this->assertModulesLoaded();
186
187 4
        if ($this->rootModuleFile->hasInstallInfo($name)) {
188 2
            $installInfo = $this->rootModuleFile->getInstallInfo($name);
189 2
            $this->rootModuleFile->removeInstallInfo($name);
190
191
            try {
192 2
                $this->jsonStorage->saveRootModuleFile($this->rootModuleFile);
193 1
            } catch (Exception $e) {
194 1
                $this->rootModuleFile->addInstallInfo($installInfo);
195
196 1
                throw $e;
197
            }
198
        }
199
200 3
        $this->modules->remove($name);
201 3
    }
202
203
    /**
204
     * {@inheritdoc}
205
     */
206 3
    public function removeModules(Expression $expr)
207
    {
208 3
        $this->assertModulesLoaded();
209
210 3
        $installInfos = $this->rootModuleFile->getInstallInfos();
211 3
        $modules = $this->modules->toArray();
212
213 3
        foreach ($this->modules->getInstalledModules() as $module) {
214 3
            if ($expr->evaluate($module)) {
215 3
                $this->rootModuleFile->removeInstallInfo($module->getName());
216 3
                $this->modules->remove($module->getName());
217
            }
218
        }
219
220 3
        if (!$installInfos) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $installInfos of type Puli\Manager\Api\Module\InstallInfo[] 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...
221
            return;
222
        }
223
224
        try {
225 3
            $this->jsonStorage->saveRootModuleFile($this->rootModuleFile);
226 1
        } catch (Exception $e) {
227 1
            $this->rootModuleFile->setInstallInfos($installInfos);
228 1
            $this->modules->replace($modules);
229
230 1
            throw $e;
231
        }
232 2
    }
233
234
    /**
235
     * {@inheritdoc}
236
     */
237 1
    public function clearModules()
238
    {
239 1
        $this->removeModules(Expr::true());
240 1
    }
241
242
    /**
243
     * {@inheritdoc}
244
     */
245 10
    public function getModule($name)
246
    {
247 10
        Assert::string($name, 'The module name must be a string. Got: %s');
248
249 10
        $this->assertModulesLoaded();
250
251 10
        return $this->modules->get($name);
252
    }
253
254
    /**
255
     * {@inheritdoc}
256
     */
257 1
    public function getRootModule()
258
    {
259 1
        $this->assertModulesLoaded();
260
261 1
        return $this->modules->getRootModule();
262
    }
263
264
    /**
265
     * {@inheritdoc}
266
     */
267 24
    public function getModules()
268
    {
269 24
        $this->assertModulesLoaded();
270
271
        // Never return he original collection
272 24
        return clone $this->modules;
273
    }
274
275
    /**
276
     * {@inheritdoc}
277
     */
278 4
    public function findModules(Expression $expr)
279
    {
280 4
        $this->assertModulesLoaded();
281
282 4
        $modules = new ModuleList();
283
284 4
        foreach ($this->modules as $module) {
285 4
            if ($expr->evaluate($module)) {
286 4
                $modules->add($module);
287
            }
288
        }
289
290 4
        return $modules;
291
    }
292
293
    /**
294
     * {@inheritdoc}
295
     */
296 11
    public function hasModule($name)
297
    {
298 11
        Assert::string($name, 'The module name must be a string. Got: %s');
299
300 11
        $this->assertModulesLoaded();
301
302 11
        return $this->modules->contains($name);
303
    }
304
305
    /**
306
     * {@inheritdoc}
307
     */
308 1 View Code Duplication
    public function hasModules(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...
309
    {
310 1
        $this->assertModulesLoaded();
311
312 1
        if (!$expr) {
313 1
            return !$this->modules->isEmpty();
314
        }
315
316 1
        foreach ($this->modules as $module) {
317 1
            if ($expr->evaluate($module)) {
318 1
                return true;
319
            }
320
        }
321
322 1
        return false;
323
    }
324
325
    /**
326
     * {@inheritdoc}
327
     */
328 1
    public function getContext()
329
    {
330 1
        return $this->context;
331
    }
332
333
    /**
334
     * Loads all modules referenced by the install file.
335
     *
336
     * @throws FileNotFoundException  If the install path of a module not exist.
337
     * @throws NoDirectoryException   If the install path of a module points to a
338
     *                                file.
339
     * @throws InvalidConfigException If a module is not configured correctly.
340
     * @throws NameConflictException  If a module has the same name as another
341
     *                                loaded module.
342
     */
343 52
    private function loadModules()
344
    {
345 52
        $this->modules = new ModuleList();
346 52
        $this->modules->add(new RootModule($this->rootModuleFile, $this->rootDir));
347
348 52
        foreach ($this->rootModuleFile->getInstallInfos() as $installInfo) {
349 51
            $this->modules->add($this->loadModule($installInfo));
350
        }
351 52
    }
352
353
    /**
354
     * Loads a module for the given install info.
355
     *
356
     * @param InstallInfo $installInfo The install info.
357
     *
358
     * @return Module The module.
359
     */
360 52
    private function loadModule(InstallInfo $installInfo)
361
    {
362 52
        $installPath = Path::makeAbsolute($installInfo->getInstallPath(), $this->rootDir);
363 52
        $moduleFile = null;
364 52
        $loadError = null;
365
366
        // Catch and log exceptions so that single modules cannot break
367
        // the whole repository
368
        try {
369 52
            $moduleFile = $this->loadModuleFile($installPath);
370 7
        } catch (InvalidConfigException $loadError) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
371 6
        } catch (UnsupportedVersionException $loadError) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
372 4
        } catch (FileNotFoundException $loadError) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
373 1
        } catch (NoDirectoryException $loadError) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
374
        }
375
376 52
        $loadErrors = $loadError ? array($loadError) : array();
377
378 52
        return new Module($moduleFile, $installPath, $installInfo, $loadErrors);
379
    }
380
381
    /**
382
     * Loads the module file for the module at the given install path.
383
     *
384
     * @param string $installPath The absolute install path of the module
385
     *
386
     * @return ModuleFile|null The loaded module file or `null` if none
387
     *                         could be found.
388
     */
389 52
    private function loadModuleFile($installPath)
390
    {
391 52
        if (!file_exists($installPath)) {
392 4
            throw FileNotFoundException::forPath($installPath);
393
        }
394
395 51
        if (!is_dir($installPath)) {
396 2
            throw new NoDirectoryException(sprintf(
397 2
                'The path %s is a file. Expected a directory.',
398
                $installPath
399
            ));
400
        }
401
402
        try {
403 50
            return $this->jsonStorage->loadModuleFile($installPath.'/puli.json');
404 4
        } catch (FileNotFoundException $e) {
405
            // Modules without module files are ok
406 1
            return null;
407
        }
408
    }
409
410 52
    private function assertModulesLoaded()
411
    {
412 52
        if (!$this->modules) {
413 52
            $this->loadModules();
414
        }
415 52
    }
416
417 6
    private function assertNoLoadErrors(Module $module)
418
    {
419 6
        $loadErrors = $module->getLoadErrors();
420
421 6
        if (count($loadErrors) > 0) {
422
            // Rethrow first error
423 1
            throw reset($loadErrors);
424
        }
425 5
    }
426
427 2
    private function renameRootModule(RootModule $module, $newName)
428
    {
429 2
        $moduleFile = $module->getModuleFile();
430 2
        $previousName = $moduleFile->getModuleName();
431 2
        $moduleFile->setModuleName($newName);
432
433
        try {
434 2
            $this->jsonStorage->saveRootModuleFile($this->rootModuleFile);
435 1
        } catch (Exception $e) {
436 1
            $moduleFile->setModuleName($previousName);
437
438 1
            throw $e;
439
        }
440
441 1
        $this->modules->remove($module->getName());
442 1
        $this->modules->add(new RootModule($moduleFile, $module->getInstallPath()));
0 ignored issues
show
Bug introduced by
It seems like $moduleFile defined by $module->getModuleFile() on line 429 can be null; however, Puli\Manager\Api\Module\RootModule::__construct() 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...
443 1
    }
444
445 2
    private function renameNonRootModule(Module $module, $newName)
446
    {
447 2
        $previousInstallInfo = $module->getInstallInfo();
448
449 2
        $installInfo = new InstallInfo($newName, $previousInstallInfo->getInstallPath());
450 2
        $installInfo->setInstallerName($previousInstallInfo->getInstallerName());
451
452 2
        foreach ($previousInstallInfo->getDisabledBindingUuids() as $uuid) {
453 1
            $installInfo->addDisabledBindingUuid($uuid);
454
        }
455
456 2
        $this->rootModuleFile->removeInstallInfo($module->getName());
457 2
        $this->rootModuleFile->addInstallInfo($installInfo);
458
459
        try {
460 2
            $this->jsonStorage->saveRootModuleFile($this->rootModuleFile);
461 1
        } catch (Exception $e) {
462 1
            $this->rootModuleFile->removeInstallInfo($newName);
463 1
            $this->rootModuleFile->addInstallInfo($previousInstallInfo);
464
465 1
            throw $e;
466
        }
467
468 1
        $this->modules->remove($module->getName());
469 1
        $this->modules->add(new Module(
470 1
            $module->getModuleFile(),
471 1
            $module->getInstallPath(),
472
            $installInfo,
473 1
            $module->getLoadErrors()
474
        ));
475 1
    }
476
}
477