Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
48 / 48 |
|
100.00% |
7 / 7 |
CRAP | |
100.00% |
1 / 1 |
Loader | |
100.00% |
48 / 48 |
|
100.00% |
7 / 7 |
21 | |
100.00% |
1 / 1 |
started | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
init | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
__construct | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
4 | |||
autoload | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
callStaticConstructor | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
7 | |||
overrideSplLoaders | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
checkLoadedClasses | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | /** |
6 | * Copyright (c) 2019-2023 NxtLvl Software Solutions. |
7 | * |
8 | * Freely available to use under the terms of the MIT license. |
9 | */ |
10 | |
11 | namespace NxtLvlSoftware\StaticConstructors; |
12 | |
13 | use InvalidArgumentException; |
14 | use NxtLvlSoftware\StaticConstructors\Policy\Class\PhpStyleConstructorPolicy; |
15 | use NxtLvlSoftware\StaticConstructors\Policy\Class\SameNameAsClassPolicy; |
16 | use NxtLvlSoftware\StaticConstructors\Policy\Method\BaseRequirementMethodPolicy; |
17 | use NxtLvlSoftware\StaticConstructors\Policy\Method\NoArgumentsMethodPolicy; |
18 | use NxtLvlSoftware\StaticConstructors\Policy\Method\Visibility\PrivateVisibilityPolicy; |
19 | use ReflectionClass; |
20 | use function class_exists; |
21 | use function get_declared_classes; |
22 | use function spl_autoload_functions; |
23 | use function spl_autoload_register; |
24 | use function spl_autoload_unregister; |
25 | |
26 | /** |
27 | * Singleton responsible for calling static constructors on classes that |
28 | * provide them. |
29 | * |
30 | * Uses {@link \NxtLvlSoftware\StaticConstructors\Policy\Class\StaticConstructorClassPolicy} |
31 | * and {@link \NxtLvlSoftware\StaticConstructors\Policy\Method\StaticConstructorMethodPolicy} |
32 | * to determine valid classes and methods. |
33 | */ |
34 | final class Loader { |
35 | |
36 | /** |
37 | * The default class policies for finding static constructor methods. |
38 | */ |
39 | public const DEFAULT_CLASS_POLICIES = [ |
40 | SameNameAsClassPolicy::class, |
41 | PhpStyleConstructorPolicy::class, |
42 | ]; |
43 | |
44 | /** |
45 | * The default method policies for determining validity of static constructor |
46 | * candidate methods. |
47 | */ |
48 | public const DEFAULT_METHOD_POLICIES = [ |
49 | NoArgumentsMethodPolicy::class, |
50 | PrivateVisibilityPolicy::class, |
51 | ]; |
52 | |
53 | private static self|null $instance = null; |
54 | |
55 | /** |
56 | * Check if {@link \NxtLvlSoftware\StaticConstructors\Loader::init()} has been called. |
57 | */ |
58 | public static function started(): bool { |
59 | return self::$instance !== null; |
60 | } |
61 | |
62 | /** |
63 | * Start the loader if an instance does not already exist. |
64 | * |
65 | * @param list<class-string<\NxtLvlSoftware\StaticConstructors\Policy\Class\StaticConstructorClassPolicy>> $classPolicies Policy class names for determining if a class has a valid static constructor method defined. |
66 | * @param list<class-string<\NxtLvlSoftware\StaticConstructors\Policy\Method\StaticConstructorMethodPolicy>> $methodPolicies Policy class names for determining validity of candidate static constructor methods. |
67 | * @param bool $checkLoadedClasses Should the loader check for a static constructor on classes that are already declared in the runtime? |
68 | */ |
69 | public static function init( |
70 | array $classPolicies = self::DEFAULT_CLASS_POLICIES, |
71 | array $methodPolicies = self::DEFAULT_METHOD_POLICIES, |
72 | bool $checkLoadedClasses = true |
73 | ): void { |
74 | if (self::started()) { |
75 | return; |
76 | } |
77 | |
78 | new self($classPolicies, $methodPolicies, $checkLoadedClasses); |
79 | } |
80 | |
81 | /** @var list<callable(class-string): void> */ |
82 | private array $proxiedLoaders = []; |
83 | |
84 | /** |
85 | * The entry to static class constructors. |
86 | * |
87 | * This class can only exist as a singleton so that we only override |
88 | * the existing autoloader's if this is the first instance constructed. |
89 | * |
90 | * @param list<class-string<\NxtLvlSoftware\StaticConstructors\Policy\Class\StaticConstructorClassPolicy>> $classPolicies |
91 | * @param list<class-string<\NxtLvlSoftware\StaticConstructors\Policy\Method\StaticConstructorMethodPolicy>> $methodPolicies |
92 | */ |
93 | private function __construct( |
94 | private readonly array $classPolicies, |
95 | private readonly array $methodPolicies, |
96 | bool $checkLoadedClasses |
97 | ) { |
98 | $policyClasses = [ |
99 | ...$classPolicies, |
100 | BaseRequirementMethodPolicy::class, |
101 | ...$this->methodPolicies |
102 | ]; // force load policy classes |
103 | foreach ($policyClasses as $class) { |
104 | if (!class_exists($class)) { |
105 | throw new InvalidArgumentException('Provided policy class that could not be found. Class: ' . $class); |
106 | } |
107 | } |
108 | |
109 | self::$instance = $this; |
110 | |
111 | $this->overrideSplLoaders(); |
112 | if ($checkLoadedClasses) { |
113 | $this->checkLoadedClasses(); |
114 | } |
115 | } |
116 | |
117 | /** |
118 | * Our custom autoload function. We loop over the registered loaders |
119 | * to load the class then call the static constructor on the class |
120 | * if it was loaded. |
121 | * |
122 | * @param class-string $className |
123 | */ |
124 | public function autoload(string $className): void { |
125 | foreach ($this->proxiedLoaders as $func) { |
126 | $func($className); |
127 | if (class_exists($className, false)) { |
128 | break; |
129 | } |
130 | } |
131 | |
132 | $this->callStaticConstructor($className); |
133 | } |
134 | |
135 | /** |
136 | * Look for a suitable static constructor on a class and call it. |
137 | * |
138 | * @param class-string $className |
139 | */ |
140 | private function callStaticConstructor(string $className): void { |
141 | /** @var \ReflectionMethod|null $method */ |
142 | $method = null; |
143 | $reflection = new ReflectionClass($className); |
144 | foreach ($this->classPolicies as $classPolicy) { |
145 | $method = $classPolicy::methodFor($reflection); |
146 | if ($method === null || !(BaseRequirementMethodPolicy::meetsRequirements($method))) { |
147 | $method = null; |
148 | continue; |
149 | } |
150 | |
151 | foreach ($this->methodPolicies as $methodPolicy) { |
152 | if (!($methodPolicy::meetsRequirements($method))) { |
153 | $method = null; |
154 | continue 2; |
155 | } |
156 | } |
157 | break; // valid |
158 | } |
159 | if ($method === null) { |
160 | return; |
161 | } |
162 | |
163 | $method->invoke(null); |
164 | } |
165 | |
166 | /** |
167 | * Store all the currently registered autoload functions and register |
168 | * this class as the primary autoloader. |
169 | */ |
170 | private function overrideSplLoaders(): void { |
171 | foreach (spl_autoload_functions() as $func) { |
172 | $this->proxiedLoaders[] = $func; |
173 | spl_autoload_unregister($func); |
174 | } |
175 | |
176 | spl_autoload_register( |
177 | function (string $className) { |
178 | /** @var class-string $className */ |
179 | $this->autoload($className); |
180 | }, |
181 | true, |
182 | true |
183 | ); |
184 | } |
185 | |
186 | /** |
187 | * Check classes already declared in the runtime for static constructor methods |
188 | * and call them. |
189 | */ |
190 | private function checkLoadedClasses(): void { |
191 | foreach (get_declared_classes() as $className) { |
192 | $this->callStaticConstructor($className); |
193 | } |
194 | } |
195 | |
196 | } |