PluginProbe
Metricool – Social media and site statistics / 2.0.2
Metricool – Social media and site statistics v2.0.2
2.1.0 2.0.2 2.0.1 2.0.0 1.27 trunk
metricool / bootstrap / App.php

App.php in Metricool – Social media and site statistics 2.0.2, at bootstrap/App.php

204 lines 7.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Metricool\Bootstrap;
6
7 /**
8 * Container class that provides dependency injection capabilities to manage
9 * object creation and resolution. Class is used for retrieving and injecting
10 * dependencies in a structured and reusable way. This is important because it
11 * decouples classes from concrete implementations (new..) and makes the
12 * codebase easier to test and maintain.
13 */
14 final class App
15 {
16 /**
17 * Singleton instance holder. Ensures a single container is shared across
18 * the plugin without globals.
19 */
20 private static ?App $instance = null;
21
22 /**
23 * Registry of service factories indexed by identifier. Allows registering
24 * lazy factory closures for services.
25 * @var array<string, \Closure> $registry
26 */
27 private array $registry = [];
28
29 /**
30 * Instances of resolved services, indexed by identifier. Used to prevent
31 * multiple instantiations and store created services.
32 * @var array<string, object> $instances
33 */
34 private array $instances = [];
35
36 /**
37 * Private constructor to enforce the singleton pattern. Prevents direct
38 * instantiation; use getInstance() instead.
39 */
40 private function __construct()
41 {
42 }
43
44 /**
45 * Retrieve the shared container instance. Provides a central access point
46 * to the container for the plugin.
47 */
48 public static function getInstance(): self
49 {
50 if (self::$instance === null) {
51 self::$instance = new self();
52 }
53
54 return self::$instance;
55 }
56
57 /**
58 * Register a service factory. Defers service creation until first use. The
59 * provided Closure will be called with no arguments when the service is
60 * resolved.
61 */
62 public function set(string $name, \Closure $value): void
63 {
64 $this->registry[$name] ??= $value;
65 }
66
67 /**
68 * Resolve an identifier to a mixed instance. It first checks the registry
69 * for a factory. If none is found, it calls {@see make} to perform
70 * constructor autowiring.
71 *
72 * @param class-string $class
73 * @return mixed
74 * @throws \Exception If the target is not instantiable or cannot resolve a dependency.
75 * @throws \ReflectionException If reflection fails.
76 */
77 public function get(string $class)
78 {
79 // Makes sure we memoize the Closure responses
80 if (array_key_exists($class, $this->instances)) {
81 return $this->instances[$class];
82 }
83
84 if (array_key_exists($class, $this->registry)) {
85 $instance = ($this->registry[$class])();
86 $this->instances[$class] = $instance; // See above
87 return $instance;
88 }
89
90 return $this->make($class);
91 }
92
93 /**
94 * Method is used for on-demand construction of an object using constructor
95 * autowiring without touching the registry. When a constructor parameter
96 * asks for the Container itself, the current Container instance is injected
97 * instead of creating a new Container. Class-typed dependencies are
98 * resolved via {@see get} so factories from the registry are honored, while
99 * scalars or unresolved parameters require defaults or will result in an
100 * exception. This keeps "make" safe for ad-hoc instances you may later
101 * choose to register manually.
102 *
103 * @param class-string $class The class to make. Dependencies are injected.
104 * @param bool $register Made classes are registered in the container on
105 * true. Useful for optimization on multi-used classes.
106 * @param bool $registerDependencies Made dependency classes are registered
107 * in the container on true. Useful for optimization on multi-used classes.
108 *
109 * @throws \Exception If the target is not instantiable or a dependency cannot be resolved.
110 * @throws \ReflectionException If reflection fails.
111 */
112 public function make(string $class, bool $register = true, bool $registerDependencies = true): object
113 {
114 $reflector = new \ReflectionClass($class);
115
116 if ($reflector->isInstantiable() === false) {
117 throw new \Exception(esc_html("Target [{$class}] is not instantiable."));
118 }
119
120 $constructor = $reflector->getConstructor();
121
122 if ($constructor === null) {
123 return new $class();
124 }
125
126 $arguments = [];
127 $parameters = $constructor->getParameters();
128
129 foreach ($parameters as $parameter) {
130 $type = $parameter->getType();
131
132 // No type hinted: allow default value, otherwise we cannot resolve.
133 if ($type === null) {
134 if ($parameter->isDefaultValueAvailable()) {
135 $arguments[] = $parameter->getDefaultValue();
136 continue;
137 }
138
139 throw new \Exception(esc_html(sprintf(
140 'Cannot resolve untyped parameter $%s for [%s] without a default value.',
141 $parameter->getName(),
142 $class
143 )));
144 }
145
146 // For PHP 7.4 only ReflectionNamedType exists (no unions).
147 if ($type instanceof \ReflectionNamedType === false) {
148 throw new \Exception(esc_html(sprintf(
149 'Unsupported parameter type for $%s in [%s].',
150 $parameter->getName(),
151 $class
152 )));
153 }
154
155 // If nullable and no default, we still must supply something;
156 if ($type->isBuiltin()) {
157 if ($parameter->isDefaultValueAvailable()) {
158 $arguments[] = $parameter->getDefaultValue();
159 continue;
160 }
161
162 throw new \Exception(esc_html(sprintf(
163 'Cannot autowire builtin parameter $%s (%s) for [%s]. Provide a default or register a factory.',
164 $parameter->getName(),
165 $type->getName(),
166 $class
167 )));
168 }
169
170 /** @var class-string $dependencyClass */
171 $dependencyClass = $type->getName();
172
173 if ($dependencyClass === self::class) {
174 throw new \Exception(esc_html(sprintf(
175 'Cannot resolve App container dependency for $%s in [%s] to prevent circular dependencies.',
176 $parameter->getName(),
177 $class
178 )));
179 }
180
181 // Using get() will also resolve dependencies of dependencies
182 $dependency = $this->get($dependencyClass);
183
184 // Note: the registry is untouched here as that is only for deferred
185 // factories (closures). The instances prop is used in get() too.
186 if ($registerDependencies === true) {
187 $this->instances[$dependencyClass] = $dependency;
188 }
189
190 $arguments[] = $dependency;
191 }
192
193 $made = new $class(...$arguments);
194
195 // Note: the registry is untouched here as that is only for deferred
196 // factories (closures). The instances prop is used in get() too.
197 if ($register) {
198 $this->instances[$class] = $made;
199 }
200
201 return $made;
202 }
203 }
204