Factories keyed by service id. */ private array $factories = []; /** @var array Resolved singletons keyed by service id. */ private array $instances = []; private function __construct() { } public static function getInstance(): self { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } /** * Register a lazy factory. It is called the first time the id is resolved. * * @param callable(self): object $factory */ public function register(string $id, callable $factory): self { $this->factories[$id] = $factory; return $this; } /** * Resolve a service, or throw. * * There is no null-returning variant on purpose. A container that answers * "not registered" with null pushes the failure to whichever line first * calls a method on it, which is rarely the line that was wrong. * * @throws ServiceNotFoundException */ public function get(string $id): object { if (isset($this->instances[$id])) { return $this->instances[$id]; } if (isset($this->factories[$id])) { return $this->instances[$id] = ($this->factories[$id])($this); } throw new ServiceNotFoundException(sprintf('WConvert service "%s" is not registered.', esc_html($id))); } /** * Resolve a service by class name, with the type preserved for callers. * * @template T of object * @param class-string $className * @return T * @throws ServiceNotFoundException */ public function resolve(string $className): object { $service = $this->get($className); if (!$service instanceof $className) { throw new ServiceNotFoundException( sprintf('WConvert service "%s" resolved to %s.', esc_html($className), esc_html($service::class)) ); } return $service; } }