Completed
Pull Request — master (#33)
by Bernhard
22:58
created

Puli   F

Complexity

Total Complexity 106

Size/Duplication

Total Lines 869
Duplicated Lines 14.15 %

Coupling/Cohesion

Components 1
Dependencies 36

Test Coverage

Coverage 33.12%

Importance

Changes 14
Bugs 4 Features 5
Metric Value
c 14
b 4
f 5
dl 123
loc 869
wmc 106
lcom 1
cbo 36
ccs 103
cts 311
cp 0.3312
rs 1.0435

39 Methods

Rating   Name   Duplication   Size   Complexity  
A parseHomeDirectory() 0 15 2
A __construct() 0 5 1
A isStarted() 0 4 1
A setRootDirectory() 0 10 3
A setEnvironment() 0 10 2
A setLogger() 0 8 2
A getLogger() 0 4 1
A setEventDispatcher() 0 8 2
A enablePlugins() 0 4 1
A disablePlugins() 0 4 1
A arePluginsEnabled() 0 4 1
A getContext() 0 8 2
A getStorage() 0 8 2
A getConfigFileSerializer() 0 8 2
A getPackageFileSerializer() 0 13 2
A activatePlugins() 0 10 2
C start() 0 43 8
A getEnvironment() 0 4 1
A getRootDirectory() 0 4 1
A getEventDispatcher() 0 4 1
A getRepository() 16 16 4
A getDiscovery() 16 16 4
A getFactory() 0 12 4
A getFactoryManager() 0 19 4
A getConfigFileManager() 0 16 4
A getRootPackageFileManager() 15 15 4
A getPackageManager() 15 15 4
A getRepositoryManager() 17 17 4
A getDiscoveryManager() 18 18 4
A getAssetManager() 0 15 4
A getInstallationManager() 0 17 4
A getInstallerManager() 0 15 4
A getServerManager() 0 15 4
B getUrlGenerator() 0 17 5
A createGlobalContext() 13 22 2
B createProjectContext() 13 30 2
A getConfigFileStorage() 0 12 2
A getPackageFileStorage() 0 12 2
A validatePluginClass() 0 16 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 Puli 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 Puli, 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\Api;
13
14
use LogicException;
15
use Psr\Log\LoggerInterface;
16
use Puli\Discovery\Api\Discovery;
17
use Puli\Discovery\Api\EditableDiscovery;
18
use Puli\Manager\Api\Asset\AssetManager;
19
use Puli\Manager\Api\Config\Config;
20
use Puli\Manager\Api\Config\ConfigFileManager;
21
use Puli\Manager\Api\Config\ConfigFileSerializer;
22
use Puli\Manager\Api\Context\Context;
23
use Puli\Manager\Api\Context\ProjectContext;
24
use Puli\Manager\Api\Discovery\DiscoveryManager;
25
use Puli\Manager\Api\Factory\FactoryManager;
26
use Puli\Manager\Api\Installation\InstallationManager;
27
use Puli\Manager\Api\Installer\InstallerManager;
28
use Puli\Manager\Api\Package\PackageFileSerializer;
29
use Puli\Manager\Api\Package\PackageManager;
30
use Puli\Manager\Api\Package\RootPackageFileManager;
31
use Puli\Manager\Api\Repository\RepositoryManager;
32
use Puli\Manager\Api\Server\ServerManager;
33
use Puli\Manager\Api\Storage\Storage;
34
use Puli\Manager\Assert\Assert;
35
use Puli\Manager\Asset\DiscoveryAssetManager;
36
use Puli\Manager\Config\ConfigFileManagerImpl;
37
use Puli\Manager\Config\ConfigFileStorage;
38
use Puli\Manager\Config\ConfigJsonSerializer;
39
use Puli\Manager\Config\DefaultConfig;
40
use Puli\Manager\Config\EnvConfig;
41
use Puli\Manager\Discovery\DiscoveryManagerImpl;
42
use Puli\Manager\Factory\FactoryManagerImpl;
43
use Puli\Manager\Factory\Generator\DefaultGeneratorRegistry;
44
use Puli\Manager\Filesystem\FilesystemStorage;
45
use Puli\Manager\Installation\InstallationManagerImpl;
46
use Puli\Manager\Installer\PackageFileInstallerManager;
47
use Puli\Manager\Migration\MigrationManager;
48
use Puli\Manager\Package\PackageFileStorage;
49
use Puli\Manager\Package\PackageJsonSerializer;
50
use Puli\Manager\Package\PackageManagerImpl;
51
use Puli\Manager\Package\RootPackageFileManagerImpl;
52
use Puli\Manager\Php\ClassWriter;
53
use Puli\Manager\Repository\RepositoryManagerImpl;
54
use Puli\Manager\Server\PackageFileServerManager;
55
use Puli\Manager\Util\System;
56
use Puli\Repository\Api\EditableRepository;
57
use Puli\Repository\Api\ResourceRepository;
58
use Puli\UrlGenerator\Api\UrlGenerator;
59
use Puli\UrlGenerator\DiscoveryUrlGenerator;
60
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
61
use Webmozart\Expression\Expr;
62
use Webmozart\PathUtil\Path;
63
64
/**
65
 * The Puli service locator.
66
 *
67
 * Use this class to access the managers provided by this package:
68
 *
69
 * ```php
70
 * $puli = new Puli(getcwd());
71
 * $puli->start();
72
 *
73
 * $packageManager = $puli->getPackageManager();
74
 * ```
75
 *
76
 * The `Puli` class either operates in the global or a project context:
77
 *
78
 *  * The "global context" is not tied to a specific root package. A global
79
 *    context only loads the settings of the "config.json" file in the home
80
 *    directory. The `Puli` class operates in the global context if no
81
 *    project root directory is passed to the constructor. In the global
82
 *    context, only the global config file manager is available.
83
 *  * The "project context" is tied to a specific Puli project. You need to
84
 *    pass the path to the project's root directory to the constructor or to
85
 *    {@link setRootDirectory()}. The configuration of the "puli.json" file in
86
 *    the root directory is used to configure the managers.
87
 *
88
 * The `Puli` class creates four kinds of managers:
89
 *
90
 *  * The "config file manager" allows you to modify entries of the
91
 *    "config.json" file in the home directory.
92
 *  * The "package file manager" manages modifications to the "puli.json" file
93
 *    of a Puli project.
94
 *  * The "package manager" manages the package repository of a Puli project.
95
 *  * The "repository manager" manages the resource repository of a Puli
96
 *    project.
97
 *  * The "discovery manager" manages the resource discovery of a Puli project.
98
 *
99
 * The home directory is read from the context variable "PULI_HOME".
100
 * If this variable is not set, the home directory defaults to:
101
 *
102
 *  * `$HOME/.puli` on Linux, where `$HOME` is the context variable
103
 *    "HOME".
104
 *  * `$APPDATA/Puli` on Windows, where `$APPDATA` is the context
105
 *    variable "APPDATA".
106
 *
107
 * If none of these variables can be found, an exception is thrown.
108
 *
109
 * A .htaccess file is put into the home directory to protect it from web
110
 * access.
111
 *
112
 * @since  1.0
113
 *
114
 * @author Bernhard Schussek <[email protected]>
115
 */
116
class Puli
117
{
118
    /**
119
     * @var string|null
120
     */
121
    private $rootDir;
122
123
    /**
124
     * @var string
125
     */
126
    private $env;
127
128
    /**
129
     * @var EventDispatcherInterface|null
130
     */
131
    private $dispatcher;
132
133
    /**
134
     * @var Context|ProjectContext
135
     */
136
    private $context;
137
138
    /**
139
     * @var ResourceRepository
140
     */
141
    private $repo;
142
143
    /**
144
     * @var Discovery
145
     */
146
    private $discovery;
147
148
    /**
149
     * @var object
150
     */
151
    private $factory;
152
153
    /**
154
     * @var FactoryManager
155
     */
156
    private $factoryManager;
157
158
    /**
159
     * @var ConfigFileManager
160
     */
161
    private $configFileManager;
162
163
    /**
164
     * @var RootPackageFileManager
165
     */
166
    private $rootPackageFileManager;
167
168
    /**
169
     * @var PackageManager
170
     */
171
    private $packageManager;
172
173
    /**
174
     * @var RepositoryManager
175
     */
176
    private $repositoryManager;
177
178
    /**
179
     * @var DiscoveryManager
180
     */
181
    private $discoveryManager;
182
183
    /**
184
     * @var AssetManager
185
     */
186
    private $assetManager;
187
188
    /**
189
     * @var InstallationManager
190
     */
191
    private $installationManager;
192
193
    /**
194
     * @var InstallerManager
195
     */
196
    private $installerManager;
197
198
    /**
199
     * @var ServerManager
200
     */
201
    private $serverManager;
202
203
    /**
204
     * @var UrlGenerator
205
     */
206
    private $urlGenerator;
207
208
    /**
209
     * @var Storage|null
210
     */
211
    private $storage;
212
213
    /**
214
     * @var ConfigFileStorage|null
215
     */
216
    private $configFileStorage;
217
218
    /**
219
     * @var ConfigFileSerializer|null
220
     */
221
    private $configFileSerializer;
222
223
    /**
224
     * @var PackageFileStorage|null
225
     */
226
    private $packageFileStorage;
227
228
    /**
229
     * @var PackageFileSerializer|null
230
     */
231
    private $packageFileSerializer;
232
233
    /**
234
     * @var LoggerInterface
235
     */
236
    private $logger;
237
238
    /**
239
     * @var bool
240
     */
241
    private $started = false;
242
243
    /**
244
     * @var bool
245
     */
246
    private $pluginsEnabled = true;
247
248
    /**
249
     * Parses the system context for a home directory.
250
     *
251
     * @return null|string Returns the path to the home directory or `null`
252
     *                     if none was found.
253
     */
254 48
    private static function parseHomeDirectory()
255
    {
256
        try {
257 48
            $homeDir = System::parseHomeDirectory();
258
259 45
            System::denyWebAccess($homeDir);
260
261 45
            return $homeDir;
262 3
        } catch (InvalidConfigException $e) {
263
            // Context variable was not found -> no home directory
264
            // This happens often on web servers where the home directory is
265
            // not set manually
266 2
            return null;
267
        }
268
    }
269
270
    /**
271
     * Creates a new instance for the given Puli project.
272
     *
273
     * @param string|null $rootDir The root directory of the Puli project.
274
     *                             If none is passed, the object operates in
275
     *                             the global context. You can set or switch
276
     *                             the root directories later on by calling
277
     *                             {@link setRootDirectory()}.
278
     * @param string      $env     One of the {@link Environment} constants.
279
     *
280
     * @see Puli, start()
281
     */
282 50
    public function __construct($rootDir = null, $env = Environment::DEV)
283
    {
284 50
        $this->setRootDirectory($rootDir);
285 50
        $this->setEnvironment($env);
286 50
    }
287
288
    /**
289
     * Starts the service container.
290
     */
291 48
    public function start()
292
    {
293 48
        if ($this->started) {
294
            throw new LogicException('Puli is already started');
295
        }
296
297 48
        if ($this->rootDir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->rootDir of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
298 26
            $this->context = $this->createProjectContext($this->rootDir, $this->env);
299 1
            $bootstrapFile = $this->context->getConfig()->get(Config::BOOTSTRAP_FILE);
300
301
            // Run the project's bootstrap file to enable project-specific
302
            // autoloading
303 1
            if (null !== $bootstrapFile) {
304
                // Backup autoload functions of the PHAR
305
                $autoloadFunctions = spl_autoload_functions();
306
307
                foreach ($autoloadFunctions as $autoloadFunction) {
308
                    spl_autoload_unregister($autoloadFunction);
309
                }
310
311
                // Add project-specific autoload functions
312
                require_once Path::makeAbsolute($bootstrapFile, $this->rootDir);
313
314
                // Prepend autoload functions of the PHAR again
315
                // This is needed if the user specific autoload functions were
316
                // added with $prepend=true (as done by Composer)
317
                // Classes in the PHAR should always take precedence
318
                for ($i = count($autoloadFunctions) - 1; $i >= 0; --$i) {
319
                    spl_autoload_register($autoloadFunctions[$i], true, true);
320
                }
321
            }
322 1
        } else {
323 22
            $this->context = $this->createGlobalContext();
324
        }
325
326 2
        $this->dispatcher = $this->context->getEventDispatcher();
327 2
        $this->started = true;
328
329
        // Start plugins once the container is running
330 2
        if ($this->rootDir && $this->pluginsEnabled) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->rootDir of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
331 1
            $this->activatePlugins();
332 1
        }
333 2
    }
334
335
    /**
336
     * Returns whether the service container is started.
337
     *
338
     * @return bool Returns `true` if the container is started and `false`
339
     *              otherwise.
340
     */
341
    public function isStarted()
342
    {
343
        return $this->started;
344
    }
345
346
    /**
347
     * Sets the root directory of the managed Puli project.
348
     *
349
     * @param string|null $rootDir The root directory of the managed Puli
350
     *                             project or `null` to start Puli outside of a
351
     *                             specific project.
352
     */
353 50
    public function setRootDirectory($rootDir)
354
    {
355 50
        if ($this->started) {
356
            throw new LogicException('Puli is already started');
357
        }
358
359 50
        Assert::nullOrDirectory($rootDir);
360
361 50
        $this->rootDir = $rootDir ? Path::canonicalize($rootDir) : null;
362 50
    }
363
364
    /**
365
     * Sets the environment of the managed Puli project.
366
     *
367
     * @param string $env One of the {@link Environment} constants.
368
     */
369 50
    public function setEnvironment($env)
370
    {
371 50
        if ($this->started) {
372
            throw new LogicException('Puli is already started');
373
        }
374
375 50
        Assert::oneOf($env, Environment::all(), 'The environment must be one of: %2$s. Got: %s');
376
377 50
        $this->env = $env;
378 50
    }
379
380
    /**
381
     * Retturns the environment of the managed Puli project.
382
     *
383
     * @return string One of the {@link Environment} constants.
384
     */
385
    public function getEnvironment()
386
    {
387
        return $this->env;
388
    }
389
390
    /**
391
     * Returns the root directory of the managed Puli project.
392
     *
393
     * If no Puli project is managed at the moment, `null` is returned.
394
     *
395
     * @return string|null The root directory of the managed Puli project or
396
     *                     `null` if none is set.
397
     */
398
    public function getRootDirectory()
399
    {
400
        return $this->rootDir;
401
    }
402
403
    /**
404
     * Sets the logger to use.
405
     *
406
     * @param LoggerInterface $logger The logger to use.
407
     */
408
    public function setLogger(LoggerInterface $logger)
409
    {
410
        if ($this->started) {
411
            throw new LogicException('Puli is already started');
412
        }
413
414
        $this->logger = $logger;
415
    }
416
417
    /**
418
     * Returns the used logger.
419
     *
420
     * @return LoggerInterface The used logger.
421
     */
422
    public function getLogger()
423
    {
424
        return $this->logger;
425
    }
426
427
    /**
428
     * Sets the event dispatcher to use.
429
     *
430
     * @param EventDispatcherInterface $dispatcher The event dispatcher to use.
431
     */
432 2
    public function setEventDispatcher(EventDispatcherInterface $dispatcher)
433
    {
434 2
        if ($this->started) {
435
            throw new LogicException('Puli is already started');
436
        }
437
438 2
        $this->dispatcher = $dispatcher;
439 2
    }
440
441
    /**
442
     * Returns the used event dispatcher.
443
     *
444
     * @return EventDispatcherInterface|null The used logger.
445
     */
446
    public function getEventDispatcher()
447
    {
448
        return $this->dispatcher;
449
    }
450
451
    /**
452
     * Enables all Puli plugins.
453
     */
454
    public function enablePlugins()
455
    {
456
        $this->pluginsEnabled = true;
457
    }
458
459
    /**
460
     * Disables all Puli plugins.
461
     */
462 1
    public function disablePlugins()
463
    {
464 1
        $this->pluginsEnabled = false;
465 1
    }
466
467
    /**
468
     * Returns whether Puli plugins are enabled.
469
     *
470
     * @return bool Returns `true` if Puli plugins will be loaded and `false`
471
     *              otherwise.
472
     */
473
    public function arePluginsEnabled()
474
    {
475
        return $this->pluginsEnabled;
476
    }
477
478
    /**
479
     * Returns the context.
480
     *
481
     * @return Context|ProjectContext The context.
482
     */
483 2
    public function getContext()
484
    {
485 2
        if (!$this->started) {
486
            throw new LogicException('Puli was not started');
487
        }
488
489 2
        return $this->context;
490
    }
491
492
    /**
493
     * Returns the resource repository of the project.
494
     *
495
     * @return EditableRepository The resource repository.
496
     */
497 View Code Duplication
    public function getRepository()
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...
498
    {
499
        if (!$this->started) {
500
            throw new LogicException('Puli was not started');
501
        }
502
503
        if (!$this->rootDir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->rootDir of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
504
            return null;
505
        }
506
507
        if (!$this->repo) {
508
            $this->repo = $this->getFactory()->createRepository();
509
        }
510
511
        return $this->repo;
512
    }
513
514
    /**
515
     * Returns the resource discovery of the project.
516
     *
517
     * @return EditableDiscovery The resource discovery.
518
     */
519 View Code Duplication
    public function getDiscovery()
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...
520
    {
521
        if (!$this->started) {
522
            throw new LogicException('Puli was not started');
523
        }
524
525
        if (!$this->rootDir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->rootDir of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
526
            return null;
527
        }
528
529
        if (!$this->discovery) {
530
            $this->discovery = $this->getFactory()->createDiscovery($this->getRepository());
531
        }
532
533
        return $this->discovery;
534
    }
535
536
    /**
537
     * @return object
538
     */
539
    public function getFactory()
540
    {
541
        if (!$this->started) {
542
            throw new LogicException('Puli was not started');
543
        }
544
545
        if (!$this->factory && $this->rootDir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->rootDir of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
546
            $this->factory = $this->getFactoryManager()->createFactory();
547
        }
548
549
        return $this->factory;
550
    }
551
552
    /**
553
     * @return FactoryManager
554
     */
555
    public function getFactoryManager()
556
    {
557
        if (!$this->started) {
558
            throw new LogicException('Puli was not started');
559
        }
560
561
        if (!$this->factoryManager && $this->rootDir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->rootDir of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
562
            $this->factoryManager = new FactoryManagerImpl(
563
                $this->context,
0 ignored issues
show
Compatibility introduced by
$this->context of type object<Puli\Manager\Api\Context\Context> is not a sub-type of object<Puli\Manager\Api\Context\ProjectContext>. It seems like you assume a child class of the class Puli\Manager\Api\Context\Context to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
564
                new DefaultGeneratorRegistry(),
565
                new ClassWriter()
566
            );
567
568
            // Don't set via the constructor to prevent a cyclic dependency
569
            $this->factoryManager->setServers($this->getServerManager()->getServers());
570
        }
571
572
        return $this->factoryManager;
573
    }
574
575
    /**
576
     * Returns the configuration file manager.
577
     *
578
     * @return ConfigFileManager The configuration file manager.
579
     */
580
    public function getConfigFileManager()
581
    {
582
        if (!$this->started) {
583
            throw new LogicException('Puli was not started');
584
        }
585
586
        if (!$this->configFileManager && $this->context->getHomeDirectory()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->context->getHomeDirectory() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
587
            $this->configFileManager = new ConfigFileManagerImpl(
588
                $this->context,
589
                $this->getConfigFileStorage(),
590
                $this->getFactoryManager()
0 ignored issues
show
Unused Code introduced by
The call to ConfigFileManagerImpl::__construct() has too many arguments starting with $this->getFactoryManager().

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
591
            );
592
        }
593
594
        return $this->configFileManager;
595
    }
596
597
    /**
598
     * Returns the root package file manager.
599
     *
600
     * @return RootPackageFileManager The package file manager.
601
     */
602 View Code Duplication
    public function getRootPackageFileManager()
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...
603
    {
604
        if (!$this->started) {
605
            throw new LogicException('Puli was not started');
606
        }
607
608
        if (!$this->rootPackageFileManager && $this->rootDir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->rootDir of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
609
            $this->rootPackageFileManager = new RootPackageFileManagerImpl(
610
                $this->context,
0 ignored issues
show
Compatibility introduced by
$this->context of type object<Puli\Manager\Api\Context\Context> is not a sub-type of object<Puli\Manager\Api\Context\ProjectContext>. It seems like you assume a child class of the class Puli\Manager\Api\Context\Context to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
611
                $this->getPackageFileStorage()
612
            );
613
        }
614
615
        return $this->rootPackageFileManager;
616
    }
617
618
    /**
619
     * Returns the package manager.
620
     *
621
     * @return PackageManager The package manager.
622
     */
623 View Code Duplication
    public function getPackageManager()
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...
624
    {
625
        if (!$this->started) {
626
            throw new LogicException('Puli was not started');
627
        }
628
629
        if (!$this->packageManager && $this->rootDir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->rootDir of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
630
            $this->packageManager = new PackageManagerImpl(
631
                $this->context,
0 ignored issues
show
Compatibility introduced by
$this->context of type object<Puli\Manager\Api\Context\Context> is not a sub-type of object<Puli\Manager\Api\Context\ProjectContext>. It seems like you assume a child class of the class Puli\Manager\Api\Context\Context to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
632
                $this->getPackageFileStorage()
633
            );
634
        }
635
636
        return $this->packageManager;
637
    }
638
639
    /**
640
     * Returns the resource repository manager.
641
     *
642
     * @return RepositoryManager The repository manager.
643
     */
644 View Code Duplication
    public function getRepositoryManager()
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...
645
    {
646
        if (!$this->started) {
647
            throw new LogicException('Puli was not started');
648
        }
649
650
        if (!$this->repositoryManager && $this->rootDir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->rootDir of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
651
            $this->repositoryManager = new RepositoryManagerImpl(
652
                $this->context,
0 ignored issues
show
Compatibility introduced by
$this->context of type object<Puli\Manager\Api\Context\Context> is not a sub-type of object<Puli\Manager\Api\Context\ProjectContext>. It seems like you assume a child class of the class Puli\Manager\Api\Context\Context to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
653
                $this->getRepository(),
654
                $this->getPackageManager()->findPackages(Expr::method('isEnabled', Expr::same(true))),
655
                $this->getPackageFileStorage()
656
            );
657
        }
658
659
        return $this->repositoryManager;
660
    }
661
662
    /**
663
     * Returns the resource discovery manager.
664
     *
665
     * @return DiscoveryManager The discovery manager.
666
     */
667 View Code Duplication
    public function getDiscoveryManager()
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...
668
    {
669
        if (!$this->started) {
670
            throw new LogicException('Puli was not started');
671
        }
672
673
        if (!$this->discoveryManager && $this->rootDir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->rootDir of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
674
            $this->discoveryManager = new DiscoveryManagerImpl(
675
                $this->context,
0 ignored issues
show
Compatibility introduced by
$this->context of type object<Puli\Manager\Api\Context\Context> is not a sub-type of object<Puli\Manager\Api\Context\ProjectContext>. It seems like you assume a child class of the class Puli\Manager\Api\Context\Context to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
676
                $this->getDiscovery(),
677
                $this->getPackageManager()->findPackages(Expr::method('isEnabled', Expr::same(true))),
678
                $this->getPackageFileStorage(),
679
                $this->logger
680
            );
681
        }
682
683
        return $this->discoveryManager;
684
    }
685
686
    /**
687
     * Returns the asset manager.
688
     *
689
     * @return AssetManager The asset manager.
690
     */
691
    public function getAssetManager()
692
    {
693
        if (!$this->started) {
694
            throw new LogicException('Puli was not started');
695
        }
696
697
        if (!$this->assetManager && $this->rootDir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->rootDir of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
698
            $this->assetManager = new DiscoveryAssetManager(
699
                $this->getDiscoveryManager(),
700
                $this->getServerManager()->getServers()
701
            );
702
        }
703
704
        return $this->assetManager;
705
    }
706
707
    /**
708
     * Returns the installation manager.
709
     *
710
     * @return InstallationManager The installation manager.
711
     */
712
    public function getInstallationManager()
713
    {
714
        if (!$this->started) {
715
            throw new LogicException('Puli was not started');
716
        }
717
718
        if (!$this->installationManager && $this->rootDir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->rootDir of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
719
            $this->installationManager = new InstallationManagerImpl(
720
                $this->getContext(),
0 ignored issues
show
Compatibility introduced by
$this->getContext() of type object<Puli\Manager\Api\Context\Context> is not a sub-type of object<Puli\Manager\Api\Context\ProjectContext>. It seems like you assume a child class of the class Puli\Manager\Api\Context\Context to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
721
                $this->getRepository(),
722
                $this->getServerManager()->getServers(),
723
                $this->getInstallerManager()
724
            );
725
        }
726
727
        return $this->installationManager;
728
    }
729
730
    /**
731
     * Returns the installer manager.
732
     *
733
     * @return InstallerManager The installer manager.
734
     */
735
    public function getInstallerManager()
736
    {
737
        if (!$this->started) {
738
            throw new LogicException('Puli was not started');
739
        }
740
741
        if (!$this->installerManager && $this->rootDir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->rootDir of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
742
            $this->installerManager = new PackageFileInstallerManager(
743
                $this->getRootPackageFileManager(),
744
                $this->getPackageManager()->getPackages()
745
            );
746
        }
747
748
        return $this->installerManager;
749
    }
750
751
    /**
752
     * Returns the server manager.
753
     *
754
     * @return ServerManager The server manager.
755
     */
756
    public function getServerManager()
757
    {
758
        if (!$this->started) {
759
            throw new LogicException('Puli was not started');
760
        }
761
762
        if (!$this->serverManager && $this->rootDir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->rootDir of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
763
            $this->serverManager = new PackageFileServerManager(
764
                $this->getRootPackageFileManager(),
765
                $this->getInstallerManager()
766
            );
767
        }
768
769
        return $this->serverManager;
770
    }
771
772
    /**
773
     * Returns the resource URL generator.
774
     *
775
     * @return UrlGenerator The resource URL generator.
776
     */
777
    public function getUrlGenerator()
778
    {
779
        if (!$this->started) {
780
            throw new LogicException('Puli was not started');
781
        }
782
783
        if (!$this->urlGenerator && $this->rootDir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->rootDir of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
784
            $urlFormats = array();
785
            foreach ($this->getServerManager()->getServers() as $server) {
786
                $urlFormats[$server->getName()] = $server->getUrlFormat();
787
            }
788
789
            $this->urlGenerator = new DiscoveryUrlGenerator($this->getDiscovery(), $urlFormats);
790
        }
791
792
        return $this->urlGenerator;
793
    }
794
795
    /**
796
     * Returns the cached file storage.
797
     *
798
     * @return Storage The storage.
799
     */
800 46
    public function getStorage()
801
    {
802 46
        if (!$this->storage) {
803 46
            $this->storage = new FilesystemStorage();
804 46
        }
805
806 46
        return $this->storage;
807
    }
808
809
    /**
810
     * Returns the cached configuration file serializer.
811
     *
812
     * @return ConfigFileSerializer The configuration file serializer.
813
     */
814 45
    public function getConfigFileSerializer()
815
    {
816 45
        if (!$this->configFileSerializer) {
817 45
            $this->configFileSerializer = new ConfigJsonSerializer();
818 45
        }
819
820 45
        return $this->configFileSerializer;
821
    }
822
823
    /**
824
     * Returns the cached package file serializer.
825
     *
826
     * @return PackageFileSerializer The package file serializer.
827
     */
828 1
    public function getPackageFileSerializer()
829
    {
830 1
        if (!$this->packageFileSerializer) {
831 1
            $this->packageFileSerializer = new PackageJsonSerializer(
832 1
                new MigrationManager(array(
833
                    // Add future migrations here
834 1
                )),
835
                __DIR__.'/../../res/schema'
836 1
            );
837 1
        }
838
839 1
        return $this->packageFileSerializer;
840
    }
841
842 1
    private function activatePlugins()
843
    {
844 1
        foreach ($this->context->getRootPackageFile()->getPluginClasses() as $pluginClass) {
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Puli\Manager\Api\Context\Context as the method getRootPackageFile() does only exist in the following sub-classes of Puli\Manager\Api\Context\Context: Puli\Manager\Api\Context\ProjectContext. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
845 1
            $this->validatePluginClass($pluginClass);
846
847
            /** @var PuliPlugin $plugin */
848 1
            $plugin = new $pluginClass();
849 1
            $plugin->activate($this);
850 1
        }
851 1
    }
852
853 22
    private function createGlobalContext()
854
    {
855 22
        $homeDir = self::parseHomeDirectory();
856
857 21 View Code Duplication
        if (null !== $homeDir) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
858 20
            Assert::fileExists($homeDir, 'Could not load Puli context: The home directory %s does not exist.');
859 20
            Assert::directory($homeDir, 'Could not load Puli context: The home directory %s is a file. Expected a directory.');
860
861
            // Create a storage without the factory manager
862 20
            $configStorage = new ConfigFileStorage($this->getStorage(), $this->getConfigFileSerializer());
863 20
            $configPath = Path::canonicalize($homeDir).'/config.json';
864 20
            $configFile = $configStorage->loadConfigFile($configPath, new DefaultConfig());
865
            $baseConfig = $configFile->getConfig();
866
        } else {
867 1
            $configFile = null;
868 1
            $baseConfig = new DefaultConfig();
869
        }
870
871 1
        $config = new EnvConfig($baseConfig);
872
873 1
        return new Context($homeDir, $config, $configFile, $this->dispatcher);
874
    }
