Total Complexity | 44 |
Total Lines | 258 |
Duplicated Lines | 0 % |
Coverage | 99.08% |
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( |
|
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 |
|
64 | } |
||
65 | |||
66 | 19 | public function set(string $id, mixed $instance): void |
|
79 | } |
||
80 | |||
81 | /** |
||
82 | * @param class-string|string $id |
||
83 | */ |
||
84 | 33 | public function get(string $id): mixed |
|
91 | } |
||
92 | |||
93 | 5 | public function resolve(callable $callable): mixed |
|
94 | { |
||
95 | 5 | $callable = Closure::fromCallable($callable); |
|
96 | 5 | $reflectionFn = new ReflectionFunction($callable); |
|
97 | 5 | $callableKey = md5(serialize($reflectionFn->__toString())); |
|
98 | |||
99 | 5 | if (!isset($this->cachedDependencies[$callableKey])) { |
|
100 | 5 | $this->cachedDependencies[$callableKey] = $this |
|
101 | 5 | ->getDependencyResolver() |
|
102 | 5 | ->resolveDependencies($callable); |
|
103 | } |
||
104 | |||
105 | /** @psalm-suppress MixedMethodCall */ |
||
106 | 5 | return $callable(...$this->cachedDependencies[$callableKey]); |
|
107 | } |
||
108 | |||
109 | 1 | public function factory(Closure $instance): Closure |
|
110 | { |
||
111 | 1 | $this->factoryInstances->attach($instance); |
|
112 | |||
113 | 1 | return $instance; |
|
114 | } |
||
115 | |||
116 | 1 | public function remove(string $id): void |
|
117 | { |
||
118 | 1 | unset( |
|
119 | 1 | $this->instances[$id], |
|
120 | 1 | $this->frozenInstances[$id] |
|
121 | 1 | ); |
|
122 | } |
||
123 | |||
124 | /** |
||
125 | * @psalm-suppress MixedAssignment |
||
126 | */ |
||
127 | 11 | public function extend(string $id, Closure $instance): Closure |
|
128 | { |
||
129 | 11 | if (!$this->has($id)) { |
|
130 | 4 | $this->extendLater($id, $instance); |
|
131 | |||
132 | 4 | return $instance; |
|
133 | } |
||
134 | |||
135 | 10 | if (isset($this->frozenInstances[$id])) { |
|
136 | 4 | throw ContainerException::frozenInstanceExtend($id); |
|
137 | } |
||
138 | |||
139 | 8 | if (is_object($this->instances[$id]) && isset($this->protectedInstances[$this->instances[$id]])) { |
|
140 | 1 | throw ContainerException::instanceProtected($id); |
|
141 | } |
||
142 | |||
143 | 7 | $factory = $this->instances[$id]; |
|
144 | 7 | $extended = $this->generateExtendedInstance($instance, $factory); |
|
145 | 6 | $this->set($id, $extended); |
|
146 | |||
147 | 6 | return $extended; |
|
148 | } |
||
149 | |||
150 | 2 | public function protect(Closure $instance): Closure |
|
155 | } |
||
156 | |||
157 | 15 | private function getInstance(string $id): mixed |
|
158 | { |
||
159 | 15 | $this->frozenInstances[$id] = true; |
|
160 | |||
161 | 15 | if (!is_object($this->instances[$id]) |
|
162 | 14 | || isset($this->protectedInstances[$this->instances[$id]]) |
|
163 | 15 | || !method_exists($this->instances[$id], '__invoke') |
|
164 | ) { |
||
165 | 8 | return $this->instances[$id]; |
|
166 | } |
||
167 | |||
168 | 12 | if (isset($this->factoryInstances[$this->instances[$id]])) { |
|
169 | 1 | return $this->instances[$id]($this); |
|
170 | } |
||
171 | |||
172 | 11 | $rawService = $this->instances[$id]; |
|
173 | |||
174 | /** @var mixed $resolvedService */ |
||
175 | 11 | $resolvedService = $rawService($this); |
|
176 | |||
177 | 11 | $this->instances[$id] = $resolvedService; |
|
178 | |||
179 | 11 | return $resolvedService; |
|
180 | } |
||
181 | |||
182 | 18 | private function createInstance(string $class): ?object |
|
183 | { |
||
184 | 18 | if (isset($this->bindings[$class])) { |
|
185 | 4 | $binding = $this->bindings[$class]; |
|
186 | 4 | if (is_callable($binding)) { |
|
187 | /** @var mixed $binding */ |
||
188 | 2 | $binding = $binding(); |
|
189 | } |
||
190 | 4 | if (is_object($binding)) { |
|
191 | 2 | return $binding; |
|
192 | } |
||
193 | |||
194 | /** @var class-string $binding */ |
||
195 | 2 | if (class_exists($binding)) { |
|
196 | 2 | return $this->instantiateClass($binding); |
|
197 | } |
||
198 | } |
||
199 | |||
200 | 14 | if (class_exists($class)) { |
|
201 | 10 | return $this->instantiateClass($class); |
|
202 | } |
||
203 | |||
204 | 4 | return null; |
|
205 | } |
||
206 | |||
207 | /** |
||
208 | * @param class-string $class |
||
209 | */ |
||
210 | 12 | private function instantiateClass(string $class): ?object |
|
211 | { |
||
212 | 12 | if (class_exists($class)) { |
|
213 | 12 | if (!isset($this->cachedDependencies[$class])) { |
|
214 | 12 | $this->cachedDependencies[$class] = $this |
|
215 | 12 | ->getDependencyResolver() |
|
216 | 12 | ->resolveDependencies($class); |
|
217 | } |
||
218 | |||
219 | /** @psalm-suppress MixedMethodCall */ |
||
220 | 12 | return new $class(...$this->cachedDependencies[$class]); |
|
221 | } |
||
222 | |||
223 | return null; |
||
224 | } |
||
225 | |||
226 | 4 | private function extendLater(string $id, Closure $instance): void |
|
227 | { |
||
228 | 4 | $this->instancesToExtend[$id][] = $instance; |
|
229 | } |
||
230 | |||
231 | 17 | private function getDependencyResolver(): DependencyResolver |
|
232 | { |
||
233 | 17 | if ($this->dependencyResolver === null) { |
|
234 | 17 | $this->dependencyResolver = new DependencyResolver( |
|
235 | 17 | $this->bindings, |
|
236 | 17 | ); |
|
237 | } |
||
238 | |||
239 | 17 | return $this->dependencyResolver; |
|
1 ignored issue
–
show
|
|||
240 | } |
||
241 | |||
242 | /** |
||
243 | * @psalm-suppress MissingClosureReturnType,MixedAssignment |
||
244 | */ |
||
245 | 7 | private function generateExtendedInstance(Closure $instance, mixed $factory): Closure |
|
246 | { |
||
247 | 7 | if (is_callable($factory)) { |
|
248 | 5 | return static function (self $container) use ($instance, $factory) { |
|
249 | 5 | $result = $factory($container); |
|
250 | |||
251 | 5 | return $instance($result, $container) ?? $result; |
|
252 | 5 | }; |
|
253 | } |
||
254 | |||
255 | 4 | if (is_object($factory) || is_array($factory)) { |
|
256 | 3 | return static fn (self $container) => $instance($factory, $container) ?? $factory; |
|
257 | } |
||
258 | |||
259 | 1 | throw ContainerException::instanceNotExtendable(); |
|
260 | } |
||
261 | |||
262 | 19 | private function extendService(string $id): void |
|
275 | } |
||
276 | } |
||
277 |