Completed
Push — master ( 6a4eb4...e06125 )
by Andrii
14:10
created

Plugin   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 372
Duplicated Lines 0 %

Test Coverage

Coverage 8.63%

Importance

Changes 31
Bugs 1 Features 7
Metric Value
wmc 59
eloc 153
c 31
b 1
f 7
dl 0
loc 372
ccs 12
cts 139
cp 0.0863
rs 4.08

19 Methods

Rating   Name   Duplication   Size   Complexity  
A onPostAutoloadDump() 0 20 2
A orderFiles() 0 14 4
A getAllFiles() 0 15 4
A scanPackages() 0 5 3
A reorderFiles() 0 7 3
A getSubscribedEvents() 0 5 1
A activate() 0 4 1
A initAutoload() 0 4 1
A readConfig() 0 9 2
B processPackage() 0 34 8
A addFile() 0 15 4
A setPackages() 0 3 1
A showDepsTree() 0 13 4
A addFiles() 0 9 4
A iteratePackage() 0 25 4
A iterateDependencies() 0 6 5
A loadDotEnv() 0 5 3
A getPackages() 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\Plugin\PluginInterface;
17
use Composer\Script\Event;
18
use Composer\Script\ScriptEvents;
19
use hiqdev\composer\config\exceptions\FailedReadException;
20
use hiqdev\composer\config\readers\ReaderFactory;
21
22
/**
23
 * Plugin class.
24
 *
25
 * @author Andrii Vasyliev <[email protected]>
26
 */
