Failed Conditions
Pull Request — master (#51)
by Bernhard
15:48 queued 17s
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 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\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\Config\ConfigFileConverter;
37
use Puli\Manager\Config\ConfigFileManagerImpl;
38
use Puli\Manager\Config\DefaultConfig;
39
use Puli\Manager\Config\EnvConfig;
40
use Puli\Manager\Discovery\DiscoveryManagerImpl;
41
use Puli\Manager\Factory\FactoryManagerImpl;
42
use Puli\Manager\Factory\Generator\DefaultGeneratorRegistry;
43
use Puli\Manager\Filesystem\FilesystemStorage;
44
use Puli\Manager\Installation\InstallationManagerImpl;
45
use Puli\Manager\Installer\ModuleFileInstallerManager;
46
use Puli\Manager\Json\ChainVersioner;
47
use Puli\Manager\Json\JsonConverterProvider;
48
use Puli\Manager\Json\JsonStorage;
49
use Puli\Manager\Json\LocalUriRetriever;
50
use Puli\Manager\Module\Migration\ModuleFile10To20Migration;
51
use Puli\Manager\Module\ModuleFileConverter;
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\JsonValidator;
70
use Webmozart\Json\Migration\MigratingConverter;
71
use Webmozart\Json\Migration\MigrationManager;
72
use Webmozart\Json\Validation\ValidatingConverter;
73
use Webmozart\Json\Versioning\JsonVersioner;
74
use Webmozart\Json\Versioning\SchemaUriVersioner;
75
use Webmozart\Json\Versioning\VersionFieldVersioner;
76
use Webmozart\PathUtil\Path;
77
78
/**
79
 * The Puli service locator.
80
 *
81
 * Use this class to access the managers provided by this module:
82
 *
83
 * ```php
84
 * $puli = new Puli(getcwd());
85
 * $puli->start();
86
 *
87
 * $moduleManager = $puli->getModuleManager();
88
 * ```
89
 *
90
 * The `Puli` class either operates in the global or a project context:
91
 *
92
 *  * The "global context" is not tied to a specific root module. A global
93
 *    context only loads the settings of the "config.json" file in the home
94
 *    directory. The `Puli` class operates in the global context if no
95
 *    project root directory is passed to the constructor. In the global
96
 *    context, only the global config file manager is available.
97
 *  * The "project context" is tied to a specific Puli project. You need to
98
 *    pass the path to the project's root directory to the constructor or to
99
 *    {@link setRootDirectory()}. The configuration of the "puli.json" file in
100
 *    the root directory is used to configure the managers.
101
 *
102
 * The `Puli` class creates four kinds of managers:
103
 *
104
 *  * The "config file manager" allows you to modify entries of the
105
 *    "config.json" file in the home directory.
106
 *  * The "module file manager" manages modifications to the "puli.json" file
107
 *    of a Puli project.
108
 *  * The "module manager" manages the module repository of a Puli project.
109
 *  * The "repository manager" manages the resource repository of a Puli
110
 *    project.
111
 *  * The "discovery manager" manages the resource discovery of a Puli project.
112
 *
113
 * The home directory is read from the context variable "PULI_HOME".
114
 * If this variable is not set, the home directory defaults to:
115
 *
116
 *  * `$HOME/.puli` on Linux, where `$HOME` is the context variable
117
 *    "HOME".
118
 *  * `$APPDATA/Puli` on Windows, where `$APPDATA` is the context
119
 *    variable "APPDATA".
120
 *
121
 * If none of these variables can be found, an exception is thrown.
122
 *
123
 * A .htaccess file is put into the home directory to protect it from web
124
 * access.
125
 *
126
 * @since  1.0
127
 *
128
 * @author Bernhard Schussek <[email protected]>
129
 */
130
class Puli
131
{
132
    /**
133
     * @var string|null
134
     */
135
    private $rootDir;
136
137
    /**
138
     * @var string
139
     */
140
    private $env;
141
142
    /**
143
     * @var EventDispatcherInterface|null
144
     */
145
    private $dispatcher;
146
147
    /**
148
     * @var Context|ProjectContext
149
     */
150
    private $context;
151
152
    /**
153
     * @var ResourceRepository
154
     */
155
    private $repo;
156
157
    /**
158
     * @var Discovery
159
     */
160
    private $discovery;
161
162
    /**
163
     * @var object
164
     */
165
    private $factory;
166
167
    /**
168
     * @var FactoryManager
169
     */
170
    private $factoryManager;
171
172
    /**
173
     * @var ConfigFileManager
174
     */
175
    private $configFileManager;
176
177
    /**
178
     * @var RootModuleFileManager
179
     */
180
    private $rootModuleFileManager;
181
182
    /**
183
     * @var ModuleManager
184
     */
185
    private $moduleManager;
186
187
    /**
188
     * @var RepositoryManager
189
     */
190
    private $repositoryManager;
191
192
    /**
193
     * @var DiscoveryManager
194
     */
195
    private $discoveryManager;
196
197
    /**
198
     * @var AssetManager
199
     */
200
    private $assetManager;
201
202
    /**
203
     * @var InstallationManager
204
     */
205
    private $installationManager;
206
207
    /**
208
     * @var InstallerManager
209
     */
210
    private $installerManager;
211
212
    /**
213
     * @var ServerManager
214
     */
215
    private $serverManager;
216
217
    /**
218
     * @var UrlGenerator
219
     */
220
    private $urlGenerator;
221
222
    /**
223
     * @var Storage|null
224
     */
225
    private $storage;
226
227
    /**
228
     * @var JsonStorage|null
229
     */
230
    private $jsonStorage;
231
232
    /**
233
     * @var ConfigFileConverter|null
234
     */
235
    private $configFileConverter;
236
237
    /**
238
     * @var JsonConverter|null
239
     */
240
    private $moduleFileConverter;
241
242
    /**
243
     * @var JsonConverter|null
244
     */
245
    private $legacyModuleFileConverter;
246
247
    /**
248
     * @var JsonConverter|null
249
     */
250
    private $rootModuleFileConverter;
251
252
    /**
253
     * @var JsonConverter|null
254
     */
255
    private $legacyRootModuleFileConverter;
256
257
    /**
258
     * @var MigrationManager|null
259
     */
260
    private $moduleFileMigrationManager;
261
262
    /**
263
     * @var JsonEncoder
264
     */
265
    private $jsonEncoder;
266
267
    /**
268
     * @var JsonDecoder
269
     */
270
    private $jsonDecoder;
271
272
    /**
273
     * @var JsonValidator
274
     */
275
    private $jsonValidator;
276
277
    /**
278
     * @var JsonVersioner
279
     */
280
    private $jsonVersioner;
281
282
    /**
283
     * @var LoggerInterface
284
     */
285
    private $logger;
286
287
    /**
288
     * @var bool
289
     */
290
    private $started = false;
291
292
    /**
293
     * @var bool
294
     */
295
    private $pluginsEnabled = true;
296
297
    /**
298
     * Parses the system context for a home directory.
299
     *
300
     * @return null|string Returns the path to the home directory or `null`
301
     *                     if none was found.
302
     */
303 50
    private static function parseHomeDirectory()
304
    {
305
        try {
306 50
            $homeDir = System::parseHomeDirectory();
307
308 47
            System::denyWebAccess($homeDir);
309
310 47
            return $homeDir;
311 3
        } catch (InvalidConfigException $e) {
312
            // Context variable was not found -> no home directory
313
            // This happens often on web servers where the home directory is
314
            // not set manually
315 2
            return null;
316
        }
317
    }
318
319
    /**
320
     * Creates a new instance for the given Puli project.
321
     *
322
     * @param string|null $rootDir The root directory of the Puli project.
323
     *                             If none is passed, the object operates in
324
     *                             the global context. You can set or switch
325
     *                             the root directories later on by calling
326
     *                             {@link setRootDirectory()}.
327
     * @param string      $env     One of the {@link Environment} constants.
328
     *
329
     * @see Puli, start()
330
     */
331 52
    public function __construct($rootDir = null, $env = Environment::DEV)
332
    {
333 52
        $this->setRootDirectory($rootDir);
334 52
        $this->setEnvironment($env);
335 52
    }
336
337
    /**
338
     * Starts the service container.
339
     */
340 50
    public function start()
341
    {
342 50
        if ($this->started) {
343
            throw new LogicException('Puli is already started');
344
        }
345
346 50
        if (null !== $this->rootDir) {
347 27
            $this->context = $this->createProjectContext($this->rootDir, $this->env);
348 27
            $bootstrapFile = $this->context->getConfig()->get(Config::BOOTSTRAP_FILE);
349
350
            // Run the project's bootstrap file to enable project-specific
351
            // autoloading
352 27
            if (null !== $bootstrapFile) {
353
                // Backup autoload functions of the PHAR
354 1
                $autoloadFunctions = spl_autoload_functions();
355
356 1
                foreach ($autoloadFunctions as $autoloadFunction) {
357 1
                    spl_autoload_unregister($autoloadFunction);
358
                }
359
360
                // Add project-specific autoload functions
361 1
                require_once Path::makeAbsolute($bootstrapFile, $this->rootDir);
362
363
                // Prepend autoload functions of the PHAR again
364
                // This is needed if the user specific autoload functions were
365
                // added with $prepend=true (as done by Composer)
366
                // Classes in the PHAR should always take precedence
367 27
                for ($i = count($autoloadFunctions) - 1; $i >= 0; --$i) {
368 1
                    spl_autoload_register($autoloadFunctions[$i], true, true);
369
                }
370
            }
371
        } else {
372 23
            $this->context = $this->createGlobalContext();
373
        }
374
375 49
        $this->dispatcher = $this->context->getEventDispatcher();
376 49
        $this->started = true;
377
378
        // Start plugins once the container is running
379 49
        if ($this->rootDir && $this->pluginsEnabled) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->rootDir of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
380 26
            $this->activatePlugins();
381
        }
382 47
    }
383
384
    /**
385
     * Returns whether the service container is started.
386
     *
387
     * @return bool Returns `true` if the container is started and `false`
388
     *              otherwise.
389
     */
390
    public function isStarted()
391
    {
392
        return $this->started;
393
    }
394
395
    /**
396
     * Sets the root directory of the managed Puli project.
397
     *
398
     * @param string|null $rootDir The root directory of the managed Puli
399
     *                             project or `null` to start Puli outside of a
400
     *                             specific project.
401
     */
402 52
    public function setRootDirectory($rootDir)
403
    {
404 52
        if ($this->started) {
405
            throw new LogicException('Puli is already started');
406
        }
407
408 52
        Assert::nullOrDirectory($rootDir);
409
410 52
        $this->rootDir = $rootDir ? Path::canonicalize($rootDir) : null;
411 52
    }
412
413
    /**
414
     * Sets the environment of the managed Puli project.
415
     *
416
     * @param string $env One of the {@link Environment} constants.
417
     */
418 52
    public function setEnvironment($env)
419
    {
420 52
        if ($this->started) {
421
            throw new LogicException('Puli is already started');
422
        }
423
424 52
        Assert::oneOf($env, Environment::all(), 'The environment must be one of: %2$s. Got: %s');
425
426 52
        $this->env = $env;
427 52
    }
428
429
    /**
430
     * Retturns the environment of the managed Puli project.
431
     *
432
     * @return string One of the {@link Environment} constants.
433
     */
434 2
    public function getEnvironment()
435
    {
436 2
        return $this->env;
437
    }
438
439
    /**
440
     * Returns the root directory of the managed Puli project.
441
     *
442
     * If no Puli project is managed at the moment, `null` is returned.
443
     *
444
     * @return string|null The root directory of the managed Puli project or
445
     *                     `null` if none is set.
446
     */
447 4
    public function getRootDirectory()
448
    {
449 4
        return $this->rootDir;
450
    }
451
452
    /**
453
     * Sets the logger to use.
454
     *
455
     * @param LoggerInterface $logger The logger to use.
456
     */
457
    public function setLogger(LoggerInterface $logger)
458
    {
459
        if ($this->started) {
460
            throw new LogicException('Puli is already started');
461
        }
462
463
        $this->logger = $logger;
464
    }
465
466
    /**
467
     * Returns the logger.
468
     *
469
     * @return LoggerInterface The logger.
470
     */
471
    public function getLogger()
472
    {
473
        return $this->logger;
474
    }
475
476
    /**
477
     * Sets the event dispatcher to use.
478
     *
479
     * @param EventDispatcherInterface $dispatcher The event dispatcher to use.
480
     */
481 2
    public function setEventDispatcher(EventDispatcherInterface $dispatcher)
482
    {
483 2
        if ($this->started) {
484
            throw new LogicException('Puli is already started');
485
        }
486
487 2
        $this->dispatcher = $dispatcher;
488 2
    }
489
490
    /**
491
     * Returns the used event dispatcher.
492
     *
493
     * @return EventDispatcherInterface|null The used logger.
494
     */
495 3
    public function getEventDispatcher()
496
    {
497 3
        return $this->dispatcher;
498
    }
499
500
    /**
501
     * Enables all Puli plugins.
502
     */
503
    public function enablePlugins()
504
    {
505
        $this->pluginsEnabled = true;
506
    }
507
508
    /**
509
     * Disables all Puli plugins.
510
     */
511 1
    public function disablePlugins()
512
    {
513 1
        $this->pluginsEnabled = false;
514 1
    }
515
516
    /**
517
     * Returns whether Puli plugins are enabled.
518
     *
519
     * @return bool Returns `true` if Puli plugins will be loaded and `false`
520
     *              otherwise.
521
     */
522
    public function arePluginsEnabled()
523
    {
524
        return $this->pluginsEnabled;
525
    }
526
527
    /**
528
     * Returns the context.
529
     *
530
     * @return Context|ProjectContext The context.
531
     */
532 30
    public function getContext()
533
    {
534 30
        if (!$this->started) {
535
            throw new LogicException('Puli was not started');
536
        }
537
538 30
        return $this->context;
539
    }
540
541
    /**
542
     * Returns the resource repository of the project.
543
     *
544
     * @return EditableRepository The resource repository.
545
     */
546 4 View Code Duplication
    public function getRepository()
547
    {
548 4
        if (!$this->started) {
549
            throw new LogicException('Puli was not started');
550
        }
551
552 4
        if (!$this->context instanceof ProjectContext) {
553 1
            return null;
554
        }
555
556 3
        if (!$this->repo) {
557 3
            $this->repo = $this->getFactory()->createRepository();
558
        }
559
560
        return $this->repo;
561
    }
562
563
    /**
564
     * Returns the resource discovery of the project.
565
     *
566
     * @return EditableDiscovery The resource discovery.
567
     */
568 4 View Code Duplication
    public function getDiscovery()
569
    {
570 4
        if (!$this->started) {
571
            throw new LogicException('Puli was not started');
572
        }
573
574 4
        if (!$this->context instanceof ProjectContext) {
575 1
            return null;
576
        }
577
578 3
        if (!$this->discovery) {
579 3
            $this->discovery = $this->getFactory()->createDiscovery($this->getRepository());
580
        }
581
582
        return $this->discovery;
583
    }
584
585
    /**
586
     * @return object
587
     */
588 8
    public function getFactory()
589
    {
590 8
        if (!$this->started) {
591
            throw new LogicException('Puli was not started');
592
        }
593
594 8
        if (!$this->factory && $this->context instanceof ProjectContext) {
595 7
            $this->factory = $this->getFactoryManager()->createFactory();
596
        }
597
598 1
        return $this->factory;
599
    }
600
601
    /**
602
     * @return FactoryManager
603
     */
604 16
    public function getFactoryManager()
605
    {
606 16
        if (!$this->started) {
607
            throw new LogicException('Puli was not started');
608
        }
609
610 16
        if (!$this->factoryManager && $this->context instanceof ProjectContext) {
611 14
            $this->factoryManager = new FactoryManagerImpl(
612 14
                $this->context,
613 14
                new DefaultGeneratorRegistry(),
614 14
                new ClassWriter()
615
            );
616
617
            // Don't set via the constructor to prevent cyclic dependencies
618 14
            $this->factoryManager->setModules($this->getModuleManager()->getModules());
619
            $this->factoryManager->setServers($this->getServerManager()->getServers());
620
        }
621
622 16
        return $this->factoryManager;
623
    }
624
625
    /**
626
     * Returns the configuration file manager.
627
     *
628
     * @return ConfigFileManager The configuration file manager.
629
     */
630 2
    public function getConfigFileManager()
631
    {
632 2
        if (!$this->started) {
633
            throw new LogicException('Puli was not started');
634
        }
635
636 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...
637 2
            $this->configFileManager = new ConfigFileManagerImpl(
638 2
                $this->context,
639 2
                $this->getJsonStorage()
640
            );
641
        }
642
643 1
        return $this->configFileManager;
644
    }
