Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
48 / 48
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
Loader
100.00% covered (success)
100.00%
48 / 48
100.00% covered (success)
100.00%
7 / 7
21
100.00% covered (success)
100.00%
1 / 1
 started
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 init
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 __construct
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 autoload
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 callStaticConstructor
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
7
 overrideSplLoaders
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 checkLoadedClasses
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(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
11namespace NxtLvlSoftware\StaticConstructors;
12
13use InvalidArgumentException;
14use NxtLvlSoftware\StaticConstructors\Policy\Class\PhpStyleConstructorPolicy;
15use NxtLvlSoftware\StaticConstructors\Policy\Class\SameNameAsClassPolicy;
16use NxtLvlSoftware\StaticConstructors\Policy\Method\BaseRequirementMethodPolicy;
17use NxtLvlSoftware\StaticConstructors\Policy\Method\NoArgumentsMethodPolicy;
18use NxtLvlSoftware\StaticConstructors\Policy\Method\Visibility\PrivateVisibilityPolicy;
19use ReflectionClass;
20use function class_exists;
21use function get_declared_classes;
22use function spl_autoload_functions;
23use function spl_autoload_register;
24use 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 */
34final 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}