Completed
Push — master ( 266fc5...d1fdc7 )
by Andrii
02:40
created

Plugin::substitutePath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

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 2
eloc 2
nc 2
nop 3
crap 6
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
        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->getName(),
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
            $this->readConfigFile($package, $files['defines']);
160
            unset($files['defines']);
161
        }
162
163
        if (isset($files['params'])) {
164
            foreach ((array)$files['params'] as $file) {
165
                $this->rawParams[] = $this->readConfigFile($package, $file);
166
            }
167
            unset($files['params']);
168
        }
169
170
        $this->raw[$package->getName()] = [
171
            'package'   => $package,
172
            'extension' => $extension,
173
            'aliases'   => $aliases,
174
            'files'     => (array) $files,
175
        ];
176
    }
177
178
    public function assembleParams()
179
    {
180
        $this->assembleFile('params', $this->rawParams);
181
    }
182
183
    public function assembleConfigs()
184
    {
185
        $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...
186
        $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...
187
        $rawConfigs = [
188
            'aliases'    => [],
189
            'extensions' => [],
190
        ];
191
192
        foreach ($this->raw as $name => $info) {
193
            $rawConfigs['extensions'][] = [
194
                $name => $info['extension'],
195
            ];
196
197
            $aliases = $info['aliases'];
198
            $rawConfigs['aliases'][] = $aliases;
199
200
            foreach ($info['files'] as $name => $pathes) {
201
                foreach ((array)$pathes as $path) {
202
                    $rawConfigs[$name][] = $this->readConfigFile($info['package'], $path);
203
                }
204
            }
205
        }
206
207
        foreach ($rawConfigs as $name => $configs) {
208
            if (!in_array($name, ['params', 'aliases', 'extensions'], true)) {
209
                $configs[] = [
210
                    'params'  => $this->data['params'],
211
                    'aliases' => $this->data['aliases'],
212
                ];
213
            }
214
            $this->assembleFile($name, $configs);
215
        }
216
    }
217
218
    protected function assembleFile($name, $configs)
219
    {
220
        $this->data[$name] = call_user_func_array([Helper::class, 'mergeConfig'], $configs);
221
        $this->writeFile($name, $this->data[$name]);
222
    }
223
224
    /**
225
     * Read extra config.
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 extension config file</error> ' . $file . ' in ' . $package->getName());
242
                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...
243
            }
244
        }
245
        extract($this->data);
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
    public function preparePath(PackageInterface $package, $path)
295
    {
296
        if (!$this->getFilesystem()->isAbsolutePath($path)) {
297
            $prefix = $package instanceof RootPackageInterface ? $this->getBaseDir() : $this->getVendorDir() . '/' . $package->getPrettyName();
298
            $path = $prefix . '/' . $path;
299
        }
300
301
        return $this->getFilesystem()->normalizePath($path);
302
    }
303
304
    /**
305
     * Get output dir.
306
     * @return string
307
     */
308
    public function getOutputDir()
309
    {
310
        return $this->getVendorDir() . DIRECTORY_SEPARATOR . static::OUTPUT_PATH;
311
    }
312
313
    /**
314
     * Build full path to write file for a given filename.
315
     * @param string $filename
316
     * @return string
317
     */
318
    public function buildOutputPath($filename)
319
    {
320
        return $this->getOutputDir() . DIRECTORY_SEPARATOR . $filename . '.php';
321
    }
322
323
    /**
324
     * Writes file.
325
     * @param string $filename
326
     * @param array $data
327
     */
328
    protected function writeFile($filename, array $data)
329
    {
330
        $path = $this->buildOutputPath($filename);
331
        if (!file_exists(dirname($path))) {
332
            mkdir(dirname($path), 0777, true);
333
        }
334
        $array = str_replace("'" . self::BASE_DIR_SAMPLE, '$baseDir . \'', Helper::exportVar($data));
335
        file_put_contents($path, "<?php\n\n\$baseDir = dirname(dirname(dirname(__DIR__)));\n\nreturn $array;\n");
336
    }
337
338
    /**
339
     * Sets [[packages]].
340
     * @param PackageInterface[] $packages
341
     */
342 2
    public function setPackages(array $packages)
343
    {
344 2
        $this->packages = $packages;
345 2
    }
346
347
    /**
348
     * Gets [[packages]].
349
     * @return \Composer\Package\PackageInterface[]
350
     */
351 1
    public function getPackages()
352
    {
353 1
        if ($this->packages === null) {
354
            $this->packages = $this->composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages();
355
        }
356
357 1
        return $this->packages;
358
    }
359
360
    /**
361
     * Get absolute path to package base dir.
362
     * @return string
363
     */
364
    public function getBaseDir()
365
    {
366
        if ($this->baseDir === null) {
367
            $this->baseDir = dirname($this->getVendorDir());
368
        }
369
370
        return $this->baseDir;
371
    }
372
373
    /**
374
     * Get absolute path to composer vendor dir.
375
     * @return string
376
     */
377
    public function getVendorDir()
378
    {
379
        if ($this->vendorDir === null) {
380
            $dir = $this->composer->getConfig()->get('vendor-dir', '/');
381
            $this->vendorDir = $this->getFilesystem()->normalizePath($dir);
382
        }
383
384
        return $this->vendorDir;
385
    }
386
387
    /**
388
     * Getter for filesystem utility.
389
     * @return Filesystem
390
     */
391
    public function getFilesystem()
392
    {
393
        if ($this->filesystem === null) {
394
            $this->filesystem = new Filesystem();
395
        }
396
397
        return $this->filesystem;
398
    }
399
}
400