Completed
Push — master ( cfc512...55a33f )
by Bernhard
04:11
created

PackageManagerImpl   F

Complexity

Total Complexity 55

Size/Duplication

Total Lines 429
Duplicated Lines 3.73 %

Coupling/Cohesion

Components 1
Dependencies 17

Test Coverage

Coverage 98.43%

Importance

Changes 11
Bugs 3 Features 4
Metric Value
wmc 55
c 11
b 3
f 4
lcom 1
cbo 17
dl 16
loc 429
ccs 188
cts 191
cp 0.9843
rs 3.5484

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A renamePackage() 0 18 4
A removePackage() 0 23 3
B removePackages() 0 27 5
A clearPackages() 0 4 1
A getPackage() 0 8 1
A getRootPackage() 0 6 1
A getPackages() 0 7 1
A findPackages() 0 14 3
A hasPackage() 0 8 1
A hasPackages() 16 16 4
A getContext() 0 4 1
A loadPackages() 0 11 2
B loadPackage() 0 18 6
B installPackage() 0 56 8
A loadPackageFile() 0 20 4
A assertPackagesLoaded() 0 6 2
A assertNoLoadErrors() 0 9 2
A renameRootPackage() 0 17 2
B renameNonRootPackage() 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 PackageManagerImpl 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 PackageManagerImpl, 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\Package;
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\NoDirectoryException;
20
use Puli\Manager\Api\Package\InstallInfo;
21
use Puli\Manager\Api\Package\NameConflictException;
22
use Puli\Manager\Api\Package\Package;
23
use Puli\Manager\Api\Package\PackageCollection;
24
use Puli\Manager\Api\Package\PackageFile;
25
use Puli\Manager\Api\Package\PackageManager;
26
use Puli\Manager\Api\Package\RootPackage;
27
use Puli\Manager\Api\Package\RootPackageFile;
28
use Puli\Manager\Api\Package\UnsupportedVersionException;
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 package repository of a Puli project.
36
 *
37
 * @since  1.0
38
 *
39
 * @author Bernhard Schussek <[email protected]>
40
 */
