Passed
Pull Request — master (#1)
by Alexander
10:30
created

Plugin::orderFiles()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 4
nop 1
dl 0
loc 14
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Yiisoft\Composer\Config;
4
5
use Composer\Composer;
6
use Composer\EventDispatcher\EventSubscriberInterface;
7
use Composer\IO\IOInterface;
8
use Composer\Plugin\PluginInterface;
9
use Composer\Script\Event;
10
use Composer\Script\ScriptEvents;
11
use Yiisoft\Composer\Config\exceptions\BadConfigurationException;
12
use Yiisoft\Composer\Config\exceptions\FailedReadException;
13
use Yiisoft\Composer\Config\readers\ReaderFactory;
14
15
/**
16
 * Plugin class.
17
 */
18
class Plugin implements PluginInterface, EventSubscriberInterface
19
{
20
    /**
21
     * @var Package[] the array of active composer packages
22
     */
23
    protected $packages;
24
25
    private $alternatives = [];
26
27
    private $outputDir;
28
29
    private $rootPackage;
30
31
    /**
32
     * @var array config name => list of files
33
     */
34
    protected $files = [
35
        'dotenv'  => [],
36
        'defines' => [],
37
        'params'  => [],
38
    ];
39
40
    protected $colors = ['red', 'green', 'yellow', 'cyan', 'magenta', 'blue'];
41
42
    /**
43
     * @var array package name => configs as listed in `composer.json`
44
     */
45
    protected $originalFiles = [];
46
47
    /**
48
     * @var Builder
49
     */
50
    protected $builder;
51
52
    /**
53
     * @var Composer instance
54
     */
55
    protected $composer;
56
57
    /**
58
     * @var IOInterface
59
     */
60
    public $io;
61
62
    /**
63
     * Initializes the plugin object with the passed $composer and $io.
64
     * @param Composer $composer
65
     * @param IOInterface $io
66
     */
67
    public function activate(Composer $composer, IOInterface $io)
68
    {
69
        $this->composer = $composer;
70
        $this->io = $io;
71
    }
72
73
    /**
74
     * Returns list of events the plugin is subscribed to.
75
     * @return array list of events
76
     */
77
    public static function getSubscribedEvents(): array
78
    {
79
        return [
80
            ScriptEvents::POST_AUTOLOAD_DUMP => [
81
                ['onPostAutoloadDump', 0],
82
            ],
83
        ];
84
    }
85
86
    /**
87
     * This is the main function.
88
     */
89
    public function onPostAutoloadDump(Event $event): void
90
    {
91
        $this->io->overwriteError('<info>Assembling config files</info>');
92
93
        $this->builder = new Builder();
94
95
        require_once $event->getComposer()->getConfig()->get('vendor-dir') . '/autoload.php';
96
        $this->scanPackages();
97
        $this->reorderFiles();
98
        $this->showDepsTree();
99
100
        $this->builder->setOutputDir($this->outputDir);
101
        $this->builder->buildAllConfigs($this->files);
102
103
        $saveFiles = $this->files;
104
        $saveEnv = $_ENV;
105
        foreach ($this->alternatives as $name => $files) {
106
            $this->files = $saveFiles;
107
            $_ENV = $saveEnv;
108
            $builder = $this->builder->createAlternative($name);
109
            $this->addFiles($this->rootPackage, $files);
110
            $builder->buildAllConfigs($this->files);
111
        }
112
    }
113
114
    protected function scanPackages(): void
115
    {
116
        foreach ($this->getPackages() as $package) {
117
            if ($package->isComplete()) {
118
                $this->processPackage($package);
119
            }
120
        }
121
    }
122
123
    protected function reorderFiles(): void
124
    {
125
        foreach (array_keys($this->files) as $name) {
126
            $this->files[$name] = $this->getAllFiles($name);
127
        }
128
        foreach ($this->files as $name => $files) {
129
            $this->files[$name] = $this->orderFiles($files);
130
        }
131
    }
132
133
    protected function getAllFiles(string $name, array $stack = []): array
134
    {
135
        if (empty($this->files[$name])) {
136
            return[];
137
        }
138
        $res = [];
139
        foreach ($this->files[$name] as $file) {
140
            if (strncmp($file, '$', 1) === 0) {
141
                if (!in_array($name, $stack, true)) {
142
                    $res = array_merge($res, $this->getAllFiles(substr($file, 1), array_merge($stack, [$name])));
143
                }
144
            } else {
145
                $res[] = $file;
146
            }
147
        }
148
149
        return $res;
150
    }
151
152
    protected function orderFiles(array $files): array
153
    {
154
        if (empty($files)) {
155
            return [];
156
        }
157
        $keys = array_combine($files, $files);
158
        $res = [];
159
        foreach ($this->orderedFiles as $file) {
160
            if (isset($keys[$file])) {
161
                $res[$file] = $file;
162
            }
163
        }
164
165
        return array_values($res);
166
    }
167
168
    /**
169
     * Scans the given package and collects packages data.
170
     * @param Package $package
171
     */
172
    protected function processPackage(Package $package)
173
    {
174
        $files = $package->getFiles();
175
        $this->originalFiles[$package->getPrettyName()] = $files;
176
177
        if (!empty($files)) {
178
            $this->addFiles($package, $files);
179
        }
180
        if ($package->isRoot()) {
181
            $this->rootPackage = $package;
182
            $this->loadDotEnv($package);
183
            $devFiles = $package->getDevFiles();
184
            if (!empty($devFiles)) {
185
                $this->addFiles($package, $devFiles);
186
            }
187
            $this->outputDir = $package->getOutputDir();
188
            $alternatives = $package->getAlternatives();
189
            if (is_string($alternatives)) {
0 ignored issues
show
introduced by
The condition is_string($alternatives) is always false.
Loading history...
190
                $this->alternatives = $this->readConfig($package, $alternatives);
191
            } elseif (is_array($alternatives)) {
0 ignored issues
show
introduced by
The condition is_array($alternatives) is always true.
Loading history...
192
                $this->alternatives = $alternatives;
193
            } elseif (!empty($alternatives)) {
194
                throw new BadConfigurationException('Alternatives must be array or path to configuration file.');
195
            }
196
        }
197
198
        $aliases = $package->collectAliases();
199
200
        $this->builder->mergeAliases($aliases);
201
        $this->builder->setPackage($package->getPrettyName(), array_filter([
202
            'name' => $package->getPrettyName(),
203
            'version' => $package->getVersion(),
204
            'reference' => $package->getSourceReference() ?: $package->getDistReference(),
205
            'aliases' => $aliases,
206
        ]));
207
    }
208
209
    private function readConfig($package, $file): array
210
    {
211
        $path = $package->preparePath($file);
212
        if (!file_exists($path)) {
213
            throw new FailedReadException("failed read file: $file");
214
        }
215
        $reader = ReaderFactory::get($this->builder, $path);
216
217
        return $reader->read($path);
218
    }
219
220
    protected function loadDotEnv(Package $package): void
221
    {
222
        $path = $package->preparePath('.env');
223
        if (file_exists($path) && class_exists('Dotenv\Dotenv')) {
224
            $this->addFile($package, 'dotenv', $path);
225
        }
226
    }
227
228
    /**
229
     * Adds given files to the list of files to be processed.
230
     * Prepares `defines` in reversed order (outer package first) because
231
     * constants cannot be redefined.
232
     * @param Package $package
233
     * @param array $files
234
     */
235
    protected function addFiles(Package $package, array $files): void
236
    {
237
        foreach ($files as $name => $paths) {
238
            $paths = (array) $paths;
239
            if ('defines' === $name) {
240
                $paths = array_reverse($paths);
241
            }
242
            foreach ($paths as $path) {
243
                $this->addFile($package, $name, $path);
244
            }
245
        }
246
    }
247
248
    protected $orderedFiles = [];
249
250
    protected function addFile(Package $package, string $name, string $path): void
251
    {
252
        $path = $package->preparePath($path);
253
        if (!isset($this->files[$name])) {
254
            $this->files[$name] = [];
255
        }
256
        if (in_array($path, $this->files[$name], true)) {
257
            return;
258
        }
259
        if ('defines' === $name) {
260
            array_unshift($this->orderedFiles, $path);
261
            array_unshift($this->files[$name], $path);
262
        } else {
263
            $this->orderedFiles[] = $path;
264
            $this->files[$name][] = $path;
265
        }
266
    }
267
268
    /**
269
     * Sets [[packages]].
270
     * @param Package[] $packages
271
     */
272
    public function setPackages(array $packages): void
273
    {
274
        $this->packages = $packages;
275
    }
276
277
    /**
278
     * Gets [[packages]].
279
     * @return Package[]
280
     */
281
    public function getPackages(): array
282
    {
283
        if (null === $this->packages) {
284
            $this->packages = $this->findPackages();
285
        }
286
287
        return $this->packages;
288
    }
289
290
    /**
291
     * Plain list of all project dependencies (including nested) as provided by composer.
292
     * The list is unordered (chaotic, can be different after every update).
293
     */
294
    protected $plainList = [];
295
296
    /**
297
     * Ordered list of package in form: package => depth
298
     * For order description @see findPackages.
299
     */
300
    protected $orderedList = [];
301
302
    /**
303
     * Returns ordered list of packages:
304
     * - listed earlier in the composer.json will get earlier in the list
305
     * - childs before parents.
306
     * @return Package[]
307
     */
308
    public function findPackages(): array
309
    {
310
        $root = new Package($this->composer->getPackage(), $this->composer);
311
        $this->plainList[$root->getPrettyName()] = $root;
312
        foreach ($this->composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages() as $package) {
313
            $this->plainList[$package->getPrettyName()] = new Package($package, $this->composer);
314
        }
315
        $this->orderedList = [];
316
        $this->iteratePackage($root, true);
317
318
        $res = [];
319
        foreach (array_keys($this->orderedList) as $name) {
320
            $res[] = $this->plainList[$name];
321
        }
322
323
        return $res;
324
    }
325
326
    /**
327
     * Iterates through package dependencies.
328
     * @param Package $package to iterate
329
     * @param bool $includingDev process development dependencies, defaults to not process
330
     */
331
    protected function iteratePackage(Package $package, bool $includingDev = false): void
332
    {
333
        $name = $package->getPrettyName();
334
335
        /// prevent infinite loop in case of circular dependencies
336
        static $processed = [];
337
        if (isset($processed[$name])) {
338
            return;
339
        }
340
341
        $processed[$name] = 1;
342
343
        /// package depth in dependency hierarchy
344
        static $depth = 0;
345
        ++$depth;
346
347
        $this->iterateDependencies($package);
348
        if ($includingDev) {
349
            $this->iterateDependencies($package, true);
350
        }
351
        if (!isset($this->orderedList[$name])) {
352
            $this->orderedList[$name] = $depth;
353
        }
354
355
        --$depth;
356
    }
357
358
    /**
359
     * Iterates dependencies of the given package.
360
     * @param Package $package
361
     * @param bool $dev which dependencies to iterate: true - dev, default - general
362
     */
363
    protected function iterateDependencies(Package $package, bool $dev = false): void
364
    {
365
        $deps = $dev ? $package->getDevRequires() : $package->getRequires();
366
        foreach (array_keys($deps) as $target) {
367
            if (isset($this->plainList[$target]) && empty($this->orderedList[$target])) {
368
                $this->iteratePackage($this->plainList[$target]);
369
            }
370
        }
371
    }
372
373
    protected function showDepsTree(): void
374
    {
375
        if (!$this->io->isVerbose()) {
376
            return;
377
        }
378
379
        foreach (array_reverse($this->orderedList) as $name => $depth) {
380
            $deps = $this->originalFiles[$name];
381
            $color = $this->colors[$depth % count($this->colors)];
382
            $indent = str_repeat('   ', $depth - 1);
383
            $package = $this->plainList[$name];
384
            $showdeps = $deps ? '<comment>[' . implode(',', array_keys($deps)) . ']</>' : '';
385
            $this->io->write(sprintf('%s - <fg=%s;options=bold>%s</> %s %s', $indent, $color, $name, $package->getFullPrettyVersion(), $showdeps));
386
        }
387
    }
388
}
389