Total Complexity | 44 |
Total Lines | 257 |
Duplicated Lines | 0 % |
Coverage | 99.07% |
Changes | 3 | ||
Bugs | 0 | Features | 0 |
Complex classes like Container often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Container, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
17 | class Container implements ContainerInterface |
||
18 | { |
||
19 | private ?DependencyResolver $dependencyResolver = null; |
||
20 | |||
21 | /** @var array<class-string|string, list<mixed>> */ |
||
1 ignored issue
–
show
|
|||
22 | private array $cachedDependencies = []; |
||
23 | |||
24 | /** @var array<string,mixed> */ |
||
25 | private array $instances = []; |
||
26 | |||
27 | private SplObjectStorage $factoryInstances; |
||
28 | |||
29 | private SplObjectStorage $protectedInstances; |
||
30 | |||
31 | /** @var array<string,bool> */ |
||
32 | private array $frozenInstances = []; |
||
33 | |||
34 | private ?string $currentlyExtending = null; |
||
35 | |||
36 | /** |
||
37 | * @param array<class-string, class-string|callable|object> $bindings |
||
38 | * @param array<string, list<Closure>> $instancesToExtend |
||
39 | */ |
||
40 | 42 | public function __construct( |
|
41 | private array $bindings = [], |
||
42 | private array $instancesToExtend = [], |
||
43 | ) { |
||
44 | 42 | $this->factoryInstances = new SplObjectStorage(); |
|
45 | 42 | $this->protectedInstances = new SplObjectStorage(); |
|
46 | } |
||
47 | |||
48 | /** |
||
49 | * @param class-string $className |
||
50 | */ |
||
51 | 5 | public static function create(string $className): mixed |
|
52 | { |
||
53 | 5 | return (new self())->get($className); |
|
54 | } |
||
55 | |||
56 | 3 | public static function resolveClosure(Closure $closure): mixed |
|
57 | { |
||
58 | 3 | return (new self())->resolve($closure); |
|
59 | } |
||
60 | |||
61 | 37 | public function has(string $id): bool |
|
62 | { |
||
63 | 37 | return isset($this->instances[$id]); |
|
64 | } |
||
65 | |||
66 | 19 | public function set(string $id, mixed $instance): void |
|
67 | { |
||
68 | 19 | if (!empty($this->frozenInstances[$id])) { |
|
69 | 1 | throw ContainerException::frozenInstanceOverride($id); |
|
70 | } |
||
71 | |||
72 | 19 | $this->instances[$id] = $instance; |
|
73 | |||
74 | 19 | if ($this->currentlyExtending === $id) { |
|
75 | 3 | return; |
|
76 | } |
||
77 | |||
78 | 19 | $this->extendService($id); |
|
79 | } |
||
80 | |||
81 | /** |
||
82 | * @param class-string|string $id |
||
83 | */ |
||
84 | 33 | public function get(string $id): mixed |
|
85 | { |
||
86 | 33 | if ($this->has($id)) { |
|
87 | 15 | return $this->getInstance($id); |
|
88 | } |
||
89 | |||
90 | 18 | return $this->createInstance($id); |
|
91 | } |
||
92 | |||
93 | 5 | public function resolve(Closure $closure): mixed |
|
94 | { |
||
95 | 5 | $reflectionFn = new ReflectionFunction($closure); |
|
96 | 5 | $callableKey = md5(serialize($reflectionFn->__toString())); |
|
97 | |||
98 | 5 | if (!isset($this->cachedDependencies[$callableKey])) { |
|
99 | 5 | $this->cachedDependencies[$callableKey] = $this |
|
100 | 5 | ->getDependencyResolver() |
|
101 | 5 | ->resolveDependencies($closure); |
|
102 | } |
||
103 | |||
104 | /** @psalm-suppress MixedMethodCall */ |
||
105 | 5 | return $closure(...$this->cachedDependencies[$callableKey]); |
|
106 | } |
||
107 | |||
108 | 1 | public function factory(Closure $instance): Closure |
|
109 | { |
||
110 | 1 | $this->factoryInstances->attach($instance); |
|
111 | |||
112 | 1 | return $instance; |
|
113 | } |
||
114 | |||
115 | 1 | public function remove(string $id): void |
|
116 | { |
||
117 | 1 | unset( |
|
118 | 1 | $this->instances[$id], |
|
119 | 1 | $this->frozenInstances[$id] |
|
120 | 1 | ); |
|
121 | } |
||
122 | |||
123 | /** |
||
124 | * @psalm-suppress MixedAssignment |
||
125 | */ |
||
126 | 11 | public function extend(string $id, Closure $instance): Closure |
|
127 | { |
||
128 | 11 | if (!$this->has($id)) { |
|
129 | 4 | $this->extendLater($id, $instance); |
|
130 | |||
131 | 4 | return $instance; |
|
132 | } |
||
133 | |||
134 | 10 | if (isset($this->frozenInstances[$id])) { |
|
135 | 4 | throw ContainerException::frozenInstanceExtend($id); |
|
136 | } |
||
137 | |||
138 | 8 | if (is_object($this->instances[$id]) && isset($this->protectedInstances[$this->instances[$id]])) { |
|
139 | 1 | throw ContainerException::instanceProtected($id); |
|
140 | } |
||
141 | |||
142 | 7 | $factory = $this->instances[$id]; |
|
143 | 7 | $extended = $this->generateExtendedInstance($instance, $factory); |
|
144 | 6 | $this->set($id, $extended); |
|
145 | |||
146 | 6 | return $extended; |
|
147 | } |
||
148 | |||
149 | 2 | public function protect(Closure $instance): Closure |
|
154 | } |
||
155 | |||
156 | 15 | private function getInstance(string $id): mixed |
|
157 | { |
||
158 | 15 | $this->frozenInstances[$id] = true; |
|
159 | |||
160 | 15 | if (!is_object($this->instances[$id]) |
|
161 | 14 | || isset($this->protectedInstances[$this->instances[$id]]) |
|
162 | 15 | || !method_exists($this->instances[$id], '__invoke') |
|
163 | ) { |
||
164 | 8 | return $this->instances[$id]; |
|
165 | } |
||
166 | |||
167 | 12 | if (isset($this->factoryInstances[$this->instances[$id]])) { |
|
168 | 1 | return $this->instances[$id]($this); |
|
169 | } |
||
170 | |||
171 | 11 | $rawService = $this->instances[$id]; |
|
172 | |||
173 | /** @var mixed $resolvedService */ |
||
174 | 11 | $resolvedService = $rawService($this); |
|
175 | |||
176 | 11 | $this->instances[$id] = $resolvedService; |
|
177 | |||
178 | 11 | return $resolvedService; |
|
179 | } |
||
180 | |||
181 | 18 | private function createInstance(string $class): ?object |
|
182 | { |
||
183 | 18 | if (isset($this->bindings[$class])) { |
|
184 | 4 | $binding = $this->bindings[$class]; |
|
185 | 4 | if (is_callable($binding)) { |
|
186 | /** @var mixed $binding */ |
||
187 | 2 | $binding = $binding(); |
|
188 | } |
||
189 | 4 | if (is_object($binding)) { |
|
190 | 2 | return $binding; |
|
191 | } |
||
192 | |||
193 | /** @var class-string $binding */ |
||
194 | 2 | if (class_exists($binding)) { |
|
195 | 2 | return $this->instantiateClass($binding); |
|
196 | } |
||
197 | } |
||
198 | |||
199 | 14 | if (class_exists($class)) { |
|
200 | 10 | return $this->instantiateClass($class); |
|
201 | } |
||
202 | |||
203 | 4 | return null; |
|
204 | } |
||
205 | |||
206 | /** |
||
207 | * @param class-string $class |
||
208 | */ |
||
209 | 12 | private function instantiateClass(string $class): ?object |
|
210 | { |
||
211 | 12 | if (class_exists($class)) { |
|
212 | 12 | if (!isset($this->cachedDependencies[$class])) { |
|
213 | 12 | $this->cachedDependencies[$class] = $this |
|
214 | 12 | ->getDependencyResolver() |
|
215 | 12 | ->resolveDependencies($class); |
|
216 | } |
||
217 | |||
218 | /** @psalm-suppress MixedMethodCall */ |
||
219 | 12 | return new $class(...$this->cachedDependencies[$class]); |
|
220 | } |
||
221 | |||
222 | return null; |
||
223 | } |
||
224 | |||
225 | 4 | private function extendLater(string $id, Closure $instance): void |
|
226 | { |
||
227 | 4 | $this->instancesToExtend[$id][] = $instance; |
|
228 | } |
||
229 | |||
230 | 17 | private function getDependencyResolver(): DependencyResolver |
|
231 | { |
||
232 | 17 | if ($this->dependencyResolver === null) { |
|
233 | 17 | $this->dependencyResolver = new DependencyResolver( |
|
234 | 17 | $this->bindings, |
|
235 | 17 | ); |
|
236 | } |
||
237 | |||
238 | 17 | return $this->dependencyResolver; |
|
1 ignored issue
–
show
|
|||
239 | } |
||
240 | |||
241 | /** |
||
242 | * @psalm-suppress MissingClosureReturnType,MixedAssignment |
||
243 | */ |
||
244 | 7 | private function generateExtendedInstance(Closure $instance, mixed $factory): Closure |
|
245 | { |
||
246 | 7 | if (is_callable($factory)) { |
|
247 | 5 | return static function (self $container) use ($instance, $factory) { |
|
248 | 5 | $result = $factory($container); |
|
249 | |||
250 | 5 | return $instance($result, $container) ?? $result; |
|
251 | 5 | }; |
|
252 | } |
||
253 | |||
254 | 4 | if (is_object($factory) || is_array($factory)) { |
|
255 | 3 | return static fn (self $container) => $instance($factory, $container) ?? $factory; |
|
256 | } |
||
257 | |||
258 | 1 | throw ContainerException::instanceNotExtendable(); |
|
259 | } |
||
260 | |||
261 | 19 | private function extendService(string $id): void |
|
274 | } |
||
275 | } |
||
276 |