41
class PackageManagerImpl implements PackageManager
42
{
43
    /**
44
     * @var ProjectContext
45
     */
46
    private $context;
47
48
    /**
49
     * @var string
50
     */
51
    private $rootDir;
52
53
    /**
54
     * @var RootPackageFile
55
     */
56
    private $rootPackageFile;
57
58
    /**
59
     * @var PackageFileStorage
60
     */
61
    private $packageFileStorage;
62
63
    /**
64
     * @var PackageCollection
65
     */
66
    private $packages;
67
68
    /**
69
     * Loads the package repository for a given project.
70
     *
71
     * @param ProjectContext     $context            The project context.
72
     * @param PackageFileStorage $packageFileStorage The package file storage.
73
     *
74
     * @throws FileNotFoundException  If the install path of a package not exist.
75
     * @throws NoDirectoryException   If the install path of a package points to a file.
76
     * @throws InvalidConfigException If a configuration file contains invalid configuration.
77
     * @throws NameConflictException  If a package has the same name as another loaded package.
78
     */
79 53
    public function __construct(ProjectContext $context, PackageFileStorage $packageFileStorage)
80
    {
81 53
        $this->context = $context;
82 53
        $this->rootDir = $context->getRootDirectory();
83 53
        $this->rootPackageFile = $context->getRootPackageFile();
84 53
        $this->packageFileStorage = $packageFileStorage;
85 53
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90 12
    public function installPackage($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::nullOrPackageName($name);
96
97 11
        $this->assertPackagesLoaded();
98
99 11
        $installPath = Path::makeAbsolute($installPath, $this->rootDir);
100
101 11
        foreach ($this->packages as $package) {
102 11
            if ($installPath === $package->getInstallPath()) {
103 1
                return;
104
            }
105 11
        }
106
107 10
        if (null === $name && $packageFile = $this->loadPackageFile($installPath)) {
108
            // Read the name from the package file
109 6
            $name = $packageFile->getPackageName();
110 6
        }
111
112 8
        if (null === $name) {
113 1
            throw new InvalidConfigException(sprintf(
114
                'Could not find a name for the package at %s. The name should '.
115 1
                'either be passed to the installer or be set in the "name" '.
116 1
                'property of %s.',
117 1
                $installPath,
118
                $installPath.'/puli.json'
119 1
            ));
120
        }
121
122 7
        if ($this->packages->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
        $package = $this->loadPackage($installInfo);
132
133 6
        $this->assertNoLoadErrors($package);
134 5
        $this->rootPackageFile->addInstallInfo($installInfo);
135
136
        try {
137 5
            $this->packageFileStorage->saveRootPackageFile($this->rootPackageFile);
138 5
        } catch (Exception $e) {
139
            $this->rootPackageFile->removeInstallInfo($name);
140
141
            throw $e;
142
        }
143
144 5
        $this->packages->add($package);
145 5
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150 8
    public function renamePackage($name, $newName)
151
    {
152 8
        $package = $this->getPackage($name);
153
154 8
        if ($name === $newName) {
155 2
            return;
156
        }
157
158 6
        if ($this->packages->contains($newName)) {
159 2
            throw NameConflictException::forName($newName);
160
        }
161
162 4
        if ($package instanceof RootPackage) {
163 2
            $this->renameRootPackage($package, $newName);
164 1
        } else {
165 2
            $this->renameNonRootPackage($package, $newName);
166
        }
167 2
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172 4
    public function removePackage($name)
173
    {
174
        // Only check that this is a string. The error message "not found" is
175
        // more helpful than e.g. "package name must contain /".
176 4
        Assert::string($name, 'The package name must be a string. Got: %s');
177
178 4
        $this->assertPackagesLoaded();
179
180 4
        if ($this->rootPackageFile->hasInstallInfo($name)) {
181 2
            $installInfo = $this->rootPackageFile->getInstallInfo($name);
182 2
            $this->rootPackageFile->removeInstallInfo($name);
183
184
            try {
185 2
                $this->packageFileStorage->saveRootPackageFile($this->rootPackageFile);
186 2
            } catch (Exception $e) {
187 1
                $this->rootPackageFile->addInstallInfo($installInfo);
188
189 1
                throw $e;
190
            }
191 1
        }
192
193 3
        $this->packages->remove($name);
194 3
    }
195
196
    /**
197
     * {@inheritdoc}
198
     */
199 3
    public function removePackages(Expression $expr)
200
    {
201 3
        $this->assertPackagesLoaded();
202
203 3
        $installInfos = $this->rootPackageFile->getInstallInfos();
204 3
        $packages = $this->packages->toArray();
205
206 3
        foreach ($this->packages->getInstalledPackages() as $package) {
207 3
            if ($expr->evaluate($package)) {
208 3
                $this->rootPackageFile->removeInstallInfo($package->getName());
209 3
                $this->packages->remove($package->getName());
210 3
            }
211 3
        }
212
213 3
        if (!$installInfos) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $installInfos of type Puli\Manager\Api\Package\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->packageFileStorage->saveRootPackageFile($this->rootPackageFile);
219 3
        } catch (Exception $e) {
220 1
            $this->rootPackageFile->setInstallInfos($installInfos);
221 1
            $this->packages->replace($packages);
222
223 1
            throw $e;
224
        }
225 2
    }
226
227
    /**
228
     * {@inheritdoc}
229
     */
230 1
    public function clearPackages()
231
    {
232 1
        $this->removePackages(Expr::true());
233 1
    }
234
235
    /**
236
     * {@inheritdoc}
237
     */
238 10
    public function getPackage($name)
239
    {
240 10
        Assert::string($name, 'The package name must be a string. Got: %s');
241
242 10
        $this->assertPackagesLoaded();
243
244 10
        return $this->packages->get($name);
245
    }
246
247
    /**
248
     * {@inheritdoc}
249
     */
250 1
    public function getRootPackage()
251
    {
252 1
        $this->assertPackagesLoaded();
253
254 1
        return $this->packages->getRootPackage();
255
    }
256
257
    /**
258
     * {@inheritdoc}
259
     */
260 24
    public function getPackages()
261
    {
262 24
        $this->assertPackagesLoaded();
263
264
        // Never return he original collection
265 24
        return clone $this->packages;
266
    }
267
268
    /**
269
     * {@inheritdoc}
270
     */
271 4
    public function findPackages(Expression $expr)
272
    {
273 4
        $this->assertPackagesLoaded();
274
275 4
        $packages = new PackageCollection();
276
277 4
        foreach ($this->packages as $package) {
278 4
            if ($expr->evaluate($package)) {
279 4
                $packages->add($package);
280 4
            }
281 4
        }
282
283 4
        return $packages;
284
    }
285
286
    /**
287
     * {@inheritdoc}
288
     */
289 11
    public function hasPackage($name)
290
    {
291 11
        Assert::string($name, 'The package name must be a string. Got: %s');
292
293 11
        $this->assertPackagesLoaded();
294
295 11
        return $this->packages->contains($name);
296
    }
297
298
    /**
299
     * {@inheritdoc}
300
     */
301 1 View Code Duplication
    public function hasPackages(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->assertPackagesLoaded();
304
305 1
        if (!$expr) {
306 1
            return !$this->packages->isEmpty();
307
        }
308
309 1
        foreach ($this->packages as $package) {
310 1
            if ($expr->evaluate($package)) {
311 1
                return true;
312
            }
313 1
        }
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 packages referenced by the install file.
328
     *
329
     * @throws FileNotFoundException  If the install path of a package not exist.
330
     * @throws NoDirectoryException   If the install path of a package points to a
331
     *                                file.
332
     * @throws InvalidConfigException If a package is not configured correctly.
333
     * @throws NameConflictException  If a package has the same name as another
334
     *                                loaded package.
335
     */
336 52
    private function loadPackages()
337
    {
338 52
        $this->packages = new PackageCollection();
339 52
        $this->packages->add(new RootPackage($this->rootPackageFile, $this->rootDir));
340
341 52
        foreach ($this->rootPackageFile->getInstallInfos() as $installInfo) {
342
            // Catch and log exceptions so that single packages cannot break
343
            // the whole repository
344 51
            $this->packages->add($this->loadPackage($installInfo));
345 52
        }
346 52
    }
347
348
    /**
349
     * Loads a package for the given install info.
350
     *
351
     * @param InstallInfo $installInfo The install info.
352
     *
353
     * @return Package The package.
354
     */
355 52
    private function loadPackage(InstallInfo $installInfo)
356
    {
357 52
        $installPath = Path::makeAbsolute($installInfo->getInstallPath(), $this->rootDir);
358 52
        $packageFile = null;
359 52
        $loadError = null;
360
361
        try {
362 52
            $packageFile = $this->loadPackageFile($installPath);
363 52
        } 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 7
        } 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 6
        } 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 4
        } 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 Package($packageFile, $installPath, $installInfo, $loadErrors);
372
    }
373
374
    /**
375
     * Loads the package file for the package at the given install path.
376
     *
377
     * @param string $installPath The absolute install path of the package
378
     *
379
     * @return PackageFile|null The loaded package file or `null` if none
380
     *                          could be found.
381
     */
382 52
    private function loadPackageFile($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 2
            ));
393
        }
