Completed
Push — master ( 5dfc66...6e95cd )
by Bryan
03:24
created

ExtraPackage::mergeExtraArray()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

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