Completed
Pull Request — master (#103)
by Florian
25:01
created

ExtraPackage::addRepositories()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 27
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 4

Importance

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