394
395
        try {
396 50
            return $this->packageFileStorage->loadPackageFile($installPath.'/puli.json');
397 4
        } catch (FileNotFoundException $e) {
398
            // Packages without package files are ok
399 1
            return null;
400
        }
401
    }
402
403 52
    private function assertPackagesLoaded()
404
    {
405 52
        if (!$this->packages) {
406 52
            $this->loadPackages();
407 52
        }
408 52
    }
409
410 6
    private function assertNoLoadErrors(Package $package)
411
    {
412 6
        $loadErrors = $package->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 renameRootPackage(RootPackage $package, $newName)
421
    {
422 2
        $packageFile = $package->getPackageFile();
423 2
        $previousName = $packageFile->getPackageName();
424 2
        $packageFile->setPackageName($newName);
425
426
        try {
427 2
            $this->packageFileStorage->saveRootPackageFile($this->rootPackageFile);
428 2
        } catch (Exception $e) {
429 1
            $packageFile->setPackageName($previousName);
430
431 1
            throw $e;
432
        }
433
434 1
        $this->packages->remove($package->getName());
435 1
        $this->packages->add(new RootPackage($packageFile, $package->getInstallPath()));
436 1
    }
437
438 2
    private function renameNonRootPackage(Package $package, $newName)
439
    {
440 2
        $previousInstallInfo = $package->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 2
        }
448
449 2
        $this->rootPackageFile->removeInstallInfo($package->getName());
450 2
        $this->rootPackageFile->addInstallInfo($installInfo);
451
452
        try {
453 2
            $this->packageFileStorage->saveRootPackageFile($this->rootPackageFile);
454 2
        } catch (Exception $e) {
455 1
            $this->rootPackageFile->removeInstallInfo($newName);
456 1
            $this->rootPackageFile->addInstallInfo($previousInstallInfo);
457
458 1
            throw $e;
459
        }
460
461 1
        $this->packages->remove($package->getName());
462 1
        $this->packages->add(new Package(
463 1
            $package->getPackageFile(),
464 1
            $package->getInstallPath(),
465 1
            $installInfo,
466 1
            $package->getLoadErrors()
467 1
        ));
468 1
    }
469
}
470