Completed
Push — master ( f64aa6...32105a )
by Andrii
01:51
created

Plugin   C

Complexity

Total Complexity 66

Size/Duplication

Total Lines 438
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 7.38%

Importance

Changes 0
Metric Value
wmc 66
lcom 1
cbo 10
dl 0
loc 438
ccs 15
cts 203
cp 0.0738
rs 5.7474
c 0
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A activate() 0 5 1
A getSubscribedEvents() 0 8 1
A onPostAutoloadDump() 0 15 1
A initAutoload() 0 9 2
A scanPackages() 0 8 3
C processPackage() 0 27 7
A loadDotEnv() 0 7 3
B addFiles() 0 20 6
A collectAliases() 0 15 2
B prepareAliases() 0 24 6
B preparePath() 0 20 6
A setPackages() 0 4 1
A getPackages() 0 8 2
A findPackages() 0 17 3
B iteratePackage() 0 26 4
B iterateDependencies() 0 16 8
A showDepsTree() 0 15 4
A getBaseDir() 0 8 2
A getVendorDir() 0 9 2
A getFilesystem() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like Plugin 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 Plugin, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Composer plugin for config assembling
4
 *
5
 * @link      https://github.com/hiqdev/composer-config-plugin
6
 * @package   composer-config-plugin
7
 * @license   BSD-3-Clause
