Completed
Pull Request — master (#116)
by Théo
02:32
created

configureExtensionRequirements()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 32
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 32
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 23
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 UnexpectedValueException;
20
use function array_diff_key;
21
use function array_key_exists;
22
use function array_map;
23
use function sprintf;
24
use function str_replace;
25
use function substr;
26
27
/**
28
 * Collect the list of requirements for running the application.
29
 *
30
 * @private
31
 */
32
final class AppRequirementsFactory
33
{
34
    private const SELF_PACKAGE = '__APPLICATION__';
35
36
    /**
37
     * @param string $composerLockContents JSON contents of the `composer.lock` file
38
     *
39
     * @return array Serialized configured requirements
40
     */
41
    public static function create(string $composerLockContents): array
42
    {
43
        $lockDecodedContents = (new Json())->decode($composerLockContents, true);
44
45
        $requirements = new RequirementCollection();
46
47
        self::configurePhpVersionRequirements($requirements, $lockDecodedContents);
0 ignored issues
show
Bug introduced by
It seems like $lockDecodedContents can also be of type stdClass; however, parameter $composerLockContents of KevinGH\Box\RequirementC...hpVersionRequirements() 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

47
        self::configurePhpVersionRequirements($requirements, /** @scrutinizer ignore-type */ $lockDecodedContents);
Loading history...
48
        self::configureExtensionRequirements($requirements, $lockDecodedContents);
0 ignored issues
show
Bug introduced by
It seems like $lockDecodedContents can also be of type stdClass; however, parameter $composerLockContents of KevinGH\Box\RequirementC...ExtensionRequirements() 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

48
        self::configureExtensionRequirements($requirements, /** @scrutinizer ignore-type */ $lockDecodedContents);
Loading history...
49
50
        return self::exportRequirementsIntoConfig($requirements);
51
    }
52
53
    private static function configurePhpVersionRequirements(RequirementCollection $requirements, array $composerLockContents): void
54
    {
55
        if (isset($composerLockContents['platform']['php'])) {
56
            $requiredPhpVersion = $composerLockContents['platform']['php'];
57
58
            $requirements->addRequirement(
59
                self::generatePhpCheckStatement((string) $requiredPhpVersion),
60
                sprintf(
61
                    'The application requires the version "%s" or greater.',
62
                    $requiredPhpVersion
63
                ),
64
                sprintf(
65
                    'The application requires the version "%s" or greater.',
66
                    $requiredPhpVersion
67
                )
68
            );
69
70
            return; // No need to check the packages requirements: the application platform config is the authority here
71
        }
72
73
        $packages = $composerLockContents['packages'] ?? [];
74
75
        foreach ($packages as $packageInfo) {
76
            $requiredPhpVersion = $packageInfo['require']['php'] ?? null;
77
78
            if (null === $requiredPhpVersion) {
79
                continue;
80
            }
81
82
            $requirements->addRequirement(
83
                self::generatePhpCheckStatement((string) $requiredPhpVersion),
84
                sprintf(
85
                    'The package "%s" requires the version "%s" or greater.',
86
                    $packageInfo['name'],
87
                    $requiredPhpVersion
88
                ),
89
                sprintf(
90
                    'The package "%s" requires the version "%s" or greater.',
91
                    $packageInfo['name'],
92
                    $requiredPhpVersion
93
                )
94
            );
95
        }
96
    }
97
98
    private static function configureExtensionRequirements(RequirementCollection $requirements, array $composerLockContents): void
99
    {
100
        $extensionRequirements = self::collectExtensionRequirements($composerLockContents);
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->addRequirement(
127
                    self::generateExtensionCheckStatement($extension),
128
                    $message,
129
                    $helpMessage
130
                );
131
            }
132
        }
133
    }
134
135
    private static function exportRequirementsIntoConfig(RequirementCollection $requirements): array
136
    {
137
        return array_map(
138
            function (Requirement $requirement): array {
139
                return [
140
                    $requirement->getIsFullfilledChecker(),
141
                    $requirement->getTestMessage(),
142
                    $requirement->getHelpText(),
143
                ];
144
            },
145
            $requirements->getRequirements()
146
        );
147
    }
148
149
    /**
150
     * Collects the extension required. It also accounts for the polyfills, i.e. if the polyfill `symfony/polyfill-mbstring` is provided
151
     * then the extension `ext-mbstring` will not be required.
152
     *
153
     * @param array $composerLockContents
154
     *
155
     * @return array Associative array containing the list of extensions required
156
     */
157
    private static function collectExtensionRequirements(array $composerLockContents): array
158
    {
159
        $requirements = [];
160
        $polyfills = [];
161
162
        $platform = $composerLockContents['platform'] ?? [];
163
164
        foreach ($platform as $package => $constraint) {
165
            if (preg_match('/^ext-(?<extension>.+)$/', $package, $matches)) {
166
                $extension = $matches['extension'];
167
168
                $requirements[$extension] = [self::SELF_PACKAGE];
169
            }
170
        }
171
172
        $packages = $composerLockContents['packages'] ?? [];
173
174
        foreach ($packages as $packageInfo) {
175
            $packageRequire = $packageInfo['require'] ?? [];
176
177
            if (1 === preg_match('/symfony\/polyfill-(?<extension>.+)/', $packageInfo['name'], $matches)) {
178
                $extension = $matches['extension'];
179
180
                if ('php' !== substr($extension, 0, 3)) {
181
                    $polyfills[$extension] = true;
182
                }
183
            }
184
185
            foreach ($packageRequire as $package => $constraint) {
186
                if (1 === preg_match('/^ext-(?<extension>.+)$/', $package, $matches)) {
187
                    $extension = $matches['extension'];
188
189
                    if (false === array_key_exists($extension, $requirements)) {
190
                        $requirements[$extension] = [];
191
                    }
192
193
                    $requirements[$extension][] = $packageInfo['name'];
194
                }
195
            }
196
        }
197
198
        return array_diff_key($requirements, $polyfills);
199
    }
200
201
    private static function generatePhpCheckStatement(string $requiredPhpVersion): string
202
    {
203
        return "return require_once 'Semver.php'; \\_HumbugBox\Composer\Semver::satisfies(preg_replace('#^([^~+-]+).*$#', '$1', \PHP_VERSION), '$requiredPhpVersion');";
204
    }
205
206
    private static function generateExtensionCheckStatement(string $extension): string
207
    {
208
        return "return \\extension_loaded('$extension');";
209
    }
210
}
211