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 @@ +