645
646
    /**
647
     * Returns the root module file manager.
648
     *
649
     * @return RootModuleFileManager The module file manager.
650
     */
651 5 View Code Duplication
    public function getRootModuleFileManager()
652
    {
653 5
        if (!$this->started) {
654
            throw new LogicException('Puli was not started');
655
        }
656
657 5
        if (!$this->rootModuleFileManager && $this->context instanceof ProjectContext) {
658 4
            $this->rootModuleFileManager = new RootModuleFileManagerImpl(
659 4
                $this->context,
660 4
                $this->getJsonStorage()
661
            );
662
        }
663
664 1
        return $this->rootModuleFileManager;
665
    }
666
667
    /**
668
     * Returns the module manager.
669
     *
670
     * @return ModuleManager The module manager.
671
     */
672 15 View Code Duplication
    public function getModuleManager()
673
    {
674 15
        if (!$this->started) {
675
            throw new LogicException('Puli was not started');
676
        }
677
678 15
        if (!$this->moduleManager && $this->context instanceof ProjectContext) {
679 14
            $this->moduleManager = new ModuleManagerImpl(
680 14
                $this->context,
681 14
                $this->getJsonStorage()
682
            );
683
        }
684
685 15
        return $this->moduleManager;
686
    }
687
688
    /**
689
     * Returns the resource repository manager.
690
     *
691
     * @return RepositoryManager The repository manager.
692
     */
