Completed
Push — master ( 5d9e04...cc1551 )
by Andrii
13:32
created

Plugin::getPackages()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
ccs 1
cts 1
cp 1
rs 9.4285
cc 2
eloc 4
nc 2
nop 0
crap 2
1
<?php
2
3
/*
4
 * Composer plugin for config assembling
5
 *
6
 * @link      https://github.com/hiqdev/composer-config-plugin
7
 * @package   composer-config-plugin
8
 * @license   BSD-3-Clause
9
 * @copyright Copyright (c) 2016, HiQDev (http://hiqdev.com/)
10
 */
11
12
namespace hiqdev\ComposerConfigPlugin;
13
14
use Composer\Composer;
15
use Composer\EventDispatcher\EventSubscriberInterface;
16
use Composer\IO\IOInterface;
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 PACKAGE_TYPE = 'yii2-extension';
32
    const EXTRA_OPTION_NAME = 'config-plugin';
33
    const OUTPUT_PATH = 'hiqdev/config';
34
    const BASE_DIR_SAMPLE = '<base-dir>';
35
    const VENDOR_DIR_SAMPLE = '<base-dir>/vendor';
36
37
    /**
38
     * @var PackageInterface[] the array of active composer packages
39
     */
40
    protected $packages;
41
42
    /**
43
     * @var string absolute path to the package base directory
44
     */
45
    protected $baseDir;
46
47
    /**
48
     * @var string absolute path to vendor directory
49
     */
50
    protected $vendorDir;
51
52
    /**
53
     * @var Filesystem utility
54
     */
55
    protected $filesystem;
56
57
    /**
58
     * @var array assembled config data
59
     */
60
    protected $data = [
61
        'aliases' => [],
62
        'extensions' => [],
63
    ];
64
65
    /**
66
     * @var array raw collected data
67
     */
68
    protected $raw = [];
69
70
    /**
71
     * @var array array of not yet merged params
72
     */
73
    protected $rawParams = [];
74
75
    /**
76
     * @var Composer instance
77
     */
78
    protected $composer;
79
80
    /**
81
     * @var IOInterface
82
     */
83
    public $io;
84
85
    /**
86
     * Initializes the plugin object with the passed $composer and $io.
87
     * @param Composer $composer
88
     * @param IOInterface $io
89
     */
90 2
    public function activate(Composer $composer, IOInterface $io)
91
    {
92 2
        $this->composer = $composer;
93 2
        $this->io = $io;
94 2
    }
95
96
    /**
97
     * Returns list of events the plugin is subscribed to.
98
     * @return array list of events
99
     */
100 1
    public static function getSubscribedEvents()
101
    {
102
        return [
103 1
            ScriptEvents::POST_AUTOLOAD_DUMP => [
104
                ['onPostAutoloadDump', 0],
105 1
            ],
106
        ];
107
    }
108
109
    /**
110
     * This is the main function.
111
     * @param Event $event
112
     */
113
    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...
114
    {
115
        $this->io->writeError('<info>Assembling config files</info>');
116
117
        /// scan packages
118
        foreach ($this->getPackages() as $package) {
119
            if ($package instanceof \Composer\Package\CompletePackageInterface) {
120
                $this->processPackage($package);
121
            }
122
        }
123
        $this->processPackage($this->composer->getPackage());
124
125
        $this->assembleParams();
126
        define('COMPOSER_CONFIG_PLUGIN_DIR', $this->getOutputDir());
127
        $this->assembleConfigs();
128
    }
129
130
    /**
131
     * Scans the given package and collects extensions data.
132
     * @param PackageInterface $package
133
     */
134
    public function processPackage(PackageInterface $package)
135
    {
136
        $extra = $package->getExtra();
137
        $files = isset($extra[self::EXTRA_OPTION_NAME]) ? $extra[self::EXTRA_OPTION_NAME] : null;
138
        if ($package->getType() !== self::PACKAGE_TYPE && is_null($files)) {
139
            return;
140
        }
141
142
        $extension = [
143
            'name' => $package->getPrettyName(),
144
            'version' => $package->getVersion(),
145
        ];
146
        if ($package->getVersion() === '9999999-dev') {
147
            $reference = $package->getSourceReference() ?: $package->getDistReference();
148
            if ($reference) {
149
                $extension['reference'] = $reference;
150
            }
151
        }
152
153
        $aliases = array_merge(
154
            $this->prepareAliases($package, 'psr-0'),
155
            $this->prepareAliases($package, 'psr-4')
156
        );
157
158
        if (isset($files['defines'])) {
159
            foreach ((array) $files['defines'] as $file) {
160
                $this->readConfigFile($package, $file);
161
            }
162
            unset($files['defines']);
163
        }
164
165
        if (isset($files['params'])) {
166
            foreach ((array) $files['params'] as $file) {
167
                $this->rawParams[] = $this->readConfigFile($package, $file);
168
            }
169
            unset($files['params']);
170
        }
171
172
        $this->raw[$package->getPrettyName()] = [
173
            'package' => $package,
174
            'extension' => $extension,
175
            'aliases' => $aliases,
176
            'files' => (array) $files,
177
        ];
178
    }
