Completed
Pull Request — master (#163)
by
unknown
20:55
created

ExtraPackage::getName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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