Failed Conditions
Pull Request — master (#33)
by Bernhard
04:32
created

Puli::createProjectContext()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 2

Importance

Changes 3
Bugs 1 Features 1
Metric Value
c 3
b 1
f 1
dl 0
loc 21
ccs 14
cts 14
cp 1
rs 9.3143
cc 2
eloc 13
nc 2
nop 2
crap 2
1
<?php
2
3
/*
4
 * This file is part of the puli/manager package.
5
 *
6
 * (c) Bernhard Schussek <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Puli\Manager\Api;
13
14
use LogicException;
15
use Psr\Log\LoggerInterface;
16
use Puli\Discovery\Api\Discovery;
17
use Puli\Discovery\Api\EditableDiscovery;
18
use Puli\Manager\Api\Asset\AssetManager;
19
use Puli\Manager\Api\Config\Config;
20
use Puli\Manager\Api\Config\ConfigFile;
21
use Puli\Manager\Api\Config\ConfigFileManager;
22
use Puli\Manager\Api\Config\ConfigFileSerializer;
23
use Puli\Manager\Api\Context\Context;
24
use Puli\Manager\Api\Context\ProjectContext;
25
use Puli\Manager\Api\Discovery\DiscoveryManager;
26
use Puli\Manager\Api\Factory\FactoryManager;
27
use Puli\Manager\Api\Installation\InstallationManager;
28
use Puli\Manager\Api\Installer\InstallerManager;
29
use Puli\Manager\Api\Package\PackageFileSerializer;
30
use Puli\Manager\Api\Package\PackageManager;
31
use Puli\Manager\Api\Package\RootPackageFileManager;
32
use Puli\Manager\Api\Repository\RepositoryManager;
33
use Puli\Manager\Api\Server\ServerManager;
34
use Puli\Manager\Api\Storage\Storage;
35
use Puli\Manager\Assert\Assert;
36
use Puli\Manager\Asset\DiscoveryAssetManager;
37
use Puli\Manager\Config\ConfigFileManagerImpl;
38
use Puli\Manager\Config\ConfigFileStorage;
39
use Puli\Manager\Config\ConfigJsonSerializer;
40
use Puli\Manager\Config\DefaultConfig;
41
use Puli\Manager\Config\EnvConfig;
42
use Puli\Manager\Discovery\DiscoveryManagerImpl;
43
use Puli\Manager\Factory\FactoryManagerImpl;
44
use Puli\Manager\Factory\Generator\DefaultGeneratorRegistry;
45
use Puli\Manager\Filesystem\FilesystemStorage;
46
use Puli\Manager\Installation\InstallationManagerImpl;
47
use Puli\Manager\Installer\PackageFileInstallerManager;
48
use Puli\Manager\Migration\MigrationManager;
49
use Puli\Manager\Package\PackageFileStorage;
50
use Puli\Manager\Package\PackageJsonSerializer;
51
use Puli\Manager\Package\PackageManagerImpl;
52
use Puli\Manager\Package\RootPackageFileManagerImpl;
53
use Puli\Manager\Php\ClassWriter;
54
use Puli\Manager\Repository\RepositoryManagerImpl;
55
use Puli\Manager\Server\PackageFileServerManager;
56
use Puli\Manager\Util\System;
57
use Puli\Repository\Api\EditableRepository;
58
use Puli\Repository\Api\ResourceRepository;
59
use Puli\UrlGenerator\Api\UrlGenerator;
60
use Puli\UrlGenerator\DiscoveryUrlGenerator;
61
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
62
use Webmozart\Expression\Expr;
63
use Webmozart\PathUtil\Path;
64
65
/**
66
 * The Puli service locator.
67
 *
68
 * Use this class to access the managers provided by this package:
69
 *
70
 * ```php
71
 * $puli = new Puli(getcwd());
72
 * $puli->start();
73
 *
74
 * $packageManager = $puli->getPackageManager();
75
 * ```
76
 *
77
 * The `Puli` class either operates in the global or a project context:
78
 *
79
 *  * The "global context" is not tied to a specific root package. A global
80
 *    context only loads the settings of the "config.json" file in the home
81
 *    directory. The `Puli` class operates in the global context if no
82
 *    project root directory is passed to the constructor. In the global
83
 *    context, only the global config file manager is available.
84
 *  * The "project context" is tied to a specific Puli project. You need to
85
 *    pass the path to the project's root directory to the constructor or to
86
 *    {@link setRootDirectory()}. The configuration of the "puli.json" file in
87
 *    the root directory is used to configure the managers.
88
 *
89
 * The `Puli` class creates four kinds of managers:
90
 *
91
 *  * The "config file manager" allows you to modify entries of the
92
 *    "config.json" file in the home directory.
93
 *  * The "package file manager" manages modifications to the "puli.json" file
94
 *    of a Puli project.
95
 *  * The "package manager" manages the package repository of a Puli project.
96
 *  * The "repository manager" manages the resource repository of a Puli
97
 *    project.
98
 *  * The "discovery manager" manages the resource discovery of a Puli project.
99
 *
100
 * The home directory is read from the context variable "PULI_HOME".
101
 * If this variable is not set, the home directory defaults to:
102
 *
103
 *  * `$HOME/.puli` on Linux, where `$HOME` is the context variable
104
 *    "HOME".
105
 *  * `$APPDATA/Puli` on Windows, where `$APPDATA` is the context
106
 *    variable "APPDATA".
107
 *
108
 * If none of these variables can be found, an exception is thrown.
109
 *
110
 * A .htaccess file is put into the home directory to protect it from web
111
 * access.
112
 *
113
 * @since  1.0
114
 *
115
 * @author Bernhard Schussek <[email protected]>
116
 */
117
class Puli
118
{
119
    /**
120
     * @var string|null
121
     */
122
    private $rootDir;
123
124
    /**
125
     * @var string
126
     */
127
    private $env;
128
129
    /**
130
     * @var EventDispatcherInterface|null
131
     */
132
    private $dispatcher;
133
134
    /**
135
     * @var Context|ProjectContext
136
     */
137
    private $context;
138
139
    /**
140
     * @var ResourceRepository
141
     */
142
    private $repo;
143
144
    /**
145
     * @var Discovery
146
     */
147
    private $discovery;
148
149
    /**
150
     * @var object
151
     */
152
    private $factory;
153
154
    /**
155
     * @var FactoryManager
156
     */
157
    private $factoryManager;
158
159
    /**
160
     * @var ConfigFileManager
161
     */
162
    private $configFileManager;
163
164
    /**
165
     * @var RootPackageFileManager
166
     */
167
    private $rootPackageFileManager;
168
169
    /**
170
     * @var PackageManager
171
     */
172
    private $packageManager;
173
174
    /**
175
     * @var RepositoryManager
176
     */
177
    private $repositoryManager;
178
179
    /**
180
     * @var DiscoveryManager
181
     */
182
    private $discoveryManager;
183
184
    /**
185
     * @var AssetManager
186
     */
187
    private $assetManager;
188
189
    /**
190
     * @var InstallationManager
191
     */
192
    private $installationManager;
193
194
    /**
195
     * @var InstallerManager
196
     */
197
    private $installerManager;
198
199
    /**
200
     * @var ServerManager
201
     */
202
    private $serverManager;
203
204
    /**
205
     * @var UrlGenerator
206
     */
207
    private $urlGenerator;
208
209
    /**
210
     * @var Storage|null
211
     */
212
    private $storage;
213
214
    /**
215
     * @var ConfigFileStorage|null
216
     */
217
    private $configFileStorage;
218
219
    /**
220
     * @var ConfigFileSerializer|null
221
     */
222
    private $configFileSerializer;
223
224
    /**
225
     * @var PackageFileStorage|null
226
     */
227
    private $packageFileStorage;
228
229
    /**
230
     * @var PackageFileSerializer|null
231
     */
232
    private $packageFileSerializer;
233
234
    /**
235
     * @var LoggerInterface
236
     */
237
    private $logger;
238
239
    /**
240
     * @var bool
241
     */
242
    private $started = false;
243
244
    /**
245
     * @var bool
246
     */
247
    private $pluginsEnabled = true;
248
249
    /**
250
     * Parses the system context for a home directory.
251
     *
252
     * @return null|string Returns the path to the home directory or `null`
253
     *                     if none was found.
254
     */
255 49
    private static function parseHomeDirectory()
256
    {
257
        try {
258 49
            $homeDir = System::parseHomeDirectory();
259
260 46
            System::denyWebAccess($homeDir);
261
262 46
            return $homeDir;
263 3
        } catch (InvalidConfigException $e) {
264
            // Context variable was not found -> no home directory
265
            // This happens often on web servers where the home directory is
266
            // not set manually
267 2
            return null;
268
        }
269
    }
270
271
    /**
272
     * Creates a new instance for the given Puli project.
273
     *
274
     * @param string|null $rootDir The root directory of the Puli project.
275
     *                             If none is passed, the object operates in
276
     *                             the global context. You can set or switch
277
     *                             the root directories later on by calling
278
     *                             {@link setRootDirectory()}.
279
     * @param string      $env     One of the {@link Environment} constants.
280
     *
281
     * @see Puli, start()
282
     */
283 51
    public function __construct($rootDir = null, $env = Environment::DEV)
284
    {
285 51
        $this->setRootDirectory($rootDir);
286 51
        $this->setEnvironment($env);
287 51
    }
288
289
    /**
290
     * Starts the service container.
291
     */
292 49
    public function start()
293
    {
294 49
        if ($this->started) {
295
            throw new LogicException('Puli is already started');
296
        }
297
298 49
        if ($this->rootDir) {
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...
299 26
            $this->context = $this->createProjectContext($this->rootDir, $this->env);
300 26
            $bootstrapFile = $this->context->getConfig()->get(Config::BOOTSTRAP_FILE);
301
302
            // Run the project's bootstrap file to enable project-specific
303
            // autoloading
304 26
            if (null !== $bootstrapFile) {
305
                // Backup autoload functions of the PHAR
306 1
                $autoloadFunctions = spl_autoload_functions();
307
308 1
                foreach ($autoloadFunctions as $autoloadFunction) {
309 1
                    spl_autoload_unregister($autoloadFunction);
310 1
                }
311
312
                // Add project-specific autoload functions
313 1
                require_once Path::makeAbsolute($bootstrapFile, $this->rootDir);
314
315
                // Prepend autoload functions of the PHAR again
316
                // This is needed if the user specific autoload functions were
317
                // added with $prepend=true (as done by Composer)
318
                // Classes in the PHAR should always take precedence
319 1
                for ($i = count($autoloadFunctions) - 1; $i >= 0; --$i) {
320 1
                    spl_autoload_register($autoloadFunctions[$i], true, true);
321 1
                }
322 1
            }
323 26
        } else {
324 23
            $this->context = $this->createGlobalContext();
325
        }
326
327 48
        $this->dispatcher = $this->context->getEventDispatcher();
328 48
        $this->started = true;
329
330
        // Start plugins once the container is running
331 48
        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...
332 25
            $this->activatePlugins();
333 23
        }
334 46
    }
335
336
    /**
337
     * Returns whether the service container is started.
338
     *
339
     * @return bool Returns `true` if the container is started and `false`
340
     *              otherwise.
341
     */
342
    public function isStarted()
343
    {
344
        return $this->started;
345
    }
346
347
    /**
348
     * Sets the root directory of the managed Puli project.
349
     *
350
     * @param string|null $rootDir The root directory of the managed Puli
351
     *                             project or `null` to start Puli outside of a
352
     *                             specific project.
353
     */
354 51
    public function setRootDirectory($rootDir)
355
    {
356 51
        if ($this->started) {
357
            throw new LogicException('Puli is already started');
358
        }
359
360 51
        Assert::nullOrDirectory($rootDir);
361
362 51
        $this->rootDir = $rootDir ? Path::canonicalize($rootDir) : null;
363 51
    }
364
365
    /**
366
     * Sets the environment of the managed Puli project.
367
     *
368
     * @param string $env One of the {@link Environment} constants.
369
     */
370 51
    public function setEnvironment($env)
371
    {
372 51
        if ($this->started) {
373
            throw new LogicException('Puli is already started');
374
        }
375
376 51
        Assert::oneOf($env, Environment::all(), 'The environment must be one of: %2$s. Got: %s');
377
378 51
        $this->env = $env;
379 51
    }
380
381
    /**
382
     * Retturns the environment of the managed Puli project.
383
     *
384
     * @return string One of the {@link Environment} constants.
385
     */
386 2
    public function getEnvironment()
387
    {
388 2
        return $this->env;
389
    }
390
391
    /**
392
     * Returns the root directory of the managed Puli project.
393
     *
394
     * If no Puli project is managed at the moment, `null` is returned.
395
     *
396
     * @return string|null The root directory of the managed Puli project or
397
     *                     `null` if none is set.
398
     */
399 4
    public function getRootDirectory()
400
    {
401 4
        return $this->rootDir;
402
    }
403
404
    /**
405
     * Sets the logger to use.
406
     *
407
     * @param LoggerInterface $logger The logger to use.
408
     */
409
    public function setLogger(LoggerInterface $logger)
410
    {
411
        if ($this->started) {
412
            throw new LogicException('Puli is already started');
413
        }
414
415
        $this->logger = $logger;
416
    }
417
418
    /**
419
     * Returns the used logger.
420
     *
421
     * @return LoggerInterface The used logger.
422
     */
423
    public function getLogger()
424
    {
425
        return $this->logger;
426
    }
427
428
    /**
429
     * Sets the event dispatcher to use.
430
     *
431
     * @param EventDispatcherInterface $dispatcher The event dispatcher to use.
432
     */
433 2
    public function setEventDispatcher(EventDispatcherInterface $dispatcher)
434
    {
435 2
        if ($this->started) {
436
            throw new LogicException('Puli is already started');
437
        }
438
439 2
        $this->dispatcher = $dispatcher;
440 2
    }
441
442
    /**
443
     * Returns the used event dispatcher.
444
     *
445
     * @return EventDispatcherInterface|null The used logger.
446
     */
447 3
    public function getEventDispatcher()
448
    {
449 3
        return $this->dispatcher;
450
    }
451
452
    /**
453
     * Enables all Puli plugins.
454
     */
455
    public function enablePlugins()
456
    {
457
        $this->pluginsEnabled = true;
458
    }
459
460
    /**
461
     * Disables all Puli plugins.
462
     */
463 1
    public function disablePlugins()
464
    {
465 1
        $this->pluginsEnabled = false;
466 1
    }
467
468
    /**
469
     * Returns whether Puli plugins are enabled.
470
     *
471
     * @return bool Returns `true` if Puli plugins will be loaded and `false`
472
     *              otherwise.
473
     */
474
    public function arePluginsEnabled()
475
    {
476
        return $this->pluginsEnabled;
477
    }
478
479
    /**
480
     * Returns the context.
481
     *
482
     * @return Context|ProjectContext The context.
483
     */
484 29
    public function getContext()
485
    {
486 29
        if (!$this->started) {
487
            throw new LogicException('Puli was not started');
488
        }
489
490 29
        return $this->context;
491
    }
492
493
    /**
494
     * Returns the resource repository of the project.
495
     *
496
     * @return EditableRepository The resource repository.
497
     */
498 8 View Code Duplication
    public function getRepository()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
499
    {
500 8
        if (!$this->started) {
501
            throw new LogicException('Puli was not started');
502
        }
503
504 8
        if (!$this->rootDir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->rootDir of type string|null is loosely compared to false; 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...
505 1
            return null;
506
        }
507
508 7
        if (!$this->repo) {
509 7
            $this->repo = $this->getFactory()->createRepository();
510 7
        }
511
512 7
        return $this->repo;
513
    }
514
515
    /**
516
     * Returns the resource discovery of the project.
517
     *
518
     * @return EditableDiscovery The resource discovery.
519
     */
520 5 View Code Duplication
    public function getDiscovery()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
521
    {
522 5
        if (!$this->started) {
523
            throw new LogicException('Puli was not started');
524
        }
525
526 5
        if (!$this->rootDir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->rootDir of type string|null is loosely compared to false; 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...
527 1
            return null;
528
        }
529
530 4
        if (!$this->discovery) {
531 4
            $this->discovery = $this->getFactory()->createDiscovery($this->getRepository());
532 4
        }
533
534 4
        return $this->discovery;
535
    }
536
537
    /**
538
     * @return object
539
     */
540 9
    public function getFactory()
541
    {
542 9
        if (!$this->started) {
543
            throw new LogicException('Puli was not started');
544
        }
545
546 9
        if (!$this->factory && $this->rootDir) {
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...
547 8
            $this->factory = $this->getFactoryManager()->createFactory();
548 8
        }
549
550 9
        return $this->factory;
551
    }
552
553
    /**
554
     * @return FactoryManager
555
     */
556 16
    public function getFactoryManager()
557
    {
558 16
        if (!$this->started) {
559
            throw new LogicException('Puli was not started');
560
        }
561
562 16
        if (!$this->factoryManager && $this->rootDir) {
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...
563 14
            $this->factoryManager = new FactoryManagerImpl(
564 14
                $this->context,
0 ignored issues
show
Compatibility introduced by
$this->context 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...
565 14
                new DefaultGeneratorRegistry(),
566 14
                new ClassWriter()
567 14
            );
568
569
            // Don't set via the constructor to prevent a cyclic dependency
570 14
            $this->factoryManager->setServers($this->getServerManager()->getServers());
571 14
        }
572
573 16
        return $this->factoryManager;
574
    }
575
576
    /**
577
     * Returns the configuration file manager.
578
     *
579
     * @return ConfigFileManager The configuration file manager.
580
     */
581 2
    public function getConfigFileManager()
582
    {
583 2
        if (!$this->started) {
584
            throw new LogicException('Puli was not started');
585
        }
586
587 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...
588 2
            $this->configFileManager = new ConfigFileManagerImpl(
589 2
                $this->context,
590 2
                $this->getConfigFileStorage(),
591 2
                $this->getFactoryManager()
0 ignored issues
show
Unused Code introduced by
The call to ConfigFileManagerImpl::__construct() has too many arguments starting with $this->getFactoryManager().

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
592 2
            );
593 2
        }
594
595 2
        return $this->configFileManager;
596
    }