27
class Plugin implements PluginInterface, EventSubscriberInterface
28
{
29
    /**
30
     * @var Package[] the array of active composer packages
31
     */
32
    protected $packages;
33
34
    private $alternatives = [];
35
36
    private $outputDir;
37
38
    private $rootPackage;
39
40
    /**
41
     * @var array config name => list of files
42
     */
43
    protected $files = [
44
        'dotenv'  => [],
45
        'defines' => [],
46
        'params'  => [],
47
    ];
48
49
    protected $colors = ['red', 'green', 'yellow', 'cyan', 'magenta', 'blue'];
50
51
    /**
52
     * @var array package name => configs as listed in `composer.json`
53
     */
54
    protected $originalFiles = [];
55
56
    /**
57
     * @var Builder
58
     */
59
    protected $builder;
60
61
    /**
62
     * @var Composer instance
63
     */
64
    protected $composer;
65
66
    /**
67
     * @var IOInterface
68
     */
69
    public $io;
70
71
    /**
72
     * Initializes the plugin object with the passed $composer and $io.
73
     * @param Composer $composer
74
     * @param IOInterface $io
75
     */
76
    public function activate(Composer $composer, IOInterface $io)
77
    {
78
        $this->composer = $composer;
79
        $this->io = $io;
80
    }
81
82
    /**
83
     * Returns list of events the plugin is subscribed to.
84
     * @return array list of events
85
     */
86
    public static function getSubscribedEvents()
87
    {
88 2
        return [
89
            ScriptEvents::POST_AUTOLOAD_DUMP => [
90 2
                ['onPostAutoloadDump', 0],
91 2
            ],
92 2
        ];
93
    }
94
95
    /**
96
     * This is the main function.
97
     * @param Event $event
98 1
     */
99
    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

99
    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...
100
    {
101 1
        $this->io->overwriteError('<info>Assembling config files</info>');
102
103
        $this->builder = new Builder();
104
105
        $this->initAutoload();
106
        $this->scanPackages();
107
        $this->reorderFiles();
108
        $this->showDepsTree();
109
110
        $this->builder->setOutputDir($this->outputDir);
111
        $this->builder->buildAllConfigs($this->files);
112
113
        $save = $this->files;
114
        foreach ($this->alternatives as $name => $files) {
115
            $this->files = $save;
116
            $builder = $this->builder->createAlternative($name);
117
            $this->addFiles($this->rootPackage, $files);
118
            $builder->buildAllConfigs($this->files);
119
        }
120
    }
121
122
    protected function initAutoload()
123
    {
124
        $dir = dirname(dirname(dirname(__DIR__)));
125
        require_once "$dir/autoload.php";
126
    }
127
128
    protected function scanPackages()
129
    {
130
        foreach ($this->getPackages() as $package) {
131
            if ($package->isComplete()) {
132
                $this->processPackage($package);
133
            }
134
        }
135
    }
136
137
    protected function reorderFiles(): void
138
    {
139
        foreach (array_keys($this->files) as $name) {
140
            $this->files[$name] = $this->getAllFiles($name);
141
        }
142
        foreach ($this->files as $name => $files) {
143
            $this->files[$name] = $this->orderFiles($files);
144
        }
145
    }
146
147
    protected function getAllFiles(string $name): array
148
    {
149
        if (empty($this->files[$name])) {
150
            return[];
151
        }
152
        $res = [];
153
        foreach ($this->files[$name] as $file) {
154
            if (strncmp($file, '$', 1) === 0) {
155
                $res = array_merge($res, $this->getAllFiles(substr($file, 1)));
156
            } else {
157
                $res[] = $file;
158
            }
159
        }
160
161
        return $res;
162
    }
163
164
    protected function orderFiles(array $files): array
165
    {
166
        if (empty($files)) {
167
            return [];
168
        }
169
        $keys = array_combine($files, $files);
170
        $res = [];
171
        foreach ($this->orderedFiles as $file) {
172
            if (isset($keys[$file])) {
173
                $res[] = $file;
174
            }
175
        }
176
177
        return $res;
178
    }
179
180
    /**
181
     * Scans the given package and collects packages data.
182
     * @param Package $package
183
     */
184
    protected function processPackage(Package $package)
185
    {
186
        $files = $package->getFiles();
187
        $this->originalFiles[$package->getPrettyName()] = $files;
188
189
        if (!empty($files)) {
190
            $this->addFiles($package, $files);
191
        }
192
        if ($package->isRoot()) {
193
            $this->rootPackage = $package;
194
            $this->loadDotEnv($package);
195
            $devFiles = $package->getDevFiles();
196
            if (!empty($devFiles)) {
197
                $this->addFiles($package, $devFiles);
198
            }
199
            $this->outputDir = $package->getOutputDir();
200
            $alternatives = $package->getAlternatives();
201
            if (is_string($alternatives)) {
0 ignored issues
show
introduced by
The condition is_string($alternatives) is always false.
Loading history...
202
                $this->alternatives = $this->readConfig($package, $alternatives);
203
            } elseif (is_array($alternatives)) {
0 ignored issues
show
introduced by
The condition is_array($alternatives) is always true.
Loading history...
204
                $this->alternatives = $alternatives;
205
            } elseif (!empty($alternatives)) {
206
                throw new BadConfigurationException('alternatives must be array or path to configuration file');
0 ignored issues
show
Bug introduced by
The type hiqdev\composer\config\BadConfigurationException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
207
            }
208
        }
209
210
        $aliases = $package->collectAliases();
211
212
        $this->builder->mergeAliases($aliases);
213
        $this->builder->setPackage($package->getPrettyName(), array_filter([
214
            'name' => $package->getPrettyName(),
215
            'version' => $package->getVersion(),
216
            'reference' => $package->getSourceReference() ?: $package->getDistReference(),
217
            'aliases' => $aliases,
218
        ]));
219
    }
220
221
    private function readConfig($package, $file): array
222
    {
223
        $path = $package->preparePath($file);
224
        if (!file_exists($path)) {
225
            throw new FailedReadException("failed read file: $file");
226
        }
227
        $reader = ReaderFactory::get($this->builder, $path);
228
229
        return $reader->read($path);
230
231
    }
232
233
    protected function loadDotEnv(Package $package)
234
    {
235
        $path = $package->preparePath('.env');
236
        if (file_exists($path) && class_exists('Dotenv\Dotenv')) {
237
            $this->addFile($package, 'dotenv', $path);
238
        }
239
    }
240
241
    /**
242
     * Adds given files to the list of files to be processed.
243
     * Prepares `defines` in reversed order (outer package first) because
244
     * constants cannot be redefined.
245
     * @param Package $package
246
     * @param array $files
247
     */
248
    protected function addFiles(Package $package, array $files)
249
    {
250
        foreach ($files as $name => $paths) {
251
            $paths = (array) $paths;
252
            if ('defines' === $name) {
253
                $paths = array_reverse($paths);
254
            }
255
            foreach ($paths as $path) {
256
                $this->addFile($package, $name, $path);
257
            }
258
        }
259
    }
260
261
    protected $orderedFiles = [];
262
263
    protected function addFile(Package $package, string $name, string $path)
264
    {
265
        $path = $package->preparePath($path);
266
        if (!isset($this->files[$name])) {
267
            $this->files[$name] = [];
268
        }
269
        if (in_array($path, $this->files[$name], true)) {
270
            return;
271
        }
272
        if ('defines' === $name) {
273
            array_unshift($this->orderedFiles, $path);
274
            array_unshift($this->files[$name], $path);
275
        } else {
276
            array_push($this->orderedFiles, $path);
277
            array_push($this->files[$name], $path);
278
        }
279
    }
280
281
    /**
282
     * Sets [[packages]].
283
     * @param Package[] $packages
284
     */
285
    public function setPackages(array $packages)
286
    {
287
        $this->packages = $packages;
288
    }
289
290
    /**
291
     * Gets [[packages]].
292
     * @return Package[]
293 2
     */
294
    public function getPackages()
295 2
    {
296 2
        if (null === $this->packages) {
297
            $this->packages = $this->findPackages();
298
        }
299
300
        return $this->packages;
301
    }
302 1
303
    /**
304 1
     * Plain list of all project dependencies (including nested) as provided by composer.
305
     * The list is unordered (chaotic, can be different after every update).
306
     */
307
    protected $plainList = [];
308 1
309
    /**
310
     * Ordered list of package in form: package => depth
311
     * For order description @see findPackages.
312
     */
313
    protected $orderedList = [];
314
315
    /**
316
     * Returns ordered list of packages:
317
     * - listed earlier in the composer.json will get earlier in the list
318
     * - childs before parents.
319
     * @return Package[]
320
     */
321
    public function findPackages()
322
    {
323
        $root = new Package($this->composer->getPackage(), $this->composer);
324
        $this->plainList[$root->getPrettyName()] = $root;
325
        foreach ($this->composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages() as $package) {
326
            $this->plainList[$package->getPrettyName()] = new Package($package, $this->composer);
327
        }
328
        $this->orderedList = [];
329
        $this->iteratePackage($root, true);
330
331
        $res = [];
332
        foreach (array_keys($this->orderedList) as $name) {
333
            $res[] = $this->plainList[$name];
334
        }
335
336
        return $res;
337
    }
338
339
    /**
340
     * Iterates through package dependencies.
341
     * @param Package $package to iterate
342
     * @param bool $includingDev process development dependencies, defaults to not process
343
     */
344
    protected function iteratePackage(Package $package, $includingDev = false)
345
    {
346
        $name = $package->getPrettyName();
347
348
        /// prevent infinite loop in case of circular dependencies
349
        static $processed = [];
350
        if (isset($processed[$name])) {
351
            return;
352
        } else {
353
            $processed[$name] = 1;
354
        }
355
356
        /// package depth in dependency hierarchy
357
        static $depth = 0;
358
        ++$depth;
359
360
        $this->iterateDependencies($package);
361
        if ($includingDev) {
362
            $this->iterateDependencies($package, true);
363
        }
364
        if (!isset($this->orderedList[$name])) {
365
            $this->orderedList[$name] = $depth;
366
        }
367
368
        --$depth;
369
    }
370
371
    /**
372
     * Iterates dependencies of the given package.
373
     * @param Package $package
374
     * @param bool $dev which dependencies to iterate: true - dev, default - general
375
     */
376
    protected function iterateDependencies(Package $package, $dev = false)
377
    {
378
        $deps = $dev ? $package->getDevRequires() : $package->getRequires();
379
        foreach (array_keys($deps) as $target) {
380
            if (isset($this->plainList[$target]) && empty($this->orderedList[$target])) {
381
                $this->iteratePackage($this->plainList[$target]);
382
            }
383
        }
384
    }
385
386
    protected function showDepsTree()
387
    {
388
        if (!$this->io->isVerbose()) {
389
            return;
390
        }
391
392
        foreach (array_reverse($this->orderedList) as $name => $depth) {
393
            $deps = $this->originalFiles[$name];
394
            $color = $this->colors[$depth % count($this->colors)];
395
            $indent = str_repeat('   ', $depth - 1);
396
            $package = $this->plainList[$name];
397
            $showdeps = $deps ? '<comment>[' . implode(',', array_keys($deps)) . ']</>' : '';
398
            $this->io->write(sprintf('%s - <fg=%s;options=bold>%s</> %s %s', $indent, $color, $name, $package->getFullPrettyVersion(), $showdeps));
399
        }
400
    }
401
}
402