Lines
76.47%
234 / 306
Methods
59.25%
32 / 54
Classes
0.00%
0 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| __construct | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | ||
| get | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| make | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| has | 100.00% | 7 / 7 | 100.00% | 1 / 1 | 5 | ||
| bound | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 4 | ||
| resolved | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| singleton | 100.00% | 8 / 8 | 100.00% | 1 / 1 | 3 | ||
| scoped | 60.00% | 3 / 5 | 0.00% | 0 / 1 | 2.26 | ||
| instance | 100.00% | 12 / 12 | 100.00% | 1 / 1 | 3 | ||
| bind | 87.50% | 7 / 8 | 0.00% | 0 / 1 | 3.02 | ||
| bindIf | 75.00% | 3 / 4 | 0.00% | 0 / 1 | 4.25 | ||
| alias | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| extend | 66.66% | 2 / 3 | 0.00% | 0 / 1 | 2.15 | ||
| tag | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 3 | ||
| tagged | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 2 | ||
| when | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| addContextualBinding | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| resolving | 66.66% | 2 / 3 | 0.00% | 0 / 1 | 3.33 | ||
| afterResolving | 66.66% | 2 / 3 | 0.00% | 0 / 1 | 3.33 | ||
| rebinding | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 2 | ||
| call | 90.90% | 10 / 11 | 0.00% | 0 / 1 | 5.02 | ||
| remove | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 3 | ||
| removeAlias | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| forgetInstance | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| forgetInstances | 0.00% | 0 / 1 | 0.00% | 0 / 1 | 2 | ||
| flush | 100.00% | 6 / 6 | 100.00% | 1 / 1 | 1 | ||
| cachePlans | 0.00% | 0 / 11 | 0.00% | 0 / 1 | 20 | ||
| loadCachedPlans | 0.00% | 0 / 13 | 0.00% | 0 / 1 | 72 | ||
| isValidPlan | 0.00% | 0 / 7 | 0.00% | 0 / 1 | 30 | ||
| resolve | 100.00% | 11 / 11 | 100.00% | 1 / 1 | 6 | ||
| resolveFactory | 100.00% | 6 / 6 | 100.00% | 1 / 1 | 1 | ||
| resolveBinding | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 1 | ||
| resolveFactoryCallable | 50.00% | 6 / 12 | 0.00% | 0 / 1 | 4.12 | ||
| resolveContextual | 66.66% | 2 / 3 | 0.00% | 0 / 1 | 2.15 | ||
| getContextualConcrete | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 2 | ||
| build | 78.94% | 15 / 19 | 0.00% | 0 / 1 | 5.23 | ||
| resolveDependencies | 58.82% | 10 / 17 | 0.00% | 0 / 1 | 12.47 | ||
| applyExtenders | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 2 | ||
| fireResolvingCallbacks | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 1 | ||
| fireCallbackArray | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 2 | ||
| rebound | 0.00% | 0 / 3 | 0.00% | 0 / 1 | 6 | ||
| normalizeHandler | 90.00% | 9 / 10 | 0.00% | 0 / 1 | 7.05 | ||
| getParameterPlan | 100.00% | 15 / 15 | 100.00% | 1 / 1 | 4 | ||
| planKey | 80.00% | 4 / 5 | 0.00% | 0 / 1 | 4.13 | ||
| getReflection | 80.00% | 4 / 5 | 0.00% | 0 / 1 | 3.07 | ||
| resolveParameter | 92.30% | 12 / 13 | 0.00% | 0 / 1 | 6.02 | ||
| castParameter | 62.50% | 5 / 8 | 0.00% | 0 / 1 | 13.27 | ||
| getTypeName | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 2 | ||
| isInstantiable | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 2 | ||
| isPlanSerializable | 0.00% | 0 / 4 | 0.00% | 0 / 1 | 12 | ||
| forget | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| resolveAlias | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 2 | ||
| resolvesTo | 100.00% | 7 / 7 | 100.00% | 1 / 1 | 4 | ||
| abstractFromCallable | 100.00% | 8 / 8 | 100.00% | 1 / 1 | 3 | ||
| 40 | class Container implements ContainerInterface | |
| 41 | { | |
| 42 | /** | |
| 43 | * Instances keyed by identifier. | |
| 44 | * | |
| 45 | * @var array<string, object> | |
| 46 | */ | |
| 47 | private array $instances = []; | |
| 48 | ||
| 49 | /** | |
| 50 | * Lazy factory closures keyed by identifier. | |
| 51 | * | |
| 52 | * Each closure is invoked on first {@see get()}, then the result is | |
| 53 | * cached as a shared singleton and the factory is removed. | |
| 54 | * | |
| 55 | * @var array<string, Closure> | |
| 56 | */ | |
| 57 | private array $factories = []; | |
| 58 | ||
| 59 | /** | |
| 60 | * Non-shared factory closures keyed by identifier. | |
| 61 | * | |
| 62 | * Unlike {@see $factories}, each closure here is invoked on every | |
| 63 | * {@see get()} call, so every resolution returns a fresh instance. | |
| 64 | * | |
| 65 | * @var array<string, Closure> | |
| 66 | */ | |
| 67 | private array $bindings = []; | |
| 68 | ||
| 69 | /** | |
| 70 | * Alias identifiers mapped to the abstract they resolve to. | |
| 71 | * | |
| 72 | * @var array<string, string> | |
| 73 | */ | |
| 74 | private array $aliases = []; | |
| 75 | ||
| 76 | /** | |
| 77 | * Contextual bindings: concrete => [abstract => implementation]. | |
| 78 | * | |
| 79 | * @var array<string, array<string, string|Closure>> | |
| 80 | */ | |
| 81 | private array $contextual = []; | |
| 82 | ||
| 83 | /** | |
| 84 | * Extenders keyed by identifier. | |
| 85 | * | |
| 86 | * Each extender decorates the resolved instance before it is returned. | |
| 87 | * | |
| 88 | * @var array<string, array<int, Closure>> | |
| 89 | */ | |
| 90 | private array $extenders = []; | |
| 91 | ||
| 92 | /** | |
| 93 | * Tags mapping a tag name to the abstracts it contains. | |
| 94 | * | |
| 95 | * @var array<string, array<int, string>> | |
| 96 | */ | |
| 97 | private array $tags = []; | |
| 98 | ||
| 99 | /** | |
| 100 | * Identifiers that have been resolved at least once. | |
| 101 | * | |
| 102 | * @var array<string, bool> | |
| 103 | */ | |
| 104 | private array $resolved = []; | |
| 105 | ||
| 106 | /** | |
| 107 | * Callbacks fired before an instance is returned, keyed by identifier. | |
| 108 | * | |
| 109 | * @var array<string, array<int, Closure>> | |
| 110 | */ | |
| 111 | private array $resolvingCallbacks = []; | |
| 112 | ||
| 113 | /** | |
| 114 | * Callbacks fired after an instance is resolved, keyed by identifier. | |
| 115 | * | |
| 116 | * @var array<string, array<int, Closure>> | |
| 117 | */ | |
| 118 | private array $afterResolvingCallbacks = []; | |
| 119 | ||
| 120 | /** | |
| 121 | * Callbacks fired when an identifier is rebound, keyed by identifier. | |
| 122 | * | |
| 123 | * @var array<string, array<int, Closure>> | |
| 124 | */ | |
| 125 | private array $reboundCallbacks = []; | |
| 126 | ||
| 127 | /** | |
| 128 | * Stack of classes currently being built, for circular detection and | |
| 129 | * contextual binding resolution. | |
| 130 | * | |
| 131 | * @var array<int, string> | |
| 132 | */ | |
| 133 | private array $buildStack = []; | |
| 134 | ||
| 135 | /** | |
| 136 | * Cached reflection parameter plans keyed by "Class::method" or closure id. | |
| 137 | * | |
| 138 | * @var array<string, array<int, array<string, mixed>>> | |
| 139 | */ | |
| 140 | private array $reflectionPlans = []; | |
| 141 | ||
| 142 | /** | |
| 143 | * Abstracts registered as scoped singletons, flushed on {@see flush()}. | |
| 144 | * | |
| 145 | * @var array<int, string> | |
| 146 | */ | |
| 147 | private array $scopedInstances = []; | |
| 148 | ||
| 149 | /** | |
| 150 | * Create a new container. | |
| 151 | * | |
| 152 | * The container registers itself as a resolvable instance so factories | |
| 153 | * and callables can type-hint it for injection. | |
| 154 | */ | |
| 155 | public function __construct() | |
| 156 | { | |
| 157 | $this->instance(Container::class, $this); | |
| 158 | $this->instance(ContainerInterface::class, $this); | |
| 159 | } | |
| 160 | ||
| 161 | /** | |
| 162 | * Get a container entry by its identifier. | |
| 163 | * | |
| 164 | * Resolves in order: cached instance, contextual binding, singleton | |
| 165 | * factory, non-shared binding, then an autowired build. Autowired results | |
| 166 | * are returned fresh (never cached) unless registered as a singleton. | |
| 167 | * | |
| 168 | * @template T | |
| 169 | * @param class-string<T>|string $id Identifier (class name or alias) for the entry | |
| 170 | * @return T The resolved entry | |
| 171 | * @throws NotFoundExceptionInterface If no entry can be resolved | |
| 172 | * @throws ContainerExceptionInterface If the entry exists but cannot be resolved | |
| 173 | */ | |
| 174 | public function get(string $id): mixed | |
| 175 | { | |
| 176 | return $this->resolve($id); | |
| 177 | } | |
| 178 | ||
| 179 | /** | |
| 180 | * Resolve an entry, autowiring its constructor dependencies. | |
| 181 | * | |
| 182 | * Behaves like {@see get()} but is the explicit "build" entry point. | |
| 183 | * Autowired results are returned fresh and never cached unless the entry | |
| 184 | * was registered as a singleton. | |
| 185 | * | |
| 186 | * ```php | |
| 187 | * $service = $container->make(MyService::class); // autowires deps | |
| 188 | * $service = $container->make(MyService::class, ['config' => $config]); | |
| 189 | * ``` | |
| 190 | * | |
| 191 | * @template T | |
| 192 | * @param class-string<T>|string $abstract Identifier (class name or alias) to resolve | |
| 193 | * @param array $parameters Explicit values keyed by constructor parameter name | |
| 194 | * @return T The resolved entry | |
| 195 | * @throws ContainerExceptionInterface If the entry cannot be resolved | |
| 196 | */ | |
| 197 | public function make(string $abstract, array $parameters = []): mixed | |
| 198 | { | |
| 199 | return $this->resolve($abstract, $parameters); | |
| 200 | } | |
| 201 | ||
| 202 | /** | |
| 203 | * Determine whether the container can resolve an identifier. | |
| 204 | * | |
| 205 | * Returns true when the identifier is registered (instance, factory, | |
| 206 | * binding, or alias) or when it names an instantiable concrete class that | |
| 207 | * can be autowired. | |
| 208 | * | |
| 209 | * @param string $id Identifier (class name or alias) for the entry | |
| 210 | * @return bool True if an entry can be resolved for the identifier | |
| 211 | */ | |
| 212 | public function has(string $id): bool | |
| 213 | { | |
| 214 | $id = $this->resolveAlias($id); | |
| 215 | ||
| 216 | if (isset($this->instances[$id]) | |
| 217 | || isset($this->factories[$id]) | |
| 218 | || isset($this->bindings[$id]) | |
| 219 | || isset($this->contextual[$id])) { | |
| 220 | return true; | |
| 221 | } | |
| 222 | ||
| 223 | return $this->isInstantiable($id); | |
| 224 | } | |
| 225 | ||
| 226 | /** | |
| 227 | * Determine whether an identifier has been explicitly bound. | |
| 228 | * | |
| 229 | * Unlike {@see has()}, this does not consider autowirable concretes — it | |
| 230 | * only reports explicit registrations. | |
| 231 | * | |
| 232 | * @param string $abstract Identifier (class name or alias) to check | |
| 233 | * @return bool True if the identifier is explicitly bound | |
| 234 | */ | |
| 235 | public function bound(string $abstract): bool | |
| 236 | { | |
| 237 | return isset($this->instances[$abstract]) | |
| 238 | || isset($this->factories[$abstract]) | |
| 239 | || isset($this->bindings[$abstract]) | |
| 240 | || isset($this->aliases[$abstract]); | |
| 241 | } | |
| 242 | ||
| 243 | /** | |
| 244 | * Determine whether an identifier has been resolved at least once. | |
| 245 | * | |
| 246 | * @param string $abstract Identifier (class name or alias) to check | |
| 247 | * @return bool True if the identifier has been resolved | |
| 248 | */ | |
| 249 | public function resolved(string $abstract): bool | |
| 250 | { | |
| 251 | return isset($this->resolved[$abstract]); | |
| 252 | } | |
| 253 | ||
| 254 | /** | |
| 255 | * Register a shared singleton entry. | |
| 256 | * | |
| 257 | * The entry is registered as a lazy factory and only instantiated on the | |
| 258 | * first {@see get()}; the result is then cached as a shared singleton. | |
| 259 | * When given a concrete class string, its constructor dependencies are | |
| 260 | * autowired. | |
| 261 | * | |
| 262 | * ```php | |
| 263 | * $container->singleton(Mailer::class); // lazy, shared | |
| 264 | * $container->singleton(MailerInterface::class, SmtpMailer::class); | |
| 265 | * $container->singleton(Mailer::class, fn () => new Mailer(...)); | |
| 266 | * $container->singleton(fn (): Mailer => new Mailer(...)); // keyed by Mailer::class | |
| 267 | * ``` | |
| 268 | * | |
| 269 | * @param string|callable $abstract Identifier (class name or alias) to register the entry under, or a factory callable whose return type names the identifier | |
| 270 | * @param string|callable|null $concrete Class name to instantiate lazily, or a factory callable returning the instance; defaults to $abstract | |
| 271 | * @return void | |
| 272 | * @throws ContainerExceptionInterface If the class cannot be instantiated, or a callable abstract has no class return type | |
| 273 | */ | |
| 274 | public function singleton(string|callable $abstract, string|callable|null $concrete = null): void | |
| 275 | { | |
| 276 | if (\is_callable($abstract)) { | |
| 277 | $concrete = $abstract; | |
| 278 | $abstract = $this->abstractFromCallable($abstract); | |
| 279 | } | |
| 280 | ||
| 281 | $concrete ??= $abstract; | |
| 282 | $this->forget($abstract); | |
| 283 | ||
| 284 | $this->factories[$abstract] = \is_string($concrete) | |
| 285 | ? fn () => $this->build($concrete) | |
| 286 | : Closure::fromCallable($concrete); | |
| 287 | } | |
| 288 | ||
| 289 | /** | |
| 290 | * Register a scoped singleton entry. | |
| 291 | * | |
| 292 | * Behaves like {@see singleton()} but the resolved instance is cleared by | |
| 293 | * {@see flush()}, so a new instance is built on the next resolution after | |
| 294 | * a flush. | |
| 295 | * | |
| 296 | * @param string|callable $abstract Identifier (class name or alias) to register the entry under, or a factory callable whose return type names the identifier | |
| 297 | * @param string|callable|null $concrete Class name to instantiate lazily, or a factory callable returning the instance; defaults to $abstract | |
| 298 | * @return void | |
| 299 | * @throws ContainerExceptionInterface If the class cannot be instantiated, or a callable abstract has no class return type | |
| 300 | */ | |
| 301 | public function scoped(string|callable $abstract, string|callable|null $concrete = null): void | |
| 302 | { | |
| 303 | if (\is_callable($abstract)) { | |
| 304 | $concrete = $abstract; | |
| 305 | $abstract = $this->abstractFromCallable($abstract); | |
| 306 | } | |
| 307 | ||
| 308 | $this->scopedInstances[] = $abstract; | |
| 309 | $this->singleton($abstract, $concrete); | |
| 310 | } | |
| 311 | ||
| 312 | /** | |
| 313 | * Register an existing object instance under an identifier. | |
| 314 | * | |
| 315 | * ```php | |
| 316 | * $container->instance(LoggerInterface::class, $logger); | |
| 317 | * $container->instance($logger); // keyed by Logger::class | |
| 318 | * ``` | |
| 319 | * | |
| 320 | * @param string|object $abstract Identifier (class name or alias) to register the instance under, or the instance itself (keyed by its class name) | |
| 321 | * @param object|null $instance The instance to register; required when $abstract is a string | |
| 322 | * @return object The registered instance | |
| 323 | * @throws ContainerExceptionInterface If $abstract is a string and no instance is given | |
| 324 | */ | |
| 325 | public function instance(string|object $abstract, ?object $instance = null): object | |
| 326 | { | |
| 327 | if (\is_object($abstract)) { | |
| 328 | $instance = $abstract; | |
| 329 | $abstract = $abstract::class; | |
| 330 | } | |
| 331 | ||
| 332 | if ($instance === null) { | |
| 333 | throw new ContainerException( | |
| 334 | 'instance() requires an instance when given a string identifier, e.g. ' . | |
| 335 | 'instance(LoggerInterface::class, $logger).' | |
| 336 | ); | |
| 337 | } | |
| 338 | ||
| 339 | $this->forget($abstract); | |
| 340 | $this->instances[$abstract] = $instance; | |
| 341 | $this->resolved[$abstract] = true; | |
| 342 | ||
| 343 | return $instance; | |
| 344 | } | |
| 345 | ||
| 346 | /** | |
| 347 | * Register a non-shared entry backed by a factory. | |
| 348 | * | |
| 349 | * Unlike {@see singleton()}, the factory is invoked on every {@see get()} | |
| 350 | * call, so each resolution returns a fresh instance. When given a concrete | |
| 351 | * class string, its constructor dependencies are autowired on each build. | |
| 352 | * | |
| 353 | * ```php | |
| 354 | * $container->bind(Connection::class, static fn () => new Connection($dsn)); | |
| 355 | * $container->bind(fn (): Connection => new Connection($dsn)); // keyed by Connection::class | |
| 356 | * | |
| 357 | * $a = $container->get(Connection::class); | |
| 358 | * $b = $container->get(Connection::class); // $a !== $b | |
| 359 | * ``` | |
| 360 | * | |
| 361 | * @param string|callable $abstract Identifier (class name or alias) to register the entry under, or a factory callable whose return type names the identifier | |
| 362 | * @param string|callable|null $concrete Class name to instantiate per resolution, or a factory callable; defaults to $abstract | |
| 363 | * @return void | |
| 364 | * @throws ContainerExceptionInterface If a callable abstract has no class return type | |
| 365 | */ | |
| 366 | public function bind(string|callable $abstract, string|callable|null $concrete = null): void | |
| 367 | { | |
| 368 | if (\is_callable($abstract)) { | |
| 369 | $concrete = $abstract; | |
| 370 | $abstract = $this->abstractFromCallable($abstract); | |
| 371 | } | |
| 372 | ||
| 373 | $concrete ??= $abstract; | |
| 374 | $this->forget($abstract); | |
| 375 | ||
| 376 | $this->bindings[$abstract] = \is_string($concrete) | |
| 377 | ? fn () => $this->build($concrete) | |
| 378 | : Closure::fromCallable($concrete); | |
| 379 | } | |
| 380 | ||
| 381 | /** | |
| 382 | * Register a binding only if the identifier is not already bound. | |
| 383 | * | |
| 384 | * @param string|callable $abstract Identifier (class name or alias) to register the entry under, or a factory callable whose return type names the identifier | |
| 385 | * @param string|callable|null $concrete Class name to instantiate, or a factory callable; defaults to $abstract | |
| 386 | * @return void | |
| 387 | * @throws ContainerExceptionInterface If a callable abstract has no class return type | |
| 388 | */ | |
| 389 | public function bindIf(string|callable $abstract, string|callable|null $concrete = null): void | |
| 390 | { | |
| 391 | $key = \is_string($abstract) ? $abstract : null; | |
| 392 | ||
| 393 | if ($key !== null && $this->bound($key)) { | |
| 394 | return; | |
| 395 | } | |
| 396 | ||
| 397 | $this->bind($abstract, $concrete); | |
| 398 | } | |
| 399 | ||
| 400 | /** | |
| 401 | * Register an alias so a second identifier resolves to the same entry as | |
| 402 | * an already-registered abstract, without re-instantiating it. | |
| 403 | * | |
| 404 | * ```php | |
| 405 | * $container->singleton(MailerInterface::class, SmtpMailer::class); | |
| 406 | * $container->alias(MailerInterface::class, SmtpMailer::class); | |
| 407 | * | |
| 408 | * $a = $container->get(MailerInterface::class); | |
| 409 | * $b = $container->get(SmtpMailer::class); // $a === $b | |
| 410 | * ``` | |
| 411 | * | |
| 412 | * Aliases are resolved lazily at {@see get()} time, so the abstract does | |
| 413 | * not need to be registered yet when the alias is created. | |
| 414 | * | |
| 415 | * @param string $abstract The identifier the alias points to | |
| 416 | * @param string $alias The additional identifier to resolve to $abstract | |
| 417 | * @return void | |
| 418 | */ | |
| 419 | public function alias(string $abstract, string $alias): void | |
| 420 | { | |
| 421 | $this->aliases[$alias] = $abstract; | |
| 422 | } | |
| 423 | ||
| 424 | /** | |
| 425 | * Register an extender that decorates a resolved instance. | |
| 426 | * | |
| 427 | * The extender receives the resolved instance and the container, and must | |
| 428 | * return the (possibly decorated) instance. Extenders run after the | |
| 429 | * instance is built and before it is returned. | |
| 430 | * | |
| 431 | * ```php | |
| 432 | * $container->extend(Logger::class, fn (Logger $logger, $container) => new DecoratedLogger($logger)); | |
| 433 | * ``` | |
| 434 | * | |
| 435 | * @param string $abstract Identifier whose resolved instances to decorate | |
| 436 | * @param Closure $extender Callable receiving (instance, container) and returning the instance | |
| 437 | * @return void | |
| 438 | */ | |
| 439 | public function extend(string $abstract, Closure $extender): void | |
| 440 | { | |
| 441 | $this->extenders[$abstract][] = $extender; | |
| 442 | ||
| 443 | if (isset($this->instances[$abstract])) { | |
| 444 | $this->rebound($abstract); | |
| 445 | } | |
| 446 | } | |
| 447 | ||
| 448 | /** | |
| 449 | * Assign one or more tags to one or more abstracts. | |
| 450 | * | |
| 451 | * @param string|array $abstracts Identifier(s) to tag | |
| 452 | * @param array $tags Tag name(s) to assign | |
| 453 | * @return void | |
| 454 | */ | |
| 455 | public function tag(string|array $abstracts, array $tags): void | |
| 456 | { | |
| 457 | foreach ((array) $abstracts as $abstract) { | |
| 458 | foreach ($tags as $tag) { | |
| 459 | $this->tags[$tag][] = $abstract; | |
| 460 | } | |
| 461 | } | |
| 462 | } | |
| 463 | ||
| 464 | /** | |
| 465 | * Resolve every abstract assigned to a tag. | |
| 466 | * | |
| 467 | * @param string $tag The tag name | |
| 468 | * @return array<int, mixed> The resolved instances | |
| 469 | * @throws ContainerExceptionInterface If any tagged abstract cannot be resolved | |
| 470 | */ | |
| 471 | public function tagged(string $tag): array | |
| 472 | { | |
| 473 | $results = []; | |
| 474 | ||
| 475 | foreach ($this->tags[$tag] ?? [] as $abstract) { | |
| 476 | $results[] = $this->make($abstract); | |
| 477 | } | |
| 478 | ||
| 479 | return $results; | |
| 480 | } | |
| 481 | ||
| 482 | /** | |
| 483 | * Begin a contextual binding for a concrete class. | |
| 484 | * | |
| 485 | * ```php | |
| 486 | * $container->when(PhotoController::class) | |
| 487 | * ->needs(Filesystem::class) | |
| 488 | * ->give(LocalFilesystem::class); | |
| 489 | * ``` | |
| 490 | * | |
| 491 | * @param string $concrete The class whose dependencies to override | |
| 492 | * @return ContextualBindingBuilder A builder to declare the needs/give pair | |
| 493 | */ | |
| 494 | public function when(string $concrete): ContextualBindingBuilder | |
| 495 | { | |
| 496 | return new ContextualBindingBuilder($this, $concrete); | |
| 497 | } | |
| 498 | ||
| 499 | /** | |
| 500 | * Register a contextual binding. | |
| 501 | * | |
| 502 | * @param string $concrete The class whose dependency to override | |
| 503 | * @param string $abstract The abstract being overridden | |
| 504 | * @param string|Closure $implementation The concrete or factory to give | |
| 505 | * @return void | |
| 506 | */ | |
| 507 | public function addContextualBinding(string $concrete, string $abstract, string|Closure $implementation): void | |
| 508 | { | |
| 509 | $this->contextual[$concrete][$abstract] = $implementation; | |
| 510 | } | |
| 511 | ||
| 512 | /** | |
| 513 | * Register a callback to run before an instance is returned. | |
| 514 | * | |
| 515 | * @param string|callable $abstract Identifier to hook, or a global callback when no identifier is given | |
| 516 | * @param Closure|null $callback The callback receiving (instance, container) | |
| 517 | * @return void | |
| 518 | */ | |
| 519 | public function resolving(string|callable $abstract, ?Closure $callback = null): void | |
| 520 | { | |
| 521 | if (\is_string($abstract) && $callback !== null) { | |
| 522 | $this->resolvingCallbacks[$abstract][] = $callback; | |
| 523 | } else { | |
| 524 | $this->resolvingCallbacks['*'][] = $abstract; | |
| 525 | } | |
| 526 | } | |
| 527 | ||
| 528 | /** | |
| 529 | * Register a callback to run after an instance is resolved. | |
| 530 | * | |
| 531 | * @param string|callable $abstract Identifier to hook, or a global callback when no identifier is given | |
| 532 | * @param Closure|null $callback The callback receiving (instance, container) | |
| 533 | * @return void | |
| 534 | */ | |
| 535 | public function afterResolving(string|callable $abstract, ?Closure $callback = null): void | |
| 536 | { | |
| 537 | if (\is_string($abstract) && $callback !== null) { | |
| 538 | $this->afterResolvingCallbacks[$abstract][] = $callback; | |
| 539 | } else { | |
| 540 | $this->afterResolvingCallbacks['*'][] = $abstract; | |
| 541 | } | |
| 542 | } | |
| 543 | ||
| 544 | /** | |
| 545 | * Register a callback to run when an identifier is rebound. | |
| 546 | * | |
| 547 | * If the identifier is already resolved, the callback fires immediately. | |
| 548 | * | |
| 549 | * @param string $abstract Identifier to watch | |
| 550 | * @param Closure $callback The callback receiving (instance, container) | |
| 551 | * @return void | |
| 552 | */ | |
| 553 | public function rebinding(string $abstract, Closure $callback): void | |
| 554 | { | |
| 555 | $this->reboundCallbacks[$abstract][] = $callback; | |
| 556 | ||
| 557 | if (isset($this->instances[$abstract])) { | |
| 558 | $callback($this->instances[$abstract], $this); | |
| 559 | } | |
| 560 | } | |
| 561 | ||
| 562 | /** | |
| 563 | * Invoke a callable, resolving its parameters from the container. | |
| 564 | * | |
| 565 | * Each parameter is resolved in order: by name from $parameters, then by | |
| 566 | * type from the container, then a default value, then null if nullable. | |
| 567 | * Otherwise a {@see ContainerException} is thrown. Primitive values are | |
| 568 | * cast to the parameter's declared type where possible. | |
| 569 | * | |
| 570 | * Handlers may be a closure, [Class, 'method'], 'Class@method', or an | |
| 571 | * invokable class/object. Class-based handlers are instantiated via | |
| 572 | * {@see make()} (constructor injection) before the method is invoked. | |
| 573 | * | |
| 574 | * ```php | |
| 575 | * $result = $container->call([$controller, 'show'], ['id' => 5]); | |
| 576 | * $result = $container->call(fn (Logger $log) => $log->info('hi')); | |
| 577 | * ``` | |
| 578 | * | |
| 579 | * @template T | |
| 580 | * @param callable(): T|string|array $callback The callable to invoke | |
| 581 | * @param array $parameters Explicit values keyed by parameter name | |
| 582 | * @param string|null $defaultMethod Method to invoke when $callback is an invokable class string | |
| 583 | * @return T The callable's return value | |
| 584 | * @throws ContainerExceptionInterface If a parameter cannot be resolved | |
| 585 | */ | |
| 586 | public function call(callable|string|array $callback, array $parameters = [], ?string $defaultMethod = null): mixed | |
| 587 | { | |
| 588 | try { | |
| 589 | [$class, $method] = $this->normalizeHandler($callback, $defaultMethod); | |
| 590 | ||
| 591 | $plan = $this->getParameterPlan($class, $method); | |
| 592 | ||
| 593 | $resolved = []; | |
| 594 | foreach ($plan as $param) { | |
| 595 | $resolved[] = $this->resolveParameter($param, $parameters); | |
| 596 | } | |
| 597 | ||
| 598 | if ($class !== null) { | |
| 599 | $instance = \is_object($class) ? $class : $this->make($class); | |
| 600 | return $instance->{$method}(...$resolved); | |
| 601 | } | |
| 602 | ||
| 603 | return $method(...$resolved); | |
| 604 | } catch (NotFoundException $e) { | |
| 605 | throw new ContainerException($e->getMessage(), 0, $e); | |
| 606 | } | |
| 607 | } | |
| 608 | ||
| 609 | /** | |
| 610 | * Remove an entry and every alias that resolves to it. | |
| 611 | * | |
| 612 | * The identifier is resolved first (like {@see get()} and {@see has()}), | |
| 613 | * so passing an alias removes the underlying entry and all aliases that | |
| 614 | * point to it. To remove a single alias without touching the entry, use | |
| 615 | * {@see removeAlias()}. | |
| 616 | * | |
| 617 | * @param string $abstract The identifier (or alias) of the entry to remove | |
| 618 | * @return void | |
| 619 | */ | |
| 620 | public function remove(string $abstract): void | |
| 621 | { | |
| 622 | $abstract = $this->resolveAlias($abstract); | |
| 623 | $this->forget($abstract); | |
| 624 | ||
| 625 | foreach ($this->aliases as $alias => $target) { | |
| 626 | if ($this->resolvesTo($alias, $abstract)) { | |
| 627 | unset($this->aliases[$alias]); | |
| 628 | } | |
| 629 | } | |
| 630 | } | |
| 631 | ||
| 632 | /** | |
| 633 | * Remove a single alias without touching the entry it points to. | |
| 634 | * | |
| 635 | * @param string $alias The alias identifier to remove | |
| 636 | * @return void | |
| 637 | */ | |
| 638 | public function removeAlias(string $alias): void | |
| 639 | { | |
| 640 | unset($this->aliases[$alias]); | |
| 641 | } | |
| 642 | ||
| 643 | /** | |
| 644 | * Forget a single resolved instance. | |
| 645 | * | |
| 646 | * The next resolution of the identifier will build a fresh instance. | |
| 647 | * | |
| 648 | * @param string $abstract Identifier whose instance to forget | |
| 649 | * @return void | |
| 650 | */ | |
| 651 | public function forgetInstance(string $abstract): void | |
| 652 | { | |
| 653 | unset($this->instances[$abstract]); | |
| 654 | } | |
| 655 | ||
| 656 | /** | |
| 657 | * Forget all resolved instances. | |
| 658 | * | |
| 659 | * @return void | |
| 660 | */ | |
| 661 | public function forgetInstances(): void | |
| 662 | { | |
| 663 | $this->instances = []; | |
| 664 | } | |
| 665 | ||
| 666 | /** | |
| 667 | * Flush the container, clearing instances, bindings, factories, aliases, | |
| 668 | * and resolution state. | |
| 669 | * | |
| 670 | * @return void | |
| 671 | */ | |
| 672 | public function flush(): void | |
| 673 | { | |
| 674 | $this->aliases = []; | |
| 675 | $this->resolved = []; | |
| 676 | $this->bindings = []; | |
| 677 | $this->instances = []; | |
| 678 | $this->factories = []; | |
| 679 | $this->scopedInstances = []; | |
| 680 | } | |
| 681 | ||
| 682 | /** | |
| 683 | * Persist the cached reflection plans to a PHP file. | |
| 684 | * | |
| 685 | * Plans whose defaults contain objects are excluded, as they cannot be | |
| 686 | * represented by var_export. | |
| 687 | * | |
| 688 | * @param string $path The file path to write (e.g. storage/cache/container.php) | |
| 689 | * @return void | |
| 690 | */ | |
| 691 | public function cachePlans(string $path): void | |
| 692 | { | |
| 693 | $plans = []; | |
| 694 | ||
| 695 | foreach ($this->reflectionPlans as $key => $plan) { | |
| 696 | if ($this->isPlanSerializable($plan)) { | |
| 697 | $plans[$key] = $plan; | |
| 698 | } | |
| 699 | } | |
| 700 | ||
| 701 | $dir = \dirname($path); | |
| 702 | if (!\is_dir($dir)) { | |
| 703 | \mkdir($dir, 0777, true); | |
| 704 | } | |
| 705 | ||
| 706 | // A marker header lets loadCachedPlans() verify the file was written | |
| 707 | // by cachePlans() before `require`-ing it, so a tampered or | |
| 708 | // attacker-written file in the cache dir cannot execute arbitrary PHP. | |
| 709 | \file_put_contents( | |
| 710 | $path, | |
| 711 | '<?php /* lucent-container-plans v1 */ return ' . \var_export($plans, true) . ';' | |
| 712 | ); | |
| 713 | } | |
| 714 | ||
| 715 | /** | |
| 716 | * Load cached reflection plans from a PHP file. | |
| 717 | * | |
| 718 | * @param string $path The file path to read (e.g. storage/cache/container.php) | |
| 719 | * @return void | |
| 720 | */ | |
| 721 | public function loadCachedPlans(string $path): void | |
| 722 | { | |
| 723 | if (!\file_exists($path)) { | |
| 724 | return; | |
| 725 | } | |
| 726 | ||
| 727 | // Only load plans written by cachePlans() (identified by the marker | |
| 728 | // header) and confined to the project root. `require` executes the | |
| 729 | // file, so a tampered or attacker-written file in the cache dir must | |
| 730 | // never reach it. | |
| 731 | $contents = \file_get_contents($path); | |
| 732 | if ($contents === false || !\str_starts_with($contents, '<?php /* lucent-container-plans v1 */')) { | |
| 733 | return; | |
| 734 | } | |
| 735 | ||
| 736 | if (!\Lucent\Facades\FileSystem::isWithinRoot($path)) { | |
| 737 | return; | |
| 738 | } | |
| 739 | ||
| 740 | $plans = require $path; | |
| 741 | ||
| 742 | if (!\is_array($plans)) { | |
| 743 | return; | |
| 744 | } | |
| 745 | ||
| 746 | foreach ($plans as $key => $plan) { | |
| 747 | if ($this->isValidPlan($plan)) { | |
| 748 | $this->reflectionPlans[$key] = $plan; | |
| 749 | } | |
| 750 | } | |
| 751 | } | |
| 752 | ||
| 753 | /** | |
| 754 | * Determine whether a cached plan has the expected structure. | |
| 755 | * | |
| 756 | * Guards against corrupt or version-mismatched cache files injecting | |
| 757 | * malformed plans. | |
| 758 | * | |
| 759 | * @param mixed $plan The plan to validate | |
| 760 | * @return bool True if the plan is a well-formed parameter plan | |
| 761 | */ | |
| 762 | private function isValidPlan(mixed $plan): bool | |
| 763 | { | |
| 764 | if (!\is_array($plan)) { | |
| 765 | return false; | |
| 766 | } | |
| 767 | ||
| 768 | foreach ($plan as $param) { | |
| 769 | if (!\is_array($param) | |
| 770 | || !isset($param['name'], $param['type'], $param['default_available'], $param['default'], $param['variadic'], $param['nullable'])) { | |
| 771 | return false; | |
| 772 | } | |
| 773 | } | |
| 774 | ||
| 775 | return true; | |
| 776 | } | |
| 777 | ||
| 778 | /** | |
| 779 | * Resolve an entry through the full resolution pipeline. | |
| 780 | * | |
| 781 | * @param string $abstract Identifier to resolve | |
| 782 | * @param array $parameters Explicit constructor values keyed by name | |
| 783 | * @return mixed The resolved entry | |
| 784 | * @throws NotFoundExceptionInterface If no entry can be resolved | |
| 785 | * @throws ContainerExceptionInterface If the entry cannot be built | |
| 786 | */ | |
| 787 | private function resolve(string $abstract, array $parameters = []): mixed | |
| 788 | { | |
| 789 | $abstract = $this->resolveAlias($abstract); | |
| 790 | ||
| 791 | if (isset($this->instances[$abstract]) && !isset($this->aliases[$abstract])) { | |
| 792 | return $this->instances[$abstract]; | |
| 793 | } | |
| 794 | ||
| 795 | $contextual = $this->getContextualConcrete($abstract); | |
| 796 | if ($contextual !== null) { | |
| 797 | return $this->resolveContextual($contextual, $parameters); | |
| 798 | } | |
| 799 | ||
| 800 | if (isset($this->factories[$abstract])) { | |
| 801 | return $this->resolveFactory($abstract, $parameters); | |
| 802 | } | |
| 803 | ||
| 804 | if (isset($this->bindings[$abstract])) { | |
| 805 | return $this->resolveBinding($abstract, $parameters); | |
| 806 | } | |
| 807 | ||
| 808 | return $this->build($abstract, $parameters); | |
| 809 | } | |
| 810 | ||
| 811 | /** | |
| 812 | * Resolve a singleton factory, caching the result as an instance. | |
| 813 | * | |
| 814 | * @param string $abstract The identifier | |
| 815 | * @param array $parameters Explicit values keyed by name | |
| 816 | * @return object The resolved instance | |
| 817 | * @throws ContainerExceptionInterface On resolution failure | |
| 818 | */ | |
| 819 | private function resolveFactory(string $abstract, array $parameters = []): object | |
| 820 | { | |
| 821 | $factory = $this->factories[$abstract]; | |
| 822 | unset($this->factories[$abstract]); | |
| 823 | ||
| 824 | $service = $this->resolveFactoryCallable($factory, $abstract, $parameters); | |
| 825 | ||
| 826 | $this->instances[$abstract] = $service; | |
| 827 | $this->resolved[$abstract] = true; | |
| 828 | ||
| 829 | return $service; | |
| 830 | } | |
| 831 | ||
| 832 | /** | |
| 833 | * Resolve a non-shared binding, returning a fresh instance. | |
| 834 | * | |
| 835 | * @param string $abstract The identifier | |
| 836 | * @param array $parameters Explicit values keyed by name | |
| 837 | * @return object The resolved instance | |
| 838 | * @throws ContainerExceptionInterface On resolution failure | |
| 839 | */ | |
| 840 | private function resolveBinding(string $abstract, array $parameters = []): object | |
| 841 | { | |
| 842 | $factory = $this->bindings[$abstract]; | |
| 843 | ||
| 844 | $service = $this->resolveFactoryCallable($factory, $abstract, $parameters); | |
| 845 | ||
| 846 | $this->resolved[$abstract] = true; | |
| 847 | ||
| 848 | return $service; | |
| 849 | } | |
| 850 | ||
| 851 | /** | |
| 852 | * Invoke a factory closure, wrapping any failure in a | |
| 853 | * {@see ContainerExceptionInterface}. | |
| 854 | * | |
| 855 | * @param Closure $factory The factory to invoke | |
| 856 | * @param string $abstract The identifier the factory is registered under | |
| 857 | * @param array $parameters Explicit values keyed by name | |
| 858 | * @return object The resolved instance | |
| 859 | * @throws ContainerExceptionInterface On resolution failure | |
| 860 | */ | |
| 861 | private function resolveFactoryCallable(Closure $factory, string $abstract, array $parameters = []): object | |
| 862 | { | |
| 863 | try { | |
| 864 | $service = $this->call($factory, $parameters); | |
| 865 | } catch (Throwable $e) { | |
| 866 | throw new ContainerException( | |
| 867 | \sprintf('Failed to resolve container factory for "%s": %s', $abstract, $e->getMessage()), | |
| 868 | 0, | |
| 869 | $e | |
| 870 | ); | |
| 871 | } | |
| 872 | ||
| 873 | if (!\is_object($service)) { | |
| 874 | throw new ContainerException( | |
| 875 | \sprintf('Container factory for "%s" must return an object, "%s" given.', $abstract, \gettype($service)) | |
| 876 | ); | |
| 877 | } | |
| 878 | ||
| 879 | return $service; | |
| 880 | } | |
| 881 | ||
| 882 | /** | |
| 883 | * Resolve a contextual binding implementation. | |
| 884 | * | |
| 885 | * @param string|Closure $implementation The concrete or factory to give | |
| 886 | * @param array $parameters Explicit values keyed by name | |
| 887 | * @return mixed The resolved value | |
| 888 | * @throws ContainerExceptionInterface On resolution failure | |
| 889 | */ | |
| 890 | private function resolveContextual(string|Closure $implementation, array $parameters = []): mixed | |
| 891 | { | |
| 892 | if (\is_string($implementation)) { | |
| 893 | return $this->resolve($implementation, $parameters); | |
| 894 | } | |
| 895 | ||
| 896 | return $this->call($implementation, $parameters); | |
| 897 | } | |
| 898 | ||
| 899 | /** | |
| 900 | * Find the contextual implementation for an abstract in the current build | |
| 901 | * context. | |
| 902 | * | |
| 903 | * @param string $abstract The abstract being resolved | |
| 904 | * @return string|Closure|null The contextual implementation, or null | |
| 905 | */ | |
| 906 | private function getContextualConcrete(string $abstract): string|Closure|null | |
| 907 | { | |
| 908 | $concrete = \end($this->buildStack); | |
| 909 | ||
| 910 | if ($concrete === false) { | |
| 911 | return null; | |
| 912 | } | |
| 913 | ||
| 914 | return $this->contextual[$concrete][$abstract] ?? null; | |
| 915 | } | |
| 916 | ||
| 917 | /** | |
| 918 | * Autowire a concrete class by reflecting its constructor. | |
| 919 | * | |
| 920 | * @param string $concrete The class to instantiate | |
| 921 | * @param array $parameters Explicit constructor values keyed by name | |
| 922 | * @return object The instantiated instance | |
| 923 | * @throws ContainerExceptionInterface If the class cannot be instantiated | |
| 924 | */ | |
| 925 | private function build(string $concrete, array $parameters = []): object | |
| 926 | { | |
| 927 | if (!\class_exists($concrete)) { | |
| 928 | throw new NotFoundException($concrete); | |
| 929 | } | |
| 930 | ||
| 931 | $reflection = new ReflectionClass($concrete); | |
| 932 | ||
| 933 | if (!$reflection->isInstantiable()) { | |
| 934 | throw new ContainerException(\sprintf('Target [%s] is not instantiable.', $concrete)); | |
| 935 | } | |
| 936 | ||
| 937 | if (\in_array($concrete, $this->buildStack, true)) { | |
| 938 | throw new ContainerException( | |
| 939 | \sprintf('Circular dependency detected: %s -> %s', \implode(' -> ', $this->buildStack), $concrete) | |
| 940 | ); | |
| 941 | } | |
| 942 | ||
| 943 | $this->buildStack[] = $concrete; | |
| 944 | ||
| 945 | try { | |
| 946 | $constructor = $reflection->getConstructor(); | |
| 947 | ||
| 948 | if ($constructor === null) { | |
| 949 | $instance = $reflection->newInstance(); | |
| 950 | } else { | |
| 951 | $dependencies = $this->resolveDependencies($constructor->getParameters(), $parameters); | |
| 952 | $instance = $reflection->newInstanceArgs($dependencies); | |
| 953 | } | |
| 954 | } finally { | |
| 955 | \array_pop($this->buildStack); | |
| 956 | } | |
| 957 | ||
| 958 | $instance = $this->applyExtenders($concrete, $instance); | |
| 959 | $this->fireResolvingCallbacks($concrete, $instance); | |
| 960 | ||
| 961 | return $instance; | |
| 962 | } | |
| 963 | ||
| 964 | /** | |
| 965 | * Resolve a list of constructor dependencies. | |
| 966 | * | |
| 967 | * @param array<int, ReflectionParameter> $parameters The constructor parameters | |
| 968 | * @param array $primitives Explicit values keyed by parameter name | |
| 969 | * @return array<int, mixed> The resolved dependency values | |
| 970 | * @throws ContainerExceptionInterface If a dependency cannot be resolved | |
| 971 | */ | |
| 972 | private function resolveDependencies(array $parameters, array $primitives = []): array | |
| 973 | { | |
| 974 | $dependencies = []; | |
| 975 | ||
| 976 | foreach ($parameters as $parameter) { | |
| 977 | $name = $parameter->getName(); | |
| 978 | ||
| 979 | if (\array_key_exists($name, $primitives)) { | |
| 980 | $dependencies[] = $primitives[$name]; | |
| 981 | continue; | |
| 982 | } | |
| 983 | ||
| 984 | $type = $parameter->getType(); | |
| 985 | ||
| 986 | if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) { | |
| 987 | $dependencies[] = $this->resolve($type->getName()); | |
| 988 | } elseif ($parameter->isDefaultValueAvailable()) { | |
| 989 | $dependencies[] = $parameter->getDefaultValue(); | |
| 990 | } elseif ($type !== null && $type->allowsNull()) { | |
| 991 | $dependencies[] = null; | |
| 992 | } else { | |
| 993 | throw new ContainerException( | |
| 994 | \sprintf('Unresolvable dependency [%s] in class [%s].', $name, $parameter->getDeclaringClass()?->getName()) | |
| 995 | ); | |
| 996 | } | |
| 997 | } | |
| 998 | ||
| 999 | return $dependencies; | |
| 1000 | } | |
| 1001 | ||
| 1002 | /** | |
| 1003 | * Apply registered extenders to a resolved instance. | |
| 1004 | * | |
| 1005 | * @param string $abstract The identifier | |
| 1006 | * @param object $instance The resolved instance | |
| 1007 | * @return object The (possibly decorated) instance | |
| 1008 | */ | |
| 1009 | private function applyExtenders(string $abstract, object $instance): object | |
| 1010 | { | |
| 1011 | foreach ($this->extenders[$abstract] ?? [] as $extender) { | |
| 1012 | $instance = $extender($instance, $this); | |
| 1013 | } | |
| 1014 | ||
| 1015 | return $instance; | |
| 1016 | } | |
| 1017 | ||
| 1018 | /** | |
| 1019 | * Fire resolving and after-resolving callbacks for an instance. | |
| 1020 | * | |
| 1021 | * @param string $abstract The identifier | |
| 1022 | * @param object $instance The resolved instance | |
| 1023 | * @return void | |
| 1024 | */ | |
| 1025 | private function fireResolvingCallbacks(string $abstract, object $instance): void | |
| 1026 | { | |
| 1027 | $this->fireCallbackArray($instance, $this->resolvingCallbacks[$abstract] ?? []); | |
| 1028 | $this->fireCallbackArray($instance, $this->resolvingCallbacks['*'] ?? []); | |
| 1029 | ||
| 1030 | $this->resolved[$abstract] = true; | |
| 1031 | ||
| 1032 | $this->fireCallbackArray($instance, $this->afterResolvingCallbacks[$abstract] ?? []); | |
| 1033 | $this->fireCallbackArray($instance, $this->afterResolvingCallbacks['*'] ?? []); | |
| 1034 | } | |
| 1035 | ||
| 1036 | /** | |
| 1037 | * Invoke a list of callbacks with an instance. | |
| 1038 | * | |
| 1039 | * @param object $instance The instance to pass | |
| 1040 | * @param array<int, Closure> $callbacks The callbacks to invoke | |
| 1041 | * @return void | |
| 1042 | */ | |
| 1043 | private function fireCallbackArray(object $instance, array $callbacks): void | |
| 1044 | { | |
| 1045 | foreach ($callbacks as $callback) { | |
| 1046 | $callback($instance, $this); | |
| 1047 | } | |
| 1048 | } | |
| 1049 | ||
| 1050 | /** | |
| 1051 | * Fire rebound callbacks for an identifier. | |
| 1052 | * | |
| 1053 | * @param string $abstract The identifier | |
| 1054 | * @return void | |
| 1055 | */ | |
| 1056 | private function rebound(string $abstract): void | |
| 1057 | { | |
| 1058 | $instance = $this->instances[$abstract] ?? null; | |
| 1059 | ||
| 1060 | foreach ($this->reboundCallbacks[$abstract] ?? [] as $callback) { | |
| 1061 | $callback($instance, $this); | |
| 1062 | } | |
| 1063 | } | |
| 1064 | ||
| 1065 | /** | |
| 1066 | * Normalize a handler into [class, method] form. | |
| 1067 | * | |
| 1068 | * @param callable|string|array $callback The handler | |
| 1069 | * @param string|null $defaultMethod Method to use for invokable class strings | |
| 1070 | * @return array{0: string|object|null, 1: string|Closure} The normalized handler | |
| 1071 | */ | |
| 1072 | private function normalizeHandler(callable|string|array $callback, ?string $defaultMethod = null): array | |
| 1073 | { | |
| 1074 | if (\is_string($callback) && \str_contains($callback, '@')) { | |
| 1075 | [$class, $method] = \explode('@', $callback, 2); | |
| 1076 | return [$class, $method]; | |
| 1077 | } | |
| 1078 | ||
| 1079 | if (\is_array($callback)) { | |
| 1080 | return [$callback[0], $callback[1]]; | |
| 1081 | } | |
| 1082 | ||
| 1083 | if (\is_string($callback)) { | |
| 1084 | return [$callback, $defaultMethod ?? '__invoke']; | |
| 1085 | } | |
| 1086 | ||
| 1087 | if (\is_object($callback) && !$callback instanceof Closure) { | |
| 1088 | return [$callback, '__invoke']; | |
| 1089 | } | |
| 1090 | ||
| 1091 | return [null, $callback]; | |
| 1092 | } | |
| 1093 | ||
| 1094 | /** | |
| 1095 | * Get (and cache) the parameter plan for a callable. | |
| 1096 | * | |
| 1097 | * @param string|object|null $class The class, or null for a closure | |
| 1098 | * @param string|Closure $method The method name or closure | |
| 1099 | * @return array<int, array<string, mixed>> The parameter plan | |
| 1100 | */ | |
| 1101 | private function getParameterPlan(string|object|null $class, string|Closure $method): array | |
| 1102 | { | |
| 1103 | $key = $this->planKey($class, $method); | |
| 1104 | ||
| 1105 | if (isset($this->reflectionPlans[$key])) { | |
| 1106 | return $this->reflectionPlans[$key]; | |
| 1107 | } | |
| 1108 | ||
| 1109 | $reflection = $this->getReflection($class, $method); | |
| 1110 | $plan = []; | |
| 1111 | ||
| 1112 | foreach ($reflection->getParameters() as $parameter) { | |
| 1113 | $plan[] = [ | |
| 1114 | 'name' => $parameter->getName(), | |
| 1115 | 'type' => $this->getTypeName($parameter), | |
| 1116 | 'default_available' => $parameter->isDefaultValueAvailable(), | |
| 1117 | 'default' => $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null, | |
| 1118 | 'variadic' => $parameter->isVariadic(), | |
| 1119 | 'nullable' => $parameter->allowsNull(), | |
| 1120 | ]; | |
| 1121 | } | |
| 1122 | ||
| 1123 | return $this->reflectionPlans[$key] = $plan; | |
| 1124 | } | |
| 1125 | ||
| 1126 | /** | |
| 1127 | * Build a cache key for a parameter plan. | |
| 1128 | * | |
| 1129 | * @param string|object|null $class The class, or null for a closure | |
| 1130 | * @param string|Closure $method The method name or closure | |
| 1131 | * @return string The cache key | |
| 1132 | */ | |
| 1133 | private function planKey(string|object|null $class, string|Closure $method): string | |
| 1134 | { | |
| 1135 | if ($method instanceof Closure) { | |
| 1136 | return 'closure:' . \spl_object_id($method); | |
| 1137 | } | |
| 1138 | ||
| 1139 | if ($class !== null) { | |
| 1140 | return (\is_object($class) ? \get_class($class) : $class) . '::' . $method; | |
| 1141 | } | |
| 1142 | ||
| 1143 | return $method; | |
| 1144 | } | |
| 1145 | ||
| 1146 | /** | |
| 1147 | * Get the reflection for a callable. | |
| 1148 | * | |
| 1149 | * @param string|object|null $class The class, or null for a closure | |
| 1150 | * @param string|Closure $method The method name or closure | |
| 1151 | * @return ReflectionMethod|ReflectionFunction The reflection | |
| 1152 | */ | |
| 1153 | private function getReflection(string|object|null $class, string|Closure $method): ReflectionMethod|ReflectionFunction | |
| 1154 | { | |
| 1155 | if ($method instanceof Closure) { | |
| 1156 | return new ReflectionFunction($method); | |
| 1157 | } | |
| 1158 | ||
| 1159 | if ($class !== null) { | |
| 1160 | return new ReflectionMethod($class, $method); | |
| 1161 | } | |
| 1162 | ||
| 1163 | return new ReflectionFunction($method); | |
| 1164 | } | |
| 1165 | ||
| 1166 | /** | |
| 1167 | * Resolve a single parameter from the plan. | |
| 1168 | * | |
| 1169 | * @param array<string, mixed> $param The parameter plan entry | |
| 1170 | * @param array $parameters Explicit values keyed by name | |
| 1171 | * @return mixed The resolved value | |
| 1172 | * @throws ContainerExceptionInterface If the parameter cannot be resolved | |
| 1173 | */ | |
| 1174 | private function resolveParameter(array $param, array $parameters): mixed | |
| 1175 | { | |
| 1176 | $name = $param['name']; | |
| 1177 | $type = $param['type']; | |
| 1178 | ||
| 1179 | if (\array_key_exists($name, $parameters)) { | |
| 1180 | return $this->castParameter($parameters[$name], $type); | |
| 1181 | } | |
| 1182 | ||
| 1183 | if ($type !== null && $this->has($type)) { | |
| 1184 | return $this->get($type); | |
| 1185 | } | |
| 1186 | ||
| 1187 | if ($param['default_available']) { | |
| 1188 | return $param['default']; | |
| 1189 | } | |
| 1190 | ||
| 1191 | if ($param['nullable']) { | |
| 1192 | return null; | |
| 1193 | } | |
| 1194 | ||
| 1195 | throw new ContainerException( | |
| 1196 | \sprintf('Unable to resolve parameter [%s] of type [%s].', $name, $type ?? 'mixed') | |
| 1197 | ); | |
| 1198 | } | |
| 1199 | ||
| 1200 | /** | |
| 1201 | * Cast a value to a parameter's declared type where possible. | |
| 1202 | * | |
| 1203 | * @param mixed $value The value to cast | |
| 1204 | * @param string|null $type The declared type name, or null | |
| 1205 | * @return mixed The cast value | |
| 1206 | */ | |
| 1207 | private function castParameter(mixed $value, ?string $type): mixed | |
| 1208 | { | |
| 1209 | if ($type === null || \is_object($value) || \is_array($value)) { | |
| 1210 | return $value; | |
| 1211 | } | |
| 1212 | ||
| 1213 | return match ($type) { | |
| 1214 | 'int' => (int) $value, | |
| 1215 | 'float' => (float) $value, | |
| 1216 | 'bool' => \filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? (bool) $value, | |
| 1217 | 'string' => (string) $value, | |
| 1218 | default => $value, | |
| 1219 | }; | |
| 1220 | } | |
| 1221 | ||
| 1222 | /** | |
| 1223 | * Get the named type of a parameter, or null. | |
| 1224 | * | |
| 1225 | * @param ReflectionParameter $parameter The parameter | |
| 1226 | * @return string|null The type name, or null | |
| 1227 | */ | |
| 1228 | private function getTypeName(ReflectionParameter $parameter): ?string | |
| 1229 | { | |
| 1230 | $type = $parameter->getType(); | |
| 1231 | ||
| 1232 | if ($type instanceof ReflectionNamedType) { | |
| 1233 | return $type->getName(); | |
| 1234 | } | |
| 1235 | ||
| 1236 | return null; | |
| 1237 | } | |
| 1238 | ||
| 1239 | /** | |
| 1240 | * Determine whether a class exists and is instantiable. | |
| 1241 | * | |
| 1242 | * @param string $class The class name | |
| 1243 | * @return bool True if the class is instantiable | |
| 1244 | */ | |
| 1245 | private function isInstantiable(string $class): bool | |
| 1246 | { | |
| 1247 | if (!\class_exists($class)) { | |
| 1248 | return false; | |
| 1249 | } | |
| 1250 | ||
| 1251 | return (new ReflectionClass($class))->isInstantiable(); | |
| 1252 | } | |
| 1253 | ||
| 1254 | /** | |
| 1255 | * Determine whether a parameter plan can be serialized to a file. | |
| 1256 | * | |
| 1257 | * @param array<int, array<string, mixed>> $plan The plan | |
| 1258 | * @return bool True if all defaults are serializable | |
| 1259 | */ | |
| 1260 | private function isPlanSerializable(array $plan): bool | |
| 1261 | { | |
| 1262 | foreach ($plan as $param) { | |
| 1263 | if (\is_object($param['default'])) { | |
| 1264 | return false; | |
| 1265 | } | |
| 1266 | } | |
| 1267 | ||
| 1268 | return true; | |
| 1269 | } | |
| 1270 | ||
| 1271 | /** | |
| 1272 | * Remove any existing registration for an identifier. | |
| 1273 | * | |
| 1274 | * Ensures a given id lives in at most one of {@see $instances}, | |
| 1275 | * {@see $factories}, or {@see $bindings} at a time, so the most recent | |
| 1276 | * registration always wins. Also clears any alias registered *under* the | |
| 1277 | * id, but deliberately preserves aliases that point *to* it so the | |
| 1278 | * "alias before abstract" pattern survives re-registration. | |
| 1279 | * | |
| 1280 | * @param string $id The identifier to clear | |
| 1281 | * @return void | |
| 1282 | */ | |
| 1283 | private function forget(string $id): void | |
| 1284 | { | |
| 1285 | unset($this->instances[$id], $this->factories[$id], $this->bindings[$id], $this->aliases[$id]); | |
| 1286 | } | |
| 1287 | ||
| 1288 | /** | |
| 1289 | * Follow the alias chain for an identifier. | |
| 1290 | * | |
| 1291 | * @param string $id The identifier to resolve | |
| 1292 | * @return string The terminal (non-alias) identifier | |
| 1293 | */ | |
| 1294 | private function resolveAlias(string $id): string | |
| 1295 | { | |
| 1296 | while (isset($this->aliases[$id])) { | |
| 1297 | $id = $this->aliases[$id]; | |
| 1298 | } | |
| 1299 | ||
| 1300 | return $id; | |
| 1301 | } | |
| 1302 | ||
| 1303 | /** | |
| 1304 | * Determine whether an alias (transitively) resolves to a target id. | |
| 1305 | * | |
| 1306 | * @param string $alias The alias to follow | |
| 1307 | * @param string $target The id to look for | |
| 1308 | * @return bool True if following the chain from $alias reaches $target | |
| 1309 | */ | |
| 1310 | private function resolvesTo(string $alias, string $target): bool | |
| 1311 | { | |
| 1312 | $seen = []; | |
| 1313 | while (isset($this->aliases[$alias]) && !isset($seen[$alias])) { | |
| 1314 | $seen[$alias] = true; | |
| 1315 | $alias = $this->aliases[$alias]; | |
| 1316 | if ($alias === $target) { | |
| 1317 | return true; | |
| 1318 | } | |
| 1319 | } | |
| 1320 | ||
| 1321 | return false; | |
| 1322 | } | |
| 1323 | ||
| 1324 | /** | |
| 1325 | * Derive an abstract identifier from a factory callable's return type. | |
| 1326 | * | |
| 1327 | * @param callable $callable The factory callable | |
| 1328 | * @return string The class name the callable returns | |
| 1329 | * @throws ContainerExceptionInterface If the callable has no class return type | |
| 1330 | */ | |
| 1331 | private function abstractFromCallable(callable $callable): string | |
| 1332 | { | |
| 1333 | $ref = new ReflectionFunction(Closure::fromCallable($callable)); | |
| 1334 | $type = $ref->getReturnType(); | |
| 1335 | ||
| 1336 | if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) { | |
| 1337 | return $type->getName(); | |
| 1338 | } | |
| 1339 | ||
| 1340 | throw new ContainerException( | |
| 1341 | 'A callable passed as the abstract must declare a class return type, e.g. ' . | |
| 1342 | 'singleton(fn (): Mailer => new Mailer(...)).' | |
| 1343 | ); | |
| 1344 | } | |
| 1345 | } |