Passed
Pull Request — master (#116)
by Théo
02:27
created

AppRequirementsFactory   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 234
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 24
dl 0
loc 234
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A checkFileExists() 0 7 2
A decodeJson() 0 15 2
D collectExtensionRequirements() 0 42 9
A exportRequirementsIntoConfig() 0 12 1
B configurePhpVersionRequirements() 0 46 4
B configureExtensionRequirements() 0 33 4
A retrieveComposerLockContents() 0 7 1
A create() 0 10 1
1
<?php
2
3
/*
4
 * This file is part of the humbug/php-scoper package.
5
 *
6
 * Copyright (c) 2017 Théo FIDRY <[email protected]>,
7
 *                    Pádraic Brady <[email protected]>
8
 *
9
 * For the full copyright and license information, please view the LICENSE
10
 * file that was distributed with this source code.
11
 */
12
13
namespace KevinGH\Box\RequirementChecker;
14
15
use function array_diff;
16
use function array_diff_key;
17
use function array_intersect_key;
18
use function array_key_exists;
19
use function array_map;
20
use function file_exists;
21
use function json_last_error_msg;
22
use function phpversion;
23
use function sprintf;
24
use function str_replace;
25
use function substr;
26
use Symfony\Requirements\Requirement;
27
use Symfony\Requirements\RequirementCollection;
28
use UnexpectedValueException;
29
use function version_compare;
30
31
/**
32
 * Collect the list of requirements for running the application.
33
 *
34
 * @private
35
 */
36
final class AppRequirementsFactory
37
{
38
    private const SELF_PACKAGE = '__APPLICATION__';
39
40
    /**
41
     * @param string $composerJson Path to the `composer.json` file
42
     *
43
     * @throws UnexpectedValueException When the file could not be found or decoded
44
     *
45
     * @return array Configured requirements
46
     */
47
    public static function create(string $composerJson): array
48
    {
49
        $requirements = new RequirementCollection();
50
51
        $composerLockContents = self::retrieveComposerLockContents($composerJson);
52
53
        self::configurePhpVersionRequirements($requirements, $composerLockContents);
54
        self::configureExtensionRequirements($requirements, $composerLockContents);
55
56
        return self::exportRequirementsIntoConfig($requirements);
57
    }
58
59
    private static function configurePhpVersionRequirements(RequirementCollection $requirements, array $composerLockContents): void
60
    {
61
        $installedPhpVersion = phpversion();
62
63
        if (isset($composerLockContents['platform']['php'])) {
64
            $requiredPhpVersion = $composerLockContents['platform']['php'];
65
66
            $requirements->addRequirement(
67
                version_compare(phpversion(), $requiredPhpVersion, '>='),
68
                sprintf(
69
                    'The application requires the version "%s" or greater. Got "%s"',
70
                    $requiredPhpVersion,
71
                    $installedPhpVersion
72
                ),
73
                '',
74
                sprintf(
75
                    'The application requires the version "%s" or greater.',
76
                    $requiredPhpVersion
77
                )
78
            );
79
80
            return; // No need to check the packages requirements: the application platform config is the authority here
81
        }
82
83
        $packages = $composerLockContents['packages'] ?? [];
84
85
        foreach ($packages as $packageInfo) {
86
            $requiredPhpVersion = $packageInfo['require']['php'] ?? null;
87
88
            if (null !== $requiredPhpVersion) {
89
                continue;
90
            }
91
92
            $requirements->addRequirement(
93
                version_compare(phpversion(), $requiredPhpVersion, '>='),
94
                sprintf(
95
                    'The package "%s" requires the version "%s" or greater. Got "%s"',
96
                    $packageInfo['name'],
97
                    $requiredPhpVersion,
98
                    $installedPhpVersion
99
                ),
100
                '',
101
                sprintf(
102
                    'The package "%s" requires the version "%s" or greater.',
103
                    $packageInfo['name'],
104
                    $requiredPhpVersion
105
                )
106
            );
107
        }
108
    }
109
110
    private static function configureExtensionRequirements(RequirementCollection $requirements, array $composerLockContents): void
111
    {
112
        $extensionRequirements = self::collectExtensionRequirements($composerLockContents);
113
114
        foreach ($extensionRequirements as $extension => $packages) {
115
            foreach ($packages as $package) {
116
                if (self::SELF_PACKAGE === $package) {
117
                    $message = sprintf(
118
                        'The application requires the extension "%s". Enable it or install a polyfill.',
119
                        $extension
120
                    );
121
                    $helpMessage = sprintf(
122
                        'The application requires the extension "%s".',
123
                        $extension
124
                    );
125
                } else {
126
                    $message = sprintf(
127
                        'The package "%s" requires the extension "%s". Enable it or install a polyfill.',
128
                        $package,
129
                        $extension
130
                    );
131
                    $helpMessage = sprintf(
132
                        'The package "%s" requires the extension "%s".',
133
                        $package,
134
                        $extension
135
                    );
136
                }
137
138
                $requirements->addRequirement(
139
                    extension_loaded($extension),
140
                    $message,
141
                    '',
142
                    $helpMessage
143
                );
144
            }
145
        }
146
    }
147
148
    private static function exportRequirementsIntoConfig(RequirementCollection $requirements): array
149
    {
150
        return array_map(
151
            function (Requirement $requirement): array {
152
                return [
153
                    $requirement->isFulfilled(),
154
                    $requirement->getTestMessage(),
155
                    $requirement->getHelpHtml(),
156
                    $requirement->getHelpText(),
157
                ];
158
            },
159
            $requirements->getRequirements()
160
        );
161
    }
162
163
    /**
164
     * Collects the extension required. It also accounts for the polyfills, i.e. if the polyfill `symfony/polyfill-mbstring` is provided
165
     * then the extension `ext-mbstring` will not be required.
166
     *
167
     * @param array $composerLockContents
168
     *
169
     * @return array Associative array containing the list of extensions required
170
     */
171
    private static function collectExtensionRequirements(array $composerLockContents): array
172
    {
173
        $requirements = [];
174
        $polyfills = [];
175
176
        $platform = $composerLockContents['platform'] ?? [];
177
178
        foreach ($platform as $package => $constraint) {
179
            if (preg_match('/^ext-(?<extension>.+)$/', $package, $matches)) {
180
                $extension = $matches['extension'];
181
182
                $requirements[$extension] = [self::SELF_PACKAGE];
183
            }
184
        }
185
186
        $packages = $composerLockContents['packages'] ?? [];
187
188
        foreach ($packages as $packageInfo) {
189
            $packageRequire = $packageInfo['require'] ?? [];
190
191
            if (preg_match('/symfony\/polyfill-(?<extension>.+)/', $packageInfo['name'], $matches)) {
192
                $extension = $matches['extension'];
193
194
                if ('php' !== substr($extension, 0, 3)) {
195
                    $polyfills[$extension] = true;
196
                }
197
            }
198
199
            foreach ($packageRequire as $package => $constraint) {
200
                if (preg_match('/^ext-(?<extension>.+)$/', $package, $matches)) {
201
                    $extension = $matches['extension'];
202
203
                    if (false === array_key_exists($extension, $requirements)) {
204
                        $requirements[$extension] = [];
205
                    }
206
207
                    $requirements[$extension][] = $packageInfo['name'];
208
                }
209
            }
210
        }
211
212
        return array_diff_key($requirements, $polyfills);
213
    }
214
215
    /**
216
     * @param string $composerJson
217
     *
218
     * @throws UnexpectedValueException
219
     *
220
     * @return array Associative array containing the application platform requirements
221
     */
222
    private static function retrieveComposerLockContents(string $composerJson): array
223
    {
224
        $composerLock = str_replace('.json', '.lock', $composerJson);
225
226
        self::checkFileExists($composerLock);
227
228
        return self::decodeJson($composerLock);
229
    }
230
231
    /**
232
     * @param string $file
233
     *
234
     * @throws UnexpectedValueException When the file does not exists
235
     */
236
    private static function checkFileExists(string $file): void
237
    {
238
        if (false === $file) {
0 ignored issues
show
introduced by
The condition false === $file is always false.
Loading history...
239
            throw new UnexpectedValueException(
240
                sprintf(
241
                    'Could not locate the file "%s"',
242
                    json_last_error_msg()
243
                )
244
            );
245
        }
246
    }
247
248
    /**
249
     * @param string $file
250
     *
251
     * @throws UnexpectedValueException When the file could not be decoded
252
     *
253
     * @return array Decoded file contents
254
     */
255
    private static function decodeJson(string $file): array
256
    {
257
        $contents = @json_decode(file_get_contents($file), true);
258
259
        if (false === $contents) {
260
            throw new UnexpectedValueException(
261
                sprintf(
262
                    'Could not decode the JSON file "%s": %s',
263
                    $file,
264
                    json_last_error_msg()
265
                )
266
            );
267
        }
268
269
        return $contents;
270
    }
271
}
272