Completed
Push — master ( edfae8...aaa59c )
by Andrii
03:20
created

Plugin::findPackages()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 22
ccs 0
cts 18
cp 0
rs 8.9197
c 0
b 0
f 0
cc 4
eloc 15
nc 8
nop 0
crap 20
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
    protected $aliases = [];
64
65
    protected $extensions = [];
66
67
    /**
68
     * @var array array of not yet merged params
69
     */
70
    protected $rawParams = [];
71
72
    /**
73
     * @var Composer instance
74
     */
75
    protected $composer;
76
77
    /**
78
     * @var IOInterface
79
     */
80
    public $io;
81
82
    /**
83
     * Initializes the plugin object with the passed $composer and $io.
84
     * @param Composer $composer
85
     * @param IOInterface $io
86
     */
87 2
    public function activate(Composer $composer, IOInterface $io)
88
    {
89 2
        $this->composer = $composer;
90 2
        $this->io = $io;
91 2
    }
92
93
    /**
94
     * Returns list of events the plugin is subscribed to.
95
     * @return array list of events
96
     */
97 1
    public static function getSubscribedEvents()
98
    {
99
        return [
100 1
            ScriptEvents::POST_AUTOLOAD_DUMP => [
101 1
                ['onPostAutoloadDump', 0],
102 1
            ],
103 1
        ];
104
    }
105
106
    /**
107
     * This is the main function.
108
     * @param Event $event
109
     */
110
    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...
111
    {
112
        $this->io->writeError('<info>Assembling config files</info>');
113
        $this->initAutoload();
114
        $this->scanPackages();
115
116
        $builder = new Builder($this->files);
117
        $builder->setAddition(['aliases' => $this->aliases]);
118
        $builder->setIo($this->io);
119
        $builder->saveFiles();
120
        $builder->writeConfig('aliases', $this->aliases);
121
        $builder->writeConfig('extensions', $this->extensions);
122
        $builder->buildConfigs();
123
    }
124
125
    protected function initAutoload()
