Completed
Pull Request — master (#28)
by Titouan
09:30
created

Puli::getUrlGenerator()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5.1502

Importance

Changes 1
Bugs 0 Features 1
Metric Value
dl 0
loc 17
c 1
b 0
f 1
ccs 9
cts 11
cp 0.8182
rs 8.8571
cc 5
eloc 9
nc 3
nop 0
crap 5.1502
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 26
            $bootstrapFile = $this->context->getConfig()->get(Config::BOOTSTRAP_FILE);
300
301
            // Run the project's bootstrap file to enable project-specific
302
            // autoloading
303 26
            if (null !== $bootstrapFile) {
304
                // Backup autoload functions of the PHAR
305 1
                $autoloadFunctions = spl_autoload_functions();
306
307 4
                foreach ($autoloadFunctions as $autoloadFunction) {
308 1
                    spl_autoload_unregister($autoloadFunction);
309 1
                }
310
311
                // Add project-specific autoload functions
312 1
                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 1
                for ($i = count($autoloadFunctions) - 1; $i >= 0; --$i) {
319 1
                    spl_autoload_register($autoloadFunctions[$i], true, true);
320 1
                }
321 1
            }
322 26
        } else {
323 22
            $this->context = $this->createGlobalContext();
324
        }
325
326 47
        $this->dispatcher = $this->context->getEventDispatcher();
327 47
        $this->started = true;
328
329
        // Start plugins once the container is running
330 47
        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 25
            $this->activatePlugins();
332 23
        }
333 45
    }
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 2
    public function getEnvironment()
386
    {
387 2
        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 4
    public function getRootDirectory()
399
    {
400 4
        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 2
    public function getEventDispatcher()
447
    {
448 2
        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 28
    public function getContext()
484
    {
485 28
        if (!$this->started) {
486
            throw new LogicException('Puli was not started');
487
        }
488
489 28
        return $this->context;
490
    }
491
492
    /**
493
     * Returns the resource repository of the project.
494
     *
495
     * @return EditableRepository The resource repository.
496
     */
497 8 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 8
        if (!$this->started) {
500
            throw new LogicException('Puli was not started');
501
        }
502
503 8
        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 1
            return null;
505
        }
506
507 7
        if (!$this->repo) {
508 7
            $this->repo = $this->getFactory()->createRepository();
509 7
        }
510
511 7
        return $this->repo;
512
    }
513
514
    /**
515
     * Returns the resource discovery of the project.
516
     *
517
     * @return EditableDiscovery The resource discovery.
518
     */
519 5 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 5
        if (!$this->started) {
522
            throw new LogicException('Puli was not started');
523
        }
524
525 5
        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 1
            return null;
527
        }
528
529 4
        if (!$this->discovery) {
530 4
            $this->discovery = $this->getFactory()->createDiscovery($this->getRepository());
531 4
        }
532
533 4
        return $this->discovery;
534
    }
535
536
    /**
537
     * @return object
538
     */
539 9
    public function getFactory()
540
    {
541 9
        if (!$this->started) {
542
            throw new LogicException('Puli was not started');
543
        }
544
545 9
        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 8
            $this->factory = $this->getFactoryManager()->createFactory();
547 8
        }
548
549 9
        return $this->factory;
550
    }
551
552
    /**
553
     * @return FactoryManager
554
     */
555 16
    public function getFactoryManager()
556
    {
557 16
        if (!$this->started) {
558
            throw new LogicException('Puli was not started');
559
        }
560
561 16
        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 14
            $this->factoryManager = new FactoryManagerImpl(
563 14
                $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 14
                new DefaultGeneratorRegistry(),
565 14
                new ClassWriter()
566 14
            );
567
568
            // Don't set via the constructor to prevent a cyclic dependency
569 14
            $this->factoryManager->setServers($this->getServerManager()->getServers());
570 14
        }
571
572 16
        return $this->factoryManager;
573
    }
574
575
    /**
576
     * Returns the configuration file manager.
577
     *
578
     * @return ConfigFileManager The configuration file manager.
579
     */
580 2
    public function getConfigFileManager()
581
    {
582 2
        if (!$this->started) {
583
            throw new LogicException('Puli was not started');
584
        }
585
586 2
        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 2
            $this->configFileManager = new ConfigFileManagerImpl(
588 2
                $this->context,
589 2
                $this->getConfigFileStorage(),
590 2
                $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 2
            );
592 2
        }
593
594 2
        return $this->configFileManager;
595
    }
596
597
    /**
598
     * Returns the root package file manager.
599
     *
600
     * @return RootPackageFileManager The package file manager.
601
     */
602 15 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 15
        if (!$this->started) {
605
            throw new LogicException('Puli was not started');
606
        }
607
608 15
        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 14
            $this->rootPackageFileManager = new RootPackageFileManagerImpl(
610 14
                $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 14
                $this->getPackageFileStorage()
612 14
            );
613 14
        }
614
615 15
        return $this->rootPackageFileManager;
616
    }
617
618
    /**
619
     * Returns the package manager.
620
     *
621
     * @return PackageManager The package manager.
622
     */
623 15 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 15
        if (!$this->started) {
626
            throw new LogicException('Puli was not started');
627
        }
628
629 15
        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 14
            $this->packageManager = new PackageManagerImpl(
631 14
                $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 14
                $this->getPackageFileStorage()
633 14
            );
634 14
        }
635
636 15
        return $this->packageManager;
637
    }
638
639
    /**
640
     * Returns the resource repository manager.
641
     *
642
     * @return RepositoryManager The repository manager.
643
     */
644 2 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 2
        if (!$this->started) {
647
            throw new LogicException('Puli was not started');
648
        }
649
650 2
        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 1
            $this->repositoryManager = new RepositoryManagerImpl(
652 1
                $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 1
                $this->getRepository(),
654 1
                $this->getPackageManager()->findPackages(Expr::method('isEnabled', Expr::same(true))),
655 1
                $this->getPackageFileStorage()
656 1
            );
657 1
        }
658
659 2
        return $this->repositoryManager;
660
    }
661
662
    /**
663
     * Returns the resource discovery manager.
664
     *
665
     * @return DiscoveryManager The discovery manager.
666
     */
667 3 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 3
        if (!$this->started) {
670
            throw new LogicException('Puli was not started');
671
        }
672
673 3
        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 2
            $this->discoveryManager = new DiscoveryManagerImpl(
675 2
                $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 2
                $this->getDiscovery(),
677 2
                $this->getPackageManager()->findPackages(Expr::method('isEnabled', Expr::same(true))),
678 2
                $this->getPackageFileStorage(),
679 2
                $this->logger
680 2
            );
681 2
        }
682
683 3
        return $this->discoveryManager;
684
    }
685
686
    /**
687
     * Returns the asset manager.
688
     *
689
     * @return AssetManager The asset manager.
690
     */
691 2
    public function getAssetManager()
692
    {
693 2
        if (!$this->started) {
694
            throw new LogicException('Puli was not started');
695
        }
696
697 2
        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 1
            $this->assetManager = new DiscoveryAssetManager(
699 1
                $this->getDiscoveryManager(),
700 1
                $this->getServerManager()->getServers()
701 1
            );
702 1
        }
703
704 2
        return $this->assetManager;
705
    }
706
707
    /**
708
     * Returns the installation manager.
709
     *
710
     * @return InstallationManager The installation manager.
711
     */
712 2
    public function getInstallationManager()
713
    {
714 2
        if (!$this->started) {
715
            throw new LogicException('Puli was not started');
716
        }
717
718 2
        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 1
            $this->installationManager = new InstallationManagerImpl(
720 1
                $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 1
                $this->getRepository(),
722 1
                $this->getServerManager()->getServers(),
723 1
                $this->getInstallerManager()
724 1
            );
725 1
        }
726
727 2
        return $this->installationManager;
728
    }
729
730
    /**
731
     * Returns the installer manager.
732
     *
733
     * @return InstallerManager The installer manager.
734
     */
735 15
    public function getInstallerManager()
736
    {
737 15
        if (!$this->started) {
738
            throw new LogicException('Puli was not started');
739
        }
740
741 15
        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 14
            $this->installerManager = new PackageFileInstallerManager(
743 14
                $this->getRootPackageFileManager(),
744 14
                $this->getPackageManager()->getPackages()
745 14
            );
746 14
        }
747
748 15
        return $this->installerManager;
749
    }
750
751
    /**
752
     * Returns the server manager.
753
     *
754
     * @return ServerManager The server manager.
755
     */
756 15
    public function getServerManager()
757
    {
758 15
        if (!$this->started) {
759
            throw new LogicException('Puli was not started');
760
        }
761
762 15
        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 14
            $this->serverManager = new PackageFileServerManager(
764 14
                $this->getRootPackageFileManager(),
765 14
                $this->getInstallerManager()
766 14
            );
767 14
        }
768
769 15
        return $this->serverManager;
770
    }
771
772
    /**
773
     * Returns the resource URL generator.
774
     *
775
     * @return UrlGenerator The resource URL generator.
776
     */
777 2
    public function getUrlGenerator()
778
    {
779 2
        if (!$this->started) {
780
            throw new LogicException('Puli was not started');
781
        }
782
783 2
        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 1
            $urlFormats = array();
785 1
            foreach ($this->getServerManager()->getServers() as $server) {
786
                $urlFormats[$server->getName()] = $server->getUrlFormat();
787 1
            }
788
789 1
            $this->urlGenerator = new DiscoveryUrlGenerator($this->getDiscovery(), $urlFormats);
790 1
        }
791
792 2
        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 26
    public function getPackageFileSerializer()
829
    {
830 26
        if (!$this->packageFileSerializer) {
831 26
            $this->packageFileSerializer = new PackageJsonSerializer(
832 26
                new MigrationManager(array(
833
                    // Add future migrations here
834 26
                )),
835
                __DIR__.'/../../res/schema'
836 26
            );
837 26
        }
838
839 26
        return $this->packageFileSerializer;
840
    }
841
842 25
    private function activatePlugins()
843
    {
844 25
        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 25
            $this->validatePluginClass($pluginClass);
846
847
            /** @var PuliPlugin $plugin */
848 23
            $plugin = new $pluginClass();
849 23
            $plugin->activate($this);
850 23
        }
851 23
    }
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 20
            $baseConfig = $configFile->getConfig();
866 20
        } else {
867 1
            $configFile = null;
868 1
            $baseConfig = new DefaultConfig();
869
        }
870
871 21
        $config = new EnvConfig($baseConfig);
872
873 21
        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 25
            $baseConfig = $configFile->getConfig();
912 25
        } else {
913 1
            $configFile = null;
914 1
            $baseConfig = new DefaultConfig();
915
        }
916
917
        // Create a storage without the factory manager
918 26
        $packageFileStorage = new PackageFileStorage($this->getStorage(), $this->getPackageFileSerializer());
919 26
        $rootDir = Path::canonicalize($rootDir);
920 26
        $rootFilePath = $this->rootDir.'/puli.json';
921 26
        $rootPackageFile = $packageFileStorage->loadRootPackageFile($rootFilePath, $baseConfig);
922 26
        $config = new EnvConfig($rootPackageFile->getConfig());
923
924 26
        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 2
    private function getConfigFileStorage()
933
    {
934 2
        if (!$this->configFileStorage) {
935 2
            $this->configFileStorage = new ConfigFileStorage(
936 2
                $this->getStorage(),
937 2
                $this->getConfigFileSerializer(),
938 2
                $this->getFactoryManager()
939 2
            );
940 2
        }
941
942 2
        return $this->configFileStorage;
943
    }
944
945
    /**
946
     * Returns the cached package file storage.
947
     *
948
     * @return PackageFileStorage The package file storage.
949
     */
950 14
    private function getPackageFileStorage()
951
    {
952 14
        if (!$this->packageFileStorage) {
953 14
            $this->packageFileStorage = new PackageFileStorage(
954 14
                $this->getStorage(),
955 14
                $this->getPackageFileSerializer(),
956 14
                $this->getFactoryManager()
957 14
            );
958 14
        }
959
960 14
        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 25
    private function validatePluginClass($pluginClass)
969
    {
970 25
        if (!class_exists($pluginClass)) {
971 1
            throw new InvalidConfigException(sprintf(
972 1
                'The plugin class %s does not exist.',
973
                $pluginClass
974 1
            ));
975
        }
976
977 24
        if (!in_array('Puli\Manager\Api\PuliPlugin', class_implements($pluginClass))) {
978 1
            throw new InvalidConfigException(sprintf(
979 1
                'The plugin class %s must implement PuliPlugin.',
980
                $pluginClass
981 1
            ));
982
        }
983 23
    }
984
}
985