PluginProbe
404 Solution / 4.1.13
404 Solution v4.1.13
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / ServiceContainer.php

ServiceContainer.php in 404 Solution 4.1.13, at includes/ServiceContainer.php

299 lines 10.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 if (!defined('ABSPATH')) {
5 exit;
6 }
7
8 /**
9 * Simple dependency injection container for managing service instances.
10 *
11 * This container provides a lightweight alternative to the singleton pattern,
12 * making dependencies explicit and enabling easier testing.
13 *
14 * Usage:
15 * $container = ABJ_404_Solution_ServiceContainer::getInstance();
16 * $service = $container->get('service_name');
17 *
18 * Or use the helper function:
19 * $service = abj_service('service_name');
20 */
21 class ABJ_404_Solution_ServiceContainer {
22
23 /**
24 * Singleton instance of the container itself.
25 * Note: The container is a singleton, but the services it manages can have any lifecycle.
26 */
27 /** @var self|null */
28 private static $instance = null;
29
30 /**
31 * Registered services and their factory functions.
32 * @var array<string, callable>
33 */
34 private $services = array();
35
36 /**
37 * Instantiated service instances (for singleton services).
38 * @var array<string, mixed>
39 */
40 private $instances = array();
41
42 /**
43 * Private constructor to enforce singleton pattern.
44 */
45 private function __construct() {
46 // Private constructor
47 }
48
49 /**
50 * Get the singleton instance of the container.
51 *
52 * @return ABJ_404_Solution_ServiceContainer
53 */
54 public static function getInstance() {
55 if (self::$instance === null) {
56 self::$instance = new self();
57 }
58 return self::$instance;
59 }
60
61 /**
62 * Register a service with a factory function.
63 *
64 * The factory function receives the container as its first parameter,
65 * allowing it to resolve dependencies.
66 *
67 * @param string $name Service identifier
68 * @param callable $factory Factory function that creates the service
69 * @return void
70 */
71 public function set($name, $factory) {
72 if (!is_callable($factory)) {
73 throw new InvalidArgumentException("Factory for service '$name' must be callable");
74 }
75 $this->services[$name] = $factory;
76 // Clear any existing instance when re-registering
77 unset($this->instances[$name]);
78 }
79
80 /**
81 * Get a service instance.
82 *
83 * Services are lazy-loaded - the factory function is only called
84 * the first time the service is requested.
85 *
86 * @param string $name Service identifier
87 * @return mixed The service instance
88 * @throws Exception if service is not registered
89 */
90 public function get($name) {
91 // Return existing instance if already created
92 if (isset($this->instances[$name])) {
93 return $this->instances[$name];
94 }
95
96 // Check if service is registered
97 if (!isset($this->services[$name])) {
98 throw new Exception("Service '$name' is not registered in the container");
99 }
100
101 // Create the instance using the factory
102 $factory = $this->services[$name];
103 $instance = $factory($this);
104
105 // Store the instance for future requests (singleton behavior)
106 $this->instances[$name] = $instance;
107
108 return $instance;
109 }
110
111 /**
112 * Check if a service is registered.
113 *
114 * @param string $name Service identifier
115 * @return bool
116 */
117 public function has($name) {
118 return isset($this->services[$name]);
119 }
120
121 /**
122 * Clear all services and instances.
123 * Useful for testing.
124 *
125 * @return void
126 */
127 public function clear() {
128 $this->services = array();
129 $this->instances = array();
130 }
131
132 /**
133 * Reset the container singleton instance.
134 * Useful for testing.
135 *
136 * @return void
137 */
138 public static function reset() {
139 self::$instance = null;
140 }
141
142 /**
143 * Non-throwing existence check. Returns true iff the container has a
144 * registered factory for the named service. Bootstraps the container
145 * singleton on demand so callers don't have to.
146 *
147 * @param string $name Service identifier
148 * @return bool
149 */
150 public static function safeHas($name) {
151 $c = self::getInstance();
152 return $c->has($name);
153 }
154
155 /**
156 * Non-throwing service resolution. Returns the resolved instance, or
157 * null if the service isn't registered or the factory raises any
158 * Throwable. Replaces the legacy `try { ServiceContainer::get(...) }
159 * catch { fall back } ` pattern at call sites — the swallow lives
160 * here, in one place, and is logged via error_log() so it isn't
161 * completely invisible.
162 *
163 * @param string $name Service identifier
164 * @return mixed The service instance, or null on any failure
165 */
166 public static function safeGet($name) {
167 $c = self::getInstance();
168 if (!$c->has($name)) {
169 return null;
170 }
171 try {
172 return $c->get($name);
173 } catch (\Throwable $e) {
174 error_log('404 Solution: ServiceContainer::safeGet(' . $name . ') failed: ' . $e->getMessage());
175 return null;
176 }
177 }
178
179 /**
180 * Returns true iff the container singleton has been instantiated AND
181 * at least one service factory has been registered. False during
182 * very early boot (autoload-only) or after `reset()` in tests.
183 *
184 * @return bool
185 */
186 public static function isInitialized() {
187 return self::$instance !== null && self::$instance->services !== array();
188 }
189 }
190
191 /**
192 * Helper function to access services from the container.
193 *
194 * This provides a shorter, more convenient syntax than calling
195 * ABJ_404_Solution_ServiceContainer::getInstance()->get().
196 *
197 * Fallback semantics: if the named service is not currently registered
198 * (e.g. a test cleared the container without re-running
199 * `abj_404_solution_init_services()`), this looks up the service name in a
200 * static name->class map and falls back to the legacy
201 * `ClassName::getInstance()` singleton. This preserves test patterns that
202 * predate the c260 codemod (clear container, register only the mocks the
203 * test cares about) without forcing every test to re-init the entire
204 * service graph. Production code paths are unaffected because services are
205 * always registered at boot via `abj_404_solution_init_services()`; the
206 * lint at `scripts/lint/lint-getinstance-callers.sh` enforces that
207 * production callers must use this helper rather than `getInstance()`
208 * directly.
209 *
210 * @param string $name Service identifier
211 * @return mixed The service instance
212 *
213 * @phpstan-return (
214 * $name is 'functions' ? ABJ_404_Solution_Functions : (
215 * $name is 'logging' ? ABJ_404_Solution_Logging : (
216 * $name is 'clock' ? ABJ_404_Solution_Clock : (
217 * $name is 'error_handler' ? class-string : (
218 * $name is 'data_access' ? ABJ_404_Solution_DataAccess : (
219 * $name is 'database_upgrades' ? ABJ_404_Solution_DatabaseUpgradesEtc : (
220 * $name is 'permalink_cache' ? ABJ_404_Solution_PermalinkCache : (
221 * $name is 'ngram_filter' ? ABJ_404_Solution_NGramFilter : (
222 * $name is 'plugin_logic' ? ABJ_404_Solution_PluginLogic : (
223 * $name is 'spell_checker' ? ABJ_404_Solution_SpellChecker : (
224 * $name is 'engine_slug' ? ABJ_404_Solution_SlugMatchingEngine : (
225 * $name is 'engine_url_fix' ? ABJ_404_Solution_UrlFixEngine : (
226 * $name is 'engine_title' ? ABJ_404_Solution_TitleMatchingEngine : (
227 * $name is 'engine_category_tag' ? ABJ_404_Solution_CategoryTagMatchingEngine : (
228 * $name is 'engine_content' ? ABJ_404_Solution_ContentMatchingEngine : (
229 * $name is 'engine_spelling' ? ABJ_404_Solution_SpellingMatchingEngine : (
230 * $name is 'engine_archive_fallback' ? ABJ_404_Solution_ArchiveFallbackEngine : (
231 * $name is 'matching_engines' ? array<int, object> : (
232 * $name is 'wordpress_connector' ? ABJ_404_Solution_WordPress_Connector : (
233 * $name is 'slug_change_handler' ? ABJ_404_Solution_SlugChangeHandler : (
234 * $name is 'published_posts_provider' ? ABJ_404_Solution_PublishedPostsProvider : (
235 * $name is 'sync_utils' ? ABJ_404_Solution_SynchronizationUtils : (
236 * $name is 'request_context' ? ABJ_404_Solution_RequestContext : (
237 * $name is 'view' ? ABJ_404_Solution_View : (
238 * $name is 'view_suggestions' ? ABJ_404_Solution_View_Suggestions : (
239 * $name is 'shortcode' ? ABJ_404_Solution_ShortCode :
240 * mixed
241 * ))))))))))))))))))))))))))
242 */
243 function abj_service($name) {
244 $container = ABJ_404_Solution_ServiceContainer::getInstance();
245 if ($container->has($name)) {
246 return $container->get($name);
247 }
248
249 // Inverse of the registration map in bootstrap.php. Lets a caller resolve
250 // a service even when the container hasn't been populated for this
251 // request (typically: a unit test that called
252 // ABJ_404_Solution_ServiceContainer::reset() / ->clear() and only
253 // registered the specific mocks it needed). Long-tail unregistered
254 // singletons that have a stable getInstance() are also reachable here so
255 // call sites don't have to know whether a class is registered yet.
256 static $serviceClassMap = array(
257 'functions' => 'ABJ_404_Solution_Functions',
258 'logging' => 'ABJ_404_Solution_Logging',
259 'data_access' => 'ABJ_404_Solution_DataAccess',
260 'plugin_logic' => 'ABJ_404_Solution_PluginLogic',
261 'view' => 'ABJ_404_Solution_View',
262 'view_suggestions' => 'ABJ_404_Solution_View_Suggestions',
263 'spell_checker' => 'ABJ_404_Solution_SpellChecker',
264 'wordpress_connector' => 'ABJ_404_Solution_WordPress_Connector',
265 'database_upgrades' => 'ABJ_404_Solution_DatabaseUpgradesEtc',
266 'permalink_cache' => 'ABJ_404_Solution_PermalinkCache',
267 'ngram_filter' => 'ABJ_404_Solution_NGramFilter',
268 'slug_change_handler' => 'ABJ_404_Solution_SlugChangeHandler',
269 'published_posts_provider' => 'ABJ_404_Solution_PublishedPostsProvider',
270 'sync_utils' => 'ABJ_404_Solution_SynchronizationUtils',
271 'shortcode' => 'ABJ_404_Solution_ShortCode',
272 'request_context' => 'ABJ_404_Solution_RequestContext',
273 );
274 if (isset($serviceClassMap[$name])) {
275 $class = $serviceClassMap[$name];
276 if (class_exists($class) && method_exists($class, 'getInstance')) {
277 /** @var callable(): mixed $callback */
278 $callback = array($class, 'getInstance');
279 try {
280 return call_user_func($callback);
281 } catch (\Throwable $e) {
282 error_log('404 Solution: abj_service(' . $name . ') legacy fallback failed: ' . $e->getMessage());
283 return null;
284 }
285 }
286 }
287
288 // Last resort — the container raises its standard "not registered"
289 // exception. Catch and return null so callers can rely on a uniform
290 // non-throwing contract; the swallow is logged via error_log() so the
291 // failure is still visible in production logs.
292 try {
293 return $container->get($name);
294 } catch (\Throwable $e) {
295 error_log('404 Solution: abj_service(' . $name . ') unresolved: ' . $e->getMessage());
296 return null;
297 }
298 }
299