126
    {
127
        require_once dirname(dirname(dirname(__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
148
        if ($package->getType() !== self::YII2_PACKAGE_TYPE && is_null($files)) {
149
            return;
150
        }
151
152
        if (is_array($files)) {
153
            $this->addFiles($package, $files);
154
        }
155
        if ($package instanceof RootPackageInterface) {
156
            $this->loadDotEnv($package);
157
        }
158
159
        $aliases = $this->collectAliases($package);
160
        $this->aliases = array_merge($this->aliases, $aliases);
161
162
        $this->extensions[$package->getPrettyName()] = array_filter([
163
            'name' => $package->getPrettyName(),
164
            'version' => $package->getVersion(),
165
            'reference' => $package->getSourceReference() ?: $package->getDistReference(),
166
            'aliases' => $aliases,
167
        ]);
168
    }
169
170
    protected function loadDotEnv(RootPackageInterface $package)
171
    {
172
        $path = $this->preparePath($package, '.env');
173
        if (file_exists($path) && class_exists('Dotenv\Dotenv')) {
174
            array_push($this->files['dotenv'], $path);
175
        }
176
    }
177
178
    /**
179
     * Adds given files to the list of files to be processed.
180
     * Prepares `defines` in reversed order (outer package first) because
181
     * constants cannot be redefined.
182
     * @param CompletePackageInterface $package
183
     * @param array $files
184
     */
185
    protected function addFiles(CompletePackageInterface $package, array $files)
186
    {
187
        foreach ($files as $name => $paths) {
188
            $paths = (array) $paths;
189
            if ($name === 'defines') {
190
                $paths = array_reverse($paths);
191
            }
192
            foreach ($paths as $path) {
193
                if (!isset($this->files[$name])) {
194
                    $this->files[$name] = [];
195
                }
196
                $path = $this->preparePath($package, $path);
197
                if ($name === 'defines') {
198
                    array_unshift($this->files[$name], $path);
199
                } else {
200
                    array_push($this->files[$name], $path);
201
                }
202
            }
203
        }
204
    }
205
206
    /**
207
     * Collects package aliases.
208
     * @param CompletePackageInterface $package
209
     * @return array collected aliases
210
     */
211
    protected function collectAliases(CompletePackageInterface $package)
212
    {
213
        $aliases = array_merge(
214
            $this->prepareAliases($package, 'psr-0'),
215
            $this->prepareAliases($package, 'psr-4')
216
        );
217
        if ($package instanceof RootPackageInterface) {
218
            $aliases = array_merge($aliases,
219
                $this->prepareAliases($package, 'psr-0', true),
220
                $this->prepareAliases($package, 'psr-4', true)
221
            );
222
        }
223
224
        return $aliases;
225
    }
226
227
    /**
228
     * Prepare aliases.
229
     * @param PackageInterface $package
230
     * @param string 'psr-0' or 'psr-4'
231
     * @return array
232
     */
233
    protected function prepareAliases(PackageInterface $package, $psr, $dev = false)
234
    {
235
        $autoload = $dev ? $package->getDevAutoload() : $package->getAutoload();
236
        if (empty($autoload[$psr])) {
237
            return [];
238
        }
239
240
        $aliases = [];
241
        foreach ($autoload[$psr] as $name => $path) {
242
            if (is_array($path)) {
243
                // ignore psr-4 autoload specifications with multiple search paths
244
                // we can not convert them into aliases as they are ambiguous
245
                continue;
246
            }
247
            $name = str_replace('\\', '/', trim($name, '\\'));
248
            $path = $this->preparePath($package, $path);
249
            if ('psr-0' === $psr) {
250
                $path .= '/' . $name;
251
            }
252
            $aliases["@$name"] = $path;
253
        }
254
255
        return $aliases;
256
    }
257
258
    /**
259
     * Builds path inside of a package.
260
     * @param PackageInterface $package
261
     * @param mixed $path can be absolute or relative
262
     * @return string absolute paths will stay untouched
263
     */
264
    public function preparePath(PackageInterface $package, $path)
265
    {
266
        if (strncmp($path, '$', 1) === 0) {
267
            return $path;
268
        }
269
270
        $skippable = strncmp($path, '?', 1) === 0 ? '?' : '';
271
        if ($skippable) {
272
            $path = substr($path, 1);
273
        }
274
275
        if (!$this->getFilesystem()->isAbsolutePath($path)) {
276
            $prefix = $package instanceof RootPackageInterface
277
                ? $this->getBaseDir()
278
                : $this->getVendorDir() . '/' . $package->getPrettyName();
279
            $path = $prefix . '/' . $path;
280
        }
281
282
        return $skippable . $this->getFilesystem()->normalizePath($path);
283
    }
284
285
    /**
286
     * Sets [[packages]].
287
     * @param PackageInterface[] $packages
288
     */
289 2
    public function setPackages(array $packages)
290
    {
291 2
        $this->packages = $packages;
292 2
    }
293
294
    /**
295
     * Gets [[packages]].
296
     * @return \Composer\Package\PackageInterface[]
297
     */
298 1
    public function getPackages()
299
    {
300 1
        if ($this->packages === null) {
301
            $this->packages = $this->findPackages();
302
        }
303
304 1
        return $this->packages;
305
    }
306
307
    /**
308
     * Plain list of all project dependencies (including nested) as provided by composer.
309
     * The list is unordered (chaotic, can be different after every update).
310
     */
311
    protected $plainList = [];
312
313
    /**
314
     * Ordered list of package. Order @see findPackages.
315
     */
316
    protected $orderedList = [];
317
318
    /**
319
     * Returns ordered list of packages:
320
     * - listed earlier in the composer.json will get earlier in the list
321
     * - childs before parents.
322
     * @return \Composer\Package\PackageInterface[]
323
     */
324
    public function findPackages()
325
    {
326
        $root = $this->composer->getPackage();
327
        $this->plainList[$root->getPrettyName()] = $root;
328
        foreach ($this->composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages() as $package) {
329
            $this->plainList[$package->getPrettyName()] = $package;
330
        }
331
        $this->orderedList = [];
332
        $this->iteratePackage($root, true);
333
334
        if ($this->io->isVerbose()) {
335
            $indent = ' - ';
336
            $packages = $indent . implode("\n$indent", $this->orderedList);
337
            $this->io->writeError($packages);
338
        }
339
        $res = [];
340
        foreach ($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
    public 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
        $this->iterateDependencies($package);
365
        if ($includingDev) {
366
            $this->iterateDependencies($package, true);
367
        }
368
        if (!isset($this->orderedList[$name])) {
369
            $this->orderedList[$name] = $name;
370
        }
371
    }
372
373
    /**
374
     * Iterates dependencies of the given package.
375
     * @param PackageInterface $package
376
     * @param bool $dev which dependencies to iterate: true - dev, default - general
377
     */
378
    public function iterateDependencies(PackageInterface $package, $dev = false)
379
    {
380
        $path = $this->preparePath($package, 'composer.json');
381
        if (file_exists($path)) {
382
            $conf = json_decode(file_get_contents($path), true);
383
            $what = $dev ? 'require-dev' : 'require';
384
            $deps = isset($conf[$what]) ? $conf[$what] : [];
385
        } else {
386
            $deps = $dev ? $package->getDevRequires() : $package->getRequires();
387
        }
388
        foreach (array_keys($deps) as $target) {
389
            if (isset($this->plainList[$target]) && !isset($this->orderedList[$target])) {
390
                $this->iteratePackage($this->plainList[$target]);
391
            }
392
        }
393
    }
394
395
    /**
396
     * Get absolute path to package base dir.
397
     * @return string
398
     */
399
    public function getBaseDir()
400
    {
401
        if ($this->baseDir === null) {
402
            $this->baseDir = dirname($this->getVendorDir());
403
        }
404
405
        return $this->baseDir;
406
    }
407
408
    /**
409
     * Get absolute path to composer vendor dir.
410
     * @return string
411
     */
412
    public function getVendorDir()
413
    {
414
        if ($this->vendorDir === null) {
415
            $dir = $this->composer->getConfig()->get('vendor-dir');
416
            $this->vendorDir = $this->getFilesystem()->normalizePath($dir);
417
        }
418
419
        return $this->vendorDir;
420
    }
421
422
    /**
423
     * Getter for filesystem utility.
424
     * @return Filesystem
425
     */
426
    public function getFilesystem()
427
    {
428
        if ($this->filesystem === null) {
429
            $this->filesystem = new Filesystem();
430
        }
431
432
        return $this->filesystem;
433
    }
434
}
435