Completed
Push — master ( e44765...ab86c3 )
by Andrii
04:18
created

Plugin::assembleConfigs()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 32
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 32
rs 8.439
c 0
b 0
f 0
ccs 0
cts 24
cp 0
cc 6
eloc 18
nc 12
nop 0
crap 42
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\composer\config;
13
14
use Composer\Composer;
15
use Composer\EventDispatcher\EventSubscriberInterface;
16
use Composer\IO\IOInterface;
17
use Composer\Package\PackageInterface;
18
use Composer\Package\CompletePackageInterface;
19
use Composer\Package\RootPackageInterface;
20
use Composer\Plugin\PluginInterface;
21
use Composer\Script\Event;
22
use Composer\Script\ScriptEvents;
23
use Composer\Util\Filesystem;
24
25
/**
26
 * Plugin class.
27
 *
28
 * @author Andrii Vasyliev <[email protected]>
29
 */
30
class Plugin implements PluginInterface, EventSubscriberInterface
31
{
32
    const OUTPUT_DIR = 'output';
33
34
    const PACKAGE_TYPE = 'yii2-extension';
35
    const EXTRA_OPTION_NAME = 'config-plugin';
36
    const VENDOR_DIR_SAMPLE = '<base-dir>/vendor';
37
38
    /**
39
     * @var PackageInterface[] the array of active composer packages
40
     */
41
    protected $packages;
42
43
    /**
44
     * @var string absolute path to the package base directory
45
     */
46
    protected $baseDir;
47
48
    /**
49
     * @var string absolute path to vendor directory
50
     */
51
    protected $vendorDir;
52
53
    /**
54
     * @var Filesystem utility
55
     */
56
    protected $filesystem;
57
58
    /**
59
     * @var array assembled config data
60
     */
61
    protected $data = [
62
        'aliases' => [],
63
        'extensions' => [],
64
    ];
65
66
    /**
67
     * @var array raw collected data
68
     */
69
    protected $raw = [];
70
71
    /**
72
     * @var array array of not yet merged params
73
     */
74
    protected $rawParams = [];
75
76
    /**
77
     * @var Composer instance
78
     */
79
    protected $composer;
80
81
    /**
82
     * @var IOInterface
83
     */
84
    public $io;
85
86
    /**
87
     * Initializes the plugin object with the passed $composer and $io.
88
     * @param Composer $composer
89
     * @param IOInterface $io
90 2
     */
91
    public function activate(Composer $composer, IOInterface $io)
92 2
    {
93 2
        $this->composer = $composer;
94 2
        $this->io = $io;
95
    }
96
97
    /**
98
     * Returns list of events the plugin is subscribed to.
99
     * @return array list of events
100 1
     */
101
    public static function getSubscribedEvents()
102
    {
103 1
        return [
104 1
            ScriptEvents::POST_AUTOLOAD_DUMP => [
105 1
                ['onPostAutoloadDump', 0],
106 1
            ],
107
        ];
108
    }
109
110
    /**
111
     * This is the main function.
112
     * @param Event $event
113
     */
114
    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...
115
    {
116
        $this->io->writeError('<info>Assembling config files</info>');
117
        $this->io->writeError('<info>Assembling config files</info>');
118
119
        /// scan packages
120
        foreach ($this->getPackages() as $package) {
121
            if ($package instanceof CompletePackageInterface) {
122
                $this->processPackage($package);
123
            }
124
        }
125
        $this->processPackage($this->composer->getPackage());
126
127
        var_dump($this->raw);die('sfdasfsa');
0 ignored issues
show
Security Debugging Code introduced by
var_dump($this->raw); looks like debug code. Are you sure you do not want to remove it? This might expose sensitive data.
Loading history...
Coding Style Compatibility introduced by
The method onPostAutoloadDump() 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...
128
129
        $this->assembleParams();
0 ignored issues
show
Unused Code introduced by
$this->assembleParams(); does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
130
        $this->assembleConfigs();
131
    }
132
133
    /**
134
     * Scans the given package and collects extensions data.
135
     * @param PackageInterface $package
136
     */
137
    public function processPackage(PackageInterface $package)
138
    {
139
        $extra = $package->getExtra();
140
        $files = isset($extra[self::EXTRA_OPTION_NAME]) ? $extra[self::EXTRA_OPTION_NAME] : null;
141
        if ($package->getType() !== self::PACKAGE_TYPE && is_null($files)) {
142
            return;
143
        }
144
145
        $extension = [
146
            'name' => $package->getPrettyName(),
147
            'version' => $package->getVersion(),
148
        ];
149
        if ($package->getVersion() === '9999999-dev') {
150
            $reference = $package->getSourceReference() ?: $package->getDistReference();
151
            if ($reference) {
152
                $extension['reference'] = $reference;
153
            }
154
        }
155
156
        $aliases = array_merge(
157
            $this->prepareAliases($package, 'psr-0'),
158
            $this->prepareAliases($package, 'psr-4')
159
        );
160
161
        if (isset($files['defines'])) {
162
            foreach ((array) $files['defines'] as $file) {
163
                $this->readConfigFile($package, $file);
164
            }
165
            unset($files['defines']);
166
        }
167
168
        if (isset($files['params'])) {
169
            foreach ((array) $files['params'] as $file) {
170
                $this->rawParams[] = $this->readConfigFile($package, $file);
171
            }
172
            unset($files['params']);
173
        }
174
175
        $this->raw[$package->getPrettyName()] = [
176
            'package' => $package,
177
            'extension' => $extension,
178
            'aliases' => $aliases,
179
            'files' => (array) $files,
180
        ];
181
    }
182
183
    public function assembleParams()
184
    {
185
        $this->assembleFile('params', $this->rawParams);
0 ignored issues
show
Bug introduced by
The method assembleFile() does not seem to exist on object<hiqdev\composer\config\Plugin>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
186
    }
187
188
    public function assembleConfigs()
189
    {
190
        $rawConfigs = [
191
            'aliases' => [],
192
            'extensions' => [],
193
        ];
194
195
        foreach ($this->raw as $name => $info) {
196
            $rawConfigs['extensions'][] = [
197
                $name => $info['extension'],
198
            ];
199
200
            $aliases = $info['aliases'];
201
            $rawConfigs['aliases'][] = $aliases;
202
203
            foreach ($info['files'] as $name => $pathes) {
204
                foreach ((array) $pathes as $path) {
205
                    $rawConfigs[$name][] = $this->readConfigFile($info['package'], $path);
206
                }
207
            }
208
        }
209
210
        foreach ($rawConfigs as $name => $configs) {
211
            if (!in_array($name, ['params', 'aliases', 'extensions'], true)) {
212
                $configs[] = [
213
                    'params' => $this->data['params'],
214
                    'aliases' => $this->data['aliases'],
215
                ];
216
            }
217
            $this->assembleFile($name, $configs);
0 ignored issues
show
Bug introduced by
The method assembleFile() does not seem to exist on object<hiqdev\composer\config\Plugin>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

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