Completed
Pull Request — master (#148)
by Patrick D
06:52
created

ExtraPackage::mergeExtraArray()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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