Passed
Push — master ( 8bd994...8bfb27 )
by Théo
02:28
created

generatePhpCheckRequirement()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 24
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 8.9713
c 0
b 0
f 0
cc 3
eloc 19
nc 4
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the box project.
7
 *
8
 * (c) Kevin Herrera <[email protected]>
9
 *     Théo Fidry <[email protected]>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14
15
namespace KevinGH\Box\RequirementChecker;
16
17
use Phar;
18
use function array_diff_key;
19
use function array_key_exists;
20
use function sprintf;
21
use function substr;
22
23
/**
24
 * Collect the list of requirements for running the application.
25
 *
26
 * @private
27
 */
28
final class AppRequirementsFactory
29
{
30
    private const SELF_PACKAGE = '__APPLICATION__';
31
32
    /**
33
     * @param array $composerJsonDecodedContents Decoded JSON contents of the `composer.json` file
34
     * @param array $composerLockDecodedContents Decoded JSON contents of the `composer.lock` file
35
     *
36
     * @return array Serialized configured requirements
37
     */
38
    public static function create(array $composerJsonDecodedContents, array $composerLockDecodedContents, ?int $compressionAlgorithm): array
39
    {
40
        return self::configureExtensionRequirements(
41
            self::retrievePhpVersionRequirements([], $composerJsonDecodedContents, $composerLockDecodedContents),
42
            $composerJsonDecodedContents,
43
            $composerLockDecodedContents,
44
            $compressionAlgorithm
45
        );
46
    }
47
48
    private static function retrievePhpVersionRequirements(
49
        array $requirements,
50
        array $composerJsonContents,
51
        array $composerLockContents
52
    ): array {
53
        if (([] === $composerLockContents && isset($composerJsonContents['require']['php']))
54
            || isset($composerLockContents['platform']['php'])
55
        ) {
56
            // No need to check the packages requirements: the application platform config is the authority here
57
            return self::retrievePlatformPhpRequirement($requirements, $composerJsonContents, $composerLockContents);
58
        }
59
60
        return self::retrievePackagesPhpRequirement($requirements, $composerLockContents);
61
    }
62
63
    private static function retrievePlatformPhpRequirement(
64
        array $requirements,
65
        array $composerJsonContents,
66
        array $composerLockContents
67
    ): array {
68
        $requiredPhpVersion = [] === $composerLockContents
69
            ? $composerJsonContents['require']['php']
70
            : $composerLockContents['platform']['php'];
71
72
        $requirements[] = self::generatePhpCheckRequirement((string) $requiredPhpVersion, null);
73
74
        return $requirements;
75
    }
76
77
    private static function retrievePackagesPhpRequirement(array $requirements, array $composerLockContents): array
78
    {
79
        $packages = $composerLockContents['packages'] ?? [];
80
81
        foreach ($packages as $packageInfo) {
82
            $requiredPhpVersion = $packageInfo['require']['php'] ?? null;
83
84
            if (null === $requiredPhpVersion) {
85
                continue;
86
            }
87
88
            $requirements[] = self::generatePhpCheckRequirement((string) $requiredPhpVersion, $packageInfo['name']);
89
        }
90
91
        return $requirements;
92
    }
93
94
    private static function configureExtensionRequirements(
95
        array $requirements,
96
        array $composerJsonContents,
97
        array $composerLockContents,
98
        ?int $compressionAlgorithm
99
    ): array {
100
        $extensionRequirements = self::collectExtensionRequirements($composerJsonContents, $composerLockContents, $compressionAlgorithm);
101
102
        foreach ($extensionRequirements as $extension => $packages) {
103
            foreach ($packages as $package) {
104
                if (self::SELF_PACKAGE === $package) {
105
                    $message = sprintf(
106
                        'The application requires the extension "%s". Enable it or install a polyfill.',
107
                        $extension
108
                    );
109
                    $helpMessage = sprintf(
110
                        'The application requires the extension "%s".',
111
                        $extension
112
                    );
113
                } else {
114
                    $message = sprintf(
115
                        'The package "%s" requires the extension "%s". Enable it or install a polyfill.',
116
                        $package,
117
                        $extension
118
                    );
119
                    $helpMessage = sprintf(
120
                        'The package "%s" requires the extension "%s".',
121
                        $package,
122
                        $extension
123
                    );
124
                }
125
126
                $requirements[] = [
127
                    'type' => 'extension',
128
                    'condition' => $extension,
129
                    'message' => $message,
130
                    'helpMessage' => $helpMessage,
131
                ];
132
            }
133
        }
134
135
        return $requirements;
136
    }
137
138
    /**
139
     * Collects the extension required. It also accounts for the polyfills, i.e. if the polyfill `symfony/polyfill-mbstring` is provided
140
     * then the extension `ext-mbstring` will not be required.
141
     *
142
     * @return array Associative array containing the list of extensions required
143
     */
144
    private static function collectExtensionRequirements(
145
        array $composerJsonContents,
146
        array $composerLockContents,
147
        ?int $compressionAlgorithm
148
    ): array {
149
        $requirements = [];
150
        $polyfills = [];
151
152
        if (Phar::BZ2 === $compressionAlgorithm) {
153
            $requirements['bz2'] = [self::SELF_PACKAGE];
154
        }
155
156
        if (Phar::GZ === $compressionAlgorithm) {
157
            $requirements['zlib'] = [self::SELF_PACKAGE];
158
        }
159
160
        $platform = $composerLockContents['platform'] ?? [];
161
162
        foreach ($platform as $package => $constraint) {
163
            if (preg_match('/^ext-(?<extension>.+)$/', $package, $matches)) {
164
                $extension = $matches['extension'];
165
166
                $requirements[$extension] = [self::SELF_PACKAGE];
167
            }
168
        }
169
170
        [$polyfills, $requirements] = [] === $composerLockContents
171
            ? self::collectComposerJsonExtensionRequirements($composerJsonContents, $polyfills, $requirements)
172
            : self::collectComposerLockExtensionRequirements($composerLockContents, $polyfills, $requirements)
173
        ;
174
175
        return array_diff_key($requirements, $polyfills);
176
    }
177
178
    private static function collectComposerJsonExtensionRequirements(array $composerJsonContents, $polyfills, $requirements): array
179
    {
180
        $packages = $composerJsonContents['require'] ?? [];
181
182
        foreach ($packages as $packageName => $constraint) {
183
            if (1 === preg_match('/symfony\/polyfill-(?<extension>.+)/', $packageName, $matches)) {
184
                $extension = $matches['extension'];
185
186
                if ('php' !== substr($extension, 0, 3)) {
187
                    $polyfills[$extension] = true;
188
189
                    continue;
190
                }
191
            }
192
193
            if ('paragonie/sodium_compat' === $packageName) {
194
                $polyfills['libsodium'] = true;
195
196
                continue;
197
            }
198
199
            if ('phpseclib/mcrypt_compat' === $packageName) {
200
                $polyfills['mcrypt'] = true;
201
202
                continue;
203
            }
204
205
            if ('php' !== $packageName && preg_match('/^ext-(?<extension>.+)$/', $packageName, $matches)) {
206
                $requirements[$matches['extension']] = [self::SELF_PACKAGE];
207
            }
208
        }
209
210
        return [$polyfills, $requirements];
211
    }
212
213
    private static function collectComposerLockExtensionRequirements(array $composerLockContents, $polyfills, $requirements): array
214
    {
215
        $packages = $composerLockContents['packages'] ?? [];
216
217
        foreach ($packages as $packageInfo) {
218
            $packageRequire = $packageInfo['require'] ?? [];
219
220
            if (1 === preg_match('/symfony\/polyfill-(?<extension>.+)/', $packageInfo['name'], $matches)) {
221
                $extension = $matches['extension'];
222
223
                if ('php' !== substr($extension, 0, 3)) {
224
                    $polyfills[$extension] = true;
225
                }
226
            }
227
228
            if ('paragonie/sodium_compat' === $packageInfo['name']) {
229
                $polyfills['libsodium'] = true;
230
            }
231
232
            if ('phpseclib/mcrypt_compat' === $packageInfo['name']) {
233
                $polyfills['mcrypt'] = true;
234
            }
235
236
            foreach ($packageRequire as $package => $constraint) {
237
                if (1 === preg_match('/^ext-(?<extension>.+)$/', $package, $matches)) {
238
                    $extension = $matches['extension'];
239
240
                    if (false === array_key_exists($extension, $requirements)) {
241
                        $requirements[$extension] = [];
242
                    }
243
244
                    $requirements[$extension][] = $packageInfo['name'];
245
                }
246
            }
247
        }
248
249
        return [$polyfills, $requirements];
250
    }
251
252
    /**
253
     * @return string[]
254
     */
255
    private static function generatePhpCheckRequirement(string $requiredPhpVersion, ?string $packageName): array
256
    {
257
        return [
258
            'type' => 'php',
259
            'condition' => $requiredPhpVersion,
260
            'message' => null === $packageName
261
                ? sprintf(
262
                    'The application requires the version "%s" or greater.',
263
                    $requiredPhpVersion
264
                )
265
                : sprintf(
266
                    'The package "%s" requires the version "%s" or greater.',
267
                    $packageName,
268
                    $requiredPhpVersion
269
                ),
270
            'helpMessage' => null === $packageName
271
                ? sprintf(
272
                'The application requires the version "%s" or greater.',
273
                $requiredPhpVersion
274
            )
275
                : sprintf(
276
                'The package "%s" requires the version "%s" or greater.',
277
                $packageName,
278
                $requiredPhpVersion
279
            ),
280
        ];
281
    }
282
}
283