597
598
    /**
599
     * Returns the root package file manager.
600
     *
601
     * @return RootPackageFileManager The package file manager.
602
     */
603 15 View Code Duplication
    public function getRootPackageFileManager()
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...
604
    {
605 15
        if (!$this->started) {
606
            throw new LogicException('Puli was not started');
607
        }
608
609 15
        if (!$this->rootPackageFileManager && $this->rootDir) {
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...
610 14
            $this->rootPackageFileManager = new RootPackageFileManagerImpl(
611 14
                $this->context,
0 ignored issues
show
Compatibility introduced by
$this->context 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...
612 14
                $this->getPackageFileStorage()
613 14
            );
614 14
        }
615
616 15
        return $this->rootPackageFileManager;
617
    }
618
619
    /**
620
     * Returns the package manager.
621
     *
622
     * @return PackageManager The package manager.
623
     */
624 15 View Code Duplication
    public function getPackageManager()
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...
625
    {
626 15
        if (!$this->started) {
627
            throw new LogicException('Puli was not started');
628
        }
629
630 15
        if (!$this->packageManager && $this->rootDir) {
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...
631 14
            $this->packageManager = new PackageManagerImpl(
632 14
                $this->context,
0 ignored issues
show
Compatibility introduced by
$this->context 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...
633 14
                $this->getPackageFileStorage()
634 14
            );
635 14
        }
636
637 15
        return $this->packageManager;
638
    }