693 2 View Code Duplication
    public function getRepositoryManager()
694
    {
695 2
        if (!$this->started) {
696
            throw new LogicException('Puli was not started');
697
        }
698
699 2
        if (!$this->repositoryManager && $this->context instanceof ProjectContext) {
700 1
            $this->repositoryManager = new RepositoryManagerImpl(
701 1
                $this->context,
702 1
                $this->getRepository(),
703
                $this->getModuleManager()->findModules(Expr::method('isEnabled', Expr::same(true))),
704
                $this->getJsonStorage()
705
            );
706
        }
707
708 1
        return $this->repositoryManager;
709
    }
710
711
    /**
712
     * Returns the resource discovery manager.
713
     *
714
     * @return DiscoveryManager The discovery manager.
715
     */
716 3 View Code Duplication
    public function getDiscoveryManager()
717
    {
718 3
        if (!$this->started) {
719
            throw new LogicException('Puli was not started');
720
        }
721
722 3
        if (!$this->discoveryManager && $this->context instanceof ProjectContext) {
723 2
            $this->discoveryManager = new DiscoveryManagerImpl(
724 2
                $this->context,
725 2
                $this->getDiscovery(),
726
                $this->getModuleManager()->findModules(Expr::method('isEnabled', Expr::same(true))),
727
                $this->getJsonStorage(),
728
                $this->logger
729
            );
730
        }
731
732 1
        return $this->discoveryManager;
733
    }
