Completed
Push — master ( f6e712...54142e )
by
unknown
6s
created

ExtraPackage::prependRepositories()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 27
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 4

Importance

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