Passed
Push — master ( 6faa2a...11074a )
by Alexander
11:54
created

Plugin::scanPackages()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
eloc 3
c 2
b 0
f 0
nc 3
nop 0
dl 0
loc 5
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\Package\PackageFinder;
10
use Yiisoft\Composer\Config\Readers\ReaderFactory;
11
use Composer\Util\Filesystem;
12
use Yiisoft\Composer\Config\Package\AliasesCollector;
13
14
final class Plugin
15
{
16
    /**
17
     * @var Package[] the array of active composer packages
18
     */
19
    private array $packages = [];
20
21
    private array $alternatives = [];
22
23
    private ?string $outputDir = null;
24
25
    private ?Package $rootPackage = null;
26
27
    /**
28
     * @var array config name => list of files
29
     */
30
    private array $files = [
31
        'dotenv' => [],
32
        'defines' => [],
33
        'params' => [],
34
    ];
35
36
    /**
37
     * @var array package name => configs as listed in `composer.json`
38
     */
39
    private array $originalFiles = [];
40
41
    /**
42
     * @var Builder
43
     */
44
    private Builder $builder;
45
46
    /**
47
     * @var Composer instance
48
     */
49
    private Composer $composer;
50
51
    /**
52
     * @var IOInterface
53
     */
54
    private IOInterface $io;
55
56
    private PackageFinder $packageFinder;
57
58
    private AliasesCollector $aliasesCollector;
59
60
    /**
61
     * Initializes the plugin object with the passed $composer and $io.
62
     *
63
     * @param Composer $composer
64
     * @param IOInterface $io
65
     */
66
    public function __construct(Composer $composer, IOInterface $io)
67
    {
68
        $this->builder = new Builder();
69
        $this->packageFinder = new PackageFinder($composer);
70
        $this->aliasesCollector = new AliasesCollector(new Filesystem());
71
        $this->composer = $composer;
72
        $this->io = $io;
73
    }
74
75
    public function build(): void
76
    {
77
        $this->io->overwriteError('<info>Assembling config files</info>');
78
79
        $this->scanPackages();
80
        $this->reorderFiles();
81
82
        $this->builder->setOutputDir($this->outputDir);
83
        $this->builder->buildAllConfigs($this->files);
84
85
        $saveFiles = $this->files;
86
        $saveEnv = $_ENV;
87
        foreach ($this->alternatives as $name => $files) {
88
            $this->files = $saveFiles;
89
            $_ENV = $saveEnv;
90
            $builder = $this->builder->createAlternative($name);
91
            $this->addFiles($this->rootPackage, $files);
0 ignored issues
show
Bug introduced by
It seems like $this->rootPackage can also be of type null; however, parameter $package of Yiisoft\Composer\Config\Plugin::addFiles() does only seem to accept Yiisoft\Composer\Config\Package, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

91
            $this->addFiles(/** @scrutinizer ignore-type */ $this->rootPackage, $files);
Loading history...
92
            $builder->buildAllConfigs($this->files);
93
        }
94
    }
95
96
    private function scanPackages(): void
97
    {
98
        foreach ($this->getPackages() as $package) {
99
            if ($package->isComplete()) {
100
                $this->processPackage($package);
101
            }
102
        }
103
    }
104
105
    private function reorderFiles(): void
106
    {
107
        foreach (array_keys($this->files) as $name) {
108
            $this->files[$name] = $this->getAllFiles($name);
109
        }
110
        foreach ($this->files as $name => $files) {
111
            $this->files[$name] = $this->orderFiles($files);
112
        }
113
    }
114
115
    private function getAllFiles(string $name, array $stack = []): array
116
    {
117
        if (empty($this->files[$name])) {
118
            return[];
119
        }
120
        $res = [];
121
        foreach ($this->files[$name] as $file) {
122
            if (strncmp($file, '$', 1) === 0) {
123
                if (!in_array($name, $stack, true)) {
124
                    $res = array_merge($res, $this->getAllFiles(substr($file, 1), array_merge($stack, [$name])));
125
                }
126
            } else {
127
                $res[] = $file;
128
            }
129
        }
130
131
        return $res;
132
    }
133
134
    private function orderFiles(array $files): array
135
    {
136
        if (empty($files)) {
137
            return [];
138
        }
139
        $keys = array_combine($files, $files);
140
        $res = [];
141
        foreach ($this->orderedFiles as $file) {
142
            if (isset($keys[$file])) {
143
                $res[$file] = $file;
144
            }
145
        }
146
147
        return array_values($res);
148
    }
149
150
    /**
151
     * Scans the given package and collects packages data.
152
     * @param Package $package
153
     */
154
    private function processPackage(Package $package)
155
    {
156
        $files = $package->getFiles();
157
        $this->originalFiles[$package->getPrettyName()] = $files;
158
159
        if (!empty($files)) {
160
            $this->addFiles($package, $files);
161
        }
162
        if ($package->isRoot()) {
163
            $this->rootPackage = $package;
164
            $this->loadDotEnv($package);
165
            $devFiles = $package->getDevFiles();
166
            if (!empty($devFiles)) {
167
                $this->addFiles($package, $devFiles);
168
            }
169
            $this->outputDir = $package->getOutputDir();
170
            $alternatives = $package->getAlternatives();
171
            if (is_string($alternatives)) {
0 ignored issues
show
introduced by
The condition is_string($alternatives) is always false.
Loading history...
172
                $this->alternatives = $this->readConfig($package, $alternatives);
173
            } elseif (is_array($alternatives)) {
0 ignored issues
show
introduced by
The condition is_array($alternatives) is always true.
Loading history...
174
                $this->alternatives = $alternatives;
175
            } elseif (!empty($alternatives)) {
176
                throw new BadConfigurationException('Alternatives must be array or path to configuration file.');
177
            }
178
        }
179
180
        $aliases = $this->aliasesCollector->collect($package);
181
182
        $this->builder->mergeAliases($aliases);
183
        $this->builder->setPackage($package->getPrettyName(), array_filter([
184
            'name' => $package->getPrettyName(),
185
            'version' => $package->getVersion(),
186
            'reference' => $package->getSourceReference() ?: $package->getDistReference(),
187
            'aliases' => $aliases,
188
        ]));
189
    }
190
191
    private function readConfig($package, $file): array
192
    {
193
        $path = $package->preparePath($file);
194
        if (!file_exists($path)) {
195
            throw new FailedReadException("failed read file: $file");
196
        }
197
        $reader = ReaderFactory::get($this->builder, $path);
198
199
        return $reader->read($path);
200
    }
201
202
    private function loadDotEnv(Package $package): void
203
    {
204
        $path = $package->preparePath('.env');
205
        if (file_exists($path) && class_exists('Dotenv\Dotenv')) {
206
            $this->addFile($package, 'dotenv', $path);
207
        }
208
    }
209
210
    /**
211
     * Adds given files to the list of files to be processed.
212
     * Prepares `defines` in reversed order (outer package first) because
213
     * constants cannot be redefined.
214
     * @param Package $package
215
     * @param array $files
216
     */
217
    private function addFiles(Package $package, array $files): void
218
    {
219
        foreach ($files as $name => $paths) {
220
            $paths = (array) $paths;
221
            if ('defines' === $name) {
222
                $paths = array_reverse($paths);
223
            }
224
            foreach ($paths as $path) {
225
                $this->addFile($package, $name, $path);
226
            }
227
        }
228
    }
229
230
    private array $orderedFiles = [];
231
232
    private function addFile(Package $package, string $name, string $path): void
233
    {
234
        $path = $package->preparePath($path);
235
        if (!isset($this->files[$name])) {
236
            $this->files[$name] = [];
237
        }
238
        if (in_array($path, $this->files[$name], true)) {
239
            return;
240
        }
241
        if ('defines' === $name) {
242
            array_unshift($this->orderedFiles, $path);
243
            array_unshift($this->files[$name], $path);
244
        } else {
245
            $this->orderedFiles[] = $path;
246
            $this->files[$name][] = $path;
247
        }
248
    }
249
250
    /**
251
     * Gets [[packages]].
252
     * @return Package[]
253
     */
254
    private function getPackages(): array
255
    {
256
        if ([] === $this->packages) {
257
            $this->packages = $this->packageFinder->findPackages();
258
        }
259
260
        return $this->packages;
261
    }
262
}
263