Completed
Pull Request — master (#114)
by Fabian
18:13 queued 16:19
created

ExtraPackage   C

Complexity

Total Complexity 61

Size/Duplication

Total Lines 605
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Test Coverage

Coverage 100%

Importance

Changes 21
Bugs 2 Features 5
Metric Value
wmc 61
c 21
b 2
f 5
lcom 1
cbo 13
dl 0
loc 605
ccs 251
cts 251
cp 1
rs 5.9771

21 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
A mergeInto() 0 23 3
B addRepositories() 0 27 4
B mergeRequires() 0 29 2
B mergeOrDefer() 0 22 4
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
A arrayMerge() 0 8 2
B mergeExtra() 0 32 6
C arrayMergeDeep() 0 24 7
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 105
    public function __construct($path, Composer $composer, Logger $logger)
72
    {
73 105
        $this->path = $path;
74 105
        $this->composer = $composer;
75 105
        $this->logger = $logger;
76 105
        $this->json = $this->readPackageJson($path);
77 105
        $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 105
        $this->versionParser = new VersionParser();
79 105
    }
80
81
    /**
82
     * Get list of additional packages to include if precessing recursively.
83
     *
84
     * @return array
85
     */
86 100
    public function getIncludes()
87
    {
88 100
        return isset($this->json['extra']['merge-plugin']['include']) ?
89 100
            $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 100
    public function getRequires()
98
    {
99 100
        return isset($this->json['extra']['merge-plugin']['require']) ?
100 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
     * 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 105
    protected function readPackageJson($path)
115
    {
116 105
        $file = new JsonFile($path);
117 105
        $json = $file->read();
118 105
        if (!isset($json['name'])) {
119 100
            $json['name'] = 'merge-plugin/' .
120 100
                strtr($path, DIRECTORY_SEPARATOR, '-');
121 100
        }
122 105
        if (!isset($json['version'])) {
123 105
            $json['version'] = '1.0.0';
124 105
        }
125 105
        return $json;
126
    }
127
128
    /**
129
     * @param string $json
130
     * @return CompletePackage
131
     */
132 105
    protected function loadPackage($json)
133
    {
134 105
        $loader = new ArrayLoader();
135 105
        $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 105
        return $package;
145
    }
146
147
    /**
148
     * Merge this package into a RootPackageInterface
149
     *
150
     * @param RootPackageInterface $root
151
     * @param PluginState $state
152
     */
153 105
    public function mergeInto(RootPackageInterface $root, PluginState $state)
154
    {
155 105
        $this->addRepositories($root);
156
157 105
        $this->mergeRequires('require', $root, $state);
158 105
        if ($state->isDevMode()) {
159 100
            $this->mergeRequires('require-dev', $root, $state);
160 100
        }
161
162 105
        $this->mergePackageLinks('conflict', $root);
163 105
        $this->mergePackageLinks('replace', $root);
164 105
        $this->mergePackageLinks('provide', $root);
165
166 105
        $this->mergeSuggests($root);
167
168 105
        $this->mergeAutoload('autoload', $root);
169 105
        if ($state->isDevMode()) {
170 100
            $this->mergeAutoload('devAutoload', $root);
171 100
        }
172
173 105
        $this->mergeExtra($root, $state);
174 105
        $this->mergeReferences($root);
175 105
    }
176
177
    /**
178
     * Add a collection of repositories described by the given configuration
179
     * to the given package and the global repository manager.
180
     *
181
     * @param RootPackageInterface $root
182
     */
183 105
    protected function addRepositories(RootPackageInterface $root)
184
    {
185 105
        if (!isset($this->json['repositories'])) {
186 95
            return;
187
        }
188 10
        $repoManager = $this->composer->getRepositoryManager();
189 10
        $newRepos = array();
190
191 10
        foreach ($this->json['repositories'] as $repoJson) {
192 10
            if (!isset($repoJson['type'])) {
193 10
                continue;
194
            }
195 10
            $this->logger->info("Adding {$repoJson['type']} repository");
196 10
            $repo = $repoManager->createRepository(
197 10
                $repoJson['type'],
198
                $repoJson
199 10
            );
200 10
            $repoManager->addRepository($repo);
201 10
            $newRepos[] = $repo;
202 10
        }
203
204 10
        $unwrapped = self::unwrapIfNeeded($root, 'setRepositories');
205 10
        $unwrapped->setRepositories(array_merge(
206 10
            $newRepos,
207 10
            $root->getRepositories()
208 10
        ));
209 10
    }
210
211
    /**
212
     * Merge require or require-dev into a RootPackageInterface
213
     *
214
     * @param string $type 'require' or 'require-dev'
215
     * @param RootPackageInterface $root
216
     * @param PluginState $state
217
     */
218 105
    protected function mergeRequires(
219
        $type,
220
        RootPackageInterface $root,
221
        PluginState $state
222
    ) {
223 105
        $linkType = BasePackage::$supportedLinkTypes[$type];
224 105
        $getter = 'get' . ucfirst($linkType['method']);
225 105
        $setter = 'set' . ucfirst($linkType['method']);
226
227 105
        $requires = $this->package->{$getter}();
228 105
        if (empty($requires)) {
229 85
            return;
230
        }
231
232 65
        $this->mergeStabilityFlags($root, $requires);
233
234 65
        $requires = $this->replaceSelfVersionDependencies(
235 65
            $type,
236 65
            $requires,
237
            $root
238 65
        );
239
240 65
        $root->{$setter}($this->mergeOrDefer(
241 65
            $type,
242 65
            $root->{$getter}(),
243 65
            $requires,
244
            $state
245 65
        ));
246 65
    }
247
248
    /**
249
     * Merge two collections of package links and collect duplicates for
250
     * subsequent processing.
251
     *
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
     */
258 65
    protected function mergeOrDefer(
259
        $type,
260
        array $origin,
261
        array $merge,
262
        $state
263
    ) {
264 65
        $dups = array();
265 65
        foreach ($merge as $name => $link) {
266 65
            if (!isset($origin[$name]) || $state->replaceDuplicateLinks()) {
267 65
                $this->logger->info("Merging <comment>{$name}</comment>");
268 65
                $origin[$name] = $link;
269 65
            } else {
270
                // Defer to solver.
271 15
                $this->logger->info(
272 15
                    "Deferring duplicate <comment>{$name}</comment>"
273 15
                );
274 15
                $dups[] = $link;
275
            }
276 65
        }
277 65
        $state->addDuplicateLinks($type, $dups);
278 65
        return $origin;
279
    }
280
281
    /**
282
     * Merge autoload or autoload-dev into a RootPackageInterface
283
     *
284
     * @param string $type 'autoload' or 'devAutoload'
285
     * @param RootPackageInterface $root
286
     */
287 105
    protected function mergeAutoload($type, RootPackageInterface $root)
288
    {
289 105
        $getter = 'get' . ucfirst($type);
290 105
        $setter = 'set' . ucfirst($type);
291
292 105
        $autoload = $this->package->{$getter}();
293 105
        if (empty($autoload)) {
294 100
            return;
295
        }
296
297 10
        $unwrapped = self::unwrapIfNeeded($root, $setter);
298 10
        $unwrapped->{$setter}(array_merge_recursive(
299 10
            $root->{$getter}(),
300 10
            $this->fixRelativePaths($autoload)
301 10
        ));
302 10
    }
303
304
    /**
305
     * Fix a collection of paths that are relative to this package to be
306
     * relative to the base package.
307
     *
308
     * @param array $paths
309
     * @return array
310
     */
311 10
    protected function fixRelativePaths(array $paths)
312
    {
313 10
        $base = dirname($this->path);
314 10
        $base = ($base === '.') ? '' : "{$base}/";
315
316 10
        array_walk_recursive(
317 10
            $paths,
318
            function (&$path) use ($base) {
319 10
                $path = "{$base}{$path}";
320 10
            }
321 10
        );
322 10
        return $paths;
323
    }
324
325
    /**
326
     * Extract and merge stability flags from the given collection of
327
     * requires and merge them into a RootPackageInterface
328
     *
329
     * @param RootPackageInterface $root
330
     * @param array $requires
331
     */
332 65
    protected function mergeStabilityFlags(
333
        RootPackageInterface $root,
334
        array $requires
335
    ) {
336 65
        $flags = $root->getStabilityFlags();
337 65
        $sf = new StabilityFlags($flags, $root->getMinimumStability());
338
339 65
        $unwrapped = self::unwrapIfNeeded($root, 'setStabilityFlags');
340 65
        $unwrapped->setStabilityFlags(array_merge(
341 65
            $flags,
342 65
            $sf->extractAll($requires)
343 65
        ));
344 65
    }
345
346
    /**
347
     * Merge package links of the given type  into a RootPackageInterface
348
     *
349
     * @param string $type 'conflict', 'replace' or 'provide'
350
     * @param RootPackageInterface $root
351
     */
352 105
    protected function mergePackageLinks($type, RootPackageInterface $root)
353
    {
354 105
        $linkType = BasePackage::$supportedLinkTypes[$type];
355 105
        $getter = 'get' . ucfirst($linkType['method']);
356 105
        $setter = 'set' . ucfirst($linkType['method']);
357
358 105
        $links = $this->package->{$getter}();
359 105
        if (!empty($links)) {
360 20
            $unwrapped = self::unwrapIfNeeded($root, $setter);
361
            // @codeCoverageIgnoreStart
362
            if ($root !== $unwrapped) {
363
                $this->logger->warning(
364
                    'This Composer version does not support ' .
365
                    "'{$type}' merging for aliased packages."
366
                );
367
            }
368
            // @codeCoverageIgnoreEnd
369 20
            $unwrapped->{$setter}(array_merge(
370 20
                $root->{$getter}(),
371 20
                $this->replaceSelfVersionDependencies($type, $links, $root)
372 20
            ));
373 20
        }
374 105
    }
375
376
    /**
377
     * Merge suggested packages into a RootPackageInterface
378
     *
379
     * @param RootPackageInterface $root
380
     */
381 105
    protected function mergeSuggests(RootPackageInterface $root)
382
    {
383 105
        $suggests = $this->package->getSuggests();
384 105
        if (!empty($suggests)) {
385 10
            $unwrapped = self::unwrapIfNeeded($root, 'setSuggests');
386 10
            $unwrapped->setSuggests(array_merge(
387 10
                $root->getSuggests(),
388
                $suggests
389 10
            ));
390 10
        }
391 105
    }
392
393
    /**
394
     * Merge extra config into a RootPackageInterface
395
     *
396
     * @param RootPackageInterface $root
397
     * @param PluginState $state
398
     */
399 105
    public function mergeExtra(RootPackageInterface $root, PluginState $state)
400
    {
401 105
        $extra = $this->package->getExtra();
402 105
        unset($extra['merge-plugin']);
403 105
        if (!$state->shouldMergeExtra() || empty($extra)) {
404 80
            return;
405
        }
406
407 25
        $rootExtra = $root->getExtra();
408 25
        $unwrapped = self::unwrapIfNeeded($root, 'setExtra');
409
410 25
        if ($state->replaceDuplicateLinks()) {
411 10
            $unwrapped->setExtra(
412 10
                $this->arrayMerge($state->shouldMergeDeep(), $rootExtra, $extra)
413 10
            );
414 10
        } else {
415 15
            if (!$state->shouldMergeDeep()) {
416 10
                foreach (array_intersect(
417 10
                    array_keys($extra),
418 10
                    array_keys($rootExtra)
419 10
                ) as $key) {
420 5
                    $this->logger->info(
421 5
                        "Ignoring duplicate <comment>{$key}</comment> in ".
422 5
                        "<comment>{$this->path}</comment> extra config."
423 5
                    );
424 10
                }
425 10
            }
426 15
            $unwrapped->setExtra(
427 15
                $this->arrayMerge($state->shouldMergeDeep(), $extra, $rootExtra)
428 15
            );
429
        }
430 25
    }
431
432
    /**
433
     * Merges two arrays either via arrayMergeDeep or via array_merge.
434
     *
435
     * @param bool $mergeDeep
436
     * @param array $array1
437
     * @param array $array2
438
     * @return array
439
     */
440 25
    protected function arrayMerge($mergeDeep, $array1, $array2)
441
    {
442 25
        if ($mergeDeep) {
443 10
            return $this->arrayMergeDeep($array1, $array2);
444
        }
445
446 15
        return array_merge($array1, $array2);
447
    }
448
449
    /**
450
     * Merges multiple arrays, recursively, and returns the merged array.
451
     *
452
     * This function is similar to PHP's array_merge_recursive() function, but it
453
     * handles non-array values differently. When merging values that are not both
454
     * arrays, the latter value replaces the former rather than merging with it.
455
     *
456
     * Example:
457
     * @code
458
     * $link_options_1 = array('fragment' => 'x', 'attributes' => array('title' => t('X'), 'class' => array('a', 'b')));
459
     * $link_options_2 = array('fragment' => 'y', 'attributes' => array('title' => t('Y'), 'class' => array('c', 'd')));
460
     *
461
     * // This results in array(
462
     * //     'fragment' => array('x', 'y'),
463
     * //     'attributes' => array('title' => array(t('X'), t('Y')), 'class' => array('a', 'b', 'c', 'd'))
464
     * // ).
465
     * $incorrect = array_merge_recursive($link_options_1, $link_options_2);
466
     *
467
     * // This results in array(
468
     * //     'fragment' => 'y',
469
     * //     'attributes' => array('title' => t('Y'), 'class' => array('a', 'b', 'c', 'd'))
470
     * // ).
471
     * $correct = $this->arrayMergeDeep($link_options_1, $link_options_2);
472
     * @endcode
473
     *
474
     * Note: This function was derived from Drupal's drupal_array_merge_deep().
475
     *
476
     * @param array ...
477
     *   Arrays to merge.
478
     *
479
     * @return array
480
     *   The merged array.
481
     */
482 10
    protected function arrayMergeDeep()
483
    {
484 10
        $arrays = func_get_args();
485 10
        $result = array();
486
487 10
        foreach ($arrays as $array) {
488 10
            foreach ($array as $key => $value) {
489
                // Renumber integer keys as array_merge_recursive() does. Note that PHP
490
                // automatically converts array keys that are integer strings (e.g., '1')
491
                // to integers.
492 10
                if (is_integer($key)) {
493 10
                    $result[] = $value;
494 10
                } elseif (isset($result[$key]) && is_array($result[$key]) && is_array($value)) {
495
                    // Recurse when both values are arrays.
496 10
                    $result[$key] = $this->arrayMergeDeep($result[$key], $value);
497 10
                } else {
498
                    // Otherwise, use the latter value, overriding any previous value.
499 10
                    $result[$key] = $value;
500
                }
501 10
            }
502 10
        }
503
504 10
        return $result;
505
    }
506
507
    /**
508
     * Update Links with a 'self.version' constraint with the root package's
509
     * version.
510
     *
511
     * @param string $type Link type
512
     * @param array $links
513
     * @param RootPackageInterface $root
514
     * @return array
515
     */
516 75
    protected function replaceSelfVersionDependencies(
517
        $type,
518
        array $links,
519
        RootPackageInterface $root
520
    ) {
521 75
        $linkType = BasePackage::$supportedLinkTypes[$type];
522 75
        $version = $root->getVersion();
523 75
        $prettyVersion = $root->getPrettyVersion();
524 75
        $vp = $this->versionParser;
525
526 75
        $method = 'get' . ucfirst($linkType['method']);
527 75
        $packages = $root->$method();
528
529 75
        return array_map(
530 75
            function ($link) use ($linkType, $version, $prettyVersion, $vp, $packages) {
531 75
                if ('self.version' === $link->getPrettyConstraint()) {
532 10
                    if (isset($packages[$link->getSource()])) {
533
                        /** @var Link $package */
534 5
                        $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 5
                            $package->getPrettyConstraint()
541 5
                        );
542
                    }
543
544 5
                    return new Link(
545 5
                        $link->getSource(),
546 5
                        $link->getTarget(),
547 5
                        $vp->parseConstraints($version),
548 5
                        $linkType['description'],
549
                        $prettyVersion
550 5
                    );
551
                }
552 75
                return $link;
553 75
            },
554
            $links
555 75
        );
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
     * 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 105
    public static function unwrapIfNeeded(
576
        RootPackageInterface $root,
577
        $method = 'setExtra'
578
    ) {
579
        // @codeCoverageIgnoreStart
580
        if ($root instanceof RootAliasPackage &&
581
            !method_exists($root, $method)
582
        ) {
583
            // Unwrap and return the aliased RootPackage.
584
            $root = $root->getAliasOf();
585
        }
586
        // @codeCoverageIgnoreEnd
587 105
        return $root;
588
    }
589
590
    /**
591
     * Update the root packages reference information.
592
     *
593
     * @param RootPackageInterface $root
594
     */
595 105
    protected function mergeReferences(RootPackageInterface $root)
596
    {
597
        // Merge source reference information for merged packages.
598
        // @see RootPackageLoader::load
599 105
        $references = array();
600 105
        $unwrapped = $this->unwrapIfNeeded($root, 'setReferences');
601 105
        foreach (array('require', 'require-dev') as $linkType) {
602 105
            $linkInfo = BasePackage::$supportedLinkTypes[$linkType];
603 105
            $method = 'get'.ucfirst($linkInfo['method']);
604 105
            $links = array();
605 105
            foreach ($unwrapped->$method() as $link) {
606 35
                $links[$link->getTarget()] = $link->getConstraint()->getPrettyString();
607 105
            }
608 105
            $references = $this->extractReferences($links, $references);
609 105
        }
610 105
        $unwrapped->setReferences($references);
611 105
    }
612
613
    /**
614
     * Extract vcs revision from version constraint (dev-master#abc123.
615
     *
616
     * @param array $requires
617
     * @param array $references
618
     * @return array
619
     * @see RootPackageLoader::extractReferences()
620
     */
621 105
    protected function extractReferences(array $requires, array $references)
622
    {
623 105
        foreach ($requires as $reqName => $reqVersion) {
624 35
            $reqVersion = preg_replace('{^([^,\s@]+) as .+$}', '$1', $reqVersion);
625 35
            $stabilityName = VersionParser::parseStability($reqVersion);
626
            if (
627 35
                preg_match('{^[^,\s@]+?#([a-f0-9]+)$}', $reqVersion, $match) &&
628
                $stabilityName === 'dev'
629 35
            ) {
630 5
                $name = strtolower($reqName);
631 5
                $references[$name] = $match[1];
632 5
            }
633 105
        }
634
635 105
        return $references;
636
    }
637
}
638
// vim:sw=4:ts=4:sts=4:et:
639