Passed
Pull Request — master (#13)
by Dmitriy
19:37 queued 04:56
created

Plugin::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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