875
876
    /**
877
     * Creates the context of a Puli project.
878
     *
879
     * The home directory is read from the context variable "PULI_HOME".
880
     * If this variable is not set, the home directory defaults to:
881
     *
882
     *  * `$HOME/.puli` on Linux, where `$HOME` is the context variable
883
     *    "HOME".
884
     *  * `$APPDATA/Puli` on Windows, where `$APPDATA` is the context
885
     *    variable "APPDATA".
886
     *
887
     * If none of these variables can be found, an exception is thrown.
888
     *
889
     * A .htaccess file is put into the home directory to protect it from web
890
     * access.
891
     *
892
     * @param string $rootDir The path to the project.
893
     *
894
     * @return ProjectContext The project context.
895
     */
896 26
    private function createProjectContext($rootDir, $env)
897
    {
898 26
        Assert::fileExists($rootDir, 'Could not load Puli context: The root %s does not exist.');
899 26
        Assert::directory($rootDir, 'Could not load Puli context: The root %s is a file. Expected a directory.');
900
901 26
        $homeDir = self::parseHomeDirectory();
902
903 26 View Code Duplication
        if (null !== $homeDir) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
904 25
            Assert::fileExists($homeDir, 'Could not load Puli context: The home directory %s does not exist.');
905 25
            Assert::directory($homeDir, 'Could not load Puli context: The home directory %s is a file. Expected a directory.');
906
907
            // Create a storage without the factory manager
908 25
            $configStorage = new ConfigFileStorage($this->getStorage(), $this->getConfigFileSerializer());
909 25
            $configPath = Path::canonicalize($homeDir).'/config.json';
910 25
            $configFile = $configStorage->loadConfigFile($configPath, new DefaultConfig());
911
            $baseConfig = $configFile->getConfig();
912
        } else {
913 1
            $configFile = null;
914 1
            $baseConfig = new DefaultConfig();
915
        }
