Failed Conditions
Pull Request — master (#51)
by Bernhard
15:48 queued 17s
created

ModuleManagerImpl::loadModules()   B

Complexity

Conditions 6
Paths 20

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 14.789

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 28
ccs 6
cts 16
cp 0.375
rs 8.439
cc 6
eloc 15
nc 20
nop 0
crap 14.789
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\Config\Config;
16
use Puli\Manager\Api\Context\ProjectContext;
17
use Puli\Manager\Api\Environment;
18
use Puli\Manager\Api\FileNotFoundException;
19
use Puli\Manager\Api\InvalidConfigException;
20
use Puli\Manager\Api\Module\DependencyFile;
21
use Puli\Manager\Api\Module\InstallInfo;
22
use Puli\Manager\Api\Module\Module;
23
use Puli\Manager\Api\Module\ModuleList;
24
use Puli\Manager\Api\Module\ModuleFile;
25
use Puli\Manager\Api\Module\ModuleManager;
26
use Puli\Manager\Api\Module\NameConflictException;
27
use Puli\Manager\Api\Module\RootModule;
28
use Puli\Manager\Api\Module\RootModuleFile;
29
use Puli\Manager\Api\Module\UnsupportedVersionException;
30
use Puli\Manager\Api\NoDirectoryException;
31
use Puli\Manager\Assert\Assert;
32
use Puli\Manager\Json\JsonStorage;
33
use Webmozart\Expression\Expr;
34
use Webmozart\Expression\Expression;
35
use Webmozart\PathUtil\Path;
36
37
/**
38
 * Manages the module repository of a Puli project.
39
 *
40
 * @since  1.0
41
 *
42
 * @author Bernhard Schussek <[email protected]>
43
 */
44
class ModuleManagerImpl implements ModuleManager
45
{
46
    /**
47
     * @var ProjectContext
48
     */
49
    private $context;
50
51
    /**
52
     * @var string
53
     */
54
    private $rootDir;
55
56
    /**
57
     * @var RootModuleFile
58
     */
59
    private $rootModuleFile;
60
61
    /**
62
     * @var JsonStorage
63
     */
64
    private $jsonStorage;
65
66
    /**
67
     * @var ModuleList
68
     */
69
    private $modules;
70
71
    /**
72
     * @var DependencyFile[]
73
     */
74
    private $dependencyFilesByInstallerName = array();
75
76
    /**
77
     * Loads the module repository for a given project.
78
     *
79
     * @param ProjectContext $context     The project context.
80
     * @param JsonStorage    $jsonStorage The module file storage.
81
     *
82
     * @throws FileNotFoundException  If the install path of a module not exist.
83
     * @throws NoDirectoryException   If the install path of a module points to a file.
84
     * @throws InvalidConfigException If a configuration file contains invalid configuration.
85
     * @throws NameConflictException  If a module has the same name as another loaded module.
86
     */
87 53
    public function __construct(ProjectContext $context, JsonStorage $jsonStorage)
88
    {
89 53
        $this->context = $context;
90 53
        $this->rootDir = $context->getRootDirectory();
91 53
        $this->rootModuleFile = $context->getRootModuleFile();
92 53
        $this->jsonStorage = $jsonStorage;
93 53
    }
94
95
    /**
96
     * {@inheritdoc}
97
     */
98 12
    public function installModule($installPath, $name = null, $installerName = InstallInfo::DEFAULT_INSTALLER_NAME, $env = Environment::PROD)
99
    {
100 12
        Assert::string($installPath, 'The install path must be a string. Got: %s');
101 12
        Assert::string($installerName, 'The installer name must be a string. Got: %s');
102 12
        Assert::oneOf($env, Environment::all(), 'The environment must be one of: %2$s. Got: %s');
103 12
        Assert::nullOrModuleName($name);
104
105 11
        $this->assertModulesLoaded();
106
107
        $installPath = Path::makeAbsolute($installPath, $this->rootDir);
108
109
        foreach ($this->modules as $module) {
110
            if ($installPath === $module->getInstallPath()) {
111
                return;
112
            }
113
        }
114
115
        if (null === $name && $moduleFile = $this->loadModuleFile($installPath)) {
116
            // Read the name from the module file
117
            $name = $moduleFile->getModuleName();
118
        }
119
120
        if (null === $name) {
121
            throw new InvalidConfigException(sprintf(
122
                'Could not find a name for the module at %s. The name should '.
123
                'either be passed to the installer or be set in the "name" '.
124
                'property of %s.',
125
                $installPath,
126
                $installPath.'/puli.json'
127
            ));
128
        }
129
130
        if ($this->modules->contains($name)) {
131
            throw NameConflictException::forName($name);
132
        }
133
134
        $relInstallPath = Path::makeRelative($installPath, $this->rootDir);
135
        $installInfo = new InstallInfo($name, $relInstallPath);
136
        $installInfo->setInstallerName($installerName);
137
        $installInfo->setEnvironment($env);
138
139
        $module = $this->loadModule($installInfo);
140
141
        $this->assertNoLoadErrors($module);
142
        $this->rootModuleFile->addInstallInfo($installInfo);
143
144
        try {
145
            $this->jsonStorage->saveRootModuleFile($this->rootModuleFile);
146
        } catch (Exception $e) {
147
            $this->rootModuleFile->removeInstallInfo($name);
148
149
            throw $e;
150
        }
151
152
        $this->modules->add($module);
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158 6
    public function renameModule($name, $newName)
159
    {
160 6
        $module = $this->getModule($name);
161
162
        if ($name === $newName) {
163
            return;
164
        }
165
166
        if ($this->modules->contains($newName)) {
167
            throw NameConflictException::forName($newName);
168
        }
169
170
        if ($module instanceof RootModule) {
171
            $this->renameRootModule($module, $newName);
172
        } else {
173
            $this->renameNonRootModule($module, $newName);
174
        }
175
    }
176
177
    /**
178
     * {@inheritdoc}
179
     */
180 2
    public function removeModule($name)
181
    {
182
        // Only check that this is a string. The error message "not found" is
183
        // more helpful than e.g. "module name must contain /".
184 2
        Assert::string($name, 'The module name must be a string. Got: %s');
185
186 2
        $this->assertModulesLoaded();
187
188
        if ($this->rootModuleFile->hasInstallInfo($name)) {
189
            $installInfo = $this->rootModuleFile->getInstallInfo($name);
190
            $this->rootModuleFile->removeInstallInfo($name);
191
192
            try {
193
                $this->jsonStorage->saveRootModuleFile($this->rootModuleFile);
194
            } catch (Exception $e) {
195
                $this->rootModuleFile->addInstallInfo($installInfo);
196
197
                throw $e;
198
            }
199
        }
200
201
        $this->modules->remove($name);
202
    }
203
204
    /**
205
     * {@inheritdoc}
206
     */
207 1
    public function removeModules(Expression $expr)
208
    {
209 1
        $this->assertModulesLoaded();
210
211
        $installInfos = $this->rootModuleFile->getInstallInfos();
212
        $modules = $this->modules->toArray();
213
214
        foreach ($this->modules->getInstalledModules() as $module) {
215
            if ($expr->evaluate($module)) {
216
                $this->rootModuleFile->removeInstallInfo($module->getName());
217
                $this->modules->remove($module->getName());
218
            }
219
        }
220
221
        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...
222
            return;
223
        }
224
225
        try {
226
            $this->jsonStorage->saveRootModuleFile($this->rootModuleFile);
227
        } catch (Exception $e) {
228
            $this->rootModuleFile->setInstallInfos($installInfos);
229
            $this->modules->replace($modules);
230
231
            throw $e;
232
        }
233
    }
234
235
    /**
236
     * {@inheritdoc}
237
     */
238
    public function clearModules()
239
    {
240
        $this->removeModules(Expr::true());
241
    }
242
243
    /**
244
     * {@inheritdoc}
245
     */
246 8
    public function getModule($name)
247
    {
248 8
        Assert::string($name, 'The module name must be a string. Got: %s');
249
250 8
        $this->assertModulesLoaded();
251
252
        return $this->modules->get($name);
253
    }
254
255
    /**
256
     * {@inheritdoc}
257
     */
258 1
    public function getRootModule()
259
    {
260 1
        $this->assertModulesLoaded();
261
262
        return $this->modules->getRootModule();
263
    }
264
265
    /**
266
     * {@inheritdoc}
267
     */
268 21
    public function getModules()
269
    {
270 21
        $this->assertModulesLoaded();
271
272
        // Never return he original collection
273
        return clone $this->modules;
274
    }
275
276
    /**
277
     * {@inheritdoc}
278
     */
279 1
    public function findModules(Expression $expr)
280
    {
281 1
        $this->assertModulesLoaded();
282
283
        $modules = new ModuleList();
284
285
        foreach ($this->modules as $module) {
286
            if ($expr->evaluate($module)) {
287
                $modules->add($module);
288
            }
289
        }
290
291
        return $modules;
292
    }
293
294
    /**
295
     * {@inheritdoc}
296
     */
297 6
    public function hasModule($name)
298
    {
299 6
        Assert::string($name, 'The module name must be a string. Got: %s');
300
301 6
        $this->assertModulesLoaded();
302
303
        return $this->modules->contains($name);
304
    }
305
306
    /**
307
     * {@inheritdoc}
308
     */
309 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...
310
    {
311 1
        $this->assertModulesLoaded();
312
313
        if (!$expr) {
314
            return !$this->modules->isEmpty();
315
        }
316
317
        foreach ($this->modules as $module) {
318
            if ($expr->evaluate($module)) {
319
                return true;
320
            }
321
        }
322
323
        return false;
324
    }
325
326
    /**
327
     * {@inheritdoc}
328
     */
329
    public function getContext()
330
    {
331
        return $this->context;
332
    }
333
334
    /**
335
     * Loads all modules referenced by the install file.
336
     *
337
     * @throws FileNotFoundException  If the install path of a module not exist.
338
     * @throws NoDirectoryException   If the install path of a module points to a
339
     *                                file.
340
     * @throws InvalidConfigException If a module is not configured correctly.
341
     * @throws NameConflictException  If a module has the same name as another
342
     *                                loaded module.
343
     */
344 52
    private function loadModules()
345
    {
346 52
        $this->modules = new ModuleList();
347 52
        $this->modules->add(new RootModule($this->rootModuleFile, $this->rootDir));
348
349 52
        foreach ($this->rootModuleFile->getInstallInfos() as $installInfo) {
350 51
            $this->modules->add($this->loadModule($installInfo));
351
        }
352
353 52
        $dependencyFiles = $this->context->getConfig()->get(Config::DEPENDENCY_FILES, array());
354
        $installerNamesByDependencyFile = array();
355
356
        foreach ($dependencyFiles as $installerName => $dependencyFile) {
0 ignored issues
show
Bug introduced by
The expression $dependencyFiles of type array|object|integer|double|null|boolean|string is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
357
            $installerNamesByDependencyFile[$dependencyFile][] = $installerName;
358
        }
359
360
        foreach ($installerNamesByDependencyFile as $dependencyFile => $installerNames) {
361
            $dependencyFile = $this->jsonStorage->loadDependencyFile($dependencyFile, $installerNames);
0 ignored issues
show
Bug introduced by
The method loadDependencyFile() does not seem to exist on object<Puli\Manager\Json\JsonStorage>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
362
363
            foreach ($installerNames as $installerName) {
364
                $this->dependencyFilesByInstallerName[$installerName] = $dependencyFile;
365
            }
366
367
            foreach ($dependencyFile->getInstallInfos() as $installInfo) {
368
                $this->modules->add($this->loadModule($installInfo));
369
            }
370
        }
371
    }
372
373
    /**
374
     * Loads a module for the given install info.
375
     *
376
     * @param InstallInfo $installInfo The install info.
377
     *
378
     * @return Module The module.
379
     */
380 51
    private function loadModule(InstallInfo $installInfo)
381
    {
382 51
        $installPath = Path::makeAbsolute($installInfo->getInstallPath(), $this->rootDir);
383 51
        $moduleFile = null;
384 51
        $loadError = null;
385
386
        // Catch and log exceptions so that single modules cannot break
387
        // the whole repository
388
        try {
389 51
            $moduleFile = $this->loadModuleFile($installPath);
390 6
        } catch (InvalidConfigException $loadError) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
391 5
        } catch (UnsupportedVersionException $loadError) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
392 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...
393 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...
394
        }
395
396 51
        $loadErrors = $loadError ? array($loadError) : array();
397
398 51
        return new Module($moduleFile, $installPath, $installInfo, $loadErrors);
399
    }
