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

ModuleManagerImpl   F

Complexity

Total Complexity 55

Size/Duplication

Total Lines 429
Duplicated Lines 3.73 %

Coupling/Cohesion

Components 1
Dependencies 17

Test Coverage

Coverage 97.7%

Importance

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