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

Puli::loadConfigFile()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 21
ccs 10
cts 10
cp 1
rs 9.3143
cc 3
eloc 11
nc 3
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\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 49
    private static function parseHomeDirectory()
255
    {
256
        try {
257 49
            $homeDir = System::parseHomeDirectory();
258
259 46
            System::denyWebAccess($homeDir);
260
261 46
            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 51
    public function __construct($rootDir = null, $env = Environment::DEV)
283
    {
284 51
        $this->setRootDirectory($rootDir);
285 51
        $this->setEnvironment($env);
286 51
    }
287
288
    /**
289
     * Starts the service container.
290
     */
291 49
    public function start()
292
    {
293 49
        if ($this->started) {
294
            throw new LogicException('Puli is already started');
295
        }
296
297 49
        if (null !== $this->rootDir) {
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 1
                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 23
            $this->context = $this->createGlobalContext();
324
        }
325
326 48
        $this->dispatcher = $this->context->getEventDispatcher();
327 48
        $this->started = true;
328
329
        // Start plugins once the container is running
330 48
        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 46
    }
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 51
    public function setRootDirectory($rootDir)
354
    {
355 51
        if ($this->started) {
356
            throw new LogicException('Puli is already started');
357
        }
358
359 51
        Assert::nullOrDirectory($rootDir);
360
361 51
        $this->rootDir = $rootDir ? Path::canonicalize($rootDir) : null;
362 51
    }
363
364
    /**
365
     * Sets the environment of the managed Puli project.
366
     *
367
     * @param string $env One of the {@link Environment} constants.
368
     */
369 51
    public function setEnvironment($env)
370
    {
371 51
        if ($this->started) {
372
            throw new LogicException('Puli is already started');
373
        }
374
375 51
        Assert::oneOf($env, Environment::all(), 'The environment must be one of: %2$s. Got: %s');
376
377 51
        $this->env = $env;
378 51
    }
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 3
    public function getEventDispatcher()
447
    {
448 3
        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 29
    public function getContext()
484
    {
485 29
        if (!$this->started) {
486
            throw new LogicException('Puli was not started');
487
        }
488
489 29
        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->context instanceof ProjectContext) {
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->context instanceof ProjectContext) {
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->context instanceof ProjectContext) {
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->context instanceof ProjectContext) {
562 14
            $this->factoryManager = new FactoryManagerImpl(
563 14
                $this->context,
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->context instanceof ProjectContext) {
609 14
            $this->rootPackageFileManager = new RootPackageFileManagerImpl(
610 14
                $this->context,
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->context instanceof ProjectContext) {
630 14
            $this->packageManager = new PackageManagerImpl(
631 14
                $this->context,
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->context instanceof ProjectContext) {
651 1
            $this->repositoryManager = new RepositoryManagerImpl(
652 1
                $this->context,
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->context instanceof ProjectContext) {
674 2
            $this->discoveryManager = new DiscoveryManagerImpl(
675 2
                $this->context,
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->context instanceof ProjectContext) {
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->context instanceof ProjectContext) {
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->context instanceof ProjectContext) {
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->context instanceof ProjectContext) {
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->context instanceof ProjectContext) {
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 47
    public function getStorage()
801
    {
802 47
        if (!$this->storage) {
803 47
            $this->storage = new FilesystemStorage();
804 47
        }
805
806 47
        return $this->storage;
807
    }
808
809
    /**
810
     * Returns the cached configuration file serializer.
811
     *
812
     * @return ConfigFileSerializer The configuration file serializer.
813
     */
814 46
    public function getConfigFileSerializer()
815
    {
816 46
        if (!$this->configFileSerializer) {
817 46
            $this->configFileSerializer = new ConfigJsonSerializer();
818 46
        }
819
820 46
        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 23
    private function createGlobalContext()
854
    {
855 23
        $baseConfig = new DefaultConfig();
856 23
        $homeDir = self::parseHomeDirectory();
857
858 22
        if (null !== $configFile = $this->loadConfigFile($homeDir, $baseConfig)) {
859 20
            $baseConfig = $configFile->getConfig();
860 20
        }
861
862 22
        $config = new EnvConfig($baseConfig);
863
864 22
        return new Context($homeDir, $config, $configFile, $this->dispatcher);
865
    }
866
867
    /**
868
     * Creates the context of a Puli project.
869
     *
870
     * The home directory is read from the context variable "PULI_HOME".
871
     * If this variable is not set, the home directory defaults to:
872
     *
873
     *  * `$HOME/.puli` on Linux, where `$HOME` is the context variable
874
     *    "HOME".
875
     *  * `$APPDATA/Puli` on Windows, where `$APPDATA` is the context
876
     *    variable "APPDATA".
877
     *
878
     * If none of these variables can be found, an exception is thrown.
879
     *
880
     * A .htaccess file is put into the home directory to protect it from web
881
     * access.
882
     *
883
     * @param string $rootDir The path to the project.
884
     *
885
     * @return ProjectContext The project context.
886
     */
887 26
    private function createProjectContext($rootDir, $env)
888
    {
889 26
        Assert::fileExists($rootDir, 'Could not load Puli context: The root %s does not exist.');
890 26
        Assert::directory($rootDir, 'Could not load Puli context: The root %s is a file. Expected a directory.');
891
892 26
        $baseConfig = new DefaultConfig();
893 26
        $homeDir = self::parseHomeDirectory();
894
895 26
        if (null !== $configFile = $this->loadConfigFile($homeDir, $baseConfig)) {
896 25
            $baseConfig = $configFile->getConfig();
897 25
        }
898
899
        // Create a storage without the factory manager
900 26
        $packageFileStorage = new PackageFileStorage($this->getStorage(), $this->getPackageFileSerializer());
901 26
        $rootDir = Path::canonicalize($rootDir);
902 26
        $rootFilePath = $this->rootDir.'/puli.json';
903 26
        $rootPackageFile = $packageFileStorage->loadRootPackageFile($rootFilePath, $baseConfig);
904 26
        $config = new EnvConfig($rootPackageFile->getConfig());
905
906 26
        return new ProjectContext($homeDir, $rootDir, $config, $rootPackageFile, $configFile, $this->dispatcher, $env);
907
    }
908
909
    /**
910
     * Returns the cached configuration file storage.
911
     *
912
     * @return ConfigFileStorage The configuration file storage.
913
     */
914 2
    private function getConfigFileStorage()
915
    {
916 2
        if (!$this->configFileStorage) {
917 2
            $this->configFileStorage = new ConfigFileStorage(
918 2
                $this->getStorage(),
919 2
                $this->getConfigFileSerializer(),
920 2
                $this->getFactoryManager()
921 2
            );
922 2
        }
923
924 2
        return $this->configFileStorage;
925
    }
926
927
    /**
928
     * Returns the cached package file storage.
929
     *
930
     * @return PackageFileStorage The package file storage.
931
     */
932 14
    private function getPackageFileStorage()
933
    {
934 14
        if (!$this->packageFileStorage) {
935 14
            $this->packageFileStorage = new PackageFileStorage(
936 14
                $this->getStorage(),
937 14
                $this->getPackageFileSerializer(),
938 14
                $this->getFactoryManager()
939 14
            );
940 14
        }
941
942 14
        return $this->packageFileStorage;
943
    }
944
945
    /**
946
     * Validates the given plugin class name.
947
     *
948
     * @param string $pluginClass The fully qualified name of a plugin class.
949
     */
950 25
    private function validatePluginClass($pluginClass)
951
    {
952 25
        if (!class_exists($pluginClass)) {
953 1
            throw new InvalidConfigException(sprintf(
954 1
                'The plugin class %s does not exist.',
955
                $pluginClass
956 1
            ));
957
        }
958
959 24
        if (!in_array('Puli\Manager\Api\PuliPlugin', class_implements($pluginClass))) {
960 1
            throw new InvalidConfigException(sprintf(
961 1
                'The plugin class %s must implement PuliPlugin.',
962
                $pluginClass
963 1
            ));
964
        }
965 23
    }
966
967 48
    private function loadConfigFile($homeDir, Config $baseConfig)
968
    {
969 48
        if (null === $homeDir) {
970 2
            return null;
971
        }
972
973 46
        Assert::fileExists($homeDir, 'Could not load Puli context: The home directory %s does not exist.');
974 46
        Assert::directory($homeDir, 'Could not load Puli context: The home directory %s is a file. Expected a directory.');
975
976
        // Create a storage without the factory manager
977 46
        $configStorage = new ConfigFileStorage($this->getStorage(), $this->getConfigFileSerializer());
978 46
        $configPath = Path::canonicalize($homeDir).'/config.json';
979
980
        try {
981 46
            return $configStorage->loadConfigFile($configPath, $baseConfig);
982 1
        } catch (FileNotFoundException $e) {
983
            // It's ok if no config.json exists. We'll work with
984
            // DefaultConfig instead
985 1
            return null;
986
        }
987
    }
988
}
989