Completed
Push — master ( a79d3f...dddf5f )
by Bernhard
04:07
created

Puli::createProjectContext()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 3

Importance

Changes 4
Bugs 2 Features 1
Metric Value
c 4
b 2
f 1
dl 0
loc 27
ccs 16
cts 16
cp 1
rs 8.8571
cc 3
eloc 16
nc 4
nop 2
crap 3
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\RootPackageFile;
31
use Puli\Manager\Api\Package\RootPackageFileManager;
32
use Puli\Manager\Api\Repository\RepositoryManager;
33
use Puli\Manager\Api\Server\ServerManager;
34
use Puli\Manager\Api\Storage\Storage;
35
use Puli\Manager\Assert\Assert;
36
use Puli\Manager\Asset\DiscoveryAssetManager;
37
use Puli\Manager\Config\ConfigFileManagerImpl;
38
use Puli\Manager\Config\ConfigFileStorage;
39
use Puli\Manager\Config\ConfigJsonSerializer;
40
use Puli\Manager\Config\DefaultConfig;
41
use Puli\Manager\Config\EnvConfig;
42
use Puli\Manager\Discovery\DiscoveryManagerImpl;
43
use Puli\Manager\Factory\FactoryManagerImpl;
44
use Puli\Manager\Factory\Generator\DefaultGeneratorRegistry;
45
use Puli\Manager\Filesystem\FilesystemStorage;
46
use Puli\Manager\Installation\InstallationManagerImpl;
47
use Puli\Manager\Installer\PackageFileInstallerManager;
48
use Puli\Manager\Migration\MigrationManager;
49
use Puli\Manager\Package\PackageFileStorage;
50
use Puli\Manager\Package\PackageJsonSerializer;
51
use Puli\Manager\Package\PackageManagerImpl;
52
use Puli\Manager\Package\RootPackageFileManagerImpl;
53
use Puli\Manager\Php\ClassWriter;
54
use Puli\Manager\Repository\RepositoryManagerImpl;
55
use Puli\Manager\Server\PackageFileServerManager;
56
use Puli\Manager\Util\System;
57
use Puli\Repository\Api\EditableRepository;
58
use Puli\Repository\Api\ResourceRepository;
59
use Puli\UrlGenerator\Api\UrlGenerator;
60
use Puli\UrlGenerator\DiscoveryUrlGenerator;
61
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
62
use Webmozart\Expression\Expr;
63
use Webmozart\PathUtil\Path;
64
65
/**
66
 * The Puli service locator.
67
 *
68
 * Use this class to access the managers provided by this package:
69
 *
70
 * ```php
71
 * $puli = new Puli(getcwd());
72
 * $puli->start();
73
 *
74
 * $packageManager = $puli->getPackageManager();
75
 * ```
76
 *
77
 * The `Puli` class either operates in the global or a project context:
78
 *
79
 *  * The "global context" is not tied to a specific root package. A global
80
 *    context only loads the settings of the "config.json" file in the home
81
 *    directory. The `Puli` class operates in the global context if no
82
 *    project root directory is passed to the constructor. In the global
83
 *    context, only the global config file manager is available.
84
 *  * The "project context" is tied to a specific Puli project. You need to
85
 *    pass the path to the project's root directory to the constructor or to
86
 *    {@link setRootDirectory()}. The configuration of the "puli.json" file in
87
 *    the root directory is used to configure the managers.
88
 *
89
 * The `Puli` class creates four kinds of managers:
90
 *
91
 *  * The "config file manager" allows you to modify entries of the
92
 *    "config.json" file in the home directory.
93
 *  * The "package file manager" manages modifications to the "puli.json" file
94
 *    of a Puli project.
95
 *  * The "package manager" manages the package repository of a Puli project.
96
 *  * The "repository manager" manages the resource repository of a Puli
97
 *    project.
98
 *  * The "discovery manager" manages the resource discovery of a Puli project.
99
 *
100
 * The home directory is read from the context variable "PULI_HOME".
101
 * If this variable is not set, the home directory defaults to:
102
 *
103
 *  * `$HOME/.puli` on Linux, where `$HOME` is the context variable
104
 *    "HOME".
105
 *  * `$APPDATA/Puli` on Windows, where `$APPDATA` is the context
106
 *    variable "APPDATA".
107
 *
108
 * If none of these variables can be found, an exception is thrown.
109
 *
110
 * A .htaccess file is put into the home directory to protect it from web
111
 * access.
112
 *
113
 * @since  1.0
114
 *
115
 * @author Bernhard Schussek <[email protected]>
116
 */
