Completed
Push — master ( 4c022b...0bdf85 )
by Bryan
8s
created

ExtraPackage::unwrapIfNeeded()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 3

Importance

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