Completed
Push — master ( 5de825...c87cd0 )
by Andrii
02:23
created

Plugin::getOutputDir()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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