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

AppRequirementsFactory   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 182
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 20
dl 0
loc 182
rs 10
c 0
b 0
f 0

6 Methods

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