Completed
Push — master ( c1e757...7f5dc4 )
by Andrii
02:29
created

Plugin::processPackage()   D

Complexity

Conditions 9
Paths 32

Size

Total Lines 31
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 8
Bugs 2 Features 0
Metric Value
c 8
b 2
f 0
dl 0
loc 31
ccs 0
cts 25
cp 0
rs 4.909
cc 9
eloc 20
nc 32
nop 1
crap 90
1
<?php
2
3
/*
4
 * Composer plugin for pluggable extensions
5
 *
6
 * @link      https://github.com/hiqdev/composer-extension-plugin
7
 * @package   composer-extension-plugin
8
 * @license   BSD-3-Clause
9
 * @copyright Copyright (c) 2016, HiQDev (http://hiqdev.com/)
10
 */
11
12
namespace hiqdev\composerextensionplugin;
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     = 'extension-plugin';
33
    const OUTPUT_PATH           = 'hiqdev';
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 whole data
59
     */
60
    protected $data = [
61
        'extensions' => [],
62
        'common' => [
63
            'aliases' => [
64
                '@vendor' => self::VENDOR_DIR_SAMPLE,
65
            ],
66
        ],
67
    ];
68
69
    /**
70
     * @var Composer instance
71
     */
72
    protected $composer;
73
74
    /**
75
     * @var IOInterface
76
     */
77
    public $io;
78
79
    /**
80
     * Initializes the plugin object with the passed $composer and $io.
81
     * @param Composer $composer
82
     * @param IOInterface $io
83
     */
84 2
    public function activate(Composer $composer, IOInterface $io)
85
    {
86 2
        $this->composer = $composer;
87 2
        $this->io = $io;
88 2
    }
89
90
    /**
91
     * Returns list of events the plugin is subscribed to.
92
     * @return array list of events
93
     */
94 1
    public static function getSubscribedEvents()
95
    {
96
        return [
97 1
            ScriptEvents::POST_AUTOLOAD_DUMP => [
98 1
                ['onPostAutoloadDump', 0],
99 1
            ],
100 1
        ];
101
    }
102
103
    /**
104
     * Simply rewrites extensions file from scratch.
105
     * @param Event $event
106
     */
107
    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...
108
    {
109
        $this->io->writeError('<info>Generating extensions files</info>');
110
        $this->processPackage($this->composer->getPackage());
111
        foreach ($this->getPackages() as $package) {
112
            if ($package instanceof \Composer\Package\CompletePackageInterface) {
113
                $this->processPackage($package);
114
            }
115
        }
116
117
        foreach ($this->data as $name => $data) {
118
            $this->saveFile($this->buildOutputPath($name), $data);
119
        }
120
    }
121
122
    public function buildOutputPath($name)
123
    {
124
        return static::OUTPUT_PATH . DIRECTORY_SEPARATOR . $name . '.php';
125
    }
126
127
    /**
128
     * Writes file.
129
     * @param string $file
130
     * @param array $data
131
     */
132
    protected function saveFile($file, array $data)
133
    {
134
        $path = $this->getVendorDir() . '/' . $file;
135
        if (!file_exists(dirname($path))) {
136
            mkdir(dirname($path), 0777, true);
137
        }
138
        $array = str_replace("'" . self::BASE_DIR_SAMPLE, '$baseDir . \'', var_export($data, true));
139
        file_put_contents($path, "<?php\n\n\$baseDir = dirname(dirname(__DIR__));\n\nreturn $array;\n");
140
    }
141
142
    /**
143
     * Scans the given package and collects extensions data.
144
     * @param PackageInterface $package
145
     */
146
    public function processPackage(PackageInterface $package)
147
    {
148
        $extra = $package->getExtra();
149
        $files = isset($extra[self::EXTRA_OPTION_NAME]) ? $extra[self::EXTRA_OPTION_NAME] : [];
150
        if ($package->getType() !== self::PACKAGE_TYPE && empty($files)) {
151
            return;
152
        }
153
154
        $extension = [
155
            'name'    => $package->getName(),
156
            'version' => $package->getVersion(),
157
        ];
158
        if ($package->getVersion() === '9999999-dev') {
159
            $reference = $package->getSourceReference() ?: $package->getDistReference();
160
            if ($reference) {
161
                $extension['reference'] = $reference;
162
            }
163
        }
164
        $this->data['extensions'][$package->getName()] = $extension;
165
166
        $this->data['common']['aliases'] = array_merge(
167
            $this->data['common']['aliases'],
168
            $this->prepareAliases($package, 'psr-0'),
169
            $this->prepareAliases($package, 'psr-4')
170
        );
171
172
        foreach ($files as $name => $path) {
173
            $config = $this->readExtraConfig($package, $path);
174
            $this->data[$name] = isset($this->data[$name]) ? static::mergeConfig($this->data[$name], $config) : $config;
175
        }
176
    }
177
178
    /**
179
     * Merges two or more arrays into one recursively.
180
     * Based on Yii2 yii\helpers\BaseArrayHelper::merge.
181
     * @param array $a array to be merged to
182
     * @param array $b array to be merged from
183
     * @return array the merged array
184
     */
