Completed
Pull Request — master (#47)
by Mateusz
05:43
created

Container::getCacheFileConverter()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1060 26
            $this->validatePluginClass($pluginClass);
1061
1062
            /** @var PuliPlugin $plugin */
1063 24
            $plugin = new $pluginClass();
1064 24
            $plugin->activate($this);
1065
        }
1066 25
    }
1067
1068 24
    private function createGlobalContext()
1069
    {
1070 24
        $baseConfig = new DefaultConfig();
1071 24
        $homeDir = self::parseHomeDirectory();
1072
1073 23
        if (null !== $configFile = $this->loadConfigFile($homeDir, $baseConfig)) {
1074 21
            $baseConfig = $configFile->getConfig();
1075
        }
1076
1077 23
        $config = new EnvConfig($baseConfig);
1078
1079 23
        return new Context($homeDir, $config, $configFile, $this->dispatcher);
1080
    }
1081
1082
    /**
1083
     * Creates the context of a Puli project.
1084
     *
1085
     * The home directory is read from the context variable "PULI_HOME".
1086
     * If this variable is not set, the home directory defaults to:
1087
     *
1088
     *  * `$HOME/.puli` on Linux, where `$HOME` is the context variable
1089
     *    "HOME".
1090
     *  * `$APPDATA/Puli` on Windows, where `$APPDATA` is the context
1091
     *    variable "APPDATA".
1092
     *
1093
     * If none of these variables can be found, an exception is thrown.
1094
     *
1095
     * A .htaccess file is put into the home directory to protect it from web
1096
     * access.
1097
     *
1098
     * @param string $rootDir The path to the project
1099
     *
1100
     * @return ProjectContext The project context
1101
     */
1102 28
    private function createProjectContext($rootDir, $env)
1103
    {
1104 28
        Assert::fileExists($rootDir, 'Could not load Puli context: The root %s does not exist.');
1105 28
        Assert::directory($rootDir, 'Could not load Puli context: The root %s is a file. Expected a directory.');
1106
1107 28
        $baseConfig = new DefaultConfig();
1108 28
        $homeDir = self::parseHomeDirectory();
1109
1110 28
        if (null !== $configFile = $this->loadConfigFile($homeDir, $baseConfig)) {
1111 27
            $baseConfig = $configFile->getConfig();
1112
        }
1113
1114
        // Create a storage without the factory manager
1115 28
        $jsonStorage = new JsonStorage(
1116 28
            $this->getStorage(),
1117 28
            new JsonConverterProvider($this),
1118 28
            $this->getJsonEncoder(),
1119 28
            $this->getJsonDecoder()
1120
        );
1121
1122 28
        $rootDir = Path::canonicalize($rootDir);
1123 28
        $rootFilePath = $this->rootDir.'/puli.json';
1124
1125
        try {
1126 28
            $rootModuleFile = $jsonStorage->loadRootModuleFile($rootFilePath, $baseConfig);
1127 1
        } catch (FileNotFoundException $e) {
1128 1
            $rootModuleFile = new RootModuleFile(null, $rootFilePath, $baseConfig);
1129
        }
1130
1131 28
        $config = new EnvConfig($rootModuleFile->getConfig());
1132
1133 28
        return new ProjectContext($homeDir, $rootDir, $config, $rootModuleFile, $configFile, $this->dispatcher, $env);
1134
    }
1135
1136
    /**
1137
     * Decorates a converter with a {@link ValidatingConverter}.
1138
     *
1139
     * @param JsonConverter        $innerConverter The converter to decorate
1140
     * @param string|callable|null $schema         The schema
1141
     *
1142
     * @return ValidatingConverter The decorated converter
1143
     */
1144 27
    private function createValidatingConverter(JsonConverter $innerConverter, $schema = null)
1145
    {
1146 27
        return new ValidatingConverter($innerConverter, $schema, $this->getJsonValidator());
1147
    }
