Passed
Pull Request — master (#67)
by Dmitriy
36:12 queued 21:07
created

Plugin::addFile()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
eloc 11
c 0
b 0
f 0
dl 0
loc 15
ccs 0
cts 13
cp 0
rs 9.9
cc 4
nc 6
nop 3
crap 20
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Composer\Config;
6
7
use Composer\Composer;
8
use Composer\IO\IOInterface;
9
use Composer\Util\Filesystem;
10
use Dotenv\Dotenv;
0 ignored issues
show
Bug introduced by
The type Dotenv\Dotenv was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
11
use Yiisoft\Composer\Config\Config\ConfigFactory;
12
use Yiisoft\Composer\Config\Exception\BadConfigurationException;
13
use Yiisoft\Composer\Config\Exception\FailedReadException;
14
use Yiisoft\Composer\Config\Package\AliasesCollector;
15
use Yiisoft\Composer\Config\Package\PackageFinder;
16
use Yiisoft\Composer\Config\Reader\ReaderFactory;
17
18
final class Plugin
19
{
20
    /**
21
     * @var Package[] the array of active composer packages
22
     */
23
    private array $packages;
24
25
    private array $alternatives = [];
26
27
    private ?Package $rootPackage = null;
28
29
    /**
30
     * @var array config name => list of files
31
     * Important: defines config files processing order:
32
     * envs then constants then params then other configs
33
     */
34
    private array $files = [
35
        'envs' => [],
36
        'constants' => [],
37
        'params' => [],
38
    ];
39
40
    /**
41
     * @var array package name => configs as listed in `composer.json`
42
     */
43
    private array $originalFiles = [];
44
45
    private Builder $builder;
46
47
    private IOInterface $io;
48
49
    private AliasesCollector $aliasesCollector;
50
51
    /**
52
     * Initializes the plugin object with the passed $composer and $io.
53
     *
54
     * @param Composer $composer
55
     * @param IOInterface $io
56
     */
57
    public function __construct(Composer $composer, IOInterface $io)
58
    {
59
        $baseDir = dirname($composer->getConfig()->get('vendor-dir')) . DIRECTORY_SEPARATOR;
60
        $this->builder = new Builder(new ConfigFactory(), realpath($baseDir));
61
        $this->aliasesCollector = new AliasesCollector(new Filesystem());
62
        $this->io = $io;
63
        $this->collectPackages($composer);
64
    }
65
66
    public function build(): void
67
    {
68
        $this->io->overwriteError('<info>Assembling config files</info>');
69
70
        $this->scanPackages();
71
        $this->reorderFiles();
72
73
        $this->builder->buildAllConfigs($this->files);
74
75
        $saveFiles = $this->files;
76
        $saveEnv = getenv();
77
        foreach ($this->alternatives as $name => $files) {
78
            $this->files = $saveFiles;
79
            $this->overloadEnvs($saveEnv);
0 ignored issues
show
Bug introduced by
$saveEnv of type string is incompatible with the type array expected by parameter $envs of Yiisoft\Composer\Config\Plugin::overloadEnvs(). ( Ignorable by Annotation )

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

79
            $this->overloadEnvs(/** @scrutinizer ignore-type */ $saveEnv);
Loading history...
80
            $builder = $this->builder->createAlternative($name);
81
            $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

81
            $this->addFiles(/** @scrutinizer ignore-type */ $this->rootPackage, $files);
Loading history...
82
            $builder->buildAllConfigs($this->files);
83
        }
84
    }
85
86
    private function scanPackages(): void
87
    {
88
        foreach ($this->packages as $package) {
89
            if ($package->isComplete()) {
90
                $this->processPackage($package);
91
            }
92
        }
93
    }
94
95
    private function reorderFiles(): void
96
    {
97
        foreach (array_keys($this->files) as $name) {
98
            $this->files[$name] = $this->getAllFiles($name);
99
        }
100
        foreach ($this->files as $name => $files) {
101
            $this->files[$name] = $this->orderFiles($files);
102
        }
103
    }
104
105
    private function getAllFiles(string $name, array $stack = []): array
106
    {
107
        if (empty($this->files[$name])) {
108
            return [];
109
        }
110
        $res = [];
111
        foreach ($this->files[$name] as $file) {
112
            if (strncmp($file, '$', 1) === 0) {
113
                if (!in_array($name, $stack, true)) {
114
                    $res = array_merge($res, $this->getAllFiles(substr($file, 1), array_merge($stack, [$name])));
115
                }
116
            } else {
117
                $res[] = $file;
118
            }
119
        }
120
121
        return $res;
122
    }
123
124
    private function orderFiles(array $files): array
125
    {
126
        if ($files === []) {
127
            return [];
128
        }
129
        $keys = array_combine($files, $files);
130
        $res = [];
131
        foreach ($this->orderedFiles as $file) {
132
            if (array_key_exists($file, $keys)) {
0 ignored issues
show
Bug introduced by
It seems like $keys can also be of type false; however, parameter $search of array_key_exists() does only seem to accept array, 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

132
            if (array_key_exists($file, /** @scrutinizer ignore-type */ $keys)) {
Loading history...
133
                $res[$file] = $file;
134
            }
135
        }
136
137
        return array_values($res);
138
    }
139
140
    /**
141
     * Scans the given package and collects packages data.
142
     *
143
     * @param Package $package
144
     */
145
    private function processPackage(Package $package): void
146
    {
147
        $files = $package->getFiles();
148
        $this->originalFiles[$package->getPrettyName()] = $files;
149
150
        if (!empty($files)) {
151
            $this->addFiles($package, $files);
152
        }
153
        if ($package->isRoot()) {
154
            $this->rootPackage = $package;
155
            $this->loadDotEnv($package);
156
            $devFiles = $package->getDevFiles();
157
            if (!empty($devFiles)) {
158
                $this->addFiles($package, $devFiles);
159
            }
160
            $alternatives = $package->getAlternatives();
161
            if (is_string($alternatives)) {
162
                $this->alternatives = $this->readConfig($package, $alternatives);
163
            } elseif (is_array($alternatives)) {
164
                $this->alternatives = $alternatives;
165
            } elseif (!empty($alternatives)) {
166
                throw new BadConfigurationException('Alternatives must be array or path to configuration file.');
167
            }
168
        }
169
170
        $aliases = $this->aliasesCollector->collect($package);
171
172
        $this->builder->mergeAliases($aliases);
173
        $this->builder->setPackage($package->getPrettyName(), array_filter([
174
            'name' => $package->getPrettyName(),
175
            'version' => $package->getVersion(),
176
            'reference' => $package->getSourceReference() ?: $package->getDistReference(),
177
            'aliases' => $aliases,
178
        ]));
179
    }
180
181
    private function readConfig($package, $file): array
182
    {
183
        $path = $package->preparePath($file);
184
        if (!file_exists($path)) {
185
            throw new FailedReadException("failed read file: $file");
186
        }
187
        $reader = ReaderFactory::get($this->builder, $path);
188
189
        return $reader->read($path);
190
    }
191
192
    private function loadDotEnv(Package $package): void
193
    {
194
        $path = $package->preparePath('.env');
195
        if (file_exists($path) && class_exists(Dotenv::class)) {
196
            $this->addFile($package, 'envs', $path);
197
        }
198
    }
199
200
    /**
201
     * Adds given files to the list of files to be processed.
202
     * Prepares `constants` in reversed order (outer package first) because
203
     * constants cannot be redefined.
204
     *
205
     * @param Package $package
206
     * @param array $files
207
     */
208
    private function addFiles(Package $package, array $files): void
209
    {
210
        foreach ($files as $name => $paths) {
211
            $paths = (array) $paths;
212
            if ('constants' === $name) {
213
                $paths = array_reverse($paths);
214
            }
215
            foreach ($paths as $path) {
216
                $this->addFile($package, $name, $path);
217
            }
218
        }
219
    }
220
221
    private array $orderedFiles = [];
222
223
    private function addFile(Package $package, string $name, string $path): void
224
    {
225
        $path = $package->preparePath($path);
226
        if (!array_key_exists($name, $this->files)) {
227
            $this->files[$name] = [];
228
        }
229
        if (in_array($path, $this->files[$name], true)) {
230
            return;
231
        }
232
        if ('constants' === $name) {
233
            array_unshift($this->orderedFiles, $path);
234
            array_unshift($this->files[$name], $path);
235
        } else {
236
            $this->orderedFiles[] = $path;
237
            $this->files[$name][] = $path;
238
        }
239
    }
240
241
    private function collectPackages(Composer $composer): void
242
    {
243
        $vendorDir = $composer->getConfig()->get('vendor-dir');
244
        $rootPackage = $composer->getPackage();
245
        $packages = $composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages();
246
        $packageFinder = new PackageFinder($vendorDir, $rootPackage, $packages);
247
248
        $this->packages = $packageFinder->findPackages();
249
    }
250
251
    private function overloadEnvs(array $envs): void
252
    {
253
        foreach ($envs as $key => $value) {
254
            putenv("$key=$value");
255
        }
256
    }
257
}
258