| 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 |
|