400
401
    /**
402
     * Loads the module file for the module at the given install path.
403
     *
404
     * @param string $installPath The absolute install path of the module
405
     *
406
     * @return ModuleFile|null The loaded module file or `null` if none
407
     *                         could be found.
408
     */
409 51
    private function loadModuleFile($installPath)
410
    {
411 51
        if (!file_exists($installPath)) {
412 3
            throw FileNotFoundException::forPath($installPath);
413
        }
414
415 50
        if (!is_dir($installPath)) {
416 1
            throw new NoDirectoryException(sprintf(
417 1
                'The path %s is a file. Expected a directory.',
418
                $installPath
419
            ));
420
        }
421
422
        try {
423 49
            return $this->jsonStorage->loadModuleFile($installPath.'/puli.json');
424 3
        } catch (FileNotFoundException $e) {
425
            // Modules without module files are ok
426 1
            return null;
427
        }
428
    }
429
430 52
    private function assertModulesLoaded()
431
    {
432 52
        if (!$this->modules) {
433 52
            $this->loadModules();
434
        }
435
    }
436
437
    private function assertNoLoadErrors(Module $module)
438
    {
439
        $loadErrors = $module->getLoadErrors();
440
441
        if (count($loadErrors) > 0) {
442
            // Rethrow first error
443
            throw reset($loadErrors);
444
        }
445
    }
