Completed
Pull Request — master (#136)
by
unknown
03:02
created

ExtraPackage::mergePackageLinks()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 3

Importance

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