185
    public static function mergeConfig($a, $b)
0 ignored issues
show
Unused Code introduced by
The parameter $a 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...
Unused Code introduced by
The parameter $b 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...
186
    {
187
        $args = func_get_args();
188
        $res = array_shift($args);
189
        foreach ($args as $items) {
190
            if (!is_array($items)) {
191
                continue;
192
            }
193
            foreach ($items as $k => $v) {
194
                if (is_int($k)) {
195
                    if (isset($res[$k])) {
196
                        $res[] = $v;
197
                    } else {
198
                        $res[$k] = $v;
199
                    }
200
                } elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) {
201
                    $res[$k] = self::mergeConfig($res[$k], $v);
202
                } else {
203
                    $res[$k] = $v;
204
                }
205
            }
206
        }
207
208
        return $res;
209
    }
210
211
    /**
212
     * Read extra config.
213
     * @param string $file
214
     * @return array
215
     */
216
    protected function readExtraConfig(PackageInterface $package, $file)
217
    {
218
        $path = $this->preparePath($package, $file);
219
        if (!file_exists($path)) {
220
            $this->io->writeError('<error>Non existent extraconfig file</error> ' . $file . ' in ' . $package->getName());
221
            exit(1);
0 ignored issues
show
Coding Style Compatibility introduced by
The method readExtraConfig() 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...
222
        }
223
        return require $path;
224
    }
225
226
    /**
227
     * Prepare aliases.
228
     *
229
     * @param PackageInterface $package
230
     * @param string 'psr-0' or 'psr-4'
231
     * @return array
232
     */
233
    protected function prepareAliases(PackageInterface $package, $psr)
234
    {
235
        $autoload = $package->getAutoload();
236
        if (empty($autoload[$psr])) {
237
            return [];
238
        }
239
240
        $aliases = [];
241
        foreach ($autoload[$psr] as $name => $path) {
242
            if (is_array($path)) {
243
                // ignore psr-4 autoload specifications with multiple search paths
244
                // we can not convert them into aliases as they are ambiguous
245
                continue;
246
            }
247
            $name = str_replace('\\', '/', trim($name, '\\'));
248
            $path = $this->preparePath($package, $path);
249
            $path = $this->substitutePath($path, $this->getBaseDir(), self::BASE_DIR_SAMPLE);
250
            if ('psr-0' === $psr) {
251
                $path .= '/' . $name;
252
            }
253
            $aliases["@$name"] = $path;
254
        }
255
256
        return $aliases;
257
    }
258
259
    /**
260
     * Substitute path with alias if applicable.
261
     * @param string $path
262
     * @param string $dir
263
     * @param string $alias
264
     * @return string
265
     */
266
    public function substitutePath($path, $dir, $alias)
267
    {
268
        return (substr($path, 0, strlen($dir) + 1) === $dir . '/') ? $alias . substr($path, strlen($dir)) : $path;
269
    }
270
271
    public function preparePath(PackageInterface $package, $path)
272
    {
273
        if (!$this->getFilesystem()->isAbsolutePath($path)) {
274
            $prefix = $package instanceof RootPackageInterface ? $this->getBaseDir() : $this->getVendorDir() . '/' . $package->getPrettyName();
275
            $path = $prefix . '/' . $path;
276
        }
277
278
        return $this->getFilesystem()->normalizePath($path);
279
    }
280
281
    /**
282
     * Sets [[packages]].
283
     * @param PackageInterface[] $packages
284
     */
285 2
    public function setPackages(array $packages)
286
    {
287 2
        $this->packages = $packages;
288 2
    }
289
290
    /**
291
     * Gets [[packages]].
292
     * @return \Composer\Package\PackageInterface[]
293
     */
294 1
    public function getPackages()
295
    {
296 1
        if ($this->packages === null) {
297
            $this->packages = $this->composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages();
298
        }
299
300 1
        return $this->packages;
301
    }
302
303
    /**
304
     * Get absolute path to package base dir.
305
     * @return string
306
     */
307
    public function getBaseDir()
308
    {
309
        if ($this->baseDir === null) {
310
            $this->baseDir = dirname($this->getVendorDir());
311
        }
312
313
        return $this->baseDir;
314
    }
315
316
    /**
317
     * Get absolute path to composer vendor dir.
318
     * @return string
319
     */
320
    public function getVendorDir()
321
    {
322
        if ($this->vendorDir === null) {
323
            $dir = $this->composer->getConfig()->get('vendor-dir', '/');
324
            $this->vendorDir = $this->getFilesystem()->normalizePath($dir);
325
        }
326
327
        return $this->vendorDir;
328
    }
329
330
    /**
331
     * Getter for filesystem utility.
332
     * @return Filesystem
333
     */
334
    public function getFilesystem()
335
    {
336
        if ($this->filesystem === null) {
337
            $this->filesystem = new Filesystem();
338
        }
339
340
        return $this->filesystem;
341
    }
342
}
343