Completed
Push — master ( a9c0ba...826930 )
by Andrii
03:40
created

Plugin::setPackages()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
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 1
                ['onPostAutoloadDump', 0],
105 1
            ],
106 1
        ];
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
        $this->assembleConfigs();
127
    }
128
129
    /**
130
     * Scans the given package and collects extensions data.
131
     * @param PackageInterface $package
132
     */
133
    public function processPackage(PackageInterface $package)
134
    {
135
        $extra = $package->getExtra();
136
        $files = isset($extra[self::EXTRA_OPTION_NAME]) ? $extra[self::EXTRA_OPTION_NAME] : null;
137
        if ($package->getType() !== self::PACKAGE_TYPE && is_null($files)) {
138
            return;
139
        }
140
141
        $extension = [
142
            'name' => $package->getPrettyName(),
143
            'version' => $package->getVersion(),
144
        ];
145
        if ($package->getVersion() === '9999999-dev') {
146
            $reference = $package->getSourceReference() ?: $package->getDistReference();
147
            if ($reference) {
148
                $extension['reference'] = $reference;
149
            }
150
        }
151
152
        $aliases = array_merge(
153
            $this->prepareAliases($package, 'psr-0'),
154
            $this->prepareAliases($package, 'psr-4')
155
        );
156
157
        if (isset($files['defines'])) {
158
            foreach ((array) $files['defines'] as $file) {
159
                $this->readConfigFile($package, $file);
160
            }
161
            unset($files['defines']);
162
        }
163
164
        if (isset($files['params'])) {
165
            foreach ((array) $files['params'] as $file) {
166
                $this->rawParams[] = $this->readConfigFile($package, $file);
167
            }
168
            unset($files['params']);
169
        }
170
171
        $this->raw[$package->getPrettyName()] = [
172
            'package' => $package,
173
            'extension' => $extension,
174
            'aliases' => $aliases,
175
            'files' => (array) $files,
176
        ];
177
    }
178
179
    public function assembleParams()
180
    {
181
        $this->assembleFile('params', $this->rawParams);
182
    }
183
184
    public function assembleConfigs()
185
    {
186
        $rawConfigs = [
187
            'aliases' => [],
188
            'extensions' => [],
189
        ];
190
191
        foreach ($this->raw as $name => $info) {
192
            $rawConfigs['extensions'][] = [
193
                $name => $info['extension'],
194
            ];
195
196
            $aliases = $info['aliases'];
197
            $rawConfigs['aliases'][] = $aliases;
198
199
            foreach ($info['files'] as $name => $pathes) {
200
                foreach ((array) $pathes as $path) {
201
                    $rawConfigs[$name][] = $this->readConfigFile($info['package'], $path);
202
                }
203
            }
204
        }
205
206
        foreach ($rawConfigs as $name => $configs) {
207
            if (!in_array($name, ['params', 'aliases', 'extensions'], true)) {
208
                $configs[] = [
209
                    'params' => $this->data['params'],
210
                    'aliases' => $this->data['aliases'],
211
                ];
212
            }
213
            $this->assembleFile($name, $configs);
214
        }
215
    }
216
217
    protected function assembleFile($name, array $configs)
218
    {
219
        $this->data[$name] = call_user_func_array([Helper::class, 'mergeConfig'], $configs);
220
        $this->writeFile($name, (array) $this->data[$name]);
221
    }
222
223
    /**
224
     * Read extra config.
225
     * @param string $file
0 ignored issues
show
Bug introduced by
There is no parameter named $file. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
226
     * @return array
227
     */
228
    protected function readConfigFile(PackageInterface $__package, $__file)
229
    {
230
        $__skippable = false;
231
        if (strncmp($__file, '?', 1) === 0) {
232
            $__skippable = true;
233
            $__file = substr($__file, 1);
234
        }
235
        $__path = $this->preparePath($__package, $__file);
236
        if (!file_exists($__path)) {
237
            if ($__skippable) {
238
                return [];
239
            } else {
240
                $this->io->writeError('<error>Non existent extension config file</error> ' . $__file . ' in ' . $__package->getPrettyName());
241
            }
242
        }
243
        extract($this->data);
244
245
        return (array) require $__path;
246
    }
247
248
    /**
249
     * Prepare aliases.
250
     *
251
     * @param PackageInterface $package
252
     * @param string 'psr-0' or 'psr-4'
253
     * @return array
254
     */
255
    protected function prepareAliases(PackageInterface $package, $psr)
256
    {
257
        $autoload = $package->getAutoload();
258
        if (empty($autoload[$psr])) {
259
            return [];
260
        }
261
262
        $aliases = [];
263
        foreach ($autoload[$psr] as $name => $path) {
264
            if (is_array($path)) {
265
                // ignore psr-4 autoload specifications with multiple search paths
266
                // we can not convert them into aliases as they are ambiguous
267
                continue;
268
            }
269
            $name = str_replace('\\', '/', trim($name, '\\'));
270
            $path = $this->preparePath($package, $path);
271
            $path = $this->substitutePath($path, $this->getBaseDir(), self::BASE_DIR_SAMPLE);
272
            if ('psr-0' === $psr) {
273
                $path .= '/' . $name;
274
            }
275
            $aliases["@$name"] = $path;
276
        }
277
278
        return $aliases;
279
    }
280
281
    /**
282
     * Substitute path with alias if applicable.
283
     * @param string $path
284
     * @param string $dir
285
     * @param string $alias
286
     * @return string
287
     */
288
    public function substitutePath($path, $dir, $alias)
289
    {
290
        return (substr($path, 0, strlen($dir) + 1) === $dir . '/') ? $alias . substr($path, strlen($dir)) : $path;
291
    }
