Completed
Push — master ( acee11...f0efb2 )
by Théo
02:43
created

configurePhpVersionRequirements()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 45
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 45
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 27
nc 4
nop 2

2 Methods

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