734
735
    /**
736
     * Returns the asset manager.
737
     *
738
     * @return AssetManager The asset manager.
739
     */
740 2
    public function getAssetManager()
741
    {
742 2
        if (!$this->started) {
743
            throw new LogicException('Puli was not started');
744
        }
745
746 2
        if (!$this->assetManager && $this->context instanceof ProjectContext) {
747 1
            $this->assetManager = new DiscoveryAssetManager(
748 1
                $this->getDiscoveryManager(),
749
                $this->getServerManager()->getServers()
750
            );
751
        }
752
753 1
        return $this->assetManager;
754
    }
755
756
    /**
757
     * Returns the installation manager.
758
     *
759
     * @return InstallationManager The installation manager.
760
     */
761 2
    public function getInstallationManager()
762
    {
763 2
        if (!$this->started) {
764
            throw new LogicException('Puli was not started');
765
        }
766
767 2
        if (!$this->installationManager && $this->context instanceof ProjectContext) {
768 1
            $this->installationManager = new InstallationManagerImpl(
769 1
                $this->getContext(),
770 1
                $this->getRepository(),
771
                $this->getServerManager()->getServers(),
772
                $this->getInstallerManager()
773
            );
774
        }
775
776 1
        return $this->installationManager;
777
    }
778
779
    /**
780
     * Returns the installer manager.
781
     *
782
     * @return InstallerManager The installer manager.
783
     */
