Completed
Push — master ( 2aa028...155152 )
by Bernhard
17:49 queued 05:52
created

Puli::createGlobalContext()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 3
Bugs 1 Features 1
Metric Value
c 3
b 1
f 1
dl 0
loc 13
ccs 8
cts 8
cp 1
rs 9.4286
cc 2
eloc 7
nc 2
nop 0
crap 2
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 cyclic dependencies
570 14
            $this->factoryManager->setPackages($this->getPackageManager()->getPackages());
571 14
            $this->factoryManager->setServers($this->getServerManager()->getServers());
572 14
        }
573
574 16
        return $this->factoryManager;
575
    }
576
577
    /**
578
     * Returns the configuration file manager.
579
     *
580
     * @return ConfigFileManager The configuration file manager.
581
     */
582 2
    public function getConfigFileManager()
583
    {
584 2
        if (!$this->started) {
585
            throw new LogicException('Puli was not started');
586
        }
587
588 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...
589 2
            $this->configFileManager = new ConfigFileManagerImpl(
590 2
                $this->context,
591 2
                $this->getConfigFileStorage(),
592 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...
593 2
            );
594 2
        }
595
596 2
        return $this->configFileManager;
597
    }
598
599
    /**
600
     * Returns the root package file manager.
601
     *
602
     * @return RootPackageFileManager The package file manager.
603
     */
604 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...
605
    {
606 15
        if (!$this->started) {
607
            throw new LogicException('Puli was not started');
608
        }
609
610 15
        if (!$this->rootPackageFileManager && $this->context instanceof ProjectContext) {
611 14
            $this->rootPackageFileManager = new RootPackageFileManagerImpl(
612 14
                $this->context,
613 14
                $this->getPackageFileStorage()
614 14
            );
615 14
        }
616
617 15
        return $this->rootPackageFileManager;
618
    }
619
620
    /**
621
     * Returns the package manager.
622
     *
623
     * @return PackageManager The package manager.
624
     */
625 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...
626
    {
627 15
        if (!$this->started) {
628
            throw new LogicException('Puli was not started');
629
        }
630
631 15
        if (!$this->packageManager && $this->context instanceof ProjectContext) {
632 14
            $this->packageManager = new PackageManagerImpl(
633 14
                $this->context,
634 14
                $this->getPackageFileStorage()
635 14
            );
636 14
        }
637
638 15
        return $this->packageManager;
639
    }
640
641
    /**
642
     * Returns the resource repository manager.
643
     *
644
     * @return RepositoryManager The repository manager.
645
     */
646 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...
647
    {
648 2
        if (!$this->started) {
649
            throw new LogicException('Puli was not started');
650
        }
651
652 2
        if (!$this->repositoryManager && $this->context instanceof ProjectContext) {
653 1
            $this->repositoryManager = new RepositoryManagerImpl(
654 1
                $this->context,
655 1
                $this->getRepository(),
656 1
                $this->getPackageManager()->findPackages(Expr::method('isEnabled', Expr::same(true))),
657 1
                $this->getPackageFileStorage()
658 1
            );
659 1
        }
660
661 2
        return $this->repositoryManager;
662
    }
663
664
    /**
665
     * Returns the resource discovery manager.
666
     *
667
     * @return DiscoveryManager The discovery manager.
668
     */
669 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...
670
    {
671 3
        if (!$this->started) {
672
            throw new LogicException('Puli was not started');
673
        }
674
675 3
        if (!$this->discoveryManager && $this->context instanceof ProjectContext) {
676 2
            $this->discoveryManager = new DiscoveryManagerImpl(
677 2
                $this->context,
678 2
                $this->getDiscovery(),
679 2
                $this->getPackageManager()->findPackages(Expr::method('isEnabled', Expr::same(true))),
680 2
                $this->getPackageFileStorage(),
681 2
                $this->logger
682 2
            );
683 2
        }
684
685 3
        return $this->discoveryManager;
686
    }
687
688
    /**
689
     * Returns the asset manager.
690
     *
691
     * @return AssetManager The asset manager.
692
     */
693 2
    public function getAssetManager()
694
    {
695 2
        if (!$this->started) {
696
            throw new LogicException('Puli was not started');
697
        }
698
699 2
        if (!$this->assetManager && $this->context instanceof ProjectContext) {
700 1
            $this->assetManager = new DiscoveryAssetManager(
701 1
                $this->getDiscoveryManager(),
702 1
                $this->getServerManager()->getServers()
703 1
            );
704 1
        }
705
706 2
        return $this->assetManager;
707
    }
708
709
    /**
710
     * Returns the installation manager.
711
     *
712
     * @return InstallationManager The installation manager.
713
     */
714 2
    public function getInstallationManager()
