Completed
Pull Request — master (#117)
by Fabian
22:08
created

ExtraPackage::getIncludes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

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