1148
1149
    /**
1150
     * Returns the JSON file storage.
1151
     *
1152
     * @return JsonStorage The JSON file storage
1153
     */
1154 16
    private function getJsonStorage()
1155
    {
1156 16
        if (!$this->jsonStorage) {
1157 16
            $this->jsonStorage = new JsonStorage(
1158 16
                $this->getStorage(),
1159 16
                new JsonConverterProvider($this),
1160 16
                $this->getJsonEncoder(),
1161 16
                $this->getJsonDecoder(),
1162 16
                $this->getFactoryManager()
1163
            );
1164
        }
1165
1166 16
        return $this->jsonStorage;
1167
    }
1168
1169
    /**
1170
     * Returns the JSON versioner.
1171
     *
1172
     * @return JsonVersioner The JSON versioner
1173
     */
1174 27
    private function getJsonVersioner()
1175
    {
1176 27
        if (!$this->jsonVersioner) {
1177 27
            $this->jsonVersioner = new ChainVersioner(array(
1178
                // check the schema of the "$schema" field by default
1179 27
                new SchemaUriVersioner(),
1180
                // fall back to the "version" field for 1.0
1181 27
                new VersionFieldVersioner(),
1182
            ));
1183
        }
1184
1185 27
        return $this->jsonVersioner;
1186
    }
1187
1188
    /**
1189
     * Returns the migration manager for module files.
1190
     *
1191
     * @return MigrationManager The migration manager
1192
     */
1193 27
    private function getModuleFileMigrationManager()
1194
    {
1195 27
        if (!$this->moduleFileMigrationManager) {
1196 27
            $this->moduleFileMigrationManager = new MigrationManager(array(
1197 27
                new ModuleFile10To20Migration(),
1198 27
            ), $this->getJsonVersioner());
1199
        }
1200
1201 27
        return $this->moduleFileMigrationManager;
1202
    }
1203
1204
    /**
1205
     * Validates the given plugin class name.
1206
     *
1207
     * @param string $pluginClass The fully qualified name of a plugin class
1208
     */
1209 26
    private function validatePluginClass($pluginClass)
1210
    {
1211 26
        if (!class_exists($pluginClass)) {
1212 1
            throw new InvalidConfigException(sprintf(
1213 1
                'The plugin class %s does not exist.',
1214
                $pluginClass
1215
            ));
1216
        }
1217
1218 25
        if (!in_array('Puli\Manager\Api\PuliPlugin', class_implements($pluginClass))) {
1219 1
            throw new InvalidConfigException(sprintf(
1220 1
                'The plugin class %s must implement PuliPlugin.',
1221
                $pluginClass
1222
            ));
1223
        }
1224 24
    }
1225
1226 51
    private function loadConfigFile($homeDir, Config $baseConfig)
1227
    {
1228 51
        if (null === $homeDir) {
1229 2
            return null;
1230
        }
1231
1232 49
        Assert::fileExists($homeDir, 'Could not load Puli context: The home directory %s does not exist.');
1233 49
        Assert::directory($homeDir, 'Could not load Puli context: The home directory %s is a file. Expected a directory.');
1234
1235
        // Create a storage without the factory manager
1236 49
        $jsonStorage = new JsonStorage(
1237 49
            $this->getStorage(),
1238 49
            new JsonConverterProvider($this),
1239 49
            $this->getJsonEncoder(),
1240 49
            $this->getJsonDecoder()
1241
        );
1242
1243 49
        $configPath = Path::canonicalize($homeDir).'/config.json';
1244
1245
        try {
1246 49
            return $jsonStorage->loadConfigFile($configPath, $baseConfig);
1247 1
        } catch (FileNotFoundException $e) {
1248
            // It's ok if no config.json exists. We'll work with
1249
            // DefaultConfig instead
1250 1
            return null;
1251
        }
1252
    }
1253
}
1254