715
    {
716 2
        if (!$this->started) {
717
            throw new LogicException('Puli was not started');
718
        }
719
720 2
        if (!$this->installationManager && $this->context instanceof ProjectContext) {
721 1
            $this->installationManager = new InstallationManagerImpl(
722 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...
723 1
                $this->getRepository(),
724 1
                $this->getServerManager()->getServers(),
725 1
                $this->getInstallerManager()
726 1
            );
727 1
        }
728
729 2
        return $this->installationManager;
730
    }
731
732
    /**
733
     * Returns the installer manager.
734
     *
735
     * @return InstallerManager The installer manager.
736
     */
737 15
    public function getInstallerManager()
738
    {
739 15
        if (!$this->started) {
740
            throw new LogicException('Puli was not started');
741
        }
742
743 15
        if (!$this->installerManager && $this->context instanceof ProjectContext) {
744 14
            $this->installerManager = new PackageFileInstallerManager(
745 14
                $this->getRootPackageFileManager(),
746 14
                $this->getPackageManager()->getPackages()
747 14
            );
748 14
        }
749
750 15
        return $this->installerManager;
751
    }
752
753
    /**
754
     * Returns the server manager.
755
     *
756
     * @return ServerManager The server manager.
757
     */
758 15
    public function getServerManager()
759
    {
760 15
        if (!$this->started) {
761
            throw new LogicException('Puli was not started');
762
        }
763
764 15
        if (!$this->serverManager && $this->context instanceof ProjectContext) {
765 14
            $this->serverManager = new PackageFileServerManager(
766 14
                $this->getRootPackageFileManager(),
767 14
                $this->getInstallerManager()
768 14
            );
769 14
        }
770
771 15
        return $this->serverManager;
772
    }
773
774
    /**
775
     * Returns the resource URL generator.
776
     *
777
     * @return UrlGenerator The resource URL generator.
778
     */
779 2
    public function getUrlGenerator()
780
    {
781 2
        if (!$this->started) {
782
            throw new LogicException('Puli was not started');
783
        }
784
785 2
        if (!$this->urlGenerator && $this->context instanceof ProjectContext) {
786 1
            $urlFormats = array();
787 1
            foreach ($this->getServerManager()->getServers() as $server) {
788
                $urlFormats[$server->getName()] = $server->getUrlFormat();
789 1
            }
790
791 1
            $this->urlGenerator = new DiscoveryUrlGenerator($this->getDiscovery(), $urlFormats);
792 1
        }
793
794 2
        return $this->urlGenerator;
795
    }
796
797
    /**
798
     * Returns the cached file storage.
799
     *
800
     * @return Storage The storage.
801
     */
802 48
    public function getStorage()
803
    {
804 48
        if (!$this->storage) {
805 48
            $this->storage = new FilesystemStorage();
806 48
        }
807
808 48
        return $this->storage;
809
    }
810
811
    /**
812
     * Returns the cached configuration file serializer.
813
     *
814
     * @return ConfigFileSerializer The configuration file serializer.
815
     */
816 47
    public function getConfigFileSerializer()
817
    {
818 47
        if (!$this->configFileSerializer) {
819 47
            $this->configFileSerializer = new ConfigJsonSerializer();
820 47
        }
821
822 47
        return $this->configFileSerializer;
823
    }
824
825
    /**
826
     * Returns the cached package file serializer.
827
     *
828
     * @return PackageFileSerializer The package file serializer.
829
     */
830 27
    public function getPackageFileSerializer()
831
    {
832 27
        if (!$this->packageFileSerializer) {
833 27
            $this->packageFileSerializer = new PackageJsonSerializer(
834 27
                new MigrationManager(array(
835
                    // Add future migrations here
836 27
                )),
837
                __DIR__.'/../../res/schema'
838 27
            );
839 27
        }
840
841 27
        return $this->packageFileSerializer;
842
    }
843
844 26
    private function activatePlugins()
845
    {
846 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...
847 25
            $this->validatePluginClass($pluginClass);
848
849
            /** @var PuliPlugin $plugin */
850 23
            $plugin = new $pluginClass();
851 23
            $plugin->activate($this);
852 24
        }
853 24
    }
854
855 23
    private function createGlobalContext()
856
    {
857 23
        $baseConfig = new DefaultConfig();
858 23
        $homeDir = self::parseHomeDirectory();
859
860 22
        if (null !== $configFile = $this->loadConfigFile($homeDir, $baseConfig)) {
861 20
            $baseConfig = $configFile->getConfig();
862 20
        }
863
864 22
        $config = new EnvConfig($baseConfig);
865
866 22
        return new Context($homeDir, $config, $configFile, $this->dispatcher);
867
    }
