Failed Conditions
Pull Request — master (#52)
by Bernhard
05:53 queued 01:44
created

Container::setRootDirectory()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 10
rs 9.4285
cc 3
eloc 5
nc 3
nop 1
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 container.
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 Container
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
    private static function parseHomeDirectory()
304
    {
305
        try {
306
            $homeDir = System::parseHomeDirectory();
307
308
            System::denyWebAccess($homeDir);
309
310
            return $homeDir;
311
        } 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
            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
    public function __construct($rootDir = null, $env = Environment::DEV)
332
    {
333
        $this->setRootDirectory($rootDir);
334
        $this->setEnvironment($env);
335
    }
336
337
    /**
338
     * Starts the service container.
339
     */
340
    public function start()
341
    {
342
        if ($this->started) {
343
            throw new LogicException('Puli is already started');
344
        }
345
346
        if (null !== $this->rootDir) {
347
            $this->context = $this->createProjectContext($this->rootDir, $this->env);
348
            $bootstrapFile = $this->context->getConfig()->get(Config::BOOTSTRAP_FILE);
349
350
            // Run the project's bootstrap file to enable project-specific
351
            // autoloading
352
            if (null !== $bootstrapFile) {
353
                // Backup autoload functions of the PHAR
354
                $autoloadFunctions = spl_autoload_functions();
355
356
                foreach ($autoloadFunctions as $autoloadFunction) {
357
                    spl_autoload_unregister($autoloadFunction);
358
                }
359
360
                // Add project-specific autoload functions
361
                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
                for ($i = count($autoloadFunctions) - 1; $i >= 0; --$i) {
368
                    spl_autoload_register($autoloadFunctions[$i], true, true);
369
                }
370
            }
371
        } else {
372
            $this->context = $this->createGlobalContext();
373
        }
374
375
        $this->dispatcher = $this->context->getEventDispatcher();
376
        $this->started = true;
377
378
        // Start plugins once the container is running
379
        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
            $this->activatePlugins();
381
        }
382
    }
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
    public function setRootDirectory($rootDir)
403
    {
404
        if ($this->started) {
405
            throw new LogicException('Puli is already started');
406
        }
407
408
        Assert::nullOrDirectory($rootDir);
409
410
        $this->rootDir = $rootDir ? Path::canonicalize($rootDir) : null;
411
    }
412
413
    /**
414
     * Sets the environment of the managed Puli project.
415
     *
416
     * @param string $env One of the {@link Environment} constants.
417
     */
418
    public function setEnvironment($env)
419
    {
420
        if ($this->started) {
421
            throw new LogicException('Puli is already started');
422
        }
423
424
        Assert::oneOf($env, Environment::all(), 'The environment must be one of: %2$s. Got: %s');
425
426
        $this->env = $env;
427
    }
428
429
    /**
430
     * Retturns the environment of the managed Puli project.
431
     *
432
     * @return string One of the {@link Environment} constants.
433
     */
434
    public function getEnvironment()
435
    {
436
        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
    public function getRootDirectory()
448
    {
449
        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
    public function setEventDispatcher(EventDispatcherInterface $dispatcher)
482
    {
483
        if ($this->started) {
484
            throw new LogicException('Puli is already started');
485
        }
486
487
        $this->dispatcher = $dispatcher;
488
    }
489
490
    /**
491
     * Returns the used event dispatcher.
492
     *
493
     * @return EventDispatcherInterface|null The used logger.
494
     */
495
    public function getEventDispatcher()
496
    {
497
        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
    public function disablePlugins()
512
    {
513
        $this->pluginsEnabled = false;
514
    }
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
    public function getContext()
533
    {
534
        if (!$this->started) {
535
            throw new LogicException('Puli was not started');
536
        }
537
538
        return $this->context;
539
    }
540
541
    /**
542
     * Returns the resource repository of the project.
543
     *
544
     * @return EditableRepository The resource repository.
545
     */
546 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...
547
    {
548
        if (!$this->started) {
549
            throw new LogicException('Puli was not started');
550
        }
551
552
        if (!$this->context instanceof ProjectContext) {
553
            return null;
554
        }
555
556
        if (!$this->repo) {
557
            $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 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...
569
    {
570
        if (!$this->started) {
571
            throw new LogicException('Puli was not started');
572
        }
573
574
        if (!$this->context instanceof ProjectContext) {
575
            return null;
576
        }
577
578
        if (!$this->discovery) {
579
            $this->discovery = $this->getFactory()->createDiscovery($this->getRepository());
580
        }
581
582
        return $this->discovery;
583
    }
584
585
    /**
586
     * @return object
587
     */
588
    public function getFactory()
589
    {
590
        if (!$this->started) {
591
            throw new LogicException('Puli was not started');
592
        }
593
594
        if (!$this->factory && $this->context instanceof ProjectContext) {
595
            $this->factory = $this->getFactoryManager()->createFactory();
596
        }
597
598
        return $this->factory;
599
    }
600
601
    /**
602
     * @return FactoryManager
603
     */
604
    public function getFactoryManager()
605
    {
606
        if (!$this->started) {
607
            throw new LogicException('Puli was not started');
608
        }
609
610
        if (!$this->factoryManager && $this->context instanceof ProjectContext) {
611
            $this->factoryManager = new FactoryManagerImpl(
612
                $this->context,
613
                new DefaultGeneratorRegistry(),
614
                new ClassWriter()
615
            );
616
617
            // Don't set via the constructor to prevent cyclic dependencies
618
            $this->factoryManager->setModules($this->getModuleManager()->getModules());
619
            $this->factoryManager->setServers($this->getServerManager()->getServers());
620
        }
621
622
        return $this->factoryManager;
623
    }
624
625
    /**
626
     * Returns the configuration file manager.
627
     *
628
     * @return ConfigFileManager The configuration file manager.
629
     */
630
    public function getConfigFileManager()
631
    {
632
        if (!$this->started) {
633
            throw new LogicException('Puli was not started');
634
        }
635
636
        if (!$this->configFileManager && $this->context->getHomeDirectory()) {
637
            $this->configFileManager = new ConfigFileManagerImpl(
638
                $this->context,
639
                $this->getJsonStorage()
640
            );
641
        }
642
643
        return $this->configFileManager;
644
    }
645
646
    /**
647
     * Returns the root module file manager.
648
     *
649
     * @return RootModuleFileManager The module file manager.
650
     */
651 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...
652
    {
653
        if (!$this->started) {
654
            throw new LogicException('Puli was not started');
655
        }
656
657
        if (!$this->rootModuleFileManager && $this->context instanceof ProjectContext) {
658
            $this->rootModuleFileManager = new RootModuleFileManagerImpl(
659
                $this->context,
660
                $this->getJsonStorage()
661
            );
662
        }
663
664
        return $this->rootModuleFileManager;
665
    }
666
667
    /**
668
     * Returns the module manager.
669
     *
670
     * @return ModuleManager The module manager.
671
     */
672 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...
673
    {
674
        if (!$this->started) {
675
            throw new LogicException('Puli was not started');
676
        }
677
678
        if (!$this->moduleManager && $this->context instanceof ProjectContext) {
679
            $this->moduleManager = new ModuleManagerImpl(
680
                $this->context,
681
                $this->getJsonStorage()
682
            );
683
        }
684
685
        return $this->moduleManager;
686
    }
687
688
    /**
689
     * Returns the resource repository manager.
690
     *
691
     * @return RepositoryManager The repository manager.
692
     */
693 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...
694
    {
695
        if (!$this->started) {
696
            throw new LogicException('Puli was not started');
697
        }
698
699
        if (!$this->repositoryManager && $this->context instanceof ProjectContext) {
700
            $this->repositoryManager = new RepositoryManagerImpl(
701
                $this->context,
702
                $this->getRepository(),
703
                $this->getModuleManager()->findModules(Expr::method('isEnabled', Expr::same(true))),
704
                $this->getJsonStorage()
705
            );
706
        }
707
708
        return $this->repositoryManager;
709
    }
710
711
    /**
712
     * Returns the resource discovery manager.
713
     *
714
     * @return DiscoveryManager The discovery manager.
715
     */
716 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...
717
    {
718
        if (!$this->started) {
719
            throw new LogicException('Puli was not started');
720
        }
721
722
        if (!$this->discoveryManager && $this->context instanceof ProjectContext) {
723
            $this->discoveryManager = new DiscoveryManagerImpl(
724
                $this->context,
725
                $this->getDiscovery(),
726
                $this->getModuleManager()->findModules(Expr::method('isEnabled', Expr::same(true))),
727
                $this->getJsonStorage(),
728
                $this->logger
729
            );
730
        }
731
732
        return $this->discoveryManager;
733
    }
734
735
    /**
736
     * Returns the asset manager.
737
     *
738
     * @return AssetManager The asset manager.
739
     */
740
    public function getAssetManager()
741
    {
742
        if (!$this->started) {
743
            throw new LogicException('Puli was not started');
744
        }
745
746
        if (!$this->assetManager && $this->context instanceof ProjectContext) {
747
            $this->assetManager = new DiscoveryAssetManager(
748
                $this->getDiscoveryManager(),
749
                $this->getServerManager()->getServers()
750
            );
751
        }
752
753
        return $this->assetManager;
754
    }
755
756
    /**
757
     * Returns the installation manager.
758
     *
759
     * @return InstallationManager The installation manager.
760
     */
761
    public function getInstallationManager()
762
    {
763
        if (!$this->started) {
764
            throw new LogicException('Puli was not started');
765
        }
766
767
        if (!$this->installationManager && $this->context instanceof ProjectContext) {
768
            $this->installationManager = new InstallationManagerImpl(
769
                $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...
770
                $this->getRepository(),
771
                $this->getServerManager()->getServers(),
772
                $this->getInstallerManager()
773
            );
774
        }
775
776
        return $this->installationManager;
777
    }
778
779
    /**
780
     * Returns the installer manager.
781
     *
782
     * @return InstallerManager The installer manager.
783
     */
784
    public function getInstallerManager()
785
    {
786
        if (!$this->started) {
787
            throw new LogicException('Puli was not started');
788
        }
789
790
        if (!$this->installerManager && $this->context instanceof ProjectContext) {
791
            $this->installerManager = new ModuleFileInstallerManager(
792
                $this->getRootModuleFileManager(),
793
                $this->getModuleManager()->getModules()
794
            );
795
        }
796
797
        return $this->installerManager;
798
    }
799
800
    /**
801
     * Returns the server manager.
802
     *
803
     * @return ServerManager The server manager.
804
     */
805
    public function getServerManager()
806
    {
807
        if (!$this->started) {
808
            throw new LogicException('Puli was not started');
809
        }
810
811
        if (!$this->serverManager && $this->context instanceof ProjectContext) {
812
            $this->serverManager = new ModuleFileServerManager(
813
                $this->getRootModuleFileManager(),
814
                $this->getInstallerManager()
815
            );
816
        }
817
818
        return $this->serverManager;
819
    }
820
821
    /**
822
     * Returns the resource URL generator.
823
     *
824
     * @return UrlGenerator The resource URL generator.
825
     */
826
    public function getUrlGenerator()
827
    {
828
        if (!$this->started) {
829
            throw new LogicException('Puli was not started');
830
        }
831
832
        if (!$this->urlGenerator && $this->context instanceof ProjectContext) {
833
            $urlFormats = array();
834
            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
        return $this->urlGenerator;
842
    }
843
844
    /**
845
     * Returns the file storage.
846
     *
847
     * @return Storage The storage.
848
     */
849
    public function getStorage()
850
    {
851
        if (!$this->storage) {
852
            $this->storage = new FilesystemStorage();
853
        }
854
855
        return $this->storage;
856
    }
857
858
    /**
859
     * Returns the configuration file serializer.
860
     *
861
     * @return ConfigFileConverter The configuration file serializer.
862
     */
863
    public function getConfigFileConverter()
864
    {
865
        if (!$this->configFileConverter) {
866
            $this->configFileConverter = new ConfigFileConverter();
867
        }
868
869
        return $this->configFileConverter;
870
    }
871
872
    /**
873
     * Returns the module file converter.
874
     *
875
     * @return JsonConverter The module file converter.
876
     */
877
    public function getModuleFileConverter()
878
    {
879
        if (!$this->moduleFileConverter) {
880
            $this->moduleFileConverter = $this->createValidatingConverter(
881
                new ModuleFileConverter($this->getJsonVersioner())
882
            );
883
        }
884
885
        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 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...
894
    {
895
        if (!$this->legacyModuleFileConverter) {
896
            $this->legacyModuleFileConverter = $this->createValidatingConverter(
897
                new MigratingConverter(
898
                    $this->getModuleFileConverter(),
899
                    ModuleFileConverter::VERSION,
900
                    $this->getModuleFileMigrationManager()
901
                ),
902
                function (stdClass $jsonData) {
903
                    if (isset($jsonData->{'$schema'})) {
904
                        return $jsonData->{'$schema'};
905
                    }
906
907
                    // BC with 1.0
908
                    return 'http://puli.io/schema/1.0/manager/module';
909
                }
910
            );
911
        }
912
913
        return $this->legacyModuleFileConverter;
914
    }
915
916
    /**
917
     * Returns the module file converter.
918
     *
919
     * @return JsonConverter The module file converter.
920
     */
921
    public function getRootModuleFileConverter()
922
    {
923
        if (!$this->rootModuleFileConverter) {
924
            $this->rootModuleFileConverter = $this->createValidatingConverter(
925
                new RootModuleFileConverter($this->getJsonVersioner())
926
            );
927
        }
928
929
        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 View Code Duplication
    public function getLegacyRootModuleFileConverter()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
938
    {
939
        if (!$this->legacyRootModuleFileConverter) {
940
            $this->legacyRootModuleFileConverter = $this->createValidatingConverter(
941
                new MigratingConverter(
942
                    $this->getRootModuleFileConverter(),
943
                    RootModuleFileConverter::VERSION,
944
                    $this->getModuleFileMigrationManager()
945
                ),
946
                function (stdClass $jsonData) {
947
                    if (isset($jsonData->{'$schema'})) {
948
                        return $jsonData->{'$schema'};
949
                    }
950
951
                    // BC with 1.0
952
                    return 'http://puli.io/schema/1.0/manager/module';
953
                }
954
            );
955
        }
956
957
        return $this->legacyRootModuleFileConverter;
958
    }
959
960
    /**
961
     * Returns the JSON encoder.
962
     *
963
     * @return JsonEncoder The JSON encoder.
964
     */
965
    public function getJsonEncoder()
966
    {
967
        if (!$this->jsonEncoder) {
968
            $this->jsonEncoder = new JsonEncoder();
969
            $this->jsonEncoder->setPrettyPrinting(true);
970
            $this->jsonEncoder->setEscapeSlash(false);
971
            $this->jsonEncoder->setTerminateWithLineFeed(true);
972
        }
973
974
        return $this->jsonEncoder;
975
    }
976
977
    /**
978
     * Returns the JSON decoder.
979
     *
980
     * @return JsonDecoder The JSON decoder.
981
     */
982
    public function getJsonDecoder()
983
    {
984
        if (!$this->jsonDecoder) {
985
            $this->jsonDecoder = new JsonDecoder();
986
        }
987
988
        return $this->jsonDecoder;
989
    }
990
991
    /**
992
     * Returns the JSON validator.
993
     *
994
     * @return JsonValidator The JSON validator.
995
     */
996
    public function getJsonValidator()
997
    {
998
        if (!$this->jsonValidator) {
999
            $uriRetriever = new UriRetriever();
1000
1001
            // Load puli.io schemas from the schema/ directory
1002
            $uriRetriever->setUriRetriever(new LocalUriRetriever());
1003
1004
            $this->jsonValidator = new JsonValidator(null, $uriRetriever);
1005
        }
1006
1007
        return $this->jsonValidator;
1008
    }
1009
1010
    private function activatePlugins()
1011
    {
1012
        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...
1013
            $this->validatePluginClass($pluginClass);
1014
1015
            /** @var PuliPlugin $plugin */
1016
            $plugin = new $pluginClass();
1017
            $plugin->activate($this);
1018
        }
1019
    }
1020
1021
    private function createGlobalContext()
1022
    {
1023
        $baseConfig = new DefaultConfig();
1024
        $homeDir = self::parseHomeDirectory();
1025
1026
        if (null !== $configFile = $this->loadConfigFile($homeDir, $baseConfig)) {
1027
            $baseConfig = $configFile->getConfig();
1028
        }
1029
1030
        $config = new EnvConfig($baseConfig);
1031
1032
        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
    private function createProjectContext($rootDir, $env)
1056
    {
1057
        Assert::fileExists($rootDir, 'Could not load Puli context: The root %s does not exist.');
1058
        Assert::directory($rootDir, 'Could not load Puli context: The root %s is a file. Expected a directory.');
1059
1060
        $baseConfig = new DefaultConfig();
1061
        $homeDir = self::parseHomeDirectory();
1062
1063
        if (null !== $configFile = $this->loadConfigFile($homeDir, $baseConfig)) {
1064
            $baseConfig = $configFile->getConfig();
1065
        }
1066
1067
        // Create a storage without the factory manager
1068
        $jsonStorage = new JsonStorage(
1069
            $this->getStorage(),
1070
            new JsonConverterProvider($this),
1071
            $this->getJsonEncoder(),
1072
            $this->getJsonDecoder()
1073
        );
1074
1075
        $rootDir = Path::canonicalize($rootDir);
1076
        $rootFilePath = $this->rootDir.'/puli.json';
1077
1078
        try {
1079
            $rootModuleFile = $jsonStorage->loadRootModuleFile($rootFilePath, $baseConfig);
1080
        } catch (FileNotFoundException $e) {
1081
            $rootModuleFile = new RootModuleFile(null, $rootFilePath, $baseConfig);
1082
        }
1083
1084
        $config = new EnvConfig($rootModuleFile->getConfig());
1085
1086
        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
    private function createValidatingConverter(JsonConverter $innerConverter, $schema = null)
1098
    {
1099
        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
    private function getJsonStorage()
1108
    {
1109
        if (!$this->jsonStorage) {
1110
            $this->jsonStorage = new JsonStorage(
1111
                $this->getStorage(),
1112
                new JsonConverterProvider($this),
1113
                $this->getJsonEncoder(),
1114
                $this->getJsonDecoder(),
1115
                $this->getFactoryManager()
1116
            );
1117
        }
1118
1119
        return $this->jsonStorage;
1120
    }
1121
1122
    /**
1123
     * Returns the JSON versioner.
1124
     *
1125
     * @return JsonVersioner The JSON versioner.
1126
     */
1127
    private function getJsonVersioner()
1128
    {
1129
        if (!$this->jsonVersioner) {
1130
            $this->jsonVersioner = new ChainVersioner(array(
1131
                // check the schema of the "$schema" field by default
1132
                new SchemaUriVersioner(),
1133
                // fall back to the "version" field for 1.0
1134
                new VersionFieldVersioner(),
1135
            ));
1136
        }
1137
1138
        return $this->jsonVersioner;
1139
    }
1140
1141
    /**
1142
     * Returns the migration manager for module files.
1143
     *
1144
     * @return MigrationManager The migration manager.
1145
     */
1146
    private function getModuleFileMigrationManager()
1147
    {
1148
        if (!$this->moduleFileMigrationManager) {
1149
            $this->moduleFileMigrationManager = new MigrationManager(array(
1150
                new ModuleFile10To20Migration(),
1151
            ), $this->getJsonVersioner());
1152
        }
1153
1154
        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
    private function validatePluginClass($pluginClass)
1163
    {
1164
        if (!class_exists($pluginClass)) {
1165
            throw new InvalidConfigException(sprintf(
1166
                'The plugin class %s does not exist.',
1167
                $pluginClass
1168
            ));
1169
        }
1170
1171
        if (!in_array('Puli\Manager\Api\PuliPlugin', class_implements($pluginClass))) {
1172
            throw new InvalidConfigException(sprintf(
1173
                'The plugin class %s must implement PuliPlugin.',
1174
                $pluginClass
1175
            ));
1176
        }
1177
    }
1178
1179
    private function loadConfigFile($homeDir, Config $baseConfig)
1180
    {
1181
        if (null === $homeDir) {
1182
            return null;
1183
        }
1184
1185
        Assert::fileExists($homeDir, 'Could not load Puli context: The home directory %s does not exist.');
1186
        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
        $jsonStorage = new JsonStorage(
1190
            $this->getStorage(),
1191
            new JsonConverterProvider($this),
1192
            $this->getJsonEncoder(),
1193
            $this->getJsonDecoder()
1194
        );
1195
1196
        $configPath = Path::canonicalize($homeDir).'/config.json';
1197
1198
        try {
1199
            return $jsonStorage->loadConfigFile($configPath, $baseConfig);
1200
        } catch (FileNotFoundException $e) {
1201
            // It's ok if no config.json exists. We'll work with
1202
            // DefaultConfig instead
1203
            return null;
1204
        }
1205
    }
1206
}
1207