Passed
Pull Request — master (#8)
by Dmitriy
11:32 queued 10s
created

Plugin::processPackages()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 3
nc 3
nop 0
dl 0
loc 5
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 Composer\Util\Filesystem;
12
use Yiisoft\Composer\Config\Configs\ConfigFactory;
13
use Yiisoft\Composer\Config\exceptions\BadConfigurationException;
14
use Yiisoft\Composer\Config\exceptions\FailedReadException;
15
use Yiisoft\Composer\Config\Package\AliasesCollector;
16
use Yiisoft\Composer\Config\Package\PackageFinder;
17
use Yiisoft\Composer\Config\readers\ReaderFactory;
18
19
/**
20
 * Plugin class.
21
 */
22
class Plugin implements PluginInterface, EventSubscriberInterface
23
{
24
    /**
25
     * @var Package[] the array of active composer packages
26
     */
27
    private array $packages;
28
29
    private array $alternatives = [];
30
31
    private ?string $outputDir = null;
32
33
    private ?Package $rootPackage = null;
34
35
    /**
36
     * @var array config name => list of files
37
     */
38
    private array $files = [
39
        'envs' => [],
40
        'params' => [],
41
        'constants' => [],
42
    ];
43
44
    /**
45
     * @var array package name => configs as listed in `composer.json`
46
     */
47
    private array $originalFiles = [];
48
49
    private Builder $builder;
50
51
    private Composer $composer;
52
53
    private IOInterface $io;
54
55
    private AliasesCollector $aliasesCollector;
56
57
    /**
58
     * Initializes the plugin object with the passed $composer and $io.
59
     *
60
     * @param Composer $composer
61
     * @param IOInterface $io
62
     */
63
    public function activate(Composer $composer, IOInterface $io): void
64
    {
65
        $this->composer = $composer;
66
        $this->io = $io;
67
        $this->builder = new Builder(new ConfigFactory());
68
69
        $this->packages = $this->collectPackages();
70
        $this->aliasesCollector = new AliasesCollector(new Filesystem());
71
    }
72
73
    /**
74
     * Returns list of events the plugin is subscribed to.
75
     *
76
     * @return array list of events
77
     */
78
    public static function getSubscribedEvents(): array
79
    {
80
        return [
81
            ScriptEvents::POST_AUTOLOAD_DUMP => [
82
                ['onPostAutoloadDump', 0],
83
            ],
84
        ];
85
    }
86
87
    /**
88
     * This is the main function.
89
     */
90
    public function onPostAutoloadDump(Event $event): void
91
    {
92
        $this->io->overwriteError('<info>Assembling config files</info>');
93
94
        require_once $event->getComposer()->getConfig()->get('vendor-dir') . '/autoload.php';
95
        $this->processPackages();
96
        $this->reorderFiles();
97
98
        $this->builder->setOutputDir($this->outputDir);
99
        $this->builder->buildAllConfigs($this->files);
100
101
        $saveFiles = $this->files;
102
        $saveEnv = $_ENV;
103
        foreach ($this->alternatives as $name => $files) {
104
            $this->files = $saveFiles;
105
            $_ENV = $saveEnv;
106
            $builder = $this->builder->createAlternative($name);
107
            $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

107
            $this->addFiles(/** @scrutinizer ignore-type */ $this->rootPackage, $files);
Loading history...
108
            $builder->buildAllConfigs($this->files);
109
        }
110
    }
111
112
    private function processPackages(): void
113
    {
114
        foreach ($this->packages as $package) {
115
            if ($package->isComplete()) {
116
                $this->processPackage($package);
117
            }
118
        }
119
    }
120
121
    private function reorderFiles(): void
122
    {
123
        foreach (array_keys($this->files) as $name) {
124
            $this->files[$name] = $this->getAllFiles($name);
125
        }
126
        foreach ($this->files as $name => $files) {
127
            $this->files[$name] = $this->orderFiles($files);
128
        }
129
    }
130
131
    private function getAllFiles(string $name, array $stack = []): array
132
    {
133
        if (empty($this->files[$name])) {
134
            return [];
135
        }
136
        $res = [];
137
        foreach ($this->files[$name] as $file) {
138
            if (strncmp($file, '$', 1) === 0) {
139
                if (!in_array($name, $stack, true)) {
140
                    $res = array_merge($res, $this->getAllFiles(substr($file, 1), array_merge($stack, [$name])));
141
                }
142
            } else {
143
                $res[] = $file;
144
            }
145
        }
146
147
        return $res;
148
    }
149
150
    private function orderFiles(array $files): array
151
    {
152
        if ($files === []) {
153
            return [];
154
        }
155
        $keys = array_combine($files, $files);
156
        $res = [];
157
        foreach ($this->orderedFiles as $file) {
158
            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

158
            if (array_key_exists($file, /** @scrutinizer ignore-type */ $keys)) {
Loading history...
159
                $res[$file] = $file;
160
            }
161
        }
162
163
        return array_values($res);
164
    }
165
166
    /**
167
     * Scans the given package and collects packages data.
168
     *
169
     * @param Package $package
170
     */
171
    private function processPackage(Package $package): void
172
    {
173
        $files = $package->getFiles();
174
        $this->originalFiles[$package->getPrettyName()] = $files;
175
176
        if (!empty($files)) {
177
            $this->addFiles($package, $files);
178
        }
179
        if ($package->isRoot()) {
180
            $this->rootPackage = $package;
181
            $this->loadDotEnv($package);
182
            $devFiles = $package->getDevFiles();
183
            if (!empty($devFiles)) {
184
                $this->addFiles($package, $devFiles);
185
            }
186
            $this->outputDir = $package->getOutputDir();
187
            $alternatives = $package->getAlternatives();
188
            if (is_string($alternatives)) {
0 ignored issues
show
introduced by
The condition is_string($alternatives) is always false.
Loading history...
189
                $this->alternatives = $this->readConfig($package, $alternatives);
190
            } elseif (is_array($alternatives)) {
0 ignored issues
show
introduced by
The condition is_array($alternatives) is always true.
Loading history...
191
                $this->alternatives = $alternatives;
192
            } elseif (!empty($alternatives)) {
193
                throw new BadConfigurationException('Alternatives must be array or path to configuration file.');
194
            }
195
        }
196
197
        $aliases = $this->aliasesCollector->collect($package);
198
199
        $this->builder->mergeAliases($aliases);
200
        $this->builder->setPackage($package->getPrettyName(), array_filter([
201
            'name' => $package->getPrettyName(),
202
            'version' => $package->getVersion(),
203
            'reference' => $package->getSourceReference() ?: $package->getDistReference(),
204
            'aliases' => $aliases,
205
        ]));
206
    }
207
208
    private function readConfig($package, $file): array
209
    {
210
        $path = $package->preparePath($file);
211
        if (!file_exists($path)) {
212
            throw new FailedReadException("failed read file: $file");
213
        }
214
        $reader = ReaderFactory::get($this->builder, $path);
215
216
        return $reader->read($path);
217
    }
218
219
    private function loadDotEnv(Package $package): void
220
    {
221
        $path = $package->preparePath('.env');
222
        if (file_exists($path) && class_exists('Dotenv\Dotenv')) {
223
            $this->addFile($package, 'envs', $path);
224
        }
225
    }
226
227
    /**
228
     * Adds given files to the list of files to be processed.
229
     * Prepares `constants` in reversed order (outer package first) because
230
     * constants cannot be redefined.
231
     *
232
     * @param Package $package
233
     * @param array $files
234
     */
235
    private function addFiles(Package $package, array $files): void
236
    {
237
        foreach ($files as $name => $paths) {
238
            $paths = (array) $paths;
239
            if ('constants' === $name) {
240
                $paths = array_reverse($paths);
241
            }
242
            foreach ($paths as $path) {
243
                $this->addFile($package, $name, $path);
244
            }
245
        }
246
    }
247
248
    private array $orderedFiles = [];
249
250
    private 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 ('constants' === $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
    private function collectPackages(): array
269
    {
270
        $vendorDir = $this->composer->getConfig()->get('vendor-dir');
271
        $rootPackage = $this->composer->getPackage();
272
        $packages = $this->composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages();
273
        $packageFinder = new PackageFinder($vendorDir, $rootPackage, $packages);
274
275
        return $packageFinder->findPackages();
276
    }
277
}
278