639
640
    /**
641
     * Returns the resource repository manager.
642
     *
643
     * @return RepositoryManager The repository manager.
644
     */
645 2 View Code Duplication
    public function getRepositoryManager()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
646
    {
647 2
        if (!$this->started) {
648
            throw new LogicException('Puli was not started');
649
        }
650
651 2
        if (!$this->repositoryManager && $this->rootDir) {
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...
652 1
            $this->repositoryManager = new RepositoryManagerImpl(
653 1
                $this->context,
0 ignored issues
show
Compatibility introduced by
$this->context 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...
654 1
                $this->getRepository(),
655 1
                $this->getPackageManager()->findPackages(Expr::method('isEnabled', Expr::same(true))),
656 1
                $this->getPackageFileStorage()
657 1
            );
658 1
        }
659
660 2
        return $this->repositoryManager;
661
    }
662
663
    /**
664
     * Returns the resource discovery manager.
665
     *
666
     * @return DiscoveryManager The discovery manager.
667
     */
668 3 View Code Duplication
    public function getDiscoveryManager()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
669
    {
670 3
        if (!$this->started) {
671
            throw new LogicException('Puli was not started');
672
        }
673
674 3
        if (!$this->discoveryManager && $this->rootDir) {
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...
675 2
            $this->discoveryManager = new DiscoveryManagerImpl(
676 2
                $this->context,
0 ignored issues
show
Compatibility introduced by
$this->context 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...
677 2
                $this->getDiscovery(),
678 2
                $this->getPackageManager()->findPackages(Expr::method('isEnabled', Expr::same(true))),
679 2
                $this->getPackageFileStorage(),
680 2
                $this->logger
681 2
            );
682 2
        }
683
684 3
        return $this->discoveryManager;
685
    }
