Completed
Pull Request — master (#114)
by Fabian
25:22 queued 23:00
created

ExtraPackage::mergeAutoload()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 2

Importance

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