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

AppRequirementsFactory::create()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 1
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 array_map;
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 $composerLockDecodedContents Decoded JSON contents of the `composer.lock` file
34
     *
35
     * @return array Serialized configured requirements
36
     */
37
    public static function create(array $composerLockDecodedContents): array
38
    {
39
        $requirements = new RequirementCollection();
40
41
        self::configurePhpVersionRequirements($requirements, $composerLockDecodedContents);
42
        self::configureExtensionRequirements($requirements, $composerLockDecodedContents);
43
44
        return self::exportRequirementsIntoConfig($requirements);
45
    }
46
47
    private static function configurePhpVersionRequirements(RequirementCollection $requirements, array $composerLockContents): void
48
    {
49
        if (isset($composerLockContents['platform']['php'])) {
50
            $requiredPhpVersion = $composerLockContents['platform']['php'];
51
52
            $requirements->addRequirement(
53
                self::generatePhpCheckStatement((string) $requiredPhpVersion),
54
                sprintf(
55
                    'The application requires the version "%s" or greater.',
56
                    $requiredPhpVersion
57
                ),
58
                sprintf(
59
                    'The application requires the version "%s" or greater.',
60
                    $requiredPhpVersion
61
                )
62
            );
63
64
            return; // No need to check the packages requirements: the application platform config is the authority here
65
        }
66
67
        $packages = $composerLockContents['packages'] ?? [];
68
69
        foreach ($packages as $packageInfo) {
70
            $requiredPhpVersion = $packageInfo['require']['php'] ?? null;
71
72
            if (null === $requiredPhpVersion) {
73
                continue;
74
            }
75
76
            $requirements->addRequirement(
77
                self::generatePhpCheckStatement((string) $requiredPhpVersion),
78
                sprintf(
79
                    'The package "%s" requires the version "%s" or greater.',
80
                    $packageInfo['name'],
81
                    $requiredPhpVersion
82
                ),
83
                sprintf(
84
                    'The package "%s" requires the version "%s" or greater.',
85
                    $packageInfo['name'],
86
                    $requiredPhpVersion
87
                )
88
            );
89
        }
90
    }
91
92
    private static function configureExtensionRequirements(RequirementCollection $requirements, array $composerLockContents): void
93
    {
94
        $extensionRequirements = self::collectExtensionRequirements($composerLockContents);
95
96
        foreach ($extensionRequirements as $extension => $packages) {
97
            foreach ($packages as $package) {
98
                if (self::SELF_PACKAGE === $package) {
99
                    $message = sprintf(
100
                        'The application requires the extension "%s". Enable it or install a polyfill.',
101
                        $extension
102
                    );
103
                    $helpMessage = sprintf(
104
                        'The application requires the extension "%s".',
105
                        $extension
106
                    );
107
                } else {
108
                    $message = sprintf(
109
                        'The package "%s" requires the extension "%s". Enable it or install a polyfill.',
110
                        $package,
111
                        $extension
112
                    );
113
                    $helpMessage = sprintf(
114
                        'The package "%s" requires the extension "%s".',
115
                        $package,
116
                        $extension
117
                    );
118
                }
119
120
                $requirements->addRequirement(
121
                    self::generateExtensionCheckStatement($extension),
122
                    $message,
123
                    $helpMessage
124
                );
125
            }
126
        }
127
    }
128
129
    private static function exportRequirementsIntoConfig(RequirementCollection $requirements): array
130
    {
131
        return array_map(
132
            function (Requirement $requirement): array {
133
                return [
134
                    $requirement->getIsFullfilledChecker(),
135
                    $requirement->getTestMessage(),
136
                    $requirement->getHelpText(),
137
                ];
138
            },
139
            $requirements->getRequirements()
140
        );
141
    }
142
143
    /**
144
     * Collects the extension required. It also accounts for the polyfills, i.e. if the polyfill `symfony/polyfill-mbstring` is provided
145
     * then the extension `ext-mbstring` will not be required.
146
     *
147
     * @param array $composerLockContents
148
     *
149
     * @return array Associative array containing the list of extensions required
150
     */
151
    private static function collectExtensionRequirements(array $composerLockContents): array
152
    {
153
        $requirements = [];
154
        $polyfills = [];
155
156
        $platform = $composerLockContents['platform'] ?? [];
157
158
        foreach ($platform as $package => $constraint) {
159
            if (preg_match('/^ext-(?<extension>.+)$/', $package, $matches)) {
160
                $extension = $matches['extension'];
161
162
                $requirements[$extension] = [self::SELF_PACKAGE];
163
            }
164
        }
165
166
        $packages = $composerLockContents['packages'] ?? [];
167
168
        foreach ($packages as $packageInfo) {
169
            $packageRequire = $packageInfo['require'] ?? [];
170
171
            if (1 === preg_match('/symfony\/polyfill-(?<extension>.+)/', $packageInfo['name'], $matches)) {
172
                $extension = $matches['extension'];
173
174
                if ('php' !== substr($extension, 0, 3)) {
175
                    $polyfills[$extension] = true;
176
                }
177
            }
178
179
            foreach ($packageRequire as $package => $constraint) {
180
                if (1 === preg_match('/^ext-(?<extension>.+)$/', $package, $matches)) {
181
                    $extension = $matches['extension'];
182
183
                    if (false === array_key_exists($extension, $requirements)) {
184
                        $requirements[$extension] = [];
185
                    }
186
187
                    $requirements[$extension][] = $packageInfo['name'];
188
                }
189
            }
190
        }
191
192
        return array_diff_key($requirements, $polyfills);
193
    }
194
195
    private static function generatePhpCheckStatement(string $requiredPhpVersion): string
196
    {
197
        return "return require_once 'Semver.php'; \\_HumbugBox\Composer\Semver::satisfies(sprintf('%d.%d.%d', \PHP_MAJOR_VERSION, \PHP_MINOR_VERSION, \PHP_RELEASE_VERSION), '$requiredPhpVersion');";
198
    }
199
200
    private static function generateExtensionCheckStatement(string $extension): string
201
    {
202
        return "return \\extension_loaded('$extension');";
203
    }
204
}
205