686
687
    /**
688
     * Returns the asset manager.
689
     *
690
     * @return AssetManager The asset manager.
691
     */
692 2
    public function getAssetManager()
693
    {
694 2
        if (!$this->started) {
695
            throw new LogicException('Puli was not started');
696
        }
697
698 2
        if (!$this->assetManager && $this->rootDir) {
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...
699 1
            $this->assetManager = new DiscoveryAssetManager(
700 1
                $this->getDiscoveryManager(),
701 1
                $this->getServerManager()->getServers()
702 1
            );
703 1
        }
704
705 2
        return $this->assetManager;
706
    }
707
708
    /**
709
     * Returns the installation manager.
710
     *
711
     * @return InstallationManager The installation manager.
712
     */
713 2
    public function getInstallationManager()
714
    {
715 2
        if (!$this->started) {
716
            throw new LogicException('Puli was not started');
717
        }
718
719 2
        if (!$this->installationManager && $this->rootDir) {
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...
720 1
            $this->installationManager = new InstallationManagerImpl(
721 1
                $this->getContext(),
0 ignored issues
show
Compatibility introduced by
$this->getContext() of type object<Puli\Manager\Api\Context\Context> is not a sub-type of object<Puli\Manager\Api\Context\ProjectContext>. It seems like you assume a child class of the class Puli\Manager\Api\Context\Context to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
722 1
                $this->getRepository(),
723 1
                $this->getServerManager()->getServers(),
724 1
                $this->getInstallerManager()
725 1
            );
726 1
        }
727
728 2
        return $this->installationManager;
729
    }
