injectable factory create resolved

This commit is contained in:
Yuri Kuznetsov
2022-12-25 20:22:53 +02:00
parent f463421a33
commit e6f081c7d1
4 changed files with 193 additions and 80 deletions
@@ -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<object>|null $class
* Has binding by a reflection parameter.
*
* @param ?ReflectionClass<object> $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<object>|null $class
* Get binding by a reflection parameter.
*
* @param ?ReflectionClass<object> $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<object>|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<object> $class
*/
private function getInternal(?ReflectionClass $class, ReflectionParameter $param): ?Binding
{
+70 -14
View File
@@ -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<T> $className
* @param class-string<T> $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<T> $className
* @param array<string,mixed> $with
* @param class-string<T> $className An instantiatable class.
* @param array<string, mixed> $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<T> $className
* @param class-string<T> $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<T> $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<T> $className
* @param ?array<string,mixed> $with
* @param ?array<string, mixed> $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) {
@@ -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));
}
}
+44 -18
View File
@@ -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);
}
}