ModuleManagerImpl::installModule()   B
last analyzed

Complexity

Conditions 8
Paths 17

Size

Total Lines 56
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 8.058

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 56
ccs 28
cts 31
cp 0.9032
rs 7.3333
cc 8
eloc 34
nc 17
nop 4
crap 8.058

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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