730
731
    /**
732
     * Returns the installer manager.
733
     *
734
     * @return InstallerManager The installer manager.
735
     */
736 15
    public function getInstallerManager()
737
    {
738 15
        if (!$this->started) {
739
            throw new LogicException('Puli was not started');
740
        }
741
742 15
        if (!$this->installerManager && $this->rootDir) {
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...
743 14
            $this->installerManager = new PackageFileInstallerManager(
744 14
                $this->getRootPackageFileManager(),
745 14
                $this->getPackageManager()->getPackages()
746 14
            );
747 14
        }
748
749 15
        return $this->installerManager;
750
    }
751
752
    /**
753
     * Returns the server manager.
754
     *
755
     * @return ServerManager The server manager.
756
     */
757 15
    public function getServerManager()
758
    {
759 15
        if (!$this->started) {
760
            throw new LogicException('Puli was not started');
761
        }
762
763 15
        if (!$this->serverManager && $this->rootDir) {
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...
764 14
            $this->serverManager = new PackageFileServerManager(
765 14
                $this->getRootPackageFileManager(),
766 14
                $this->getInstallerManager()
767 14
            );
768 14
        }
769
770 15
        return $this->serverManager;
771
    }
772
773
    /**
774
     * Returns the resource URL generator.
775
     *
776
     * @return UrlGenerator The resource URL generator.
777
     */
