feat: implement exception handling system with error context and policies
This commit is contained in:
@@ -5,11 +5,17 @@ declare(strict_types=1);
|
||||
namespace App\Framework\DI;
|
||||
|
||||
use App\Framework\Core\ValueObjects\ClassName;
|
||||
use App\Framework\DI\Exceptions\ClassNotInstantiable;
|
||||
use App\Framework\DI\Exceptions\ClassNotResolvableException;
|
||||
use App\Framework\DI\Exceptions\ClassResolutionException;
|
||||
use App\Framework\DI\Exceptions\ContainerException;
|
||||
use App\Framework\DI\Exceptions\CyclicDependencyException;
|
||||
use App\Framework\DI\Exceptions\LazyLoadingException;
|
||||
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
||||
use App\Framework\Metrics\FrameworkMetricsCollector;
|
||||
use App\Framework\Reflection\CachedReflectionProvider;
|
||||
use App\Framework\Reflection\ReflectionProvider;
|
||||
use Stringable;
|
||||
use Throwable;
|
||||
|
||||
final class DefaultContainer implements Container
|
||||
@@ -54,8 +60,9 @@ final class DefaultContainer implements Container
|
||||
$this->instance(ReflectionProvider::class, $this->reflectionProvider);
|
||||
}
|
||||
|
||||
public function bind(string $abstract, callable|string|object $concrete): void
|
||||
public function bind(ClassName|Stringable|string $abstract, callable|string|object $concrete): void
|
||||
{
|
||||
$abstract = (string) $abstract;
|
||||
$this->bindings->bind($abstract, $concrete);
|
||||
|
||||
// Only clear caches for valid class names, skip string keys like 'filesystem.storage.local'
|
||||
@@ -64,23 +71,26 @@ final class DefaultContainer implements Container
|
||||
}
|
||||
}
|
||||
|
||||
public function singleton(string $abstract, callable|string|object $concrete): void
|
||||
public function singleton(ClassName|Stringable|string $abstract, callable|string|object $concrete): void
|
||||
{
|
||||
$abstract = (string) $abstract;
|
||||
$this->bind($abstract, $concrete);
|
||||
$this->instances->markAsSingleton($abstract);
|
||||
}
|
||||
|
||||
public function instance(string $abstract, object $instance): void
|
||||
public function instance(ClassName|Stringable|string $abstract, object $instance): void
|
||||
{
|
||||
$this->instances->setInstance($abstract, $instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @template T of object
|
||||
* @param class-string<T>|ClassName|Stringable $class
|
||||
* @return T
|
||||
*/
|
||||
public function get(string|ClassName $class): object
|
||||
public function get(ClassName|Stringable|string $class): object
|
||||
{
|
||||
$className = $class instanceof ClassName ? $class->toString() : $class;
|
||||
$className = (string) $class;
|
||||
|
||||
// Bereits instanziierte Objekte zurückgeben
|
||||
if ($this->instances->hasSingleton($className)) {
|
||||
@@ -154,9 +164,9 @@ final class DefaultContainer implements Container
|
||||
|
||||
// For string keys without bindings, throw immediately
|
||||
if (!class_exists($class) && !interface_exists($class)) {
|
||||
throw new \RuntimeException(
|
||||
"Cannot resolve '{$class}': not a valid class and no binding exists. " .
|
||||
"Available bindings: " . implode(', ', array_keys($this->bindings->getAllBindings()))
|
||||
throw new ClassNotResolvableException(
|
||||
class: $class,
|
||||
availableBindings: array_keys($this->bindings->getAllBindings())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -174,27 +184,16 @@ final class DefaultContainer implements Container
|
||||
$dependencies = $this->dependencyResolver->resolveDependencies($className);
|
||||
|
||||
return $reflection->newInstance(...$dependencies->toArray());
|
||||
} catch (\RuntimeException $e) {
|
||||
// If it's already our detailed exception, just re-throw
|
||||
if (str_contains($e->getMessage(), 'Dependency resolution chain:')) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Otherwise, wrap with binding information
|
||||
throw new \RuntimeException(
|
||||
"Cannot resolve class '{$class}': {$e->getMessage()}. " .
|
||||
"Available bindings: " . implode(', ', array_keys($this->bindings->getAllBindings())) .
|
||||
". Dependency chain: " . implode(' -> ', $this->resolving),
|
||||
0,
|
||||
$e
|
||||
);
|
||||
} catch (\ReflectionException $e) {
|
||||
throw new \RuntimeException(
|
||||
"Cannot resolve class '{$class}': {$e->getMessage()}. " .
|
||||
"Available bindings: " . implode(', ', array_keys($this->bindings->getAllBindings())) .
|
||||
". Dependency chain: " . implode(' -> ', $this->resolving),
|
||||
0,
|
||||
$e
|
||||
} catch (ContainerException $e) {
|
||||
// If it's already a ContainerException, just re-throw
|
||||
throw $e;
|
||||
} catch (\RuntimeException|\ReflectionException $e) {
|
||||
// Wrap with binding information
|
||||
throw ClassResolutionException::fromRuntimeException(
|
||||
class: $class,
|
||||
exception: $e,
|
||||
availableBindings: array_keys($this->bindings->getAllBindings()),
|
||||
dependencyChain: $this->resolving
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -202,38 +201,67 @@ final class DefaultContainer implements Container
|
||||
private function throwDetailedBindingException(string $class/*, $reflection*/): never
|
||||
{
|
||||
$availableBindings = array_keys($this->bindings->getAllBindings());
|
||||
$dependencyChain = implode(' -> ', $this->resolving);
|
||||
|
||||
// Look for similar interface bindings
|
||||
$similarBindings = array_filter($availableBindings, function ($binding) use ($class) {
|
||||
return str_contains($binding, basename(str_replace('\\', '/', $class)));
|
||||
});
|
||||
// Try to get DiscoveryRegistry from container and include discovered initializers
|
||||
$discoveredInitializers = [];
|
||||
if ($this->has(DiscoveryRegistry::class)) {
|
||||
try {
|
||||
$discoveryRegistry = $this->get(DiscoveryRegistry::class);
|
||||
$initializerResults = $discoveryRegistry->attributes->get(Initializer::class);
|
||||
|
||||
$message = "Cannot instantiate class '{$class}': class is not instantiable (interface, abstract class, or trait).\n" .
|
||||
"Dependency resolution chain: {$dependencyChain}\n" .
|
||||
"Total available bindings: " . count($availableBindings) . "\n";
|
||||
|
||||
if (! empty($similarBindings)) {
|
||||
$message .= "Similar bindings found: " . implode(', ', $similarBindings) . "\n";
|
||||
if (! empty($initializerResults)) {
|
||||
$discoveredInitializers = array_map(
|
||||
fn($attr) => $attr->className->getFullyQualified(),
|
||||
$initializerResults
|
||||
);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Silently ignore errors when trying to access DiscoveryRegistry to avoid masking the original error
|
||||
}
|
||||
}
|
||||
|
||||
$message .= "All bindings: " . implode(', ', $availableBindings);
|
||||
|
||||
throw new \RuntimeException($message);
|
||||
throw ClassNotInstantiable::fromContainerContext(
|
||||
class: $class,
|
||||
dependencyChain: $this->resolving,
|
||||
availableBindings: $availableBindings,
|
||||
discoveredInitializers: $discoveredInitializers
|
||||
);
|
||||
}
|
||||
|
||||
private function resolveBinding(string $class, callable|string|object $concrete): object
|
||||
{
|
||||
return match (true) {
|
||||
is_callable($concrete) => $concrete($this),
|
||||
is_string($concrete) => $this->get($concrete),
|
||||
default => $concrete
|
||||
};
|
||||
try {
|
||||
return match (true) {
|
||||
is_callable($concrete) => $concrete($this),
|
||||
is_string($concrete) => $this->get($concrete),
|
||||
/* @var object $concrete */
|
||||
default => $concrete
|
||||
};
|
||||
} catch (ContainerException $e) {
|
||||
// Re-throw ContainerExceptions as-is (they already have proper context)
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
// Determine binding type for better error messages
|
||||
$bindingType = match (true) {
|
||||
is_callable($concrete) => 'callable',
|
||||
is_string($concrete) => 'string',
|
||||
default => 'object'
|
||||
};
|
||||
|
||||
throw ClassResolutionException::fromBindingResolution(
|
||||
class: $class,
|
||||
previous: $e,
|
||||
availableBindings: array_keys($this->bindings->getAllBindings()),
|
||||
dependencyChain: $this->resolving,
|
||||
bindingType: $bindingType
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param class-string $class */
|
||||
public function has(string $class): bool
|
||||
public function has(ClassName|Stringable|string $class): bool
|
||||
{
|
||||
$class = (string) $class;
|
||||
return $this->instances->hasSingleton($class)
|
||||
|| $this->instances->hasInstance($class)
|
||||
|| $this->bindings->hasBinding($class)
|
||||
@@ -241,8 +269,9 @@ final class DefaultContainer implements Container
|
||||
}
|
||||
|
||||
/** @param class-string $class */
|
||||
public function forget(string $class): void
|
||||
public function forget(ClassName|Stringable|string $class): void
|
||||
{
|
||||
$class = (string) $class;
|
||||
$this->instances->forget($class);
|
||||
$this->bindings->forget($class);
|
||||
$this->clearCaches(ClassName::create($class));
|
||||
|
||||
Reference in New Issue
Block a user