179
180
    public function assembleParams()
181
    {
182
        $this->assembleFile('params', $this->rawParams);
183
    }
184
185
    public function assembleConfigs()
186
    {
187
        $allAliases = [];
0 ignored issues
show
Unused Code introduced by
$allAliases is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
188
        $extensions = [];
0 ignored issues
show
Unused Code introduced by
$extensions is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
189
        $rawConfigs = [
190
            'aliases' => [],
191
            'extensions' => [],
192
        ];
193
194
        foreach ($this->raw as $name => $info) {
195
            $rawConfigs['extensions'][] = [
196
                $name => $info['extension'],
197
            ];
198
199
            $aliases = $info['aliases'];
200
            $rawConfigs['aliases'][] = $aliases;
201
202
            foreach ($info['files'] as $name => $pathes) {
203
                foreach ((array) $pathes as $path) {
204
                    $rawConfigs[$name][] = $this->readConfigFile($info['package'], $path);
205
                }
206
            }
207
        }
208
209
        foreach ($rawConfigs as $name => $configs) {
210
            if (!in_array($name, ['params', 'aliases', 'extensions'], true)) {
211
                $configs[] = [
212
                    'params' => $this->data['params'],
213
                    'aliases' => $this->data['aliases'],
214
                ];
215
            }
216
            $this->assembleFile($name, $configs);
217
        }
218
    }
219
220
    protected function assembleFile($name, array $configs)
221
    {
222
        $this->data[$name] = call_user_func_array([Helper::class, 'mergeConfig'], $configs);
223
        $this->writeFile($name, (array) $this->data[$name]);
224
    }
225
226
    /**
227
     * Read extra config.
228
     * @param string $file
229
     * @return array
230
     */
231
    protected function readConfigFile(PackageInterface $package, $file)
232
    {
233
        $skippable = false;
234
        if (strncmp($file, '?', 1) === 0) {
235
            $skippable = true;
236
            $file = substr($file, 1);
237
        }
238
        $__path = $this->preparePath($package, $file);
239
        if (!file_exists($__path)) {
240
            if ($skippable) {
241
                return [];
242
            } else {
243
                $this->io->writeError('<error>Non existent extension config file</error> ' . $file . ' in ' . $package->getPrettyName());
244
                exit(1);
0 ignored issues
show
Coding Style Compatibility introduced by
The method readConfigFile() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
245
            }
246
        }
247
        extract($this->data);
248
249
        return (array) require $__path;
250
    }
251
252
    /**
253
     * Prepare aliases.
254
     *
255
     * @param PackageInterface $package
256
     * @param string 'psr-0' or 'psr-4'
257
     * @return array
258
     */
259
    protected function prepareAliases(PackageInterface $package, $psr)
260
    {
261
        $autoload = $package->getAutoload();
262
        if (empty($autoload[$psr])) {
263
            return [];
264
        }
265
266
        $aliases = [];
267
        foreach ($autoload[$psr] as $name => $path) {
268
            if (is_array($path)) {
269
                // ignore psr-4 autoload specifications with multiple search paths
270
                // we can not convert them into aliases as they are ambiguous
271
                continue;
272
            }
273
            $name = str_replace('\\', '/', trim($name, '\\'));
274
            $path = $this->preparePath($package, $path);
275
            $path = $this->substitutePath($path, $this->getBaseDir(), self::BASE_DIR_SAMPLE);
276
            if ('psr-0' === $psr) {
277
                $path .= '/' . $name;
278
            }
279
            $aliases["@$name"] = $path;
280
        }
281
282
        return $aliases;
283
    }
284
285
    /**
286
     * Substitute path with alias if applicable.
287
     * @param string $path
288
     * @param string $dir
289
     * @param string $alias
290
     * @return string
291
     */
292
    public function substitutePath($path, $dir, $alias)
293
    {
294
        return (substr($path, 0, strlen($dir) + 1) === $dir . '/') ? $alias . substr($path, strlen($dir)) : $path;
295
    }
296
297
    /**
298
     * Builds path inside of a package.
299
     * @param PackageInterface $package
300
     * @param mixed $path can be absolute or relative
301
     * @return string absolute pathes will stay untouched
302
     */
303
    public function preparePath(PackageInterface $package, $path)
304
    {
305
        if (!$this->getFilesystem()->isAbsolutePath($path)) {
306
            $prefix = $package instanceof RootPackageInterface ? $this->getBaseDir() : $this->getVendorDir() . '/' . $package->getPrettyName();
307
            $path = $prefix . '/' . $path;
308
        }
309
310
        return $this->getFilesystem()->normalizePath($path);
311
    }
312
313
    /**
314
     * Get output dir.
315
     * @return string
316
     */
317
    public function getOutputDir()
318
    {
319
        return $this->getVendorDir() . DIRECTORY_SEPARATOR . static::OUTPUT_PATH;
320
    }