292
293
    /**
294
     * Builds path inside of a package.
295
     * @param PackageInterface $package
296
     * @param mixed $path can be absolute or relative
297
     * @return string absolute pathes will stay untouched
298
     */
299
    public function preparePath(PackageInterface $package, $path)
300
    {
301
        if (!$this->getFilesystem()->isAbsolutePath($path)) {
302
            $prefix = $package instanceof RootPackageInterface ? $this->getBaseDir() : $this->getVendorDir() . '/' . $package->getPrettyName();
303
            $path = $prefix . '/' . $path;
304
        }
305
306
        return $this->getFilesystem()->normalizePath($path);
307
    }
308
309
    /**
310
     * Get output dir.
311
     * @return string
312
     */
313
    public function getOutputDir()
314
    {
315
        return $this->getVendorDir() . DIRECTORY_SEPARATOR . static::OUTPUT_PATH;
316
    }
317
318
    /**
319
     * Build full path to write file for a given filename.
320
     * @param string $filename
321
     * @return string
322
     */
323
    public function buildOutputPath($filename)
324
    {
325
        return $this->getOutputDir() . DIRECTORY_SEPARATOR . $filename . '.php';
326
    }
327
328
    /**
329
     * Writes config file.
330
     * @param string $filename
331
     * @param array $data
332
     */
333
    protected function writeFile($filename, array $data)
334
    {
335
        $path = $this->buildOutputPath($filename);
336
        if (!file_exists(dirname($path))) {
337
            mkdir(dirname($path), 0777, true);
338
        }
339
        $array = str_replace("'" . self::BASE_DIR_SAMPLE, '$baseDir . \'', Helper::exportVar($data));
340
        file_put_contents($path, "<?php\n\n\$baseDir = dirname(dirname(dirname(__DIR__)));\n\nreturn $array;\n");
341
    }
342
343
    /**
344
     * Sets [[packages]].
345
     * @param PackageInterface[] $packages
346
     */
347 2
    public function setPackages(array $packages)
348
    {
349 2
        $this->packages = $packages;
350 2
    }
351
352
    /**
353
     * Gets [[packages]].
354
     * @return \Composer\Package\PackageInterface[]
355
     */
356 1
    public function getPackages()
357
    {
358 1
        if ($this->packages === null) {
359
            $this->packages = $this->findPackages();
360
        }
361
362 1
        return $this->packages;
363
    }
364
365
    protected $plainList = [];
366
    protected $orderedList = [];
367
368
    /**
369
     * Returns ordered list of packages:
370
     * - listed earlier in the composer.json will get earlier in the list
371
     * - childs before parents.
372
     * @return \Composer\Package\PackageInterface[]
373
     */
374
    public function findPackages()
375
    {
376
        $root = $this->composer->getPackage();
377
        $this->plainList[$root->getPrettyName()] = $root;
378
        foreach ($this->composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages() as $package) {
379
            $this->plainList[$package->getPrettyName()] = $package;
380
        }
381
        $this->orderedList = [];
382
        $this->iteratePackage($root, true);
383
        //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...
384
        $res = [];
385
        foreach ($this->orderedList as $name) {
386
            $res[] = $this->plainList[$name];
387
        }
388
389
        return $res;
390
    }
391
392
    /**
393
     * Iterates through package dependencies.
394
     * @param PackageInterface $package to iterate
395
     * @param bool $includingDev process development dependencies, defaults to not process
396
     */
397
    public function iteratePackage(PackageInterface $package, $includingDev = false)
398
    {
399
        $this->iterateDependencies($package);
400
        if ($includingDev) {
401
            $this->iterateDependencies($package, true);
402
        }
403
        $name = $package->getPrettyName();
404
        if (!isset($this->orderedList[$name])) {
405
            $this->orderedList[$name] = $name;
406
        }
407
    }
408
409
    /**
410
     * Iterates dependencies of the given package.
411
     * @param PackageInterface $package
412
     * @param bool $dev which dependencies to iterate: true - dev, default - general
413
     */
414
    public function iterateDependencies(PackageInterface $package, $dev = false)
415
    {
416
        $path = $this->preparePath($package, 'composer.json');
417
        if (file_exists($path)) {
418
            $conf = json_decode(file_get_contents($path), true);
419
            $what = $dev ? 'require-dev' : 'require';
420
            $deps = isset($conf[$what]) ? $conf[$what] : [];
421
        } else {
422
            $deps = $dev ? $package->getDevRequires() : $package->getRequires();
423
        }
424
        foreach (array_keys($deps) as $target) {
425
            if (isset($this->plainList[$target]) && !isset($this->orderedList[$target])) {
426
                $this->iteratePackage($this->plainList[$target]);
427
            }
428
        }
429
    }
430
431
    /**
432
     * Get absolute path to package base dir.
433
     * @return string
434
     */
435
    public function getBaseDir()
436
    {
437
        if ($this->baseDir === null) {
438
            $this->baseDir = dirname($this->getVendorDir());
439
        }
440
441
        return $this->baseDir;
442
    }
443
444
    /**
445
     * Get absolute path to composer vendor dir.
446
     * @return string
447
     */
448
    public function getVendorDir()
449
    {
450
        if ($this->vendorDir === null) {
451
            $dir = $this->composer->getConfig()->get('vendor-dir', '/');
452
            $this->vendorDir = $this->getFilesystem()->normalizePath($dir);
453
        }
454
455
        return $this->vendorDir;
456
    }
457
458
    /**
459
     * Getter for filesystem utility.
460
     * @return Filesystem
461
     */
462
    public function getFilesystem()
463
    {
464
        if ($this->filesystem === null) {
465
            $this->filesystem = new Filesystem();
466
        }
467
468
        return $this->filesystem;
469
    }
470
}
471