PluginProbe
Metricool – Social media and site statistics / 2.1.1
Metricool – Social media and site statistics v2.1.1
2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.27 trunk
← All changes | bootstrap/App.php +203 -0 2.0.1 → 2.1.1 View file →
@@ -1,0 +1,203 @@
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 +}