Completed
Pull Request — master (#103)
by Florian
23:54
created

ExtraPackage::extractReferences()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 12
ccs 0
cts 0
cp 0
rs 9.2
cc 4
eloc 7
nc 3
nop 2
crap 20
1
<?php
2
/**
3
 * This file is part of the Composer Merge plugin.
4
 *
5
 * Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
6
 *
7
 * This software may be modified and distributed under the terms of the MIT
8
 * license. See the LICENSE file for details.
9
 */
10
11
namespace Wikimedia\Composer\Merge;
12
13
use Wikimedia\Composer\Logger;
14
15
use Composer\Composer;
16
use Composer\Json\JsonFile;
17
use Composer\Package\BasePackage;
18
use Composer\Package\CompletePackage;
19
use Composer\Package\Link;
20
use Composer\Package\Loader\ArrayLoader;
21
use Composer\Package\RootAliasPackage;
22
use Composer\Package\RootPackage;
23
use Composer\Package\RootPackageInterface;
24
use Composer\Package\Version\VersionParser;
25
use UnexpectedValueException;
26
27
/**
28
 * Processing for a composer.json file that will be merged into
29
 * a RootPackageInterface
30
 *
31
 * @author Bryan Davis <[email protected]>
32
 */
33
class ExtraPackage
34
{
35
36
    /**
37
     * @var Composer $composer
38
     */
39
    protected $composer;
40
41
    /**
42
     * @var Logger $logger
43
     */
44
    protected $logger;
45
46
    /**
47
     * @var string $path
48
     */
49
    protected $path;
50
51
    /**
52
     * @var array $json
53
     */
54
    protected $json;
55
56
    /**
57
     * @var CompletePackage $package
58
     */
59
    protected $package;
60
61
    protected $versionParser;
62
63
    /**
64
     * @param string $path Path to composer.json file
65
     * @param Composer $composer
66 80
     * @param Logger $logger
67
     */
68 80
    public function __construct($path, Composer $composer, Logger $logger)
69 80
    {
70 80
        $this->path = $path;
71 80
        $this->composer = $composer;
72 80
        $this->logger = $logger;
73 80
        $this->json = $this->readPackageJson($path);
74
        $this->package = $this->loadPackage($this->json);
0 ignored issues
show
Documentation introduced by
$this->json is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
75
        $this->versionParser = new VersionParser();
76
    }
77
78
    /**
79
     * Get list of additional packages to include if precessing recursively.
80 75
     *
81
     * @return array
82 75
     */
83 75
    public function getIncludes()
84
    {
85
        return isset($this->json['extra']['merge-plugin']['include']) ?
86
            $this->json['extra']['merge-plugin']['include'] : array();
87
    }
88
89
    /**
90
     * Get list of additional packages to require if precessing recursively.
91 75
     *
92
     * @return array
93 75
     */
94 75
    public function getRequires()
95
    {
96
        return isset($this->json['extra']['merge-plugin']['require']) ?
97
            $this->json['extra']['merge-plugin']['require'] : array();
98
    }
99
100
    /**
101
     * Read the contents of a composer.json style file into an array.
102
     *
103
     * The package contents are fixed up to be usable to create a Package
104
     * object by providing dummy "name" and "version" values if they have not
105
     * been provided in the file. This is consistent with the default root
106
     * package loading behavior of Composer.
107
     *
108 80
     * @param string $path
109
     * @return array
110 80
     */
111 80
    protected function readPackageJson($path)
112 80
    {
113 80
        $file = new JsonFile($path);
114 80
        $json = $file->read();
115 80
        if (!isset($json['name'])) {
116 80
            $json['name'] = 'merge-plugin/' .
117 80
                strtr($path, DIRECTORY_SEPARATOR, '-');
118 80
        }
119 80
        if (!isset($json['version'])) {
120
            $json['version'] = '1.0.0';
121
        }
122
        return $json;
123
    }
124
125
    /**
126 80
     * @param string $json
127
     * @return CompletePackage
128 80
     */
129 80
    protected function loadPackage($json)
130
    {
131
        $loader = new ArrayLoader();
132
        $package = $loader->load($json);
0 ignored issues
show
Documentation introduced by
$json is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
133
        // @codeCoverageIgnoreStart
134
        if (!$package instanceof CompletePackage) {
135
            throw new UnexpectedValueException(
136
                'Expected instance of CompletePackage, got ' .
137
                get_class($package)
138 80
            );
139
        }
140
        // @codeCoverageIgnoreEnd
141
        return $package;
142
    }
143
144
    /**
145
     * Merge this package into a RootPackageInterface
146
     *
147 80
     * @param RootPackageInterface $root
148
     * @param PluginState $state
149 80
     */
150
    public function mergeInto(RootPackageInterface $root, PluginState $state)
151 80
    {
152 80
        $this->addRepositories($root);
153 75
154 75
        $this->mergeRequires('require', $root, $state);
155
        if ($state->isDevMode()) {
156 80
            $this->mergeRequires('require-dev', $root, $state);
157 80
        }
158 80
159
        $this->mergePackageLinks('conflict', $root);
160 80
        $this->mergePackageLinks('replace', $root);
161
        $this->mergePackageLinks('provide', $root);
162 80
163 80
        $this->mergeSuggests($root);
164 75
165 75
        $this->mergeAutoload('autoload', $root);
166
        if ($state->isDevMode()) {
167 80
            $this->mergeAutoload('devAutoload', $root);
168 80
        }
169
170
        $this->mergeExtra($root, $state);
171
172
        // Merge source reference information for merged packages.
173
        // @see RootPackageLoader::load
174
        $references = array();
175
        foreach (array('require', 'require-dev') as $linkType) {
176 80
            $linkInfo = BasePackage::$supportedLinkTypes[$linkType];
177
            $method = 'get'.ucfirst($linkInfo['method']);
178 80
            $links = array();
179 75
            foreach ($root->$method() as $link) {
180
                $links[$link->getTarget()] = $link->getConstraint()->getPrettyString();
181 5
            }
182 5
            $references = $this->extractReferences($links, $references);
183
            $root->setReferences($references);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Composer\Package\RootPackageInterface as the method setReferences() does only exist in the following implementations of said interface: Composer\Package\RootPackage.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
184 5
        }
185 5
    }
186 5
187
    /**
188 5
     * Add a collection of repositories described by the given configuration
189 5
     * to the given package and the global repository manager.
190 5
     *
191
     * @param RootPackageInterface $root
192 5
     */
193 5
    protected function addRepositories(RootPackageInterface $root)
194 5
    {
195 5
        if (!isset($this->json['repositories'])) {
196
            return;
197 5
        }
198 5
        $repoManager = $this->composer->getRepositoryManager();
199 5
        $newRepos = array();
200 5
201 5
        foreach ($this->json['repositories'] as $repoJson) {
202 5
            if (!isset($repoJson['type'])) {
203
                continue;
204
            }
205
            $this->logger->info("Adding {$repoJson['type']} repository");
206
            $repo = $repoManager->createRepository(
207
                $repoJson['type'],
208
                $repoJson
209
            );
210
            $repoManager->addRepository($repo);
211 80
            $newRepos[] = $repo;
212
        }
213
214
        $unwrapped = self::unwrapIfNeeded($root, 'setRepositories');
215
        $unwrapped->setRepositories(array_merge(
216 80
            $newRepos,
217 80
            $root->getRepositories()
218 80
        ));
219
    }
220 80
221 80
    /**
222 70
     * Merge require or require-dev into a RootPackageInterface
223
     *
224
     * @param string $type 'require' or 'require-dev'
225 55
     * @param RootPackageInterface $root
226
     * @param PluginState $state
227 55
     */
228 55
    protected function mergeRequires(
229 55
        $type,
230
        RootPackageInterface $root,
231 55
        PluginState $state
232
    ) {
233 55
        $linkType = BasePackage::$supportedLinkTypes[$type];
234 55
        $getter = 'get' . ucfirst($linkType['method']);
235 55
        $setter = 'set' . ucfirst($linkType['method']);
236 55
237
        $requires = $this->package->{$getter}();
238 55
        if (empty($requires)) {
239 55
            return;
240
        }
241
242
        $this->mergeStabilityFlags($root, $requires);
243
244
        $requires = $this->replaceSelfVersionDependencies(
245
            $type,
246
            $requires,
247
            $root
248
        );
249
250
        $root->{$setter}($this->mergeOrDefer(
251 55
            $type,
252
            $root->{$getter}(),
253
            $requires,
254
            $state
255
        ));
256
    }
257 55
258 55
    /**
259 55
     * Merge two collections of package links and collect duplicates for
260 55
     * subsequent processing.
261 55
     *
262 55
     * @param string $type 'require' or 'require-dev'
263
     * @param array $origin Primary collection
264 10
     * @param array $merge Additional collection
265 10
     * @param PluginState $state
266 10
     * @return array Merged collection
267 10
     */
268
    protected function mergeOrDefer(
269 55
        $type,
270 55
        array $origin,
271 55
        array $merge,
272
        $state
273
    ) {
274
        $dups = array();
275
        foreach ($merge as $name => $link) {
276
            if (!isset($origin[$name]) || $state->replaceDuplicateLinks()) {
277
                $this->logger->info("Merging <comment>{$name}</comment>");
278
                $origin[$name] = $link;
279
            } else {
280 80
                // Defer to solver.
281
                $this->logger->info(
282 80
                    "Deferring duplicate <comment>{$name}</comment>"
283 80
                );
284
                $dups[] = $link;
285 80
            }
286 80
        }
287 80
        $state->addDuplicateLinks($type, $dups);
288
        return $origin;
289
    }
290 5
291 5
    /**
292 5
     * Merge autoload or autoload-dev into a RootPackageInterface
293 5
     *
294 5
     * @param string $type 'autoload' or 'devAutoload'
295 5
     * @param RootPackageInterface $root
296
     */
297
    protected function mergeAutoload($type, RootPackageInterface $root)
298
    {
299
        $getter = 'get' . ucfirst($type);
300
        $setter = 'set' . ucfirst($type);
301
302
        $autoload = $this->package->{$getter}();
303
        if (empty($autoload)) {
304 5
            return;
305
        }
306 5
307 5
        $unwrapped = self::unwrapIfNeeded($root, $setter);
308
        $unwrapped->{$setter}(array_merge_recursive(
309 5
            $root->{$getter}(),
310 5
            $this->fixRelativePaths($autoload)
311
        ));
312 5
    }
313 5
314 5
    /**
315 5
     * Fix a collection of paths that are relative to this package to be
316
     * relative to the base package.
317
     *
318
     * @param array $paths
319
     * @return array
320
     */
321
    protected function fixRelativePaths(array $paths)
322
    {
323
        $base = dirname($this->path);
324
        $base = ($base === '.') ? '' : "{$base}/";
325 55
326
        array_walk_recursive(
327
            $paths,
328
            function (&$path) use ($base) {
329 55
                $path = "{$base}{$path}";
330 55
            }
331
        );
332 55
        return $paths;
333 55
    }
334 55
335 55
    /**
336 55
     * Extract and merge stability flags from the given collection of
337 55
     * requires and merge them into a RootPackageInterface
338
     *
339
     * @param RootPackageInterface $root
340
     * @param array $requires
341
     */
342
    protected function mergeStabilityFlags(
343
        RootPackageInterface $root,
344
        array $requires
345 80
    ) {
346
        $flags = $root->getStabilityFlags();
347 80
        $sf = new StabilityFlags($flags, $root->getMinimumStability());
348 80
349 80
        $unwrapped = self::unwrapIfNeeded($root, 'setStabilityFlags');
350
        $unwrapped->setStabilityFlags(array_merge(
351 80
            $flags,
352 80
            $sf->extractAll($requires)
353 10
        ));
354 10
    }
355
356
    /**
357
     * Merge package links of the given type  into a RootPackageInterface
358
     *
359
     * @param string $type 'conflict', 'replace' or 'provide'
360 10
     * @param RootPackageInterface $root
361 10
     */
362 10
    protected function mergePackageLinks($type, RootPackageInterface $root)
363 10
    {
364 10
        $linkType = BasePackage::$supportedLinkTypes[$type];
365 80
        $getter = 'get' . ucfirst($linkType['method']);
366
        $setter = 'set' . ucfirst($linkType['method']);
367
368
        $links = $this->package->{$getter}();
369
        if (!empty($links)) {
370
            $unwrapped = self::unwrapIfNeeded($root, $setter);
371
            if ($root !== $unwrapped) {
372 80
                $this->logger->warning(
373
                    'This Composer version does not support ' .
374 80
                    "'{$type}' merging for aliased packages."
375 80
                );
376 5
            }
377 5
            $unwrapped->{$setter}(array_merge(
378 5
                $root->{$getter}(),
379
                $this->replaceSelfVersionDependencies($type, $links, $root)
380 5
            ));
381 5
        }
382 80
    }
383
384
    /**
385
     * Merge suggested packages into a RootPackageInterface
386
     *
387
     * @param RootPackageInterface $root
388
     */
389
    protected function mergeSuggests(RootPackageInterface $root)
390 80
    {
391
        $suggests = $this->package->getSuggests();
392 80
        if (!empty($suggests)) {
393 80
            $unwrapped = self::unwrapIfNeeded($root, 'setSuggests');
394 80
            $unwrapped->setSuggests(array_merge(
395 65
                $root->getSuggests(),
396
                $suggests
397
            ));
398 15
        }
399 15
    }
400
401 15
    /**
402 5
     * Merge extra config into a RootPackageInterface
403 5
     *
404 5
     * @param RootPackageInterface $root
405
     * @param PluginState $state
406 5
     */
407 10
    public function mergeExtra(RootPackageInterface $root, PluginState $state)
408 10
    {
409 10
        $extra = $this->package->getExtra();
410 10
        unset($extra['merge-plugin']);
411 5
        if (!$state->shouldMergeExtra() || empty($extra)) {
412 5
            return;
413 5
        }
414 5
415 10
        $rootExtra = $root->getExtra();
416 10
        $unwrapped = self::unwrapIfNeeded($root, 'setExtra');
417 10
418 10
        if ($state->replaceDuplicateLinks()) {
419
            $unwrapped->setExtra(
420 15
                array_merge($rootExtra, $extra)
421
            );
422
423
        } else {
424
            foreach (array_intersect(
425
                array_keys($extra),
426
                array_keys($rootExtra)
427
            ) as $key) {
428
                $this->logger->info(
429
                    "Ignoring duplicate <comment>{$key}</comment> in ".
430
                    "<comment>{$this->path}</comment> extra config."
431 60
                );
432
            }
433
            $unwrapped->setExtra(
434
                array_merge($extra, $rootExtra)
435
            );
436 60
        }
437 60
    }
438 60
439 60
    /**
440
     * Update Links with a 'self.version' constraint with the root package's
441 60
     * version.
442 60
     *
443 60
     * @param string $type Link type
444 5
     * @param array $links
445 5
     * @param RootPackageInterface $root
446 5
     * @return array
447 5
     */
448 5
    protected function replaceSelfVersionDependencies(
449
        $type,
450 5
        array $links,
451
        RootPackageInterface $root
452 60
    ) {
453 60
        $linkType = BasePackage::$supportedLinkTypes[$type];
454
        $version = $root->getVersion();
455 60
        $prettyVersion = $root->getPrettyVersion();
456
        $vp = new VersionParser();
457
458
        return array_map(
459
            function ($link) use ($linkType, $version, $prettyVersion, $vp) {
460
                if ('self.version' === $link->getPrettyConstraint()) {
461
                    return new Link(
462
                        $link->getSource(),
463
                        $link->getTarget(),
464
                        $vp->parseConstraints($version),
465
                        $linkType['description'],
466
                        $prettyVersion
467
                    );
468
                }
469
                return $link;
470
            },
471
            $links
472
        );
473
    }
474
475 80
    /**
476
     * Get a full featured Package from a RootPackageInterface.
477
     *
478
     * In Composer versions before 599ad77 the RootPackageInterface only
479 80
     * defines a sub-set of operations needed by composer-merge-plugin and
480
     * RootAliasPackage only implemented those methods defined by the
481 80
     * interface. Most of the unimplemented methods in RootAliasPackage can be
482
     * worked around because the getter methods that are implemented proxy to
483
     * the aliased package which we can modify by unwrapping. The exception
484
     * being modifying the 'conflicts', 'provides' and 'replaces' collections.
485 80
     * We have no way to actually modify those collections unfortunately in
486
     * older versions of Composer.
487
     *
488
     * @param RootPackageInterface $root
489
     * @param string $method Method needed
490
     * @return RootPackageInterface|RootPackage
491
     */
492
    public static function unwrapIfNeeded(
493
        RootPackageInterface $root,
494
        $method = 'setExtra'
495
    ) {
496
        if ($root instanceof RootAliasPackage &&
497
            !method_exists($root, $method)
498
        ) {
499
            // Unwrap and return the aliased RootPackage.
500
            $root = $root->getAliasOf();
501
        }
502
        return $root;
503
    }
504
505
    /**
506
     * Extract vcs revision from version constraint (dev-master#abc123.
507
     *
508
     * @param array $requires
509
     * @param array $references
510
     * @return array
511
     * @see RootPackageLoader::extractReferences()
512
     */
513
    protected function extractReferences(array $requires, array $references)
514
    {
515
        foreach ($requires as $reqName => $reqVersion) {
516
            $reqVersion = preg_replace('{^([^,\s@]+) as .+$}', '$1', $reqVersion);
517
            if (preg_match('{^[^,\s@]+?#([a-f0-9]+)$}', $reqVersion, $match) && 'dev' === ($stabilityName = VersionParser::parseStability($reqVersion))) {
518
                $name = strtolower($reqName);
519
                $references[$name] = $match[1];
520
            }
521
        }
522
523
        return $references;
524
    }
525
}
526
// vim:sw=4:ts=4:sts=4:et:
527