778 2
    public function getUrlGenerator()
779
    {
780 2
        if (!$this->started) {
781
            throw new LogicException('Puli was not started');
782
        }
783
784 2
        if (!$this->urlGenerator && $this->rootDir) {
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...
785 1
            $urlFormats = array();
786 1
            foreach ($this->getServerManager()->getServers() as $server) {
787
                $urlFormats[$server->getName()] = $server->getUrlFormat();
788 1
            }
789
790 1
            $this->urlGenerator = new DiscoveryUrlGenerator($this->getDiscovery(), $urlFormats);
791 1
        }
792
793 2
        return $this->urlGenerator;
794
    }
795
796
    /**
797
     * Returns the cached file storage.
798
     *
799
     * @return Storage The storage.
800
     */
801 47
    public function getStorage()
802
    {
803 47
        if (!$this->storage) {
804 47
            $this->storage = new FilesystemStorage();
805 47
        }
806
807 47
        return $this->storage;
808
    }
809
810
    /**
811
     * Returns the cached configuration file serializer.
812
     *
813
     * @return ConfigFileSerializer The configuration file serializer.
814
     */
815 46
    public function getConfigFileSerializer()
816
    {
817 46
        if (!$this->configFileSerializer) {
818 46
            $this->configFileSerializer = new ConfigJsonSerializer();
819 46
        }
820
821 46
        return $this->configFileSerializer;
822
    }
