From 6b5e9668f0a932e7dbe0165a9c56e200d12942f2 Mon Sep 17 00:00:00 2001 From: Franck DAKIA Date: Sat, 17 Oct 2026 02:37:12 +0000 Subject: [PATCH] Container: fail clearly on an unbound interface or abstract class Capsule::resolve() returned the key itself when the class was not instantiable, so make() on an interface or abstract class with no binding handed back the type's *name* as a string. Callers expecting an instance broke far from the cause, most visibly as "Compass::getInjectParameter(): Return value must be of type ?object, string returned" when a controller constructor depended on an interface nobody had bound. resolve() now throws ContainerException::notInstantiable(), whose message names the type, says whether it is an interface or an abstract class, and shows the bind() call to add. Covers make(), makeWith() and dependencies resolved while building another class. --- src/Container/Capsule.php | 10 +++- .../Exception/ContainerException.php | 33 +++++++++++++ tests/Container/CapsuleTest.php | 48 +++++++++++++++++++ tests/Container/Stubs/AbstractRepository.php | 8 ++++ 4 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 src/Container/Exception/ContainerException.php create mode 100644 tests/Container/Stubs/AbstractRepository.php diff --git a/src/Container/Capsule.php b/src/Container/Capsule.php index 28634c42..9b65d4c2 100644 --- a/src/Container/Capsule.php +++ b/src/Container/Capsule.php @@ -5,6 +5,7 @@ namespace Bow\Container; use ArrayAccess; +use Bow\Container\Exception\ContainerException; use Closure; use InvalidArgumentException; use ReflectionClass; @@ -86,14 +87,19 @@ public function makeWith(string $key, array $parameters = []): mixed * * @param string $key * @return mixed - * @throws + * @throws ContainerException when $key is an interface or abstract class without a binding + * @throws \ReflectionException when $key is not a class at all */ private function resolve(string $key): mixed { $reflection = new ReflectionClass($key); + // An interface or abstract class with no binding cannot be built. + // Returning the key here handed a *string* to every caller expecting + // an instance, which surfaced far away as a TypeError in the + // controller injector. Fail here, naming the missing binding. if (!$reflection->isInstantiable()) { - return $key; + throw ContainerException::notInstantiable($key); } $constructor = $reflection->getConstructor(); diff --git a/src/Container/Exception/ContainerException.php b/src/Container/Exception/ContainerException.php new file mode 100644 index 00000000..38f80808 --- /dev/null +++ b/src/Container/Exception/ContainerException.php @@ -0,0 +1,33 @@ +bind(%s::class, Concrete::class).', + $key, + $kind, + $key + )); + } +} diff --git a/tests/Container/CapsuleTest.php b/tests/Container/CapsuleTest.php index 08fc65c2..661d1c69 100644 --- a/tests/Container/CapsuleTest.php +++ b/tests/Container/CapsuleTest.php @@ -3,6 +3,8 @@ namespace Bow\Tests\Container; use Bow\Container\Capsule; +use Bow\Container\Exception\ContainerException; +use Bow\Tests\Container\Stubs\AbstractRepository; use Bow\Tests\Container\Stubs\FileLogger; use Bow\Tests\Container\Stubs\LoggerInterface; use Bow\Tests\Container\Stubs\MyClass; @@ -217,6 +219,52 @@ public function test_array_access_offset_unset() $capsule->make('removable'); } + /** + * Regression: make() on an interface with no binding used to return the + * interface *name* (a string), which then blew up far away as + * "Compass::getInjectParameter(): Return value must be of type ?object, + * string returned" when a controller depended on that interface. + */ + public function test_make_unbound_interface_throws_a_clear_exception() + { + $capsule = new Capsule(); + + $this->expectException(ContainerException::class); + $this->expectExceptionMessage(PaymentGatewayInterface::class); + + $capsule->make(PaymentGatewayInterface::class); + } + + public function test_make_unbound_abstract_class_throws_a_clear_exception() + { + $capsule = new Capsule(); + + $this->expectException(ContainerException::class); + $this->expectExceptionMessage('abstract class'); + + $capsule->make(AbstractRepository::class); + } + + public function test_make_with_unbound_interface_throws_a_clear_exception() + { + $capsule = new Capsule(); + + $this->expectException(ContainerException::class); + + $capsule->makeWith(PaymentGatewayInterface::class, []); + } + + public function test_dependency_on_unbound_interface_throws_a_clear_exception() + { + // OrderService needs a PaymentGatewayInterface; nothing is bound. + $capsule = new Capsule(); + + $this->expectException(ContainerException::class); + $this->expectExceptionMessage(PaymentGatewayInterface::class); + + $capsule->make(OrderService::class); + } + public function test_make_with_parameters() { $capsule = new Capsule(); diff --git a/tests/Container/Stubs/AbstractRepository.php b/tests/Container/Stubs/AbstractRepository.php new file mode 100644 index 00000000..3a0f1ad1 --- /dev/null +++ b/tests/Container/Stubs/AbstractRepository.php @@ -0,0 +1,8 @@ +