Skip to main content

Error handling

Catching exceptions​

Every exception the package throws implements Dirthara\Container\Exception\ContainerException, which extends the PSR-11 ContainerExceptionInterface, so one catch covers them all. Each also extends the SPL exception that describes the failure: an invalid registration throws an InvalidArgumentException, and a failure to resolve an entry or a registration on a locked container throws a RuntimeException.

use Dirthara\Container\Exception\ContainerException;
use Dirthara\Container\Exception\EntryNotFoundException;

try {
$mailer = $container->get(Mailer::class);
} catch (EntryNotFoundException $exception) {
// nothing is registered for Mailer, and it cannot be autowired
} catch (ContainerException $exception) {
// Mailer exists but could not be built
}
ExceptionExtendsAlso implementsThrown when
EntryNotFoundExceptionRuntimeExceptionNotFoundExceptionInterfaceThe requested identifier is not registered and is not an instantiable class.
ResolutionExceptionRuntimeExceptionThe entry exists but cannot be built: a binding points at nothing, a parameter cannot be filled in, or a factory failed.
CircularDependencyExceptionRuntimeExceptionResolving the entry requires the entry itself.
ContainerLockedExceptionRuntimeExceptionA method of ContainerConfigurator is called after lock().
InvalidContextualBindingExceptionInvalidArgumentExceptionwhen() names something that is not a class, or needs() something that is neither a class or interface nor a parameter name.
InvalidAttributeExceptionInvalidArgumentExceptionAn attribute on the type being resolved is used incorrectly.
InvalidRegistrationExceptionInvalidArgumentExceptionA registration names something it cannot apply to, such as lazy() for an interface, or a value of another type than its identifier names.
InvalidCallableExceptionInvalidArgumentExceptioncall() is given something that is not a closure, a function, a public method, or an invokable class.

All exception classes are final; catch them by class or by ContainerException.

Not found means the requested entry​

get() throws EntryNotFoundException only when the identifier you asked for has no entry, as PSR-11 requires. When the entry exists but something it depends on does not, get() throws a ResolutionException instead, so a caller that catches NotFoundExceptionInterface to fall back to a default never mistakes a broken dependency for a missing entry.

Situationget() throws
get(Mailer::class) when Mailer is an unbound interfaceEntryNotFoundException
get(Mailer::class) when Mailer needs an unbound LoggerInterfaceResolutionException::missingDependency()
get(Mailer::class) when Mailer needs an int without a defaultResolutionException::unresolvableParameter()
get(Mailer::class) after bind(Mailer::class, 'missing')ResolutionException::unresolvableBinding()
get(Mailer::class) when its factory calls get('missing')ResolutionException::factoryFailed(), wrapping the EntryNotFoundException
get(Mailer::class) when a contextual binding for it gives 'missing'ResolutionException::unresolvableContextualBinding()
get(Mailer::class) when its factory returns something that is not a MailerResolutionException::incompatibleType()
get(Mailer::class) when a contextual factory for it throwsResolutionException::contextualFactoryFailed(), wrapping what it threw

Factories that throw​

When a factory registered with bind(), singleton(), scoped(), or a contextual binding's give(), an extender, or a callback throws, the container wraps the exception in a ResolutionException and passes the original as its previous exception, with three exceptions to that rule:

  • This package's own exceptions pass through unchanged, so a CircularDependencyException from inside a factory keeps its type. The one case that is wrapped is an EntryNotFoundException, which would otherwise claim that the factory's own entry is missing.
  • A LogicException passes through unchanged. It signals a bug in the factory rather than a failure to resolve.
  • An Error, such as a TypeError, passes through unchanged for the same reason.
use Dirthara\Container\Exception\ResolutionException;

try {
$connection = $container->get(Connection::class);
} catch (ResolutionException $exception) {
$cause = $exception->getPrevious(); // the PDOException the factory threw
}

The message of the wrapping exception names the class of the original, not its message: a factory's failure can hold anything the factory saw, such as a connection string with a password. Read the original through getPrevious().

Context​

Every exception carries diagnostic metadata in its public, read-only context property, an array<string, mixed>.

FactoryContext keys
EntryNotFoundException::forId()id
CircularDependencyException::forEntry()id, and chain: the list of entries from the one requested to the one that repeats
ResolutionException::missingDependency()target, parameter, and dependency: the type the container had no entry for
ResolutionException::unresolvableParameter()target, parameter
ResolutionException::unknownParameters()target, and parameters: the given names the target has no parameter for
ResolutionException::notBuildable()id
ResolutionException::unresolvableBinding()id, concrete
ResolutionException::factoryFailed()id, and exceptionClass: the class of the exception the factory threw
ResolutionException::unresolvableContextualBinding()class, need, concrete
ResolutionException::contextualFactoryFailed()class, need, and exceptionClass
ResolutionException::extenderFailed()id, and exceptionClass: the class of the exception the extender threw
ResolutionException::incompatibleType()id, and type: the type of the value it resolved to, such as a class name or array
ResolutionException::delegateFailed()id, delegate: the class of the delegate container, and exceptionClass
ResolutionException::callbackFailed()type: the type the callback is for, class: the class of the built object, and exceptionClass
InvalidContextualBindingException::notAClass()class
InvalidContextualBindingException::invalidNeed()need
InvalidCallableException::notCallable()callable
InvalidAttributeException::notASubtype()class, and concrete: the class its #[BoundTo] names
InvalidAttributeException::conflictingLifetimes()class
InvalidAttributeException::unknownClass()class
InvalidAttributeException::notADecorator()class, and decorator: the class its #[DecoratedBy] names
InvalidAttributeException::decoratorWithoutParameter()class, decorator
InvalidRegistrationException::notALazyClass()class
InvalidRegistrationException::incompatibleInstance()id, and type: the type of the value that was given
InvalidRegistrationException::incompatibleConcrete()id, concrete
ContainerLockedException::cannotRegister()id
ContainerLockedException::cannotAddContextualBinding()classes: the classes given to when()
ContainerLockedException::cannotConfigure()method: the method that was called, such as tag

A target is what the parameters belong to: a class for a constructor, Class::method for a method, Closure for a closure, or the name of a function.

Code that catches an exception and knows more can add to it with addContext(), which merges the given array into the context, replacing matching keys, and returns the exception:

throw $exception->addContext(['route' => 'users.store']);

Every exception can also be built directly with the constructor, as the named factories do:

Constructor parameterTypeDefaultMeaning
messagestring''The exception message.
codeint0The exception code.
previous?ThrowablenullThe exception this one wraps.
contextarray<string, mixed>[]The diagnostic context.

When a message quotes an identifier, class, or parameter name, control characters in it are escaped, so a line break shows as \n and cannot forge a line in a log. The context holds the value as it was given.

Logging​

Exceptions do not log themselves or depend on a logger. An application handler can pass the context to a PSR-3 logger. Add the caught exception under the exception key after reading the context:

use Dirthara\Container\Exception\ContainerException;

try {
$mailer = $container->get(Mailer::class);
} catch (ContainerException $exception) {
$context = $exception->context;
$context['exception'] = $exception;

$logger->error($exception->getMessage(), $context);
}