Passed
Push — master ( 14a42b...4e4f6b )
by Andrii
02:16
created

Plugin   F

Complexity

Total Complexity 66

Size/Duplication

Total Lines 428
Duplicated Lines 0 %

Test Coverage

Coverage 7.32%

Importance

Changes 0
Metric Value
wmc 66
eloc 156
dl 0
loc 428
ccs 12
cts 164
cp 0.0732
rs 3.12
c 0
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A initAutoload() 0 4 1
A prepareAliases() 0 23 6
A onPostAutoloadDump() 0 11 1
B processPackage() 0 25 7
A setPackages() 0 3 1
A preparePath() 0 19 6
A showDepsTree() 0 13 4
B addFiles() 0 19 7
A getVendorDir() 0 8 2
A scanPackages() 0 5 3
A iteratePackage() 0 25 4
A getSubscribedEvents() 0 5 1
B iterateDependencies() 0 13 8
A loadDotEnv() 0 5 3
A getFilesystem() 0 7 2
A getPackages() 0 7 2
A collectAliases() 0 14 2
A activate() 0 4 1
A getBaseDir() 0 7 2
A findPackages() 0 16 3

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.

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-2018, 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
    /**
69
     * @var Builder
70
     */
71
    protected $builder;
72
73
    /**
74
     * @var Composer instance
75
     */
76
    protected $composer;
77
78
    /**
79
     * @var IOInterface
80
     */
81
    public $io;
82
83
    /**
84
     * Initializes the plugin object with the passed $composer and $io.
85
     * @param Composer $composer
86
     * @param IOInterface $io
87
     */
88 2
    public function activate(Composer $composer, IOInterface $io)
89
    {
90 2
        $this->composer = $composer;
91 2
        $this->io = $io;
92 2
    }
93
94
    /**
95
     * Returns list of events the plugin is subscribed to.
96
     * @return array list of events
97
     */
98 1
    public static function getSubscribedEvents()
99
    {
100
        return [
101 1
            ScriptEvents::POST_AUTOLOAD_DUMP => [
102
                ['onPostAutoloadDump', 0],
103
            ],
104
        ];
105
    }
106
107
    /**
108
     * This is the main function.
109
     * @param Event $event
110
     */
111
    public function onPostAutoloadDump(Event $event)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

111
    public function onPostAutoloadDump(/** @scrutinizer ignore-unused */ Event $event)

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

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