117
class Puli
118
{
119
    /**
120
     * @var string|null
121
     */
122
    private $rootDir;
123
124
    /**
125
     * @var string
126
     */
127
    private $env;
128
129
    /**
130
     * @var EventDispatcherInterface|null
131
     */
132
    private $dispatcher;
133
134
    /**
135
     * @var Context|ProjectContext
136
     */
137
    private $context;
138
139
    /**
140
     * @var ResourceRepository
141
     */
142
    private $repo;
143
144
    /**
145
     * @var Discovery
146
     */
147
    private $discovery;
148
149
    /**
150
     * @var object
151
     */
152
    private $factory;
153
154
    /**
155
     * @var FactoryManager
156
     */
157
    private $factoryManager;
158
159
    /**
160
     * @var ConfigFileManager
161
     */
162
    private $configFileManager;
163
164
    /**
165
     * @var RootPackageFileManager
166
     */
167
    private $rootPackageFileManager;
168
169
    /**
170
     * @var PackageManager
171
     */
172
    private $packageManager;
173
174
    /**
175
     * @var RepositoryManager
176
     */
177
    private $repositoryManager;
178
179
    /**
180
     * @var DiscoveryManager
181
     */
182
    private $discoveryManager;
183
184
    /**
185
     * @var AssetManager
186
     */
187
    private $assetManager;
188
189
    /**
190
     * @var InstallationManager
191
     */
192
    private $installationManager;
193
194
    /**
195
     * @var InstallerManager
196
     */
197
    private $installerManager;
198
199
    /**
200
     * @var ServerManager
201
     */
202
    private $serverManager;
203
204
    /**
205
     * @var UrlGenerator
206
     */
207
    private $urlGenerator;
208
209
    /**
210
     * @var Storage|null
211
     */
212
    private $storage;
213
214
    /**
215
     * @var ConfigFileStorage|null
216
     */
217
    private $configFileStorage;
218
219
    /**
220
     * @var ConfigFileSerializer|null
221
     */
222
    private $configFileSerializer;
223
224
    /**
225
     * @var PackageFileStorage|null
226
     */
227
    private $packageFileStorage;
228
229
    /**
230
     * @var PackageFileSerializer|null
231
     */
232
    private $packageFileSerializer;
233
234
    /**
235
     * @var LoggerInterface
236
     */
237
    private $logger;
238
239
    /**
240
     * @var bool
241
     */
242
    private $started = false;
243
244
    /**
245
     * @var bool
246
     */
247
    private $pluginsEnabled = true;
248
249
    /**
250
     * Parses the system context for a home directory.
251
     *
252
     * @return null|string Returns the path to the home directory or `null`
253
     *                     if none was found.
254
     */
255 50
    private static function parseHomeDirectory()
256
    {
257
        try {
258 50
            $homeDir = System::parseHomeDirectory();
259
260 47
            System::denyWebAccess($homeDir);
261
262 47
            return $homeDir;
263 3
        } catch (InvalidConfigException $e) {
264
            // Context variable was not found -> no home directory
265
            // This happens often on web servers where the home directory is
266
            // not set manually
267 2
            return null;
268
        }
269
    }
270
271
    /**
272
     * Creates a new instance for the given Puli project.
273
     *
274
     * @param string|null $rootDir The root directory of the Puli project.
275
     *                             If none is passed, the object operates in
276
     *                             the global context. You can set or switch
277
     *                             the root directories later on by calling
278
     *                             {@link setRootDirectory()}.
279
     * @param string      $env     One of the {@link Environment} constants.
280
     *
281
     * @see Puli, start()
282
     */
283 52
    public function __construct($rootDir = null, $env = Environment::DEV)
284
    {
285 52
        $this->setRootDirectory($rootDir);
286 52
        $this->setEnvironment($env);
287 52
    }
288
289
    /**
290
     * Starts the service container.
291
     */
292 50
    public function start()
293
    {
294 50
        if ($this->started) {
295
            throw new LogicException('Puli is already started');
296
        }
297
298 50
        if (null !== $this->rootDir) {
299 27
            $this->context = $this->createProjectContext($this->rootDir, $this->env);
300 27
            $bootstrapFile = $this->context->getConfig()->get(Config::BOOTSTRAP_FILE);
301
302
            // Run the project's bootstrap file to enable project-specific
303
            // autoloading
304 27
            if (null !== $bootstrapFile) {
305
                // Backup autoload functions of the PHAR
306 1
                $autoloadFunctions = spl_autoload_functions();
307
308 1
                foreach ($autoloadFunctions as $autoloadFunction) {
309 1
                    spl_autoload_unregister($autoloadFunction);
310 1
                }
311
312
                // Add project-specific autoload functions
313 1
                require_once Path::makeAbsolute($bootstrapFile, $this->rootDir);
314
315
                // Prepend autoload functions of the PHAR again
316
                // This is needed if the user specific autoload functions were
317
                // added with $prepend=true (as done by Composer)
318
                // Classes in the PHAR should always take precedence
319 1
                for ($i = count($autoloadFunctions) - 1; $i >= 0; --$i) {
320 1
                    spl_autoload_register($autoloadFunctions[$i], true, true);
321 1
                }
322 1
            }
323 27
        } else {
324 23
            $this->context = $this->createGlobalContext();
325
        }
326
327 49
        $this->dispatcher = $this->context->getEventDispatcher();
328 49
        $this->started = true;
329
330
        // Start plugins once the container is running
331 49
        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...
332 26
            $this->activatePlugins();
333 24
        }
334 47
    }
