From e6f081c7d128657e89fddb962aec91a4251a8076 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sun, 25 Dec 2022 20:22:53 +0200 Subject: [PATCH] injectable factory create resolved --- .../Espo/Core/Binding/BindingContainer.php | 57 ++++++++++--- application/Espo/Core/InjectableFactory.php | 84 +++++++++++++++---- .../Core/Binding/BindingContainerTest.php | 70 ++++++++-------- .../unit/Espo/Core/InjectableFactoryTest.php | 62 ++++++++++---- 4 files changed, 193 insertions(+), 80 deletions(-) diff --git a/application/Espo/Core/Binding/BindingContainer.php b/application/Espo/Core/Binding/BindingContainer.php index 0c11e224f9..aa465e5ca0 100644 --- a/application/Espo/Core/Binding/BindingContainer.php +++ b/application/Espo/Core/Binding/BindingContainer.php @@ -34,19 +34,20 @@ use ReflectionParameter; use ReflectionNamedType; use LogicException; +/** + * Access point for bindings. + */ class BindingContainer { - private BindingData $data; - - public function __construct(BindingData $data) - { - $this->data = $data; - } + public function __construct(private BindingData $data) + {} /** - * @param ReflectionClass|null $class + * Has binding by a reflection parameter. + * + * @param ?ReflectionClass $class */ - public function has(?ReflectionClass $class, ReflectionParameter $param): bool + public function hasByParam(?ReflectionClass $class, ReflectionParameter $param): bool { if ($this->getInternal($class, $param) === null) { return false; @@ -56,12 +57,14 @@ class BindingContainer } /** - * @param ReflectionClass|null $class + * Get binding by a reflection parameter. + * + * @param ?ReflectionClass $class */ - public function get(?ReflectionClass $class, ReflectionParameter $param): Binding + public function getByParam(?ReflectionClass $class, ReflectionParameter $param): Binding { - if (!$this->has($class, $param)) { - throw new LogicException("BindingContainer: Can't get not existing binding."); + if (!$this->hasByParam($class, $param)) { + throw new LogicException("Cannot get not existing binding."); } /** @var Binding */ @@ -69,7 +72,35 @@ class BindingContainer } /** - * @param ReflectionClass|null $class + * Has global binding by an interface. + * + * @param class-string $interfaceName + */ + public function hasByInterface(string $interfaceName): bool + { + return $this->data->hasGlobal($interfaceName); + } + + /** + * Get global binding by an interface. + * + * @param class-string $interfaceName + */ + public function getByInterface(string $interfaceName): Binding + { + if (!$this->hasByInterface($interfaceName)) { + throw new LogicException("Binding for interface `{$interfaceName}` does not exist."); + } + + if (!interface_exists($interfaceName) && !class_exists($interfaceName)) { + throw new LogicException("Interface `{$interfaceName}` does not exist."); + } + + return $this->data->getGlobal($interfaceName); + } + + /** + * @param ?ReflectionClass $class */ private function getInternal(?ReflectionClass $class, ReflectionParameter $param): ?Binding { diff --git a/application/Espo/Core/InjectableFactory.php b/application/Espo/Core/InjectableFactory.php index 8264fef2c8..063b5b456b 100644 --- a/application/Espo/Core/InjectableFactory.php +++ b/application/Espo/Core/InjectableFactory.php @@ -34,6 +34,7 @@ use Espo\Core\Binding\Binding; use Espo\Core\Binding\Factory; use Espo\Core\Interfaces\Injectable; +use Psr\Container\NotFoundExceptionInterface; use ReflectionClass; use ReflectionParameter; use ReflectionFunction; @@ -64,7 +65,7 @@ class InjectableFactory * Create an instance by a class name. * * @template T of object - * @param class-string $className + * @param class-string $className An instantiatable class. * @return T */ public function create(string $className): object @@ -77,8 +78,8 @@ class InjectableFactory * defined in an associative array. A key should match the parameter name. * * @template T of object - * @param class-string $className - * @param array $with + * @param class-string $className An instantiatable class. + * @param array $with Constructor parameter values. * @return T */ public function createWith(string $className, array $with): object @@ -90,7 +91,8 @@ class InjectableFactory * Create an instance by a class name with a specific binding. * * @template T of object - * @param class-string $className + * @param class-string $className An instantiatable class. + * @param BindingContainer $bindingContainer A binding container. * @return T */ public function createWithBinding(string $className, BindingContainer $bindingContainer): object @@ -98,10 +100,62 @@ class InjectableFactory return $this->createInternal($className, null, $bindingContainer); } + /** + * Create an instance by an interface with an optional additional binding. + * An interface will be resolved by the global binding. If a class is provided, it will be tried to + * be resolved (if it's bound to an extended class). If class is not bound, it will be instantiated + * (with the same behavior as with the `createWithBinding` method). + * + * @template T of object + * @param class-string $interfaceName An interface or class. + * @param ?BindingContainer $bindingContainer A binding container. + * @return T + */ + public function createResolved(string $interfaceName, ?BindingContainer $bindingContainer = null): object + { + $binding = $this->bindingContainer && $this->bindingContainer->hasByInterface($interfaceName) ? + $this->bindingContainer->getByInterface($interfaceName) : + null; + + if (!$binding) { + $class = new ReflectionClass($interfaceName); + + if ($class->isInterface()) { + throw new RuntimeException("Could not resolve interface `{$interfaceName}`."); + } + + $obj = $this->createInternal($interfaceName, null, $bindingContainer); + + if (!$obj instanceof $interfaceName) { + throw new RuntimeException("Class `{$interfaceName}` resolved to another type."); + } + + return $obj; + } + + $typeList = [ + Binding::IMPLEMENTATION_CLASS_NAME, + Binding::FACTORY_CLASS_NAME, + Binding::CALLBACK, + ]; + + if (!in_array($binding->getType(), $typeList)) { + throw new RuntimeException("Bad resolution for interface `{$interfaceName}`."); + } + + $obj = $this->resolveBinding($binding, $bindingContainer); + + if (!$obj instanceof $interfaceName) { + throw new RuntimeException("Class `{$interfaceName}` resolved to another type."); + } + + return $obj; + } + /** * @template T of object * @param class-string $className - * @param ?array $with + * @param ?array $with * @return T */ private function createInternal( @@ -203,14 +257,14 @@ class InjectableFactory } } - if ($bindingContainer && $bindingContainer->has($class, $param)) { - $binding = $bindingContainer->get($class, $param); + if ($bindingContainer && $bindingContainer->hasByParam($class, $param)) { + $binding = $bindingContainer->getByParam($class, $param); return $this->resolveBinding($binding, $bindingContainer); } - if ($this->bindingContainer && $this->bindingContainer->has($class, $param)) { - $binding = $this->bindingContainer->get($class, $param); + if ($this->bindingContainer && $this->bindingContainer->hasByParam($class, $param)) { + $binding = $this->bindingContainer->getByParam($class, $param); return $this->resolveBinding($binding, $bindingContainer); } @@ -267,16 +321,18 @@ class InjectableFactory return $injectionList; } - /** - * @return mixed - */ - private function resolveBinding(Binding $binding, ?BindingContainer $bindingContainer) + private function resolveBinding(Binding $binding, ?BindingContainer $bindingContainer): mixed { $type = $binding->getType(); $value = $binding->getValue(); if ($type === Binding::CONTAINER_SERVICE) { - return $this->container->get($value); + try { + return $this->container->get($value); + } + catch (NotFoundExceptionInterface $e) { + throw new RuntimeException($e->getMessage()); + } } if ($type === Binding::IMPLEMENTATION_CLASS_NAME) { diff --git a/tests/unit/Espo/Core/Binding/BindingContainerTest.php b/tests/unit/Espo/Core/Binding/BindingContainerTest.php index 6f876aac25..f9e9623f13 100644 --- a/tests/unit/Espo/Core/Binding/BindingContainerTest.php +++ b/tests/unit/Espo/Core/Binding/BindingContainerTest.php @@ -140,7 +140,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test', 'Espo\\Test'); $this->assertTrue( - $this->createContainer()->has($class, $param) + $this->createContainer()->hasByParam($class, $param) ); } @@ -151,7 +151,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test', 'Espo\\Test'); $this->assertTrue( - $this->createContainer()->has(null, $param) + $this->createContainer()->hasByParam(null, $param) ); } @@ -164,7 +164,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test', 'Espo\\Hello'); $this->assertFalse( - $this->createContainer()->has($class, $param) + $this->createContainer()->hasByParam($class, $param) ); } @@ -175,7 +175,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test', 'Espo\\Hello'); $this->assertFalse( - $this->createContainer()->has(null, $param) + $this->createContainer()->hasByParam(null, $param) ); } @@ -191,7 +191,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test', 'Espo\\Test'); $this->assertTrue( - $this->createContainer()->has($class, $param) + $this->createContainer()->hasByParam($class, $param) ); } @@ -206,7 +206,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test', 'Espo\\Test'); $this->assertTrue( - $this->createContainer()->has($class, $param) + $this->createContainer()->hasByParam($class, $param) ); } @@ -221,7 +221,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test'); $this->assertTrue( - $this->createContainer()->has($class, $param) + $this->createContainer()->hasByParam($class, $param) ); } @@ -236,7 +236,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test'); $this->assertTrue( - $this->createContainer()->has($class, $param) + $this->createContainer()->hasByParam($class, $param) ); } @@ -250,7 +250,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test', 'Espo\\Test'); $this->assertTrue( - $this->createContainer()->has($class, $param) + $this->createContainer()->hasByParam($class, $param) ); } @@ -264,7 +264,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test', 'Espo\\Hello'); $this->assertFalse( - $this->createContainer()->has($class, $param) + $this->createContainer()->hasByParam($class, $param) ); } @@ -279,7 +279,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test', 'Espo\\Test'); $this->assertFalse( - $this->createContainer()->has($class, $param) + $this->createContainer()->hasByParam($class, $param) ); } @@ -294,7 +294,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test'); $this->assertFalse( - $this->createContainer()->has($class, $param) + $this->createContainer()->hasByParam($class, $param) ); } @@ -305,7 +305,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test', 'Espo\\Test'); - $binding = $this->createContainer()->get($class, $param); + $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::IMPLEMENTATION_CLASS_NAME, $binding->getType()); $this->assertEquals('Espo\\ImplTest', $binding->getValue()); @@ -318,7 +318,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test', 'Espo\\Test'); - $binding = $this->createContainer()->get($class, $param); + $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::FACTORY_CLASS_NAME, $binding->getType()); $this->assertEquals('Espo\\TestFactory', $binding->getValue()); @@ -332,7 +332,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test', 'Espo\\Test'); - $binding = $this->createContainer()->get($class, $param); + $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::CONTAINER_SERVICE, $binding->getType()); @@ -352,7 +352,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test', 'Espo\\Test'); - $binding = $this->createContainer()->get($class, $param); + $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::CALLBACK, $binding->getType()); @@ -369,7 +369,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test', $className); - $binding = $this->createContainer()->get(null, $param); + $binding = $this->createContainer()->getByParam(null, $param); $this->assertEquals(Binding::VALUE, $binding->getType()); @@ -390,7 +390,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $class = $this->createClassMock('Espo\\Context'); - $binding = $this->createContainer()->get($class, $param); + $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::VALUE, $binding->getType()); @@ -412,7 +412,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test', 'Espo\\Test'); - $binding = $this->createContainer()->get($class, $param); + $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::CALLBACK, $binding->getType()); @@ -429,7 +429,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test', 'Espo\\Test'); - $binding = $this->createContainer()->get($class, $param); + $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::CONTAINER_SERVICE, $binding->getType()); @@ -446,7 +446,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('name', 'Espo\\Test'); - $binding = $this->createContainer()->get($class, $param); + $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::CONTAINER_SERVICE, $binding->getType()); @@ -454,7 +454,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('hello', 'Espo\\Test'); - $binding = $this->createContainer()->get($class, $param); + $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::CONTAINER_SERVICE, $binding->getType()); @@ -471,7 +471,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test', 'Espo\\Test'); - $binding = $this->createContainer()->get($class, $param); + $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::IMPLEMENTATION_CLASS_NAME, $binding->getType()); $this->assertEquals('Espo\\ImplTest', $binding->getValue()); @@ -487,7 +487,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test', 'Espo\\Test'); - $binding = $this->createContainer()->get($class, $param); + $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::FACTORY_CLASS_NAME, $binding->getType()); $this->assertEquals('Espo\\TestFactory', $binding->getValue()); @@ -504,7 +504,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test', 'Espo\\Test'); $this->assertFalse( - $this->createContainer()->has($class, $param) + $this->createContainer()->hasByParam($class, $param) ); } @@ -520,7 +520,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('name', 'Espo\\Test'); - $binding = $this->createContainer()->get($class, $param); + $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::CONTAINER_SERVICE, $binding->getType()); @@ -528,7 +528,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('hello', 'Espo\\Test'); - $binding = $this->createContainer()->get($class, $param); + $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::CONTAINER_SERVICE, $binding->getType()); @@ -545,7 +545,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test'); - $binding = $this->createContainer()->get($class, $param); + $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::VALUE, $binding->getType()); @@ -563,7 +563,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test', 'Espo\\SomeClass'); - $binding = $this->createContainer()->get($class, $param); + $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::VALUE, $binding->getType()); $this->assertEquals($instance, $binding->getValue()); @@ -580,7 +580,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test', 'Espo\\SomeClass'); - $binding = $this->createContainer()->get($class, $param); + $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::VALUE, $binding->getType()); $this->assertEquals($instance, $binding->getValue()); @@ -596,7 +596,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test', 'Espo\\Test'); - $binding = $this->createContainer()->get($class, $param); + $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::CONTAINER_SERVICE, $binding->getType()); @@ -617,7 +617,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase $param = $this->createParamMock('test', 'Espo\\Test'); - $binding = $this->createContainer()->get($class, $param); + $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::CONTAINER_SERVICE, $binding->getType()); @@ -634,13 +634,13 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase ->build(); $param1 = $this->createParamMock('test', SomeInterface1::class); - $this->assertTrue($container->has(null, $param1)); + $this->assertTrue($container->hasByParam(null, $param1)); $param2 = $this->createParamMock('test', SomeInterface2::class); - $this->assertFalse($container->has(null, $param2)); + $this->assertFalse($container->hasByParam(null, $param2)); $class3 = $this->createClassMock(SomeClass1::class); $param3 = $this->createParamMock('test', SomeInterface2::class); - $this->assertTrue($container->has($class3, $param3)); + $this->assertTrue($container->hasByParam($class3, $param3)); } } diff --git a/tests/unit/Espo/Core/InjectableFactoryTest.php b/tests/unit/Espo/Core/InjectableFactoryTest.php index acad6bc276..209b60f159 100644 --- a/tests/unit/Espo/Core/InjectableFactoryTest.php +++ b/tests/unit/Espo/Core/InjectableFactoryTest.php @@ -29,26 +29,22 @@ namespace tests\unit\Espo\Core; -use Espo\Core\{ - Binding\BindingData, - Binding\Binder, - Binding\BindingContainer, - InjectableFactory, - Container, -}; +use Espo\Core\Binding\Binder; +use Espo\Core\Binding\BindingContainer; +use Espo\Core\Binding\BindingContainerBuilder; +use Espo\Core\Binding\BindingData; +use Espo\Core\Container; +use Espo\Core\InjectableFactory; -use tests\integration\testClasses\Binding\{ - SomeInterface, - SomeClass, -}; +use tests\integration\testClasses\Binding\SomeClass; +use tests\integration\testClasses\Binding\SomeImplementation; +use tests\integration\testClasses\Binding\SomeInterface; -use tests\unit\testClasses\Core\Binding\{ - SomeClass0, - SomeClass1, - SomeInterface1, - SomeClass2, - SomeInterface2, -}; +use tests\unit\testClasses\Core\Binding\SomeClass0; +use tests\unit\testClasses\Core\Binding\SomeClass1; +use tests\unit\testClasses\Core\Binding\SomeClass2; +use tests\unit\testClasses\Core\Binding\SomeInterface1; +use tests\unit\testClasses\Core\Binding\SomeInterface2; class InjectableFactoryTest extends \PHPUnit\Framework\TestCase { @@ -91,4 +87,34 @@ class InjectableFactoryTest extends \PHPUnit\Framework\TestCase $this->assertNotNull($obj); } + + public function testCreateResolved1(): void + { + $container = $this->createMock(Container::class); + + $bindingContainer = BindingContainerBuilder::create() + ->bindImplementation(SomeInterface::class, SomeImplementation::class) + ->build(); + + $injectableFactory = new InjectableFactory($container, $bindingContainer); + + $obj = $injectableFactory->createResolved(SomeInterface::class); + + $this->assertInstanceOf(SomeImplementation::class, $obj); + } + + public function testCreateResolved2(): void + { + $container = $this->createMock(Container::class); + + $bindingContainer = BindingContainerBuilder::create()->build(); + + $injectableFactory = new InjectableFactory($container, $bindingContainer); + + $bindingContainer1 = BindingContainerBuilder::create()->build(); + + $obj = $injectableFactory->createResolved(SomeImplementation::class, $bindingContainer1); + + $this->assertInstanceOf(SomeImplementation::class, $obj); + } }