Completed
Pull Request — master (#116)
by Théo
04:23 queued 01:53
created

configureExtensionRequirements()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 33
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 33
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 24
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 Assert\Assertion;
18
use KevinGH\Box\Json\Json;
19
use Symfony\Requirements\Requirement;
20
use Symfony\Requirements\RequirementCollection;
21
use UnexpectedValueException;
22
use function array_diff_key;
23
use function array_key_exists;
24
use function array_map;
25
use function sprintf;
26
use function str_replace;
27
use function substr;
28
use function version_compare;
29
30
/**
31
 * Collect the list of requirements for running the application.
32
 *
33
 * @private
34
 */
35
final class AppRequirementsFactory
36
{
37
    private const SELF_PACKAGE = '__APPLICATION__';
38
39
    /**
40
     * @param string $composerJson Path to the `composer.json` file
41
     *
42
     * @throws UnexpectedValueException When the file could not be found or decoded
43
     *
44
     * @return array Configured requirements
45
     */
46
    public static function create(string $composerJson): array
47
    {
48
        $requirements = new RequirementCollection();
49
50
        $composerLockContents = self::retrieveComposerLockContents($composerJson);
51
52
        self::configurePhpVersionRequirements($requirements, $composerLockContents);
53
        self::configureExtensionRequirements($requirements, $composerLockContents);
54
55
        return self::exportRequirementsIntoConfig($requirements);
56
    }
57
58
    private static function configurePhpVersionRequirements(RequirementCollection $requirements, array $composerLockContents): void
59
    {
60
        $installedPhpVersion = PHP_VERSION;
61
62
        if (isset($composerLockContents['platform']['php'])) {
63
            $requiredPhpVersion = $composerLockContents['platform']['php'];
64
65
            $requirements->addRequirement(
66
                version_compare(PHP_VERSION, $requiredPhpVersion, '>='),
67
                sprintf(
68
                    'The application requires the version "%s" or greater. Got "%s"',
69
                    $requiredPhpVersion,
70
                    $installedPhpVersion
71
                ),
72
                '',
73
                sprintf(
74
                    'The application requires the version "%s" or greater.',
75
                    $requiredPhpVersion
76
                )
77
            );
78
79
            return; // No need to check the packages requirements: the application platform config is the authority here
80
        }
81
82
        $packages = $composerLockContents['packages'] ?? [];
83
84
        foreach ($packages as $packageInfo) {
85
            $requiredPhpVersion = $packageInfo['require']['php'] ?? null;
86
87
            if (null !== $requiredPhpVersion) {
88
                continue;
89
            }
90
91
            $requirements->addRequirement(
92
                version_compare(PHP_VERSION, $requiredPhpVersion, '>='),
93
                sprintf(
94
                    'The package "%s" requires the version "%s" or greater. Got "%s"',
95
                    $packageInfo['name'],
96
                    $requiredPhpVersion,
97
                    $installedPhpVersion
98
                ),
99
                '',
100
                sprintf(
101
                    'The package "%s" requires the version "%s" or greater.',
102
                    $packageInfo['name'],
103
                    $requiredPhpVersion
104
                )
105
            );
106
        }
107
    }
108
109
    private static function configureExtensionRequirements(RequirementCollection $requirements, array $composerLockContents): void
110
    {
111
        $extensionRequirements = self::collectExtensionRequirements($composerLockContents);
112
113
        foreach ($extensionRequirements as $extension => $packages) {
114
            foreach ($packages as $package) {
115
                if (self::SELF_PACKAGE === $package) {
116
                    $message = sprintf(
117
                        'The application requires the extension "%s". Enable it or install a polyfill.',
118
                        $extension
119
                    );
120
                    $helpMessage = sprintf(
121
                        'The application requires the extension "%s".',
122
                        $extension
123
                    );
124
                } else {
125
                    $message = sprintf(
126
                        'The package "%s" requires the extension "%s". Enable it or install a polyfill.',
127
                        $package,
128
                        $extension
129
                    );
130
                    $helpMessage = sprintf(
131
                        'The package "%s" requires the extension "%s".',
132
                        $package,
133
                        $extension
134
                    );
135
                }
136
137
                $requirements->addRequirement(
138
                    extension_loaded($extension),
139
                    $message,
140
                    '',
141
                    $helpMessage
142
                );
143
            }
144
        }
145
    }
146
147
    private static function exportRequirementsIntoConfig(RequirementCollection $requirements): array
148
    {
149
        return array_map(
150
            function (Requirement $requirement): array {
151
                return [
152
                    $requirement->isFulfilled(),
153
                    $requirement->getTestMessage(),
154
                    $requirement->getHelpHtml(),
155
                    $requirement->getHelpText(),
156
                ];
157
            },
158
            $requirements->getRequirements()
159
        );
160
    }
161
162
    /**
163
     * Collects the extension required. It also accounts for the polyfills, i.e. if the polyfill `symfony/polyfill-mbstring` is provided
164
     * then the extension `ext-mbstring` will not be required.
165
     *
166
     * @param array $composerLockContents
167
     *
168
     * @return array Associative array containing the list of extensions required
169
     */
170
    private static function collectExtensionRequirements(array $composerLockContents): array
171
    {
172
        $requirements = [];
173
        $polyfills = [];
174
175
        $platform = $composerLockContents['platform'] ?? [];
176
177
        foreach ($platform as $package => $constraint) {
178
            if (preg_match('/^ext-(?<extension>.+)$/', $package, $matches)) {
179
                $extension = $matches['extension'];
180
181
                $requirements[$extension] = [self::SELF_PACKAGE];
182
            }
183
        }
184
185
        $packages = $composerLockContents['packages'] ?? [];
186
187
        foreach ($packages as $packageInfo) {
188
            $packageRequire = $packageInfo['require'] ?? [];
189
190
            if (preg_match('/symfony\/polyfill-(?<extension>.+)/', $packageInfo['name'], $matches)) {
191
                $extension = $matches['extension'];
192
193
                if ('php' !== substr($extension, 0, 3)) {
194
                    $polyfills[$extension] = true;
195
                }
196
            }
197
198
            foreach ($packageRequire as $package => $constraint) {
199
                if (preg_match('/^ext-(?<extension>.+)$/', $package, $matches)) {
200
                    $extension = $matches['extension'];
201
202
                    if (false === array_key_exists($extension, $requirements)) {
203
                        $requirements[$extension] = [];
204
                    }
205
206
                    $requirements[$extension][] = $packageInfo['name'];
207
                }
208
            }
209
        }
210
211
        return array_diff_key($requirements, $polyfills);
212
    }
213
214
    private static function retrieveComposerLockContents(string $composerJson): array
215
    {
216
        $composerLock = str_replace('.json', '.lock', $composerJson);
217
218
        Assertion::file($composerLock);
219
220
        return (new Json())->decodeFile($composerLock, true);
0 ignored issues
show
Bug Best Practice introduced by
The expression return new KevinGH\Box\J...le($composerLock, true) could return the type stdClass which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
221
    }
222
}
223