784 2
    public function getInstallerManager()
785
    {
786 2
        if (!$this->started) {
787
            throw new LogicException('Puli was not started');
788
        }
789
790 2
        if (!$this->installerManager && $this->context instanceof ProjectContext) {
791 1
            $this->installerManager = new ModuleFileInstallerManager(
792 1
                $this->getRootModuleFileManager(),
793
                $this->getModuleManager()->getModules()
794
            );
795
        }
796
797 1
        return $this->installerManager;
798
    }
799
800
    /**
801
     * Returns the server manager.
802
     *
803
     * @return ServerManager The server manager.
804
     */
805 3
    public function getServerManager()
806
    {
807 3
        if (!$this->started) {
808
            throw new LogicException('Puli was not started');
809
        }
810
811 3
        if (!$this->serverManager && $this->context instanceof ProjectContext) {
812 2
            $this->serverManager = new ModuleFileServerManager(
813 2
                $this->getRootModuleFileManager(),
814
                $this->getInstallerManager()
815
            );
816
        }
817
818 1
        return $this->serverManager;
819
    }
820
821
    /**
822
     * Returns the resource URL generator.
823
     *
824
     * @return UrlGenerator The resource URL generator.
825
     */
826 2
    public function getUrlGenerator()
827
    {
828 2
        if (!$this->started) {
829
            throw new LogicException('Puli was not started');
830
        }
831
832 2
        if (!$this->urlGenerator && $this->context instanceof ProjectContext) {
833 1
            $urlFormats = array();
834 1
            foreach ($this->getServerManager()->getServers() as $server) {
835
                $urlFormats[$server->getName()] = $server->getUrlFormat();
836
            }
837
838
            $this->urlGenerator = new DiscoveryUrlGenerator($this->getDiscovery(), $urlFormats);
839
        }
840
841 1
        return $this->urlGenerator;
842
    }
