Failed Conditions
Push — master ( ebb1ea...d49c03 )
by Bernhard
06:05
created

Puli::loadConfigFile()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 3

Importance

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