335
336
    /**
337
     * Returns whether the service container is started.
338
     *
339
     * @return bool Returns `true` if the container is started and `false`
340
     *              otherwise.
341
     */
342
    public function isStarted()
343
    {
344
        return $this->started;
345
    }
346
347
    /**
348
     * Sets the root directory of the managed Puli project.
349
     *
350
     * @param string|null $rootDir The root directory of the managed Puli
351
     *                             project or `null` to start Puli outside of a
352
     *                             specific project.
353
     */
354 52
    public function setRootDirectory($rootDir)
355
    {
356 52
        if ($this->started) {
357
            throw new LogicException('Puli is already started');
358
        }
359
360 52
        Assert::nullOrDirectory($rootDir);
361
362 52
        $this->rootDir = $rootDir ? Path::canonicalize($rootDir) : null;
363 52
    }
364
365
    /**
366
     * Sets the environment of the managed Puli project.
367
     *
368
     * @param string $env One of the {@link Environment} constants.
369
     */
370 52
    public function setEnvironment($env)
371
    {
372 52
        if ($this->started) {
373
            throw new LogicException('Puli is already started');
374
        }
375
376 52
        Assert::oneOf($env, Environment::all(), 'The environment must be one of: %2$s. Got: %s');
377
378 52
        $this->env = $env;
379 52
    }
380
381
    /**
382
     * Retturns the environment of the managed Puli project.
383
     *
384
     * @return string One of the {@link Environment} constants.
385
     */
386 2
    public function getEnvironment()
387
    {
388 2
        return $this->env;
389
    }
390
391
    /**
392
     * Returns the root directory of the managed Puli project.
393
     *
394
     * If no Puli project is managed at the moment, `null` is returned.
395
     *
396
     * @return string|null The root directory of the managed Puli project or
397
     *                     `null` if none is set.
398
     */
399 4
    public function getRootDirectory()
400
    {
401 4
        return $this->rootDir;
402
    }
403
404
    /**
405
     * Sets the logger to use.
406
     *
407
     * @param LoggerInterface $logger The logger to use.
408
     */
409
    public function setLogger(LoggerInterface $logger)
410
    {
411
        if ($this->started) {
412
            throw new LogicException('Puli is already started');
413
        }
414
415
        $this->logger = $logger;
416
    }
417
418
    /**
419
     * Returns the used logger.
420
     *
421
     * @return LoggerInterface The used logger.
422
     */
423
    public function getLogger()
424
    {
425
        return $this->logger;
426
    }
427
428
    /**
429
     * Sets the event dispatcher to use.
430
     *
431
     * @param EventDispatcherInterface $dispatcher The event dispatcher to use.
432
     */
433 2
    public function setEventDispatcher(EventDispatcherInterface $dispatcher)
434
    {
435 2
        if ($this->started) {
436
            throw new LogicException('Puli is already started');
437
        }
438
439 2
        $this->dispatcher = $dispatcher;
440 2
    }