843
844
    /**
845
     * Returns the file storage.
846
     *
847
     * @return Storage The storage.
848
     */
849 48
    public function getStorage()
850
    {
851 48
        if (!$this->storage) {
852 48
            $this->storage = new FilesystemStorage();
853
        }
854
855 48
        return $this->storage;
856
    }
857
858
    /**
859
     * Returns the configuration file serializer.
860
     *
861
     * @return ConfigFileConverter The configuration file serializer.
862
     */
863 46
    public function getConfigFileConverter()
864
    {
865 46
        if (!$this->configFileConverter) {
866 46
            $this->configFileConverter = new ConfigFileConverter();
867
        }
868
869 46
        return $this->configFileConverter;
870
    }
871
872
    /**
873
     * Returns the module file converter.
874
     *
875
     * @return JsonConverter The module file converter.
876
     */
877 14
    public function getModuleFileConverter()
878
    {
879 14
        if (!$this->moduleFileConverter) {
880 14
            $this->moduleFileConverter = $this->createValidatingConverter(
881 14
                new ModuleFileConverter($this->getJsonVersioner())
882
            );
883
        }
884
885 14
        return $this->moduleFileConverter;
886
    }
887
888
    /**
889
     * Returns the module file serializer with support for legacy versions.
890
     *
891
     * @return JsonConverter The module file converter.
892
     */
893 14 View Code Duplication
    public function getLegacyModuleFileConverter()
894
    {
895 14
        if (!$this->legacyModuleFileConverter) {
896 14
            $this->legacyModuleFileConverter = $this->createValidatingConverter(
897 14
                new MigratingConverter(
898 14
                    $this->getModuleFileConverter(),
899 14
                    ModuleFileConverter::VERSION,
900 14
                    $this->getModuleFileMigrationManager()
901
                ),
902
                function (stdClass $jsonData) {
903 14
                    if (isset($jsonData->{'$schema'})) {
904
                        return $jsonData->{'$schema'};
905
                    }
906
907
                    // BC with 1.0
908 14
                    return 'http://puli.io/schema/1.0/manager/module';
909 14
                }
910
            );
911
        }
912
913 14
        return $this->legacyModuleFileConverter;
914
    }
915
916
    /**
917
     * Returns the module file converter.
918
     *
919
     * @return JsonConverter The module file converter.
920
     */
921 26
    public function getRootModuleFileConverter()