868
869
    /**
870
     * Creates the context of a Puli project.
871
     *
872
     * The home directory is read from the context variable "PULI_HOME".
873
     * If this variable is not set, the home directory defaults to:
874
     *
875
     *  * `$HOME/.puli` on Linux, where `$HOME` is the context variable
876
     *    "HOME".
877
     *  * `$APPDATA/Puli` on Windows, where `$APPDATA` is the context
878
     *    variable "APPDATA".
879
     *
880
     * If none of these variables can be found, an exception is thrown.
881
     *
882
     * A .htaccess file is put into the home directory to protect it from web
883
     * access.
884
     *
885
     * @param string $rootDir The path to the project.
886
     *
887
     * @return ProjectContext The project context.
888
     */
889 27
    private function createProjectContext($rootDir, $env)
890
    {
891 27
        Assert::fileExists($rootDir, 'Could not load Puli context: The root %s does not exist.');
892 27
        Assert::directory($rootDir, 'Could not load Puli context: The root %s is a file. Expected a directory.');
893
894 27
        $baseConfig = new DefaultConfig();
895 27
        $homeDir = self::parseHomeDirectory();
896
897 27
        if (null !== $configFile = $this->loadConfigFile($homeDir, $baseConfig)) {
898 26
            $baseConfig = $configFile->getConfig();
899 26
        }
900
901
        // Create a storage without the factory manager
902 27
        $packageFileStorage = new PackageFileStorage($this->getStorage(), $this->getPackageFileSerializer());
903 27
        $rootDir = Path::canonicalize($rootDir);
904 27
        $rootFilePath = $this->rootDir.'/puli.json';
905
906
        try {
907 27
            $rootPackageFile = $packageFileStorage->loadRootPackageFile($rootFilePath, $baseConfig);
908 27
        } catch (FileNotFoundException $e) {
909 1
            $rootPackageFile = new RootPackageFile(null, $rootFilePath, $baseConfig);
910
        }
911
912 27
        $config = new EnvConfig($rootPackageFile->getConfig());
913
914 27
        return new ProjectContext($homeDir, $rootDir, $config, $rootPackageFile, $configFile, $this->dispatcher, $env);
915
    }
916
917
    /**
918
     * Returns the cached configuration file storage.
919
     *
920
     * @return ConfigFileStorage The configuration file storage.
921
     */
922 2
    private function getConfigFileStorage()
923
    {
924 2
        if (!$this->configFileStorage) {
925 2
            $this->configFileStorage = new ConfigFileStorage(
926 2
                $this->getStorage(),
927 2
                $this->getConfigFileSerializer(),
928 2
                $this->getFactoryManager()
929 2
            );
930 2
        }
931
932 2
        return $this->configFileStorage;
933
    }
934
935
    /**
936
     * Returns the cached package file storage.
937
     *
938
     * @return PackageFileStorage The package file storage.
939
     */
940 14
    private function getPackageFileStorage()
941
    {
942 14
        if (!$this->packageFileStorage) {
943 14
            $this->packageFileStorage = new PackageFileStorage(
944 14
                $this->getStorage(),
945 14
                $this->getPackageFileSerializer(),
946 14
                $this->getFactoryManager()
947 14
            );
948 14
        }
949
950 14
        return $this->packageFileStorage;
951
    }
952
953
    /**
954
     * Validates the given plugin class name.
955
     *
956
     * @param string $pluginClass The fully qualified name of a plugin class.
957
     */
958 25
    private function validatePluginClass($pluginClass)
959
    {
960 25
        if (!class_exists($pluginClass)) {
961 1
            throw new InvalidConfigException(sprintf(
962 1
                'The plugin class %s does not exist.',
963
                $pluginClass
964 1
            ));
965
        }
966
967 24
        if (!in_array('Puli\Manager\Api\PuliPlugin', class_implements($pluginClass))) {
968 1
            throw new InvalidConfigException(sprintf(
969 1
                'The plugin class %s must implement PuliPlugin.',
970
                $pluginClass
971 1
            ));
972
        }
973 23
    }
974
975 49
    private function loadConfigFile($homeDir, Config $baseConfig)
976
    {
977 49
        if (null === $homeDir) {
978 2
            return null;
979
        }
980
981 47
        Assert::fileExists($homeDir, 'Could not load Puli context: The home directory %s does not exist.');
982 47
        Assert::directory($homeDir, 'Could not load Puli context: The home directory %s is a file. Expected a directory.');
983
984
        // Create a storage without the factory manager
985 47
        $configStorage = new ConfigFileStorage($this->getStorage(), $this->getConfigFileSerializer());
986 47
        $configPath = Path::canonicalize($homeDir).'/config.json';
987
988
        try {
989 47
            return $configStorage->loadConfigFile($configPath, $baseConfig);
990 1
        } catch (FileNotFoundException $e) {
991
            // It's ok if no config.json exists. We'll work with
992
            // DefaultConfig instead
993 1
            return null;
994
        }
995
    }
996
}
997