441
442
    /**
443
     * Returns the used event dispatcher.
444
     *
445
     * @return EventDispatcherInterface|null The used logger.
446
     */
447 3
    public function getEventDispatcher()
448
    {
449 3
        return $this->dispatcher;
450
    }
451
452
    /**
453
     * Enables all Puli plugins.
454
     */
455
    public function enablePlugins()
456
    {
457
        $this->pluginsEnabled = true;
458
    }
459
460
    /**
461
     * Disables all Puli plugins.
462
     */
463 1
    public function disablePlugins()
464
    {
465 1
        $this->pluginsEnabled = false;
466 1
    }
467
468
    /**
469
     * Returns whether Puli plugins are enabled.
470
     *
471
     * @return bool Returns `true` if Puli plugins will be loaded and `false`
472
     *              otherwise.
473
     */
474
    public function arePluginsEnabled()
475
    {
476
        return $this->pluginsEnabled;
477
    }
478
479
    /**
480
     * Returns the context.
481
     *
482
     * @return Context|ProjectContext The context.
483
     */
484 30
    public function getContext()
485
    {
486 30
        if (!$this->started) {
487
            throw new LogicException('Puli was not started');
488
        }
489
490 30
        return $this->context;
491
    }
492
493
    /**
494
     * Returns the resource repository of the project.
495
     *
496
     * @return EditableRepository The resource repository.
497
     */
498 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...
499
    {
500 8
        if (!$this->started) {
501
            throw new LogicException('Puli was not started');
502
        }
503
504 8
        if (!$this->context instanceof ProjectContext) {
505 1
            return null;
506
        }
507
508 7
        if (!$this->repo) {
509 7
            $this->repo = $this->getFactory()->createRepository();
510 7
        }
511
512 7
        return $this->repo;
513
    }
514
515
    /**
516
     * Returns the resource discovery of the project.
517
     *
518
     * @return EditableDiscovery The resource discovery.
519
     */
520 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...
521
    {
522 5
        if (!$this->started) {
523
            throw new LogicException('Puli was not started');
524
        }
525
526 5
        if (!$this->context instanceof ProjectContext) {
527 1
            return null;
528
        }
529
530 4
        if (!$this->discovery) {
531 4
            $this->discovery = $this->getFactory()->createDiscovery($this->getRepository());
532 4
        }
533
534 4
        return $this->discovery;
535
    }
536
537
    /**
538
     * @return object
539
     */
540 9
    public function getFactory()
541
    {
542 9
        if (!$this->started) {
543
            throw new LogicException('Puli was not started');
544
        }
545
546 9
        if (!$this->factory && $this->context instanceof ProjectContext) {
547 8
            $this->factory = $this->getFactoryManager()->createFactory();
548 8
        }
549
550 9
        return $this->factory;
551
    }
552
553
    /**
554
     * @return FactoryManager
555
     */
556 16
    public function getFactoryManager()
557
    {
558 16
        if (!$this->started) {
559
            throw new LogicException('Puli was not started');
560
        }
561
562 16
        if (!$this->factoryManager && $this->context instanceof ProjectContext) {
563 14
            $this->factoryManager = new FactoryManagerImpl(
564 14
                $this->context,
565 14
                new DefaultGeneratorRegistry(),
566 14
                new ClassWriter()
567 14
            );
568
569
            // Don't set via the constructor to prevent a cyclic dependency
570 14
            $this->factoryManager->setServers($this->getServerManager()->getServers());
571 14
        }
572
573 16
        return $this->factoryManager;
574
    }
575
576
    /**
577
     * Returns the configuration file manager.
578
     *
579
     * @return ConfigFileManager The configuration file manager.
580
     */
581 2
    public function getConfigFileManager()
582
    {
583 2
        if (!$this->started) {
584
            throw new LogicException('Puli was not started');
585
        }
586
587 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...
588 2
            $this->configFileManager = new ConfigFileManagerImpl(
589 2
                $this->context,
590 2
                $this->getConfigFileStorage(),
591 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...
592 2
            );
593 2
        }
594
595 2
        return $this->configFileManager;
596
    }
597
598
    /**
599
     * Returns the root package file manager.
600
     *
601
     * @return RootPackageFileManager The package file manager.
602
     */