446
447
    private function renameRootModule(RootModule $module, $newName)
448
    {
449
        $moduleFile = $module->getModuleFile();
450
        $previousName = $moduleFile->getModuleName();
451
        $moduleFile->setModuleName($newName);
452
453
        try {
454
            $this->jsonStorage->saveRootModuleFile($this->rootModuleFile);
455
        } catch (Exception $e) {
456
            $moduleFile->setModuleName($previousName);
457
458
            throw $e;
459
        }
460
461
        $this->modules->remove($module->getName());
462
        $this->modules->add(new RootModule($moduleFile, $module->getInstallPath()));
463
    }
464
465
    private function renameNonRootModule(Module $module, $newName)
466
    {
467
        $previousInstallInfo = $module->getInstallInfo();
468
469
        $installInfo = new InstallInfo($newName, $previousInstallInfo->getInstallPath());
470
        $installInfo->setInstallerName($previousInstallInfo->getInstallerName());
471
472
        foreach ($previousInstallInfo->getDisabledBindingUuids() as $uuid) {
473
            $installInfo->addDisabledBindingUuid($uuid);
474
        }
475
476
        $this->rootModuleFile->removeInstallInfo($module->getName());
477
        $this->rootModuleFile->addInstallInfo($installInfo);
478
479
        try {
480
            $this->jsonStorage->saveRootModuleFile($this->rootModuleFile);
481
        } catch (Exception $e) {
482
            $this->rootModuleFile->removeInstallInfo($newName);
483
            $this->rootModuleFile->addInstallInfo($previousInstallInfo);
484
485
            throw $e;
486
        }
487
488
        $this->modules->remove($module->getName());
489
        $this->modules->add(new Module(
490
            $module->getModuleFile(),
491
            $module->getInstallPath(),
492
            $installInfo,
493
            $module->getLoadErrors()
494
        ));
495
    }
496
}
497