823
824
    /**
825
     * Returns the cached package file serializer.
826
     *
827
     * @return PackageFileSerializer The package file serializer.
828
     */
829 26
    public function getPackageFileSerializer()
830
    {
831 26
        if (!$this->packageFileSerializer) {
832 26
            $this->packageFileSerializer = new PackageJsonSerializer(
833 26
                new MigrationManager(array(
834
                    // Add future migrations here
835 26
                )),
836
                __DIR__.'/../../res/schema'
837 26
            );
838 26
        }
839
840 26
        return $this->packageFileSerializer;
841
    }
842
843 25
    private function activatePlugins()
844
    {
845 25
        foreach ($this->context->getRootPackageFile()->getPluginClasses() as $pluginClass) {
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Puli\Manager\Api\Context\Context as the method getRootPackageFile() does only exist in the following sub-classes of Puli\Manager\Api\Context\Context: Puli\Manager\Api\Context\ProjectContext. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
846 25
            $this->validatePluginClass($pluginClass);
847
848
            /** @var PuliPlugin $plugin */
849 23
            $plugin = new $pluginClass();
850 23
            $plugin->activate($this);
851 23
        }
852 23
    }
853
854 23
    private function createGlobalContext()
855
    {
856 23
        $baseConfig = new DefaultConfig();
857 23
        $homeDir = self::parseHomeDirectory();
858
859 22
        if (null !== $configFile = $this->loadConfigFile($homeDir, $baseConfig)) {
860 20
            $baseConfig = $configFile->getConfig();
861 20
        }
862
863 22
        $config = new EnvConfig($baseConfig);
864
865 22
        return new Context($homeDir, $config, $configFile, $this->dispatcher);
866
    }
867
868
    /**
869
     * Creates the context of a Puli project.
870
     *
871
     * The home directory is read from the context variable "PULI_HOME".
872
     * If this variable is not set, the home directory defaults to:
873
     *
874
     *  * `$HOME/.puli` on Linux, where `$HOME` is the context variable
875
     *    "HOME".
876
     *  * `$APPDATA/Puli` on Windows, where `$APPDATA` is the context
877
     *    variable "APPDATA".
878
     *
879
     * If none of these variables can be found, an exception is thrown.
880
     *
881
     * A .htaccess file is put into the home directory to protect it from web
882
     * access.
883
     *
884
     * @param string $rootDir The path to the project.
885
     *
886
     * @return ProjectContext The project context.
887
     */
