From a9c3eacce2c37d01c9b834713fe0400b75db33c2 Mon Sep 17 00:00:00 2001 From: Andrei Pisarevskii Date: Sat, 2 Dec 2023 15:32:28 +0300 Subject: [PATCH] Added showing stack trace of Container Exceptions. Improved try catch block coverage for Container::resolve_object() --- README.md | 10 ++-- src/Container.php | 116 ++++++++++++++++++++++++++++------------ tests/ContainerTest.php | 14 +---- 3 files changed, 90 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index b93108e..8bba080 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ -# Simple DI Container for WordPress +# Simple PHP DIC - DI Container in one file. [![UnitTests](https://github.com/renakdup/simple-wordpress-dic/actions/workflows/phpunit.yaml/badge.svg)](https://github.com/renakdup/simple-wordpress-dic/actions/workflows/phpunit.yaml) [![PHPStan](https://github.com/renakdup/simple-wordpress-dic/actions/workflows/phpstan.yaml/badge.svg)](https://github.com/renakdup/simple-wordpress-dic/actions/workflows/phpstan.yaml) -Simple DI Container with auto-wiring in a single file allows you to easily use it in your WordPress plugins and themes. +Simple DI Container with autowiring in a single file allows you to easily use it in your simple PHP applications and especially convenient for WordPress plugins and themes. ## Why choose Simple DI Container? -1. Easy to integrate into your WordPress project, just copy one file. -2. Simple DI Container hasn't any dependencies on other scripts or libraries. +1. Easy to integrate into your PHP Applocation or WordPress project, just copy one file. +2. Simple PHP DI Container hasn't any dependencies on other scripts or libraries. 3. Supports auto-wiring `__constructor` parameters for classes as well as for scalar types that have default values. 4. Allow you following the best practices for developing your code. 5. Supports PSR11 (read more about below). @@ -157,6 +157,8 @@ use Psr\Container\NotFoundExceptionInterface; - [x] Add auto-wiring support for not bounded classes. - [x] Add resolved service storage (getting singleton). - [ ] Add ability to create new instances of service every time. +- [ ] Fix deprecated `Use ReflectionParameter::getType() and the ReflectionType APIs should be used instead.` for PHP8 +- [ ] Improve performance. - [ ] Allow to set definitions via `__constructor`. - [ ] Bind $container instance by default. - [ ] Add supporting Code Driven IoC. diff --git a/src/Container.php b/src/Container.php index 872ff2f..e5ff9f3 100644 --- a/src/Container.php +++ b/src/Container.php @@ -1,14 +1,15 @@ services ); + } + + /** + * @param string $id + * * @return mixed + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ protected function resolve( string $id ) { if ( $this->has( $id ) ) { @@ -120,59 +137,90 @@ protected function resolve( string $id ) { } return $service; - } else { - if ( is_string( $id ) && class_exists( $id ) ) { - return $this->resolve_object( $id ); - } + } - throw new ContainerNotFoundException( "Service '{$id}' not found in the Container." ); + if ( class_exists( $id ) ) { + return $this->resolve_object( $id ); } + + $message = "Service '{$id}' not found in the Container.\n" + . "Stack trace: \n" + . $this->get_stack_trace(); + throw new ContainerNotFoundException( $message ); } /** * @param class-string $service + * + * @return object + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ protected function resolve_object( string $service ): object { - $reflected_class = new ReflectionClass( $service ); - $constructor = $reflected_class->getConstructor(); + try { + $reflected_class = new ReflectionClass( $service ); - if ( ! $constructor ) { - return new $service(); - } + $constructor = $reflected_class->getConstructor(); - $params = $constructor->getParameters(); + if ( ! $constructor ) { + return new $service(); + } - if ( ! $params ) { - return new $service(); - } + $params = $constructor->getParameters(); - $constructor_args = []; - foreach ( $params as $param ) { - if ( $param_class = $param->getClass() ) { - $constructor_args[] = $this->get( $param_class->getName() ); - continue; + if ( ! $params ) { + return new $service(); } - try { + $constructor_args = []; + foreach ( $params as $param ) { + if ( $param_class = $param->getClass() ) { + $constructor_args[] = $this->get( $param_class->getName() ); + continue; + } + $default_value = $param->getDefaultValue(); if ( ! $default_value && $default_value !== null ) { - throw new ContainerException( 'Service "' . $reflected_class->getName() . '" could not be resolved due constructor parameter "' . $param->getName() . '"' ); + $message = 'Service "' . $reflected_class->getName() . '" could not be resolved,' . + 'because parameter of constructor "' . $param->getName() . '" has not default value.' . "\n" . + "Stack trace: \n" . + $this->get_stack_trace(); + throw new ContainerException( $message ); } - } catch ( \ReflectionException $e ) { - throw new ContainerException( 'Service "' . $reflected_class->getName() . '" could not be resolved because parameter of constructor "' . $param . '" has the Reflection issue while resolving: ' . $e->getMessage() ); - } - $constructor_args[] = $default_value; + $constructor_args[] = $default_value; + } + } catch ( ReflectionException $e ) { + throw new ContainerException( + "Service '{$service}' could not be resolved due the reflection issue:\n '" . + $e->getMessage() . "'\n" . + "Stack trace: \n" . + $e->getTraceAsString() + ); } return new $service( ...$constructor_args ); } - /** - * @inheritdoc - */ - public function has( string $id ): bool { - return array_key_exists( $id, $this->services ); + protected function get_stack_trace(): string { + $stackTraceArray = debug_backtrace(); + $stackTraceString = ''; + + foreach ( $stackTraceArray as $item ) { + $file = $item['file'] ?? '[internal function]'; + $line = $item['line'] ?? ''; + $function = $item['function'] ?? ''; // @phpstan-ignore-line + $class = $item['class'] ?? ''; + $type = $item['type'] ?? ''; + + $stackTraceString .= "{$file}({$line}): "; + if ( ! empty( $class ) ) { + $stackTraceString .= "{$class}{$type}"; + } + $stackTraceString .= "{$function}()\n"; + } + + return $stackTraceString; } } diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index fa36c4d..9e55963 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -152,12 +152,10 @@ public function test_get__autowiring_for_not_bound_invocable() { } public function test_get__autowiring_not_bound_deps() { - $obj1 = new SimpleClass(); - $obj2 = new ClassWithConstructorPrimitives( $obj1 ); - $obj3 = new ClassWithConstructor( $obj2 ); + $obj = new ClassWithConstructor( new ClassWithConstructorPrimitives( new SimpleClass() ) ); $this->container->set( $name = ClassWithConstructor::class, ClassWithConstructor::class ); - self::assertEquals( $obj3, $this->container->get( $name ) ); + self::assertEquals( $obj, $this->container->get( $name ) ); $this->container->set( SplQueue::class , SplQueue::class ); self::assertInstanceOf( SplQueue::class, $this->container->get( SplQueue::class ) ); @@ -177,17 +175,9 @@ public function test_get__exception_not_found() { $this->container->get( 'not-exist-service' ); } -// public function test_get__bind_autowiring_container_not_found_exception_class() { -// self::expectException( ContainerNotFoundException::class ); -// -// $this->container->get( SimpleClass::class ); -// } - public function test_get__autowiring__container_exception() { self::expectException( ContainerException::class ); - $this->container->set( SimpleClass::class, SimpleClass::class ); - $this->container->set( ClassWithConstructorDepsException::class, ClassWithConstructorDepsException::class ); $this->container->get( ClassWithConstructorDepsException::class ); }