Completed
Pull Request — master (#160)
by
unknown
25:12
created

ExtraPackage   D

Complexity

Total Complexity 64

Size/Duplication

Total Lines 599
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 14

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 64
lcom 1
cbo 14
dl 0
loc 599
ccs 262
cts 262
cp 1
rs 4.9206
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A getIncludes() 0 5 2
A getRequires() 0 5 2
A readPackageJson() 0 13 3
A loadPackage() 0 14 2
B mergeInto() 0 24 2
A mergeDevInto() 0 6 1
B prependRepositories() 0 30 6
B mergeRequires() 0 29 2
C mergeOrDefer() 0 30 8
A mergeAutoload() 0 16 2
A fixRelativePaths() 0 13 2
A mergeStabilityFlags() 0 13 1
A mergePackageLinks() 0 23 3
A mergeSuggests() 0 11 2
B mergeExtra() 0 32 6
A mergeScripts() 0 20 4
A mergeExtraArray() 0 8 2
B replaceSelfVersionDependencies() 0 41 3
A unwrapIfNeeded() 0 14 3
A mergeReferences() 0 17 3
A extractReferences() 0 16 4

How to fix   Complexity   

Complex Class

Complex classes like ExtraPackage often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ExtraPackage, and based on these observations, apply Extract Interface, too.

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