Completed
Pull Request — master (#103)
by Florian
02:15
created

ExtraPackage::mergeReferences()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 16
ccs 0
cts 0
cp 0
rs 9.4285
cc 3
eloc 11
nc 3
nop 1
crap 12
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
    /**
62
     * @var VersionParser $versionParser
63
     */
64
    protected $versionParser;
65
66 80
    /**
67
     * @param string $path Path to composer.json file
68 80
     * @param Composer $composer
69 80
     * @param Logger $logger
70 80
     */
71 80
    public function __construct($path, Composer $composer, Logger $logger)
72 80
    {
73 80
        $this->path = $path;
74
        $this->composer = $composer;
75
        $this->logger = $logger;
76
        $this->json = $this->readPackageJson($path);
77
        $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...
78
        $this->versionParser = new VersionParser();
79
    }
80 75
81
    /**
82 75
     * Get list of additional packages to include if precessing recursively.
83 75
     *
84
     * @return array
85
     */
86
    public function getIncludes()
87
    {
88
        return isset($this->json['extra']['merge-plugin']['include']) ?
89
            $this->json['extra']['merge-plugin']['include'] : array();
90
    }
91 75
92
    /**
93 75
     * Get list of additional packages to require if precessing recursively.
94 75
     *
95
     * @return array
96
     */
97
    public function getRequires()
98
    {
99
        return isset($this->json['extra']['merge-plugin']['require']) ?
100
            $this->json['extra']['merge-plugin']['require'] : array();
101
    }
102
103
    /**
104
     * Read the contents of a composer.json style file into an array.
105
     *
106
     * The package contents are fixed up to be usable to create a Package
107
     * object by providing dummy "name" and "version" values if they have not
108 80
     * been provided in the file. This is consistent with the default root
109
     * package loading behavior of Composer.
110 80
     *
111 80
     * @param string $path
112 80
     * @return array
113 80
     */
114 80
    protected function readPackageJson($path)
115 80
    {
116 80
        $file = new JsonFile($path);
117 80
        $json = $file->read();
118 80
        if (!isset($json['name'])) {
119 80
            $json['name'] = 'merge-plugin/' .
120
                strtr($path, DIRECTORY_SEPARATOR, '-');
121
        }
122
        if (!isset($json['version'])) {
123
            $json['version'] = '1.0.0';
124
        }
125
        return $json;
126 80
    }
127
128 80
    /**
129 80
     * @param string $json
130
     * @return CompletePackage
131
     */
132
    protected function loadPackage($json)
133
    {
134
        $loader = new ArrayLoader();
135
        $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...
136
        // @codeCoverageIgnoreStart
137
        if (!$package instanceof CompletePackage) {
138 80
            throw new UnexpectedValueException(
139
                'Expected instance of CompletePackage, got ' .
140
                get_class($package)
141
            );
142
        }
143
        // @codeCoverageIgnoreEnd
144
        return $package;
145
    }
146
147 80
    /**
148
     * Merge this package into a RootPackageInterface
149 80
     *
150
     * @param RootPackageInterface $root
151 80
     * @param PluginState $state
152 80
     */
153 75
    public function mergeInto(RootPackageInterface $root, PluginState $state)
154 75
    {
155
        $this->addRepositories($root);
156 80
157 80
        $this->mergeRequires('require', $root, $state);
158 80
        if ($state->isDevMode()) {
159
            $this->mergeRequires('require-dev', $root, $state);
160 80
        }
161
162 80
        $this->mergePackageLinks('conflict', $root);
163 80
        $this->mergePackageLinks('replace', $root);
164 75
        $this->mergePackageLinks('provide', $root);
165 75
166
        $this->mergeSuggests($root);
167 80
168 80
        $this->mergeAutoload('autoload', $root);
169
        if ($state->isDevMode()) {
170
            $this->mergeAutoload('devAutoload', $root);
171
        }
172
173
        $this->mergeExtra($root, $state);
174
        $this->mergeReferences($root);
0 ignored issues
show
Compatibility introduced by
$root of type object<Composer\Package\RootPackageInterface> is not a sub-type of object<Composer\Package\RootPackage>. It seems like you assume a concrete implementation of the interface Composer\Package\RootPackageInterface 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...
175
    }
176 80
177
    /**
178 80
     * Add a collection of repositories described by the given configuration
179 75
     * to the given package and the global repository manager.
180
     *
181 5
     * @param RootPackageInterface $root
182 5
     */
183
    protected function addRepositories(RootPackageInterface $root)
184 5
    {
185 5
        if (!isset($this->json['repositories'])) {
186 5
            return;
187
        }
188 5
        $repoManager = $this->composer->getRepositoryManager();
189 5
        $newRepos = array();
190 5
191
        foreach ($this->json['repositories'] as $repoJson) {
192 5
            if (!isset($repoJson['type'])) {
193 5
                continue;
194 5
            }
195 5
            $this->logger->info("Adding {$repoJson['type']} repository");
196
            $repo = $repoManager->createRepository(
197 5
                $repoJson['type'],
198 5
                $repoJson
199 5
            );
200 5
            $repoManager->addRepository($repo);
201 5
            $newRepos[] = $repo;
202 5
        }
203
204
        $unwrapped = self::unwrapIfNeeded($root, 'setRepositories');
205
        $unwrapped->setRepositories(array_merge(
206
            $newRepos,
207
            $root->getRepositories()
208
        ));
209
    }
210
211 80
    /**
212
     * Merge require or require-dev into a RootPackageInterface
213
     *
214
     * @param string $type 'require' or 'require-dev'
215
     * @param RootPackageInterface $root
216 80
     * @param PluginState $state
217 80
     */
218 80
    protected function mergeRequires(
219
        $type,
220 80
        RootPackageInterface $root,
221 80
        PluginState $state
222 70
    ) {
223
        $linkType = BasePackage::$supportedLinkTypes[$type];
224
        $getter = 'get' . ucfirst($linkType['method']);
225 55
        $setter = 'set' . ucfirst($linkType['method']);
226
227 55
        $requires = $this->package->{$getter}();
228 55
        if (empty($requires)) {
229 55
            return;
230
        }
231 55
232
        $this->mergeStabilityFlags($root, $requires);
233 55
234 55
        $requires = $this->replaceSelfVersionDependencies(
235 55
            $type,
236 55
            $requires,
237
            $root
238 55
        );
239 55
240
        $root->{$setter}($this->mergeOrDefer(
241
            $type,
242
            $root->{$getter}(),
243
            $requires,
244
            $state
245
        ));
246
    }
247
248
    /**
249
     * Merge two collections of package links and collect duplicates for
250
     * subsequent processing.
251 55
     *
252
     * @param string $type 'require' or 'require-dev'
253
     * @param array $origin Primary collection
254
     * @param array $merge Additional collection
255
     * @param PluginState $state
256
     * @return array Merged collection
257 55
     */
258 55
    protected function mergeOrDefer(
259 55
        $type,
260 55
        array $origin,
261 55
        array $merge,
262 55
        $state
263
    ) {
264 10
        $dups = array();
265 10
        foreach ($merge as $name => $link) {
266 10
            if (!isset($origin[$name]) || $state->replaceDuplicateLinks()) {
267 10
                $this->logger->info("Merging <comment>{$name}</comment>");
268
                $origin[$name] = $link;
269 55
            } else {
270 55
                // Defer to solver.
271 55
                $this->logger->info(
272
                    "Deferring duplicate <comment>{$name}</comment>"
273
                );
274
                $dups[] = $link;
275
            }
276
        }
277
        $state->addDuplicateLinks($type, $dups);
278
        return $origin;
279
    }
280 80
281
    /**
282 80
     * Merge autoload or autoload-dev into a RootPackageInterface
283 80
     *
284
     * @param string $type 'autoload' or 'devAutoload'
285 80
     * @param RootPackageInterface $root
286 80
     */
287 80
    protected function mergeAutoload($type, RootPackageInterface $root)
288
    {
289
        $getter = 'get' . ucfirst($type);
290 5
        $setter = 'set' . ucfirst($type);
291 5
292 5
        $autoload = $this->package->{$getter}();
293 5
        if (empty($autoload)) {
294 5
            return;
295 5
        }
296
297
        $unwrapped = self::unwrapIfNeeded($root, $setter);
298
        $unwrapped->{$setter}(array_merge_recursive(
299
            $root->{$getter}(),
300
            $this->fixRelativePaths($autoload)
301
        ));
302
    }
303
304 5
    /**
305
     * Fix a collection of paths that are relative to this package to be
306 5
     * relative to the base package.
307 5
     *
308
     * @param array $paths
309 5
     * @return array
310 5
     */
311
    protected function fixRelativePaths(array $paths)
312 5
    {
313 5
        $base = dirname($this->path);
314 5
        $base = ($base === '.') ? '' : "{$base}/";
315 5
316
        array_walk_recursive(
317
            $paths,
318
            function (&$path) use ($base) {
319
                $path = "{$base}{$path}";
320
            }
321
        );
322
        return $paths;
323
    }
324
325 55
    /**
326
     * Extract and merge stability flags from the given collection of
327
     * requires and merge them into a RootPackageInterface
328
     *
329 55
     * @param RootPackageInterface $root
330 55
     * @param array $requires
331
     */
332 55
    protected function mergeStabilityFlags(
333 55
        RootPackageInterface $root,
334 55
        array $requires
335 55
    ) {
336 55
        $flags = $root->getStabilityFlags();
337 55
        $sf = new StabilityFlags($flags, $root->getMinimumStability());
338
339
        $unwrapped = self::unwrapIfNeeded($root, 'setStabilityFlags');
340
        $unwrapped->setStabilityFlags(array_merge(
341
            $flags,
342
            $sf->extractAll($requires)
343
        ));
344
    }
345 80
346
    /**
347 80
     * Merge package links of the given type  into a RootPackageInterface
348 80
     *
349 80
     * @param string $type 'conflict', 'replace' or 'provide'
350
     * @param RootPackageInterface $root
351 80
     */
352 80
    protected function mergePackageLinks($type, RootPackageInterface $root)
353 10
    {
354 10
        $linkType = BasePackage::$supportedLinkTypes[$type];
355
        $getter = 'get' . ucfirst($linkType['method']);
356
        $setter = 'set' . ucfirst($linkType['method']);
357
358
        $links = $this->package->{$getter}();
359
        if (!empty($links)) {
360 10
            $unwrapped = self::unwrapIfNeeded($root, $setter);
361 10
            if ($root !== $unwrapped) {
362 10
                $this->logger->warning(
363 10
                    'This Composer version does not support ' .
364 10
                    "'{$type}' merging for aliased packages."
365 80
                );
366
            }
367
            $unwrapped->{$setter}(array_merge(
368
                $root->{$getter}(),
369
                $this->replaceSelfVersionDependencies($type, $links, $root)
370
            ));
371
        }
372 80
    }
373
374 80
    /**
375 80
     * Merge suggested packages into a RootPackageInterface
376 5
     *
377 5
     * @param RootPackageInterface $root
378 5
     */
379
    protected function mergeSuggests(RootPackageInterface $root)
380 5
    {
381 5
        $suggests = $this->package->getSuggests();
382 80
        if (!empty($suggests)) {
383
            $unwrapped = self::unwrapIfNeeded($root, 'setSuggests');
384
            $unwrapped->setSuggests(array_merge(
385
                $root->getSuggests(),
386
                $suggests
387
            ));
388
        }
389
    }
390 80
391
    /**
392 80
     * Merge extra config into a RootPackageInterface
393 80
     *
394 80
     * @param RootPackageInterface $root
395 65
     * @param PluginState $state
396
     */
397
    public function mergeExtra(RootPackageInterface $root, PluginState $state)
398 15
    {
399 15
        $extra = $this->package->getExtra();
400
        unset($extra['merge-plugin']);
401 15
        if (!$state->shouldMergeExtra() || empty($extra)) {
402 5
            return;
403 5
        }
404 5
405
        $rootExtra = $root->getExtra();
406 5
        $unwrapped = self::unwrapIfNeeded($root, 'setExtra');
407 10
408 10
        if ($state->replaceDuplicateLinks()) {
409 10
            $unwrapped->setExtra(
410 10
                array_merge($rootExtra, $extra)
411 5
            );
412 5
413 5
        } else {
414 5
            foreach (array_intersect(
415 10
                array_keys($extra),
416 10
                array_keys($rootExtra)
417 10
            ) as $key) {
418 10
                $this->logger->info(
419
                    "Ignoring duplicate <comment>{$key}</comment> in ".
420 15
                    "<comment>{$this->path}</comment> extra config."
421
                );
422
            }
423
            $unwrapped->setExtra(
424
                array_merge($extra, $rootExtra)
425
            );
426
        }
427
    }
428
429
    /**
430
     * Update Links with a 'self.version' constraint with the root package's
431 60
     * version.
432
     *
433
     * @param string $type Link type
434
     * @param array $links
435
     * @param RootPackageInterface $root
436 60
     * @return array
437 60
     */
438 60
    protected function replaceSelfVersionDependencies(
439 60
        $type,
440
        array $links,
441 60
        RootPackageInterface $root
442 60
    ) {
443 60
        $linkType = BasePackage::$supportedLinkTypes[$type];
444 5
        $version = $root->getVersion();
445 5
        $prettyVersion = $root->getPrettyVersion();
446 5
447 5
        return array_map(
448 5
            function ($link) use ($linkType, $version, $prettyVersion) {
449
                if ('self.version' === $link->getPrettyConstraint()) {
450 5
                    return new Link(
451
                        $link->getSource(),
452 60
                        $link->getTarget(),
453 60
                        $this->versionParser->parseConstraints($version),
454
                        $linkType['description'],
455 60
                        $prettyVersion
456
                    );
457
                }
458
                return $link;
459
            },
460
            $links
461
        );
462
    }
463
464
    /**
465
     * Get a full featured Package from a RootPackageInterface.
466
     *
467
     * In Composer versions before 599ad77 the RootPackageInterface only
468
     * defines a sub-set of operations needed by composer-merge-plugin and
469
     * RootAliasPackage only implemented those methods defined by the
470
     * interface. Most of the unimplemented methods in RootAliasPackage can be
471
     * worked around because the getter methods that are implemented proxy to
472
     * the aliased package which we can modify by unwrapping. The exception
473
     * being modifying the 'conflicts', 'provides' and 'replaces' collections.
474
     * We have no way to actually modify those collections unfortunately in
475 80
     * older versions of Composer.
476
     *
477
     * @param RootPackageInterface $root
478
     * @param string $method Method needed
479 80
     * @return RootPackageInterface|RootPackage
480
     */
481 80
    public static function unwrapIfNeeded(
482
        RootPackageInterface $root,
483
        $method = 'setExtra'
484
    ) {
485 80
        if ($root instanceof RootAliasPackage &&
486
            !method_exists($root, $method)
487
        ) {
488
            // Unwrap and return the aliased RootPackage.
489
            $root = $root->getAliasOf();
490
        }
491
        return $root;
492
    }
493
494
    /**
495
     * Update the root packages reference information.
496
     *
497
     * @param \Composer\Package\RootPackage $root
498
     */
499
    protected function mergeReferences(RootPackage $root) {
500
        // Merge source reference information for merged packages.
501
        // @see RootPackageLoader::load
502
        $references = array();
503
        $unwrapped = $this->unwrapIfNeeded($root, 'setReferences');
504
        foreach (array('require', 'require-dev') as $linkType) {
505
            $linkInfo = BasePackage::$supportedLinkTypes[$linkType];
506
            $method = 'get'.ucfirst($linkInfo['method']);
507
            $links = array();
508
            foreach ($unwrapped->$method() as $link) {
509
                $links[$link->getTarget()] = $link->getConstraint()->getPrettyString();
510
            }
511
            $references = $this->extractReferences($links, $references);
512
        }
513
        $unwrapped->setReferences($references);
514
    }
515
516
    /**
517
     * Extract vcs revision from version constraint (dev-master#abc123.
518
     *
519
     * @param array $requires
520
     * @param array $references
521
     * @return array
522
     * @see RootPackageLoader::extractReferences()
523
     */
524
    protected function extractReferences(array $requires, array $references)
525
    {
526
        foreach ($requires as $reqName => $reqVersion) {
527
            $reqVersion = preg_replace('{^([^,\s@]+) as .+$}', '$1', $reqVersion);
528
            $stabilityName = VersionParser::parseStability($reqVersion);
529
            if (
530
                preg_match('{^[^,\s@]+?#([a-f0-9]+)$}', $reqVersion, $match) &&
531
                $stabilityName === 'dev'
532
            ) {
533
                $name = strtolower($reqName);
534
                $references[$name] = $match[1];
535
            }
536
        }
537
538
        return $references;
539
    }
540
}
541
// vim:sw=4:ts=4:sts=4:et:
542