916
917
        // Create a storage without the factory manager
918 1
        $packageFileStorage = new PackageFileStorage($this->getStorage(), $this->getPackageFileSerializer());
919 1
        $rootDir = Path::canonicalize($rootDir);
920 1
        $rootFilePath = $this->rootDir.'/puli.json';
921 1
        $rootPackageFile = $packageFileStorage->loadRootPackageFile($rootFilePath, $baseConfig);
922 1
        $config = new EnvConfig($rootPackageFile->getConfig());
923
924 1
        return new ProjectContext($homeDir, $rootDir, $config, $rootPackageFile, $configFile, $this->dispatcher, $env);
925
    }
926
927
    /**
928
     * Returns the cached configuration file storage.
929
     *
930
     * @return ConfigFileStorage The configuration file storage.
931
     */
932
    private function getConfigFileStorage()
933
    {
934
        if (!$this->configFileStorage) {
935
            $this->configFileStorage = new ConfigFileStorage(
936
                $this->getStorage(),
937
                $this->getConfigFileSerializer(),
938
                $this->getFactoryManager()
939
            );
940
        }
941
942
        return $this->configFileStorage;
943
    }
944
945
    /**
946
     * Returns the cached package file storage.
947
     *
948
     * @return PackageFileStorage The package file storage.
949
     */
950
    private function getPackageFileStorage()
951
    {
952
        if (!$this->packageFileStorage) {
953
            $this->packageFileStorage = new PackageFileStorage(
954
                $this->getStorage(),
955
                $this->getPackageFileSerializer(),
956
                $this->getFactoryManager()
957
            );
958
        }
959
960
        return $this->packageFileStorage;
961
    }
962
963
    /**
964
     * Validates the given plugin class name.
965
     *
966
     * @param string $pluginClass The fully qualified name of a plugin class.
967
     */
968 1
    private function validatePluginClass($pluginClass)
969
    {
970 1
        if (!class_exists($pluginClass)) {
971
            throw new InvalidConfigException(sprintf(
972
                'The plugin class %s does not exist.',
973
                $pluginClass
974
            ));
975
        }
976
977 1
        if (!in_array('Puli\Manager\Api\PuliPlugin', class_implements($pluginClass))) {
978
            throw new InvalidConfigException(sprintf(
979
                'The plugin class %s must implement PuliPlugin.',
980
                $pluginClass
981
            ));
982
        }
983 1
    }
984
}
985