922
    {
923 26
        if (!$this->rootModuleFileConverter) {
924 26
            $this->rootModuleFileConverter = $this->createValidatingConverter(
925 26
                new RootModuleFileConverter($this->getJsonVersioner())
926
            );
927
        }
928
929 26
        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 26 View Code Duplication
    public function getLegacyRootModuleFileConverter()
938
    {
939 26
        if (!$this->legacyRootModuleFileConverter) {
940 26
            $this->legacyRootModuleFileConverter = $this->createValidatingConverter(
941 26
                new MigratingConverter(
942 26
                    $this->getRootModuleFileConverter(),
943 26
                    RootModuleFileConverter::VERSION,
944 26
                    $this->getModuleFileMigrationManager()
945
                ),
946 26
                function (stdClass $jsonData) {
947 26
                    if (isset($jsonData->{'$schema'})) {
948
                        return $jsonData->{'$schema'};
949
                    }
950
951
                    // BC with 1.0
952 26
                    return 'http://puli.io/schema/1.0/manager/module';
953 26
                }
954
            );
955
        }
956
957 26
        return $this->legacyRootModuleFileConverter;
958
    }
959
960
    /**
961
     * Returns the JSON encoder.
962
     *
963
     * @return JsonEncoder The JSON encoder.
964
     */
965 48
    public function getJsonEncoder()
966
    {
967 48
        if (!$this->jsonEncoder) {
968 48
            $this->jsonEncoder = new JsonEncoder();
969 48
            $this->jsonEncoder->setPrettyPrinting(true);
970 48
            $this->jsonEncoder->setEscapeSlash(false);
971 48
            $this->jsonEncoder->setTerminateWithLineFeed(true);
972
        }
973
974 48
        return $this->jsonEncoder;
975
    }
976
977
    /**
978
     * Returns the JSON decoder.
979
     *
980
     * @return JsonDecoder The JSON decoder.
981
     */
982 48
    public function getJsonDecoder()
983
    {
984 48
        if (!$this->jsonDecoder) {
985 48
            $this->jsonDecoder = new JsonDecoder();
986
        }
987
988 48
        return $this->jsonDecoder;
989
    }
990
991
    /**
992
     * Returns the JSON validator.
993
     *
994
     * @return JsonValidator The JSON validator.
995
     */
996 26
    public function getJsonValidator()
997
    {
998 26
        if (!$this->jsonValidator) {
999 26
            $uriRetriever = new UriRetriever();
1000
1001
            // Load puli.io schemas from the schema/ directory
1002 26
            $uriRetriever->setUriRetriever(new LocalUriRetriever());
1003
1004 26
            $this->jsonValidator = new JsonValidator(null, $uriRetriever);
1005
        }
1006
1007 26
        return $this->jsonValidator;
1008
    }
1009
1010 26
    private function activatePlugins()
1011
    {
1012 26
        foreach ($this->context->getRootModuleFile()->getPluginClasses() as $pluginClass) {
1013 25
            $this->validatePluginClass($pluginClass);
1014
1015
            /** @var PuliPlugin $plugin */
1016 23
            $plugin = new $pluginClass();
1017 23
            $plugin->activate($this);
1018
        }
1019 24
    }
1020
1021 23
    private function createGlobalContext()
1022
    {
1023 23
        $baseConfig = new DefaultConfig();
1024 23
        $homeDir = self::parseHomeDirectory();
1025
1026 22
        if (null !== $configFile = $this->loadConfigFile($homeDir, $baseConfig)) {
1027 20
            $baseConfig = $configFile->getConfig();
1028
        }
1029
1030 22
        $config = new EnvConfig($baseConfig);
1031
1032 22
        return new Context($homeDir, $config, $configFile, $this->dispatcher);
1033
    }
1034
1035
    /**
1036
     * Creates the context of a Puli project.
1037
     *
1038
     * The home directory is read from the context variable "PULI_HOME".
1039
     * If this variable is not set, the home directory defaults to:
1040
     *
1041
     *  * `$HOME/.puli` on Linux, where `$HOME` is the context variable
1042
     *    "HOME".
1043
     *  * `$APPDATA/Puli` on Windows, where `$APPDATA` is the context
1044
     *    variable "APPDATA".
1045
     *
1046
     * If none of these variables can be found, an exception is thrown.
1047
     *
1048
     * A .htaccess file is put into the home directory to protect it from web
1049
     * access.
1050
     *
1051
     * @param string $rootDir The path to the project.
1052
     *
1053
     * @return ProjectContext The project context.
1054
     */
1055 27
    private function createProjectContext($rootDir, $env)
1056
    {
1057 27
        Assert::fileExists($rootDir, 'Could not load Puli context: The root %s does not exist.');
1058 27
        Assert::directory($rootDir, 'Could not load Puli context: The root %s is a file. Expected a directory.');
1059
1060 27
        $baseConfig = new DefaultConfig();
1061 27
        $homeDir = self::parseHomeDirectory();
1062
1063 27
        if (null !== $configFile = $this->loadConfigFile($homeDir, $baseConfig)) {
1064 26
            $baseConfig = $configFile->getConfig();
1065
        }
1066
1067
        // Create a storage without the factory manager
1068 27
        $jsonStorage = new JsonStorage(
1069 27
            $this->getStorage(),
1070 27
            new JsonConverterProvider($this),
1071 27
            $this->getJsonEncoder(),
1072 27
            $this->getJsonDecoder()
1073
        );
1074
1075 27
        $rootDir = Path::canonicalize($rootDir);
1076 27
        $rootFilePath = $this->rootDir.'/puli.json';
1077
1078
        try {
1079 27
            $rootModuleFile = $jsonStorage->loadRootModuleFile($rootFilePath, $baseConfig);
1080 1
        } catch (FileNotFoundException $e) {
1081 1
            $rootModuleFile = new RootModuleFile(null, $rootFilePath, $baseConfig);
1082
        }
1083
1084 27
        $config = new EnvConfig($rootModuleFile->getConfig());
1085
1086 27
        return new ProjectContext($homeDir, $rootDir, $config, $rootModuleFile, $configFile, $this->dispatcher, $env);
1087
    }
1088
1089
    /**
1090
     * Decorates a converter with a {@link ValidatingConverter}.
1091
     *
1092
     * @param JsonConverter        $innerConverter The converter to decorate.
1093
     * @param string|callable|null $schema         The schema.
1094
     *
1095
     * @return ValidatingConverter The decorated converter.
1096
     */
1097 26
    private function createValidatingConverter(JsonConverter $innerConverter, $schema = null)
1098
    {
1099 26
        return new ValidatingConverter($innerConverter, $schema, $this->getJsonValidator());
1100
    }
1101
1102
    /**
1103
     * Returns the JSON file storage.
1104
     *
1105
     * @return JsonStorage The JSON file storage.
1106
     */
1107 15
    private function getJsonStorage()
1108
    {
1109 15
        if (!$this->jsonStorage) {
1110 15
            $this->jsonStorage = new JsonStorage(
1111 15
                $this->getStorage(),
1112 15
                new JsonConverterProvider($this),
1113 15
                $this->getJsonEncoder(),
1114 15
                $this->getJsonDecoder(),
1115 15
                $this->getFactoryManager()
1116
            );
1117
        }
1118
1119 15
        return $this->jsonStorage;
1120
    }
1121
1122
    /**
1123
     * Returns the JSON versioner.
1124
     *
1125
     * @return JsonVersioner The JSON versioner.
1126
     */
1127 26
    private function getJsonVersioner()
1128
    {
1129 26
        if (!$this->jsonVersioner) {
1130 26
            $this->jsonVersioner = new ChainVersioner(array(
1131
                // check the schema of the "$schema" field by default
1132 26
                new SchemaUriVersioner(),
1133
                // fall back to the "version" field for 1.0
1134 26
                new VersionFieldVersioner(),
1135
            ));
1136
        }
1137
1138 26
        return $this->jsonVersioner;
1139
    }
1140
1141
    /**
1142
     * Returns the migration manager for module files.
1143
     *
1144
     * @return MigrationManager The migration manager.
1145
     */
1146 26
    private function getModuleFileMigrationManager()
1147
    {
1148 26
        if (!$this->moduleFileMigrationManager) {
1149 26
            $this->moduleFileMigrationManager = new MigrationManager(array(
1150 26
                new ModuleFile10To20Migration(),
1151 26
            ), $this->getJsonVersioner());
1152
        }
1153
1154 26
        return $this->moduleFileMigrationManager;
1155
    }
1156
1157
    /**
1158
     * Validates the given plugin class name.
1159
     *
1160
     * @param string $pluginClass The fully qualified name of a plugin class.
1161
     */
1162 25
    private function validatePluginClass($pluginClass)
1163
    {
1164 25
        if (!class_exists($pluginClass)) {
1165 1
            throw new InvalidConfigException(sprintf(
1166 1
                'The plugin class %s does not exist.',
1167
                $pluginClass
1168
            ));
1169
        }
1170
1171 24
        if (!in_array('Puli\Manager\Api\PuliPlugin', class_implements($pluginClass))) {
1172 1
            throw new InvalidConfigException(sprintf(
1173 1
                'The plugin class %s must implement PuliPlugin.',
1174
                $pluginClass
1175
            ));
1176
        }
1177 23
    }
1178
1179 49
    private function loadConfigFile($homeDir, Config $baseConfig)
1180
    {
1181 49
        if (null === $homeDir) {
1182 2
            return null;
1183
        }
1184
1185 47
        Assert::fileExists($homeDir, 'Could not load Puli context: The home directory %s does not exist.');
1186 47
        Assert::directory($homeDir, 'Could not load Puli context: The home directory %s is a file. Expected a directory.');
1187
1188
        // Create a storage without the factory manager
1189 47
        $jsonStorage = new JsonStorage(
1190 47
            $this->getStorage(),
1191 47
            new JsonConverterProvider($this),
1192 47
            $this->getJsonEncoder(),
1193 47
            $this->getJsonDecoder()
1194
        );
1195
1196 47
        $configPath = Path::canonicalize($homeDir).'/config.json';
1197
1198
        try {
1199 47
            return $jsonStorage->loadConfigFile($configPath, $baseConfig);
1200 1
        } catch (FileNotFoundException $e) {
1201
            // It's ok if no config.json exists. We'll work with
1202
            // DefaultConfig instead
1203 1
            return null;
1204
        }
1205
    }
1206
}
1207