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

src/Api/Puli.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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()
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()
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()
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()
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()
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()
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(),
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()
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()
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.
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) {
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