888 26
    private function createProjectContext($rootDir, $env)
889
    {
890 26
        Assert::fileExists($rootDir, 'Could not load Puli context: The root %s does not exist.');
891 26
        Assert::directory($rootDir, 'Could not load Puli context: The root %s is a file. Expected a directory.');
892
893 26
        $baseConfig = new DefaultConfig();
894 26
        $homeDir = self::parseHomeDirectory();
895
896 26
        if (null !== $configFile = $this->loadConfigFile($homeDir, $baseConfig)) {
897 25
            $baseConfig = $configFile->getConfig();
898 25
        }
899
900
        // Create a storage without the factory manager
901 26
        $packageFileStorage = new PackageFileStorage($this->getStorage(), $this->getPackageFileSerializer());
902 26
        $rootDir = Path::canonicalize($rootDir);
903 26
        $rootFilePath = $this->rootDir.'/puli.json';
904 26
        $rootPackageFile = $packageFileStorage->loadRootPackageFile($rootFilePath, $baseConfig);
905 26
        $config = new EnvConfig($rootPackageFile->getConfig());
906
907 26
        return new ProjectContext($homeDir, $rootDir, $config, $rootPackageFile, $configFile, $this->dispatcher, $env);
908
    }
909
910
    /**
911
     * Returns the cached configuration file storage.
912
     *
913
     * @return ConfigFileStorage The configuration file storage.
914
     */
915 2
    private function getConfigFileStorage()
916
    {
917 2
        if (!$this->configFileStorage) {
918 2
            $this->configFileStorage = new ConfigFileStorage(
919 2
                $this->getStorage(),
920 2
                $this->getConfigFileSerializer(),
921 2
                $this->getFactoryManager()
922 2
            );
923 2
        }
924
925 2
        return $this->configFileStorage;
926
    }
927
928
    /**
929
     * Returns the cached package file storage.
930
     *
931
     * @return PackageFileStorage The package file storage.
932
     */
933 14
    private function getPackageFileStorage()
934
    {
935 14
        if (!$this->packageFileStorage) {
936 14
            $this->packageFileStorage = new PackageFileStorage(
937 14
                $this->getStorage(),
938 14
                $this->getPackageFileSerializer(),
939 14
                $this->getFactoryManager()
940 14
            );
941 14
        }
942
943 14
        return $this->packageFileStorage;
944
    }
945
946
    /**
947
     * Validates the given plugin class name.
948
     *
949
     * @param string $pluginClass The fully qualified name of a plugin class.
950
     */
951 25
    private function validatePluginClass($pluginClass)
952
    {
953 25
        if (!class_exists($pluginClass)) {
954 1
            throw new InvalidConfigException(sprintf(
955 1
                'The plugin class %s does not exist.',
956
                $pluginClass
957 1
            ));
958
        }
959
960 24
        if (!in_array('Puli\Manager\Api\PuliPlugin', class_implements($pluginClass))) {
961 1
            throw new InvalidConfigException(sprintf(
962 1
                'The plugin class %s must implement PuliPlugin.',
963
                $pluginClass
964 1
            ));
965
        }
966 23
    }
967
968 48
    private function loadConfigFile($homeDir, Config $baseConfig)
969
    {
970 48
        if (null === $homeDir) {
971 2
            return null;
972
        }
973
974 46
        Assert::fileExists($homeDir, 'Could not load Puli context: The home directory %s does not exist.');
975 46
        Assert::directory($homeDir, 'Could not load Puli context: The home directory %s is a file. Expected a directory.');
976
977
        // Create a storage without the factory manager
978 46
        $configStorage = new ConfigFileStorage($this->getStorage(), $this->getConfigFileSerializer());
979 46
        $configPath = Path::canonicalize($homeDir).'/config.json';
980
981
        try {
982 46
            return $configStorage->loadConfigFile($configPath, $baseConfig);
983 1
        } catch (FileNotFoundException $e) {
984
            // It's ok if no config.json exists. We'll work with
985
            // DefaultConfig instead
986 1
            return null;
987
        }
988
    }
989
}
990