Completed
Push — master ( aaa59c...3ddd31 )
by Andrii
03:08
created

Plugin::findPackages()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 0
cts 13
cp 0
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 11
nc 4
nop 0
crap 12
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
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
24
25
/**
26
 * Plugin class.
27
 *
28
 * @author Andrii Vasyliev <[email protected]>
29
 */
30
class Plugin implements PluginInterface, EventSubscriberInterface
31
{
32
    const YII2_PACKAGE_TYPE = 'yii2-extension';
33
    const EXTRA_OPTION_NAME = 'config-plugin';
34
35
    /**
36
     * @var PackageInterface[] the array of active composer packages
37
     */
38
    protected $packages;
39
40
    /**
41
     * @var string absolute path to the package base directory
42
     */
43
    protected $baseDir;
44
45
    /**
46
     * @var string absolute path to vendor directory
47
     */
48
    protected $vendorDir;
49
50
    /**
51
     * @var Filesystem utility
52
     */
53
    protected $filesystem;
54
55
    /**
56
     * @var array config name => list of files
57
     */
58
    protected $files = [
59
        'dotenv'  => [],
60
        'defines' => [],
61
        'params'  => [],
62
    ];
63
64
    /**
65
     * @var array package name => configs as listed in `composer.json`
66
     */
67
    protected $originalFiles = [];
68
69
    protected $aliases = [];
70
71
    protected $extensions = [];
72
73
    /**
74
     * @var array array of not yet merged params
75
     */
76
    protected $rawParams = [];
77
78
    /**
79
     * @var Composer instance
80
     */
81
    protected $composer;
82
83
    /**
84
     * @var IOInterface
85
     */
86
    public $io;
87
88
    /**
89
     * Initializes the plugin object with the passed $composer and $io.
90
     * @param Composer $composer
91
     * @param IOInterface $io
92
     */
93 2
    public function activate(Composer $composer, IOInterface $io)
94
    {
95 2
        $this->composer = $composer;
96 2
        $this->io = $io;
97 2
    }
98
99
    /**
100
     * Returns list of events the plugin is subscribed to.
101
     * @return array list of events
102
     */
103 1
    public static function getSubscribedEvents()
104
    {
105
        return [
106 1
            ScriptEvents::POST_AUTOLOAD_DUMP => [
107 1
                ['onPostAutoloadDump', 0],
108 1
            ],
109 1
        ];
110
    }
111
112
    /**
113
     * This is the main function.
114
     * @param Event $event
115
     */
116
    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...
117
    {
118
        $this->io->writeError('<info>Assembling config files</info>');
119
        $this->initAutoload();
120
        $this->scanPackages();
121
        $this->showDepsTree();
122
123
        $builder = new Builder($this->files);
124
        $builder->setAddition(['aliases' => $this->aliases]);
125
        $builder->setIo($this->io);
126
        $builder->saveFiles();
127
        $builder->writeConfig('aliases', $this->aliases);
128
        $builder->writeConfig('extensions', $this->extensions);
129
        $builder->buildConfigs();
130
    }
131
132
    protected function initAutoload()
133
    {
134
        require_once dirname(dirname(dirname(__DIR__))) . '/autoload.php';
135
    }
136
137
    protected function scanPackages()
138
    {
139
        foreach ($this->getPackages() as $package) {
140
            if ($package instanceof CompletePackageInterface) {
141
                $this->processPackage($package);
142
            }
143
        }
144
    }
145
146
    /**
147
     * Scans the given package and collects extensions data.
148
     * @param PackageInterface $package
149
     */
150
    protected function processPackage(CompletePackageInterface $package)
151
    {
152
        $extra = $package->getExtra();
153
        $files = isset($extra[self::EXTRA_OPTION_NAME]) ? $extra[self::EXTRA_OPTION_NAME] : null;
154
        $this->originalFiles[$package->getPrettyName()] = $files;
155
156
        if ($package->getType() !== self::YII2_PACKAGE_TYPE && is_null($files)) {
157
            return;
158
        }
159
160
        if (is_array($files)) {
161
            $this->addFiles($package, $files);
162
        }
163
        if ($package instanceof RootPackageInterface) {
164
            $this->loadDotEnv($package);
165
        }
166
167
        $aliases = $this->collectAliases($package);
168
        $this->aliases = array_merge($this->aliases, $aliases);
169
170
        $this->extensions[$package->getPrettyName()] = array_filter([
171
            'name' => $package->getPrettyName(),
172
            'version' => $package->getVersion(),
173
            'reference' => $package->getSourceReference() ?: $package->getDistReference(),
174
            'aliases' => $aliases,
175
        ]);
176
    }
177
178
    protected function loadDotEnv(RootPackageInterface $package)
179
    {
180
        $path = $this->preparePath($package, '.env');
181
        if (file_exists($path) && class_exists('Dotenv\Dotenv')) {
182
            array_push($this->files['dotenv'], $path);
183
        }
184
    }
185
186
    /**
187
     * Adds given files to the list of files to be processed.
188
     * Prepares `defines` in reversed order (outer package first) because
189
     * constants cannot be redefined.
190
     * @param CompletePackageInterface $package
191
     * @param array $files
192
     */
193
    protected function addFiles(CompletePackageInterface $package, array $files)
194
    {
195
        foreach ($files as $name => $paths) {
196
            $paths = (array) $paths;
197
            if ($name === 'defines') {
198
                $paths = array_reverse($paths);
199
            }
200
            foreach ($paths as $path) {
201
                if (!isset($this->files[$name])) {
202
                    $this->files[$name] = [];
203
                }
204
                $path = $this->preparePath($package, $path);
205
                if ($name === 'defines') {
206
                    array_unshift($this->files[$name], $path);
207
                } else {
208
                    array_push($this->files[$name], $path);
209
                }
210
            }
211
        }
212
    }
213
214
    /**
215
     * Collects package aliases.
216
     * @param CompletePackageInterface $package
217
     * @return array collected aliases
218
     */
219
    protected function collectAliases(CompletePackageInterface $package)
220
    {
221
        $aliases = array_merge(
222
            $this->prepareAliases($package, 'psr-0'),
223
            $this->prepareAliases($package, 'psr-4')
224
        );
225
        if ($package instanceof RootPackageInterface) {
226
            $aliases = array_merge($aliases,
227
                $this->prepareAliases($package, 'psr-0', true),
228
                $this->prepareAliases($package, 'psr-4', true)
229
            );
230
        }
231
232
        return $aliases;
233
    }
234
235
    /**
236
     * Prepare aliases.
237
     * @param PackageInterface $package
238
     * @param string 'psr-0' or 'psr-4'
239
     * @return array
240
     */
241
    protected function prepareAliases(PackageInterface $package, $psr, $dev = false)
242
    {
243
        $autoload = $dev ? $package->getDevAutoload() : $package->getAutoload();
244
        if (empty($autoload[$psr])) {
245
            return [];
246
        }
247
248
        $aliases = [];
249
        foreach ($autoload[$psr] as $name => $path) {
250
            if (is_array($path)) {
251
                // ignore psr-4 autoload specifications with multiple search paths
252
                // we can not convert them into aliases as they are ambiguous
253
                continue;
254
            }
255
            $name = str_replace('\\', '/', trim($name, '\\'));
256
            $path = $this->preparePath($package, $path);
257
            if ('psr-0' === $psr) {
258
                $path .= '/' . $name;
259
            }
260
            $aliases["@$name"] = $path;
261
        }
262
263
        return $aliases;
264
    }
265
266
    /**
267
     * Builds path inside of a package.
268
     * @param PackageInterface $package
269
     * @param mixed $path can be absolute or relative
270
     * @return string absolute paths will stay untouched
271
     */
272
    public function preparePath(PackageInterface $package, $path)
273
    {
274
        if (strncmp($path, '$', 1) === 0) {
275
            return $path;
276
        }
277
278
        $skippable = strncmp($path, '?', 1) === 0 ? '?' : '';
279
        if ($skippable) {
280
            $path = substr($path, 1);
281
        }
282
283
        if (!$this->getFilesystem()->isAbsolutePath($path)) {
284
            $prefix = $package instanceof RootPackageInterface
285
                ? $this->getBaseDir()
286
                : $this->getVendorDir() . '/' . $package->getPrettyName();
287
            $path = $prefix . '/' . $path;
288
        }
289
290
        return $skippable . $this->getFilesystem()->normalizePath($path);
291
    }
292
293
    /**
294
     * Sets [[packages]].
295
     * @param PackageInterface[] $packages
296
     */
297 2
    public function setPackages(array $packages)
298
    {
299 2
        $this->packages = $packages;
300 2
    }
301
302
    /**
303
     * Gets [[packages]].
304
     * @return \Composer\Package\PackageInterface[]
305
     */
306 1
    public function getPackages()
307
    {
308 1
        if ($this->packages === null) {
309
            $this->packages = $this->findPackages();
310
        }
311
312 1
        return $this->packages;
313
    }
314
315
    /**
316
     * Plain list of all project dependencies (including nested) as provided by composer.
317
     * The list is unordered (chaotic, can be different after every update).
318
     */
319
    protected $plainList = [];
320
321
    /**
322
     * Ordered list of package in form: package => depth
323
     * For order description @see findPackages.
324
     */
325
    protected $orderedList = [];
326
327
    /**
328
     * Returns ordered list of packages:
329
     * - listed earlier in the composer.json will get earlier in the list
330
     * - childs before parents.
331
     * @return \Composer\Package\PackageInterface[]
332
     */
333
    public function findPackages()
334
    {
335
        $root = $this->composer->getPackage();
336
        $this->plainList[$root->getPrettyName()] = $root;
337
        foreach ($this->composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages() as $package) {
338
            $this->plainList[$package->getPrettyName()] = $package;
339
        }
340
        $this->orderedList = [];
341
        $this->iteratePackage($root, true);
342
343
        $res = [];
344
        foreach (array_keys($this->orderedList) as $name) {
345
            $res[] = $this->plainList[$name];
346
        }
347
348
        return $res;
349
    }
350
351
    /**
352
     * Iterates through package dependencies.
353
     * @param PackageInterface $package to iterate
354
     * @param bool $includingDev process development dependencies, defaults to not process
355
     */
356
    protected function iteratePackage(PackageInterface $package, $includingDev = false)
357
    {
358
        $name = $package->getPrettyName();
359
360
        /// prevent infinite loop in case of circular dependencies
361
        static $processed = [];
362
        if (isset($processed[$name])) {
363
            return;
364
        } else {
365
            $processed[$name] = 1;
366
        }
367
368
        /// package depth in dependency hierarchy
369
        static $depth = 0;
370
        $depth++;
371
372
        $this->iterateDependencies($package);
373
        if ($includingDev) {
374
            $this->iterateDependencies($package, true);
375
        }
376
        if (!isset($this->orderedList[$name])) {
377
            $this->orderedList[$name] = $depth;
378
        }
379
380
        $depth--;
381
    }
382
383
    /**
384
     * Iterates dependencies of the given package.
385
     * @param PackageInterface $package
386
     * @param bool $dev which dependencies to iterate: true - dev, default - general
387
     */
388
    protected function iterateDependencies(PackageInterface $package, $dev = false)
389
    {
390
        $path = $this->preparePath($package, 'composer.json');
391
        if (file_exists($path)) {
392
            $conf = json_decode(file_get_contents($path), true);
393
            $what = $dev ? 'require-dev' : 'require';
394
            $deps = isset($conf[$what]) ? $conf[$what] : [];
395
        } else {
396
            $deps = $dev ? $package->getDevRequires() : $package->getRequires();
397
        }
398
        foreach (array_keys($deps) as $target) {
399
            if (isset($this->plainList[$target]) && empty($this->orderedList[$target])) {
400
                $this->iteratePackage($this->plainList[$target]);
401
            }
402
        }
403
    }
404
405
    protected function showDepsTree()
406
    {
407
        if (!$this->io->isVerbose()) {
408
            return;
409
        }
410
411
        $this->initStyles();
412
        foreach (array_reverse($this->orderedList) as $name => $depth) {
413
            $deps = $this->originalFiles[$name];
414
            $color = $this->colors[$depth];
415
            $indent = str_repeat('   ', $depth - 1);
416
            $package = $this->plainList[$name];
417
            $showdeps = $deps ? '[' . implode(',', array_keys($deps)) . ']' : '';
418
            $this->io->write(sprintf('%s - <%s>%s</%s> %s %s', $indent, $color, $name, $color, $package->getFullPrettyVersion(), $showdeps));
419
        }
420
    }
421
422
    protected $colors = ['red', 'green', 'yellow', 'cyan', 'magenta', 'blue'];
423
424
    protected function initStyles()
425
    {
426
        $ref = new \ReflectionProperty(get_class($this->io), 'output');
427
        $ref->setAccessible(true);
428
        $output = $ref->getValue($this->io);
429
430
        foreach ($this->colors as $color) {
431
            $style = new OutputFormatterStyle($color);
432
            $output->getFormatter()->setStyle($color, $style);
433
        }
434
    }
435
436
    /**
437
     * Get absolute path to package base dir.
438
     * @return string
439
     */
440
    public function getBaseDir()
441
    {
442
        if ($this->baseDir === null) {
443
            $this->baseDir = dirname($this->getVendorDir());
444
        }
445
446
        return $this->baseDir;
447
    }
448
449
    /**
450
     * Get absolute path to composer vendor dir.
451
     * @return string
452
     */
453
    public function getVendorDir()
454
    {
455
        if ($this->vendorDir === null) {
456
            $dir = $this->composer->getConfig()->get('vendor-dir');
457
            $this->vendorDir = $this->getFilesystem()->normalizePath($dir);
458
        }
459
460
        return $this->vendorDir;
461
    }
462
463
    /**
464
     * Getter for filesystem utility.
465
     * @return Filesystem
466
     */
467
    public function getFilesystem()
468
    {
469
        if ($this->filesystem === null) {
470
            $this->filesystem = new Filesystem();
471
        }
472
473
        return $this->filesystem;
474
    }
475
}
476