8
 * @copyright Copyright (c) 2016-2017, HiQDev (http://hiqdev.com/)
9
 */
10
11
namespace hiqdev\composer\config;
12
13
use Composer\Composer;
14
use Composer\EventDispatcher\EventSubscriberInterface;
15
use Composer\IO\IOInterface;
16
use Composer\Package\CompletePackageInterface;
17
use Composer\Package\PackageInterface;
18
use Composer\Package\RootPackageInterface;
19
use Composer\Plugin\PluginInterface;
20
use Composer\Script\Event;
21
use Composer\Script\ScriptEvents;
22
use Composer\Util\Filesystem;
23
24
/**
25
 * Plugin class.
26
 *
27
 * @author Andrii Vasyliev <[email protected]>
28
 */
29
class Plugin implements PluginInterface, EventSubscriberInterface
30
{
31
    const YII2_PACKAGE_TYPE = 'yii2-extension';
32
    const EXTRA_OPTION_NAME = 'config-plugin';
33
34
    /**
35
     * @var PackageInterface[] the array of active composer packages
36
     */
37
    protected $packages;
38
39
    /**
40
     * @var string absolute path to the package base directory
41
     */
42
    protected $baseDir;
43
44
    /**
45
     * @var string absolute path to vendor directory
46
     */
47
    protected $vendorDir;
48
49
    /**
50
     * @var Filesystem utility
51
     */
52
    protected $filesystem;
53
54
    /**
55
     * @var array config name => list of files
56
     */
57
    protected $files = [
58
        'dotenv'  => [],
59
        'defines' => [],
60
        'params'  => [],
61
    ];
62
63
    /**
64
     * @var array package name => configs as listed in `composer.json`
65
     */
66
    protected $originalFiles = [];
67
68
    protected $aliases = [];
69
70
    protected $extensions = [];
71
72
    /**
73
     * @var array array of not yet merged params
74
     */
75
    protected $rawParams = [];
76
77
    /**
78
     * @var Composer instance
79
     */
80
    protected $composer;
81
82
    /**
83
     * @var IOInterface
84
     */
85
    public $io;
86
87
    /**
88
     * Initializes the plugin object with the passed $composer and $io.
89
     * @param Composer $composer
90
     * @param IOInterface $io
91
     */
92 2
    public function activate(Composer $composer, IOInterface $io)
93
    {
94 2
        $this->composer = $composer;
95 2
        $this->io = $io;
96 2
    }
97
98
    /**
99
     * Returns list of events the plugin is subscribed to.
100
     * @return array list of events
101
     */
102 1
    public static function getSubscribedEvents()
103
    {
104
        return [
105 1
            ScriptEvents::POST_AUTOLOAD_DUMP => [
106 1
                ['onPostAutoloadDump', 0],
107 1
            ],
108 1
        ];
109
    }
110
111
    /**
112
     * This is the main function.
113
     * @param Event $event
114
     */
115
    public function onPostAutoloadDump(Event $event)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
116
    {
117
        $this->io->writeError('<info>Assembling config files</info>');
118
        $this->initAutoload();
119
        $this->scanPackages();
120
        $this->showDepsTree();
121
122
        $builder = new Builder($this->files);
123
        $builder->setAddition(['aliases' => $this->aliases]);
124
        $builder->setIo($this->io);
125
        $builder->saveFiles();
126
        $builder->writeConfig('aliases', $this->aliases);
127
        $builder->writeConfig('extensions', $this->extensions);
128
        $builder->buildConfigs();
129
    }
130
131
    protected function initAutoload()
132
    {
133
        $dir = dirname(dirname(dirname(__DIR__)));
134
        $yii = "$dir/yiisoft/yii2/Yii.php";
135
        require_once "$dir/autoload.php";
136
        if (file_exists($yii)) {
137
            require_once $yii;
138
        }
139
    }
140
141
    protected function scanPackages()
142
    {
143
        foreach ($this->getPackages() as $package) {
144
            if ($package instanceof CompletePackageInterface) {
145
                $this->processPackage($package);
146
            }
147
        }
148
    }
149
150
    /**
151
     * Scans the given package and collects extensions data.
152
     * @param PackageInterface $package
153
     */
154
    protected function processPackage(CompletePackageInterface $package)
155
    {
156
        $extra = $package->getExtra();
157
        $files = isset($extra[self::EXTRA_OPTION_NAME]) ? $extra[self::EXTRA_OPTION_NAME] : null;
158
        $this->originalFiles[$package->getPrettyName()] = $files;
159
160
        if (self::YII2_PACKAGE_TYPE !== $package->getType() && is_null($files)) {
161
            return;
162
        }
163
164
        if (is_array($files)) {
165
            $this->addFiles($package, $files);
166
        }
167
        if ($package instanceof RootPackageInterface) {
168
            $this->loadDotEnv($package);
169
        }
170
171
        $aliases = $this->collectAliases($package);
172
        $this->aliases = array_merge($this->aliases, $aliases);
173
174
        $this->extensions[$package->getPrettyName()] = array_filter([
175
            'name' => $package->getPrettyName(),
176
            'version' => $package->getVersion(),
177
            'reference' => $package->getSourceReference() ?: $package->getDistReference(),
178
            'aliases' => $aliases,
179
        ]);
180
    }
181
182
    protected function loadDotEnv(RootPackageInterface $package)
183
    {
184
        $path = $this->preparePath($package, '.env');
185
        if (file_exists($path) && class_exists('Dotenv\Dotenv')) {
186
            array_push($this->files['dotenv'], $path);
187
        }
188
    }
189
190
    /**
191
     * Adds given files to the list of files to be processed.
192
     * Prepares `defines` in reversed order (outer package first) because
193
     * constants cannot be redefined.
194
     * @param CompletePackageInterface $package
195
     * @param array $files
196
     */
197
    protected function addFiles(CompletePackageInterface $package, array $files)
198
    {
199
        foreach ($files as $name => $paths) {
200
            $paths = (array) $paths;
201
            if ('defines' === $name) {
202
                $paths = array_reverse($paths);
203
            }
204
            foreach ($paths as $path) {
205
                if (!isset($this->files[$name])) {
206
                    $this->files[$name] = [];
207
                }
208
                $path = $this->preparePath($package, $path);
209
                if ('defines' === $name) {
210
                    array_unshift($this->files[$name], $path);
211
                } else {
212
                    array_push($this->files[$name], $path);
213
                }
214
            }
215
        }
216
    }
217
218
    /**
219
     * Collects package aliases.
220
     * @param CompletePackageInterface $package
221
     * @return array collected aliases
222
     */
223
    protected function collectAliases(CompletePackageInterface $package)
224
    {
225
        $aliases = array_merge(
226
            $this->prepareAliases($package, 'psr-0'),
227
            $this->prepareAliases($package, 'psr-4')
228
        );
229
        if ($package instanceof RootPackageInterface) {
230
            $aliases = array_merge($aliases,
231
                $this->prepareAliases($package, 'psr-0', true),
232
                $this->prepareAliases($package, 'psr-4', true)
233
            );
234
        }
235
236
        return $aliases;
237
    }
238
239
    /**
240
     * Prepare aliases.
241
     * @param PackageInterface $package
242
     * @param string 'psr-0' or 'psr-4'
243
     * @return array
244
     */
245
    protected function prepareAliases(PackageInterface $package, $psr, $dev = false)
246
    {
247
        $autoload = $dev ? $package->getDevAutoload() : $package->getAutoload();
248
        if (empty($autoload[$psr])) {
249
            return [];
250
        }
251
252
        $aliases = [];
253
        foreach ($autoload[$psr] as $name => $path) {
254
            if (is_array($path)) {
255
                // ignore psr-4 autoload specifications with multiple search paths
256
                // we can not convert them into aliases as they are ambiguous
257
                continue;
258
            }
259
            $name = str_replace('\\', '/', trim($name, '\\'));
260
            $path = $this->preparePath($package, $path);
261
            if ('psr-0' === $psr) {
262
                $path .= '/' . $name;
263
            }
264
            $aliases["@$name"] = $path;
265
        }
266
267
        return $aliases;
268
    }
269
270
    /**
271
     * Builds path inside of a package.
272
     * @param PackageInterface $package
273
     * @param mixed $path can be absolute or relative
274
     * @return string absolute paths will stay untouched
275
     */
276
    public function preparePath(PackageInterface $package, $path)
277
    {
278
        if (0 === strncmp($path, '$', 1)) {
279
            return $path;
280
        }
281
282
        $skippable = 0 === strncmp($path, '?', 1) ? '?' : '';
283
        if ($skippable) {
284
            $path = substr($path, 1);
285
        }
286
287
        if (!$this->getFilesystem()->isAbsolutePath($path)) {
288
            $prefix = $package instanceof RootPackageInterface
289
                ? $this->getBaseDir()
290
                : $this->getVendorDir() . '/' . $package->getPrettyName();
291
            $path = $prefix . '/' . $path;
292
        }
293
294
        return $skippable . $this->getFilesystem()->normalizePath($path);
295
    }
296
297
    /**
298
     * Sets [[packages]].
299
     * @param PackageInterface[] $packages
300
     */
301 2
    public function setPackages(array $packages)
302
    {
303 2
        $this->packages = $packages;
304 2
    }
305
306
    /**
307
     * Gets [[packages]].
308
     * @return \Composer\Package\PackageInterface[]
309
     */
310 1
    public function getPackages()
311
    {
312 1
        if (null === $this->packages) {
313
            $this->packages = $this->findPackages();
314
        }
315
316 1
        return $this->packages;
317
    }
318
319
    /**
320
     * Plain list of all project dependencies (including nested) as provided by composer.
321
     * The list is unordered (chaotic, can be different after every update).
322
     */
323
    protected $plainList = [];
324
325
    /**
326
     * Ordered list of package in form: package => depth
327
     * For order description @see findPackages.
328
     */
329
    protected $orderedList = [];
330
331
    /**
332
     * Returns ordered list of packages:
333
     * - listed earlier in the composer.json will get earlier in the list
334
     * - childs before parents.
335
     * @return \Composer\Package\PackageInterface[]
336
     */
337
    public function findPackages()
338
    {
339
        $root = $this->composer->getPackage();
340
        $this->plainList[$root->getPrettyName()] = $root;
341
        foreach ($this->composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages() as $package) {
342
            $this->plainList[$package->getPrettyName()] = $package;
343
        }
344
        $this->orderedList = [];
345
        $this->iteratePackage($root, true);
346
347
        $res = [];
348
        foreach (array_keys($this->orderedList) as $name) {
349
            $res[] = $this->plainList[$name];
350
        }
351
352
        return $res;
353
    }
354
355
    /**
356
     * Iterates through package dependencies.
357
     * @param PackageInterface $package to iterate
358
     * @param bool $includingDev process development dependencies, defaults to not process
359
     */
360
    protected function iteratePackage(PackageInterface $package, $includingDev = false)
361
    {
362
        $name = $package->getPrettyName();
363
364
        /// prevent infinite loop in case of circular dependencies
365
        static $processed = [];
366
        if (isset($processed[$name])) {
367
            return;
368
        } else {
369
            $processed[$name] = 1;
370
        }
371
372
        /// package depth in dependency hierarchy
373
        static $depth = 0;
374
        ++$depth;
375
376
        $this->iterateDependencies($package);
377
        if ($includingDev) {
378
            $this->iterateDependencies($package, true);
379
        }
380
        if (!isset($this->orderedList[$name])) {
381
            $this->orderedList[$name] = $depth;
382
        }
383
384
        --$depth;
385
    }
386
387
    /**
388
     * Iterates dependencies of the given package.
389
     * @param PackageInterface $package
390
     * @param bool $dev which dependencies to iterate: true - dev, default - general
391
     */
392
    protected function iterateDependencies(PackageInterface $package, $dev = false)
393
    {
394
        $path = $this->preparePath($package, 'composer.json');
395
        if (file_exists($path)) {
396
            $conf = json_decode(file_get_contents($path), true);
397
            $what = $dev ? 'require-dev' : 'require';
398
            $deps = isset($conf[$what]) ? $conf[$what] : [];
399
        } else {
400
            $deps = $dev ? $package->getDevRequires() : $package->getRequires();
401
        }
402
        foreach (array_keys($deps) as $target) {
403
            if (isset($this->plainList[$target]) && empty($this->orderedList[$target])) {
404
                $this->iteratePackage($this->plainList[$target]);
405
            }
406
        }
407
    }
408
409
    protected function showDepsTree()
410
    {
411
        if (!$this->io->isVerbose()) {
412
            return;
413
        }
414
415
        foreach (array_reverse($this->orderedList) as $name => $depth) {
416
            $deps = $this->originalFiles[$name];
417
            $color = $this->colors[$depth % count($this->colors)];
418
            $indent = str_repeat('   ', $depth - 1);
419
            $package = $this->plainList[$name];
420
            $showdeps = $deps ? '<comment>[' . implode(',', array_keys($deps)) . ']</>' : '';
421
            $this->io->write(sprintf('%s - <fg=%s;options=bold>%s</> %s %s', $indent, $color, $name, $package->getFullPrettyVersion(), $showdeps));
422
        }
423
    }
424
425
    protected $colors = ['red', 'green', 'yellow', 'cyan', 'magenta', 'blue'];
426
427
    /**
428
     * Get absolute path to package base dir.
429
     * @return string
430
     */
431
    public function getBaseDir()
432
    {
433
        if (null === $this->baseDir) {
434
            $this->baseDir = dirname($this->getVendorDir());
435
        }
436
437
        return $this->baseDir;
438
    }
439
440
    /**
441
     * Get absolute path to composer vendor dir.
442
     * @return string
443
     */
444
    public function getVendorDir()
445
    {
446
        if (null === $this->vendorDir) {
447
            $dir = $this->composer->getConfig()->get('vendor-dir');
448
            $this->vendorDir = $this->getFilesystem()->normalizePath($dir);
449
        }
450
451
        return $this->vendorDir;
452
    }
453
454
    /**
455
     * Getter for filesystem utility.
456
     * @return Filesystem
457
     */
458
    public function getFilesystem()
459
    {
460
        if (null === $this->filesystem) {
461
            $this->filesystem = new Filesystem();
462
        }
463
464
        return $this->filesystem;
465
    }
466
}
467