Failed Conditions
Pull Request — master (#47)
by Mateusz
04:31
created

Puli::getCacheFileStorage()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

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

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1025
     */
1026 2
    public function getCacheManager()
1027
    {
1028 2
        if (!$this->cacheManager && $this->context instanceof ProjectContext) {
1029 1
            $this->cacheManager = new CacheManagerImpl(
1030 1
                $this->getModuleManager(),
1031 1
                $this->getCacheFileStorage(),
1032 1
                $this->context
1033
            );
1034
        }
1035
1036 2
        return $this->cacheManager;
1037
    }
1038
1039 27
    private function activatePlugins()
1040
    {
1041 27
        foreach ($this->context->getRootModuleFile()->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 getRootModuleFile() 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...
1042 26
            $this->validatePluginClass($pluginClass);
1043
1044
            /** @var PuliPlugin $plugin */
1045 24
            $plugin = new $pluginClass();
1046 24
            $plugin->activate($this);
1047
        }
1048 25
    }
1049
1050 24
    private function createGlobalContext()
1051
    {
1052 24
        $baseConfig = new DefaultConfig();
1053 24
        $homeDir = self::parseHomeDirectory();
1054
1055 23
        if (null !== $configFile = $this->loadConfigFile($homeDir, $baseConfig)) {
1056 21
            $baseConfig = $configFile->getConfig();
1057
        }
1058
1059 23
        $config = new EnvConfig($baseConfig);
1060
1061 23
        return new Context($homeDir, $config, $configFile, $this->dispatcher);
1062
    }
1063
1064
    /**
1065
     * Creates the context of a Puli project.
1066
     *
1067
     * The home directory is read from the context variable "PULI_HOME".
1068
     * If this variable is not set, the home directory defaults to:
1069
     *
1070
     *  * `$HOME/.puli` on Linux, where `$HOME` is the context variable
1071
     *    "HOME".
1072
     *  * `$APPDATA/Puli` on Windows, where `$APPDATA` is the context
1073
     *    variable "APPDATA".
1074
     *
1075
     * If none of these variables can be found, an exception is thrown.
1076
     *
1077
     * A .htaccess file is put into the home directory to protect it from web
1078
     * access.
1079
     *
1080
     * @param string $rootDir The path to the project.
1081
     *
1082
     * @return ProjectContext The project context.
1083
     */
1084 28
    private function createProjectContext($rootDir, $env)
1085
    {
1086 28
        Assert::fileExists($rootDir, 'Could not load Puli context: The root %s does not exist.');
1087 28
        Assert::directory($rootDir, 'Could not load Puli context: The root %s is a file. Expected a directory.');
1088
1089 28
        $baseConfig = new DefaultConfig();
1090 28
        $homeDir = self::parseHomeDirectory();
1091
1092 28
        if (null !== $configFile = $this->loadConfigFile($homeDir, $baseConfig)) {
1093 27
            $baseConfig = $configFile->getConfig();
1094
        }
1095
1096
        // Create a storage without the factory manager
1097 28
        $moduleFileStorage = new ModuleFileStorage(
1098 28
            $this->getStorage(),
1099 28
            $this->getLegacyModuleFileConverter(),
1100 28
            $this->getLegacyRootModuleFileConverter(),
1101 28
            $this->getJsonEncoder(),
1102 28
            $this->getJsonDecoder()
1103
        );
1104
1105 28
        $rootDir = Path::canonicalize($rootDir);
1106 28
        $rootFilePath = $this->rootDir.'/puli.json';
1107
1108
        try {
1109 28
            $rootModuleFile = $moduleFileStorage->loadRootModuleFile($rootFilePath, $baseConfig);
1110 1
        } catch (FileNotFoundException $e) {
1111 1
            $rootModuleFile = new RootModuleFile(null, $rootFilePath, $baseConfig);
1112
        }
1113
1114 28
        $config = new EnvConfig($rootModuleFile->getConfig());
1115
1116 28
        return new ProjectContext($homeDir, $rootDir, $config, $rootModuleFile, $configFile, $this->dispatcher, $env);
1117
    }
1118
1119
    /**
1120
     * Returns the configuration file storage.
1121
     *
1122
     * @return ConfigFileStorage The configuration file storage.
1123
     */
1124 2
    private function getConfigFileStorage()
1125
    {
1126 2
        if (!$this->configFileStorage) {
1127 2
            $this->configFileStorage = new ConfigFileStorage(
1128 2
                $this->getStorage(),
1129 2
                $this->getConfigFileConverter(),
1130 2
                $this->getJsonEncoder(),
1131 2
                $this->getJsonDecoder(),
1132 2
                $this->getFactoryManager()
1133
            );
1134
        }
1135
1136 2
        return $this->configFileStorage;
1137
    }
1138
1139
    /**
1140
     * Returns the module file storage.
1141
     *
1142
     * @return ModuleFileStorage The module file storage.
1143
     */
1144 15
    private function getModuleFileStorage()
1145
    {
1146 15
        if (!$this->moduleFileStorage) {
1147 15
            $this->moduleFileStorage = new ModuleFileStorage(
1148 15
                $this->getStorage(),
1149 15
                $this->getLegacyModuleFileConverter(),
1150 15
                $this->getLegacyRootModuleFileConverter(),
1151 15
                $this->getJsonEncoder(),
1152 15
                $this->getJsonDecoder(),
1153 15
                $this->getFactoryManager()
1154
            );
1155
        }
1156
1157 15
        return $this->moduleFileStorage;
1158
    }
1159
1160
    /**
1161
     * Validates the given plugin class name.
1162
     *
1163
     * @param string $pluginClass The fully qualified name of a plugin class.
1164
     */
1165 26
    private function validatePluginClass($pluginClass)
1166
    {
1167 26
        if (!class_exists($pluginClass)) {
1168 1
            throw new InvalidConfigException(sprintf(
1169 1
                'The plugin class %s does not exist.',
1170
                $pluginClass
1171
            ));
1172
        }
1173
1174 25
        if (!in_array('Puli\Manager\Api\PuliPlugin', class_implements($pluginClass))) {
1175 1
            throw new InvalidConfigException(sprintf(
1176 1
                'The plugin class %s must implement PuliPlugin.',
1177
                $pluginClass
1178
            ));
1179
        }
1180 24
    }
1181
1182 51
    private function loadConfigFile($homeDir, Config $baseConfig)
1183
    {
1184 51
        if (null === $homeDir) {
1185 2
            return null;
1186
        }
1187
1188 49
        Assert::fileExists($homeDir, 'Could not load Puli context: The home directory %s does not exist.');
1189 49
        Assert::directory($homeDir, 'Could not load Puli context: The home directory %s is a file. Expected a directory.');
1190
1191
        // Create a storage without the factory manager
1192 49
        $configStorage = new ConfigFileStorage(
1193 49
            $this->getStorage(),
1194 49
            $this->getConfigFileConverter(),
1195 49
            $this->getJsonEncoder(),
1196 49
            $this->getJsonDecoder()
1197
        );
1198
1199 49
        $configPath = Path::canonicalize($homeDir).'/config.json';
1200
1201
        try {
1202 49
            return $configStorage->loadConfigFile($configPath, $baseConfig);
1203 1
        } catch (FileNotFoundException $e) {
1204
            // It's ok if no config.json exists. We'll work with
1205
            // DefaultConfig instead
1206 1
            return null;
1207
        }
1208
    }
1209
}
1210