321
322
    /**
323
     * Build full path to write file for a given filename.
324
     * @param string $filename
325
     * @return string
326
     */
327
    public function buildOutputPath($filename)
328
    {
329
        return $this->getOutputDir() . DIRECTORY_SEPARATOR . $filename . '.php';
330
    }
331
332
    /**
333
     * Writes config file.
334
     * @param string $filename
335
     * @param array $data
336
     */
337
    protected function writeFile($filename, array $data)
338
    {
339
        $path = $this->buildOutputPath($filename);
340
        if (!file_exists(dirname($path))) {
341
            mkdir(dirname($path), 0777, true);
342
        }
343
        $array = str_replace("'" . self::BASE_DIR_SAMPLE, '$baseDir . \'', Helper::exportVar($data));
344
        file_put_contents($path, "<?php\n\n\$baseDir = dirname(dirname(dirname(__DIR__)));\n\nreturn $array;\n");
345 2
    }
346
347 2
    /**
348 2
     * Sets [[packages]].
349
     * @param PackageInterface[] $packages
350
     */
351
    public function setPackages(array $packages)
352
    {
353
        $this->packages = $packages;
354 1
    }
355
356 1
    /**
357
     * Gets [[packages]].
358
     * @return \Composer\Package\PackageInterface[]
359
     */
360 1
    public function getPackages()
361
    {
362
        if ($this->packages === null) {
363
            $this->packages = $this->findPackages();
364
        }
365
366
        return $this->packages;
367
    }
368
369
    protected $plainList = [];
370
    protected $orderedList = [];
371
372
    /**
373
     * Returns ordered list of packages:
374
     * - listed earlier in the composer.json will get earlier in the list
375
     * - childs before parents.
376
     * @return \Composer\Package\PackageInterface[]
377
     */
378
    public function findPackages()
379
    {
380
        $root = $this->composer->getPackage();
381
        $this->plainList[$root->getPrettyName()] = $root;
382
        foreach ($this->composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages() as $package) {
383
            $this->plainList[$package->getPrettyName()] = $package;
384
        }
385
        $this->orderedList = [];
386
        $this->iteratePackage($root, true);
387
        #var_dump(implode("\n", $this->orderedList)); die();
0 ignored issues
show
Unused Code Comprehensibility introduced by
73% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
388
        $res = [];
389
        foreach ($this->orderedList as $name) {
390
            $res[] = $this->plainList[$name];
391
        }
392
393
        return $res;
394
    }
395
396
    /**
397
     * Iterates through package dependencies.
398
     * @param PackageInterface $package to iterate
399
     * @param bool $includingDev process development dependencies, defaults to not process
400
     */
401
    public function iteratePackage(PackageInterface $package, $includingDev = false)
402
    {
403
        $this->iterateDependencies($package);
404
        if ($includingDev) {
405
            $this->iterateDependencies($package, true);
406
        }
407
        $name = $package->getPrettyName();
408
        if (!isset($this->orderedList[$name])) {
409
            $this->orderedList[$name] = $name;
410
        }
411
    }
412
413
    /**
414
     * Iterates dependencies of the given package.
415
     * @param PackageInterface $package
416
     * @param bool $dev which dependencies to iterate: true - dev, default - general
417
     */
418
    public function iterateDependencies(PackageInterface $package, $dev = false)
419
    {
420
        $path = $this->preparePath($package, 'composer.json');
421
        if (file_exists($path)) {
422
            $conf = json_decode(file_get_contents($path), true);
423
            $what = $dev ? 'require-dev' : 'require';
424
            $deps = isset($conf[$what]) ? $conf[$what] : [];
425
        } else {
426
            $deps = $dev ? $package->getDevRequires() : $package->getRequires();
427
        }
428
        foreach (array_keys($deps) as $target) {
429
            if (isset($this->plainList[$target]) && !isset($this->orderedList[$target])) {
430
                $this->iteratePackage($this->plainList[$target]);
431
            }
432
        }
433
    }
434
435
    /**
436
     * Get absolute path to package base dir.
437
     * @return string
438
     */
439
    public function getBaseDir()
440
    {
441
        if ($this->baseDir === null) {
442
            $this->baseDir = dirname($this->getVendorDir());
443
        }
444
445
        return $this->baseDir;
446
    }
447
448
    /**
449
     * Get absolute path to composer vendor dir.
450
     * @return string
451
     */
452
    public function getVendorDir()
453
    {
454
        if ($this->vendorDir === null) {
455
            $dir = $this->composer->getConfig()->get('vendor-dir', '/');
456
            $this->vendorDir = $this->getFilesystem()->normalizePath($dir);
457
        }
458
459
        return $this->vendorDir;
460
    }
461
462
    /**
463
     * Getter for filesystem utility.
464
     * @return Filesystem
465
     */
466
    public function getFilesystem()
467
    {
468
        if ($this->filesystem === null) {
469
            $this->filesystem = new Filesystem();
470
        }
471
472
        return $this->filesystem;
473
    }
474
}
475