603 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...
604
    {
605 15
        if (!$this->started) {
606
            throw new LogicException('Puli was not started');
607
        }
608
609 15
        if (!$this->rootPackageFileManager && $this->context instanceof ProjectContext) {
610 14
            $this->rootPackageFileManager = new RootPackageFileManagerImpl(
611 14
                $this->context,
612 14
                $this->getPackageFileStorage()
613 14
            );
614 14
        }
615
616 15
        return $this->rootPackageFileManager;
617
    }
618
619
    /**
620
     * Returns the package manager.
621
     *
622
     * @return PackageManager The package manager.
623
     */
624 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...
625
    {
626 15
        if (!$this->started) {
627
            throw new LogicException('Puli was not started');
628
        }
629
630 15
        if (!$this->packageManager && $this->context instanceof ProjectContext) {
631 14
            $this->packageManager = new PackageManagerImpl(
632 14
                $this->context,
633 14
                $this->getPackageFileStorage()
634 14
            );
635 14
        }
636
637 15
        return $this->packageManager;
638
    }
639
640
    /**
641
     * Returns the resource repository manager.
642
     *
643
     * @return RepositoryManager The repository manager.
644
     */
645 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...
646
    {
647 2
        if (!$this->started) {
648
            throw new LogicException('Puli was not started');
649
        }
650
651 2
        if (!$this->repositoryManager && $this->context instanceof ProjectContext) {
652 1
            $this->repositoryManager = new RepositoryManagerImpl(
653 1
                $this->context,
654 1
                $this->getRepository(),
655 1
                $this->getPackageManager()->findPackages(Expr::method('isEnabled', Expr::same(true))),
656 1
                $this->getPackageFileStorage()
657 1
            );
658 1
        }
659
660 2
        return $this->repositoryManager;
661
    }
662
663
    /**
664
     * Returns the resource discovery manager.
665
     *
666
     * @return DiscoveryManager The discovery manager.
667
     */
668 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...
669
    {
670 3
        if (!$this->started) {
671
            throw new LogicException('Puli was not started');
672
        }
673
674 3
        if (!$this->discoveryManager && $this->context instanceof ProjectContext) {
675 2
            $this->discoveryManager = new DiscoveryManagerImpl(
676 2
                $this->context,
677 2
                $this->getDiscovery(),
678 2
                $this->getPackageManager()->findPackages(Expr::method('isEnabled', Expr::same(true))),
679 2
                $this->getPackageFileStorage(),
680 2
                $this->logger
681 2
            );
682 2
        }
683
684 3
        return $this->discoveryManager;
685
    }
686
687
    /**
688
     * Returns the asset manager.
689
     *
690
     * @return AssetManager The asset manager.
691
     */
692 2
    public function getAssetManager()
693
    {
694 2
        if (!$this->started) {
695
            throw new LogicException('Puli was not started');
696
        }
697
698 2
        if (!$this->assetManager && $this->context instanceof ProjectContext) {
699 1
            $this->assetManager = new DiscoveryAssetManager(
700 1
                $this->getDiscoveryManager(),
701 1
                $this->getServerManager()->getServers()
702 1
            );
703 1
        }
704
705 2
        return $this->assetManager;
706
    }
707
708
    /**
709
     * Returns the installation manager.
710
     *
711
     * @return InstallationManager The installation manager.
712
     */
713 2
    public function getInstallationManager()
714
    {
715 2
        if (!$this->started) {
716
            throw new LogicException('Puli was not started');
717
        }
718
719 2
        if (!$this->installationManager && $this->context instanceof ProjectContext) {
720 1
            $this->installationManager = new InstallationManagerImpl(
721 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...
722 1
                $this->getRepository(),
723 1
                $this->getServerManager()->getServers(),
724 1
                $this->getInstallerManager()
725 1
            );
726 1
        }
727
728 2
        return $this->installationManager;
729
    }
730
731
    /**
732
     * Returns the installer manager.
733
     *
734
     * @return InstallerManager The installer manager.
735
     */
736 15
    public function getInstallerManager()
737
    {
738 15
        if (!$this->started) {
739
            throw new LogicException('Puli was not started');
740
        }
741
742 15
        if (!$this->installerManager && $this->context instanceof ProjectContext) {
743 14
            $this->installerManager = new PackageFileInstallerManager(
744 14
                $this->getRootPackageFileManager(),
745 14
                $this->getPackageManager()->getPackages()
746 14
            );
747 14
        }
748
749 15
        return $this->installerManager;
750
    }
751
752
    /**
753
     * Returns the server manager.
754
     *
755
     * @return ServerManager The server manager.
756
     */
757 15
    public function getServerManager()
758
    {
759 15
        if (!$this->started) {
760
            throw new LogicException('Puli was not started');
761
        }
762
763 15
        if (!$this->serverManager && $this->context instanceof ProjectContext) {
764 14
            $this->serverManager = new PackageFileServerManager(
765 14
                $this->getRootPackageFileManager(),
766 14
                $this->getInstallerManager()
767 14
            );
768 14
        }
769
770 15
        return $this->serverManager;
771
    }
772
773
    /**
774
     * Returns the resource URL generator.
775
     *
776
     * @return UrlGenerator The resource URL generator.
777
     */
778 2
    public function getUrlGenerator()
779
    {
780 2
        if (!$this->started) {
781
            throw new LogicException('Puli was not started');
782
        }
783
784 2
        if (!$this->urlGenerator && $this->context instanceof ProjectContext) {
785 1
            $urlFormats = array();
786 1
            foreach ($this->getServerManager()->getServers() as $server) {
787
                $urlFormats[$server->getName()] = $server->getUrlFormat();
788 1
            }
789
790 1
            $this->urlGenerator = new DiscoveryUrlGenerator($this->getDiscovery(), $urlFormats);
791 1
        }
792
793 2
        return $this->urlGenerator;
794
    }
795
796
    /**
797
     * Returns the cached file storage.
798
     *
799
     * @return Storage The storage.
800
     */
801 48
    public function getStorage()
802
    {
803 48
        if (!$this->storage) {
804 48
            $this->storage = new FilesystemStorage();
805 48
        }
806
807 48
        return $this->storage;
808
    }
809
810
    /**
811
     * Returns the cached configuration file serializer.
812
     *
813
     * @return ConfigFileSerializer The configuration file serializer.
814
     */
815 47
    public function getConfigFileSerializer()
816
    {
817 47
        if (!$this->configFileSerializer) {
818 47
            $this->configFileSerializer = new ConfigJsonSerializer();
819 47
        }
820
821 47
        return $this->configFileSerializer;
822
    }
823
824
    /**
825
     * Returns the cached package file serializer.
826
     *
827
     * @return PackageFileSerializer The package file serializer.
828
     */
829 27
    public function getPackageFileSerializer()
830
    {
831 27
        if (!$this->packageFileSerializer) {
832 27
            $this->packageFileSerializer = new PackageJsonSerializer(
833 27
                new MigrationManager(array(
834
                    // Add future migrations here
835 27
                )),
836
                __DIR__.'/../../res/schema'
837 27
            );
838 27
        }
839
840 27
        return $this->packageFileSerializer;
841
    }
842
843 26
    private function activatePlugins()
844
    {
845 26
        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...
846 25
            $this->validatePluginClass($pluginClass);
847
848
            /** @var PuliPlugin $plugin */
849 23
            $plugin = new $pluginClass();
850 23
            $plugin->activate($this);
851 24
        }
852 24
    }
853
854 23
    private function createGlobalContext()
855
    {
856 23
        $baseConfig = new DefaultConfig();
857 23
        $homeDir = self::parseHomeDirectory();
858
859 22
        if (null !== $configFile = $this->loadConfigFile($homeDir, $baseConfig)) {
860 20
            $baseConfig = $configFile->getConfig();
861 20
        }
862
863 22
        $config = new EnvConfig($baseConfig);
864
865 22
        return new Context($homeDir, $config, $configFile, $this->dispatcher);
866
    }
867
868
    /**
869
     * Creates the context of a Puli project.
870
     *
871
     * The home directory is read from the context variable "PULI_HOME".
872
     * If this variable is not set, the home directory defaults to:
873
     *
874
     *  * `$HOME/.puli` on Linux, where `$HOME` is the context variable
875
     *    "HOME".
876
     *  * `$APPDATA/Puli` on Windows, where `$APPDATA` is the context
877
     *    variable "APPDATA".
878
     *
879
     * If none of these variables can be found, an exception is thrown.
880
     *
881
     * A .htaccess file is put into the home directory to protect it from web
882
     * access.
883
     *
884
     * @param string $rootDir The path to the project.
885
     *
886
     * @return ProjectContext The project context.
887
     */
888 27
    private function createProjectContext($rootDir, $env)
889
    {
890 27
        Assert::fileExists($rootDir, 'Could not load Puli context: The root %s does not exist.');
891 27
        Assert::directory($rootDir, 'Could not load Puli context: The root %s is a file. Expected a directory.');
892
893 27
        $baseConfig = new DefaultConfig();
894 27
        $homeDir = self::parseHomeDirectory();
895
896 27
        if (null !== $configFile = $this->loadConfigFile($homeDir, $baseConfig)) {
897 26
            $baseConfig = $configFile->getConfig();
898 26
        }
899
900
        // Create a storage without the factory manager
901 27
        $packageFileStorage = new PackageFileStorage($this->getStorage(), $this->getPackageFileSerializer());
902 27
        $rootDir = Path::canonicalize($rootDir);
903 27
        $rootFilePath = $this->rootDir.'/puli.json';
904
905
        try {
906 27
            $rootPackageFile = $packageFileStorage->loadRootPackageFile($rootFilePath, $baseConfig);
907 27
        } catch (FileNotFoundException $e) {
908 1
            $rootPackageFile = new RootPackageFile(null, $rootFilePath, $baseConfig);
909
        }
910
911 27
        $config = new EnvConfig($rootPackageFile->getConfig());
912
913 27
        return new ProjectContext($homeDir, $rootDir, $config, $rootPackageFile, $configFile, $this->dispatcher, $env);
914
    }
915
916
    /**
917
     * Returns the cached configuration file storage.
918
     *
919
     * @return ConfigFileStorage The configuration file storage.
920
     */
921 2
    private function getConfigFileStorage()
922
    {
923 2
        if (!$this->configFileStorage) {
924 2
            $this->configFileStorage = new ConfigFileStorage(
925 2
                $this->getStorage(),
926 2
                $this->getConfigFileSerializer(),
927 2
                $this->getFactoryManager()
928 2
            );
929 2
        }
930
931 2
        return $this->configFileStorage;
932
    }
933
934
    /**
935
     * Returns the cached package file storage.
936
     *
937
     * @return PackageFileStorage The package file storage.
938
     */
939 14
    private function getPackageFileStorage()
940
    {
941 14
        if (!$this->packageFileStorage) {
942 14
            $this->packageFileStorage = new PackageFileStorage(
943 14
                $this->getStorage(),
944 14
                $this->getPackageFileSerializer(),
945 14
                $this->getFactoryManager()
946 14
            );
947 14
        }
948
949 14
        return $this->packageFileStorage;
950
    }
951
952
    /**
953
     * Validates the given plugin class name.
954
     *
955
     * @param string $pluginClass The fully qualified name of a plugin class.
956
     */
957 25
    private function validatePluginClass($pluginClass)
958
    {
959 25
        if (!class_exists($pluginClass)) {
960 1
            throw new InvalidConfigException(sprintf(
961 1
                'The plugin class %s does not exist.',
962
                $pluginClass
963 1
            ));
964
        }
965
966 24
        if (!in_array('Puli\Manager\Api\PuliPlugin', class_implements($pluginClass))) {
967 1
            throw new InvalidConfigException(sprintf(
968 1
                'The plugin class %s must implement PuliPlugin.',
969
                $pluginClass
970 1
            ));
971
        }
972 23
    }
973
974 49
    private function loadConfigFile($homeDir, Config $baseConfig)
975
    {
976 49
        if (null === $homeDir) {
977 2
            return null;
978
        }
979
980 47
        Assert::fileExists($homeDir, 'Could not load Puli context: The home directory %s does not exist.');
981 47
        Assert::directory($homeDir, 'Could not load Puli context: The home directory %s is a file. Expected a directory.');
982
983
        // Create a storage without the factory manager
984 47
        $configStorage = new ConfigFileStorage($this->getStorage(), $this->getConfigFileSerializer());
985 47
        $configPath = Path::canonicalize($homeDir).'/config.json';
986
987
        try {
988 47
            return $configStorage->loadConfigFile($configPath, $baseConfig);
989 1
        } catch (FileNotFoundException $e) {
990
            // It's ok if no config.json exists. We'll work with
991
            // DefaultConfig instead
992 1
            return null;
993
        }
994
    }
995
}
996