binding improvements

This commit is contained in:
Yuri Kuznetsov
2022-11-20 13:25:54 +02:00
parent 8f0969eb5d
commit 047b07d3c9
15 changed files with 307 additions and 99 deletions
+6 -5
View File
@@ -31,6 +31,7 @@ namespace Espo;
use Espo\Core\Binding\Binder;
use Espo\Core\Binding\BindingProcessor;
use Espo\Core\Binding\Key\NamedClassKey;
/**
* Default binding for the dependency injection framework. Custom binding should be set up in
@@ -117,7 +118,7 @@ class Binding implements BindingProcessor
);
$binder->bindService(
'Espo\\Core\\SelectBuilderFactory',
'Espo\\Core\\Select\\SelectBuilderFactory',
'selectBuilderFactory'
);
@@ -137,7 +138,7 @@ class Binding implements BindingProcessor
);
$binder->bindService(
'Espo\\Core\\Utils\\HookManager',
'Espo\\Core\\HookManager',
'hookManager'
);
@@ -162,12 +163,12 @@ class Binding implements BindingProcessor
);
$binder->bindService(
'Espo\\Core\\Utils\\Language $baseLanguage',
NamedClassKey::create('Espo\\Core\\Utils\\Language', 'baseLanguage'),
'baseLanguage'
);
$binder->bindService(
'Espo\\Core\\Utils\\Language $defaultLanguage',
NamedClassKey::create('Espo\\Core\\Utils\\Language', 'defaultLanguage'),
'defaultLanguage'
);
@@ -182,7 +183,7 @@ class Binding implements BindingProcessor
);
$binder->bindService(
'Espo\\Core\\AclManager $internalAclManager',
NamedClassKey::create('Espo\\Core\\AclManager', 'internalAclManager'),
'internalAclManager'
);
+35 -20
View File
@@ -29,26 +29,25 @@
namespace Espo\Core\Binding;
use Espo\Core\Binding\Key\NamedClassKey;
use LogicException;
use Closure;
class Binder
{
private BindingData $data;
public function __construct(BindingData $data)
{
$this->data = $data;
}
public function __construct(private BindingData $data)
{}
/**
* Bind an interface to an implementation.
*
* @param string $key An interface or interface with a parameter name (`Interface $name`).
* @param class-string<object> $implementationClassName An implementation class name.
* @template T of object
* @param class-string<T>|NamedClassKey<T> $key An interface or interface with a parameter name.
* @param class-string<T> $implementationClassName An implementation class name.
*/
public function bindImplementation(string $key, string $implementationClassName): self
public function bindImplementation(string|NamedClassKey $key, string $implementationClassName): self
{
$key = self::keyToString($key);
$this->validateBindingKey($key);
$this->data->addGlobal(
@@ -62,11 +61,12 @@ class Binder
/**
* Bind an interface to a specific service.
*
* @param string $key An interface or interface with a parameter name (`Interface $name`).
* @param class-string<object>|NamedClassKey<object> $key An interface or interface with a parameter name.
* @param string $serviceName A service name.
*/
public function bindService(string $key, string $serviceName): self
public function bindService(string|NamedClassKey $key, string $serviceName): self
{
$key = self::keyToString($key);
$this->validateBindingKey($key);
$this->data->addGlobal(
@@ -80,11 +80,14 @@ class Binder
/**
* Bind an interface to a callback.
*
* @param string $key An interface or interface with a parameter name (`Interface $name`).
* @param callable $callback A callback that will resolve a dependency.
* @template T of object
* @param class-string<T>|NamedClassKey<T> $key An interface or interface with a parameter name.
* @param Closure $callback A callback that will resolve a dependency.
* @todo Change to Closure(...): T Once https://github.com/phpstan/phpstan/issues/8214 is implemented.
*/
public function bindCallback(string $key, callable $callback): self
public function bindCallback(string|NamedClassKey $key, Closure $callback): self
{
$key = self::keyToString($key);
$this->validateBindingKey($key);
$this->data->addGlobal(
@@ -98,11 +101,13 @@ class Binder
/**
* Bind an interface to a specific instance.
*
* @param string $key An interface or interface with a parameter name (`Interface $name`).
* @param object $instance An instance.
* @template T of object
* @param class-string<T>|NamedClassKey<T> $key An interface or interface with a parameter name.
* @param T $instance An instance.
*/
public function bindInstance(string $key, object $instance): self
public function bindInstance(string|NamedClassKey $key, object $instance): self
{
$key = self::keyToString($key);
$this->validateBindingKey($key);
$this->data->addGlobal(
@@ -116,11 +121,13 @@ class Binder
/**
* Bind an interface to a factory.
*
* @param string $key An interface or interface with a parameter name (`Interface $name`).
* @param class-string<Factory> $factoryClassName A factory class name.
* @template T of object
* @param class-string<T>|NamedClassKey<T> $key An interface or interface with a parameter name.
* @param class-string<Factory<T>> $factoryClassName A factory class name.
*/
public function bindFactory(string $key, string $factoryClassName): self
public function bindFactory(string|NamedClassKey $key, string $factoryClassName): self
{
$key = self::keyToString($key);
$this->validateBindingKey($key);
$this->data->addGlobal(
@@ -156,6 +163,14 @@ class Binder
return new ContextualBinder($this->data, $className);
}
/**
* @param string|NamedClassKey<object> $key
*/
private static function keyToString(string|NamedClassKey $key): string
{
return is_string($key) ? $key : $key->toString();
}
private function validateBindingKey(string $key): void
{
if (!$key) {
+7 -8
View File
@@ -34,20 +34,13 @@ use LogicException;
class Binding
{
public const IMPLEMENTATION_CLASS_NAME = 1;
public const CONTAINER_SERVICE = 2;
public const VALUE = 3;
public const CALLBACK = 4;
public const FACTORY_CLASS_NAME = 5;
private int $type;
/**
* @var mixed
*/
/** @var mixed */
private $value;
/**
@@ -72,6 +65,9 @@ class Binding
return $this->value;
}
/**
* @param class-string<object> $implementationClassName
*/
public static function createFromImplementationClassName(string $implementationClassName): self
{
if (!$implementationClassName) {
@@ -103,6 +99,9 @@ class Binding
return new self(self::CALLBACK, $callback);
}
/**
* @param class-string<Factory<object>> $factoryClassName
*/
public static function createFromFactoryClassName(string $factoryClassName): self
{
if (!$factoryClassName) {
@@ -30,6 +30,7 @@
namespace Espo\Core\Binding;
use Closure;
use Espo\Core\Binding\Key\NamedClassKey;
class BindingContainerBuilder
{
@@ -45,10 +46,11 @@ class BindingContainerBuilder
/**
* Bind an interface to an implementation.
*
* @param string $key An interface or interface with a parameter name (`Interface $name`).
* @param class-string<object> $implementationClassName An implementation class name.
* @template T of object
* @param class-string<T>|NamedClassKey<T> $key An interface or interface with a parameter name.
* @param class-string<T> $implementationClassName An implementation class name.
*/
public function bindImplementation(string $key, string $implementationClassName): self
public function bindImplementation(string|NamedClassKey $key, string $implementationClassName): self
{
$this->binder->bindImplementation($key, $implementationClassName);
@@ -58,10 +60,10 @@ class BindingContainerBuilder
/**
* Bind an interface to a specific service.
*
* @param string $key An interface or interface with a parameter name (`Interface $name`).
* @param class-string<object>|NamedClassKey<object> $key An interface or interface with a parameter name.
* @param string $serviceName A service name.
*/
public function bindService(string $key, string $serviceName): self
public function bindService(string|NamedClassKey $key, string $serviceName): self
{
$this->binder->bindService($key, $serviceName);
@@ -71,10 +73,12 @@ class BindingContainerBuilder
/**
* Bind an interface to a callback.
*
* @param string $key An interface or interface with a parameter name (`Interface $name`).
* @param callable $callback A callback that will resolve a dependency.
* @template T of object
* @param class-string<T>|NamedClassKey<T> $key An interface or interface with a parameter name.
* @param Closure $callback A callback that will resolve a dependency.
* @todo Change to Closure(...): T Once https://github.com/phpstan/phpstan/issues/8214 is implemented.
*/
public function bindCallback(string $key, callable $callback): self
public function bindCallback(string|NamedClassKey $key, Closure $callback): self
{
$this->binder->bindCallback($key, $callback);
@@ -84,10 +88,11 @@ class BindingContainerBuilder
/**
* Bind an interface to a specific instance.
*
* @param string $key An interface or interface with a parameter name (`Interface $name`).
* @param object $instance An instance.
* @template T of object
* @param class-string<T>|NamedClassKey<T> $key An interface or interface with a parameter name.
* @param T $instance An instance.
*/
public function bindInstance(string $key, object $instance): self
public function bindInstance(string|NamedClassKey $key, object $instance): self
{
$this->binder->bindInstance($key, $instance);
@@ -97,10 +102,11 @@ class BindingContainerBuilder
/**
* Bind an interface to a factory.
*
* @param string $key An interface or interface with a parameter name (`Interface $name`).
* @param class-string<Factory> $factoryClassName A factory class name.
* @template T of object
* @param class-string<T>|NamedClassKey<T> $key An interface or interface with a parameter name.
* @param class-string<Factory<T>> $factoryClassName A factory class name.
*/
public function bindFactory(string $key, string $factoryClassName): self
public function bindFactory(string|NamedClassKey $key, string $factoryClassName): self
{
$this->binder->bindFactory($key, $factoryClassName);
@@ -29,6 +29,9 @@
namespace Espo\Core\Binding;
use Closure;
use Espo\Core\Binding\Key\NamedClassKey;
use Espo\Core\Binding\Key\NamedKey;
use LogicException;
class ContextualBinder
@@ -49,11 +52,13 @@ class ContextualBinder
/**
* Bind an interface to an implementation.
*
* @param string $key An interface or interface with a parameter name (`Interface $name`).
* @param class-string<object> $implementationClassName An implementation class name.
* @template T of object
* @param class-string<T>|NamedClassKey<T> $key An interface or interface with a parameter name.
* @param class-string<T> $implementationClassName An implementation class name.
*/
public function bindImplementation(string $key, string $implementationClassName): self
public function bindImplementation(string|NamedClassKey $key, string $implementationClassName): self
{
$key = self::keyToString($key);
$this->validateBindingKeyNoParameterName($key);
$this->data->addContext(
@@ -68,11 +73,13 @@ class ContextualBinder
/**
* Bind an interface to a specific service.
*
* @param string $key An interface or interface with a parameter name (`Interface $name`).
* @param string $serviceName A service name.
* @template T of object
* @param class-string<T>|NamedClassKey<T> $key An interface or interface with a parameter name.
* @param class-string<T> $serviceName A service name.
*/
public function bindService(string $key, string $serviceName): self
public function bindService(string|NamedClassKey $key, string $serviceName): self
{
$key = self::keyToString($key);
$this->validateBindingKeyNoParameterName($key);
$this->data->addContext(
@@ -87,11 +94,12 @@ class ContextualBinder
/**
* Bind an interface or parameter name to a specific value.
*
* @param string $key Parameter name (`$name`) or interface with a parameter name (`Interface $name`).
* @param string|NamedKey|NamedClassKey<object> $key Parameter name (`$name`) or interface with a parameter name.
* @param mixed $value A value of any type.
*/
public function bindValue(string $key, $value): self
public function bindValue(string|NamedKey|NamedClassKey $key, $value): self
{
$key = self::keyToString($key);
$this->validateBindingKeyParameterName($key);
$this->data->addContext(
@@ -106,11 +114,13 @@ class ContextualBinder
/**
* Bind an interface to a specific instance.
*
* @param string $key An interface or interface with a parameter name (`Interface $name`).
* @param object $instance An instance.
* @template T of object
* @param class-string<T>|NamedClassKey<T> $key An interface or interface with a parameter name.
* @param T $instance An instance.
*/
public function bindInstance(string $key, object $instance): self
public function bindInstance(string|NamedClassKey $key, object $instance): self
{
$key = self::keyToString($key);
$this->validateBindingKeyNoParameterName($key);
$this->data->addContext(
@@ -125,12 +135,13 @@ class ContextualBinder
/**
* Bind an interface or parameter name to a callback.
*
* @param string $key An interface, parameter name (`$name`) or
* interface with a parameter name (`Interface $name`).
* @param callable $callback A callback that will resolve a dependency.
* @param class-string<object>|NamedClassKey<object>|NamedKey $key An interface, parameter name or both.
* @param Closure $callback A callback that will resolve a dependency.
* @todo Change to Closure(...): mixed Once https://github.com/phpstan/phpstan/issues/8214 is implemented.
*/
public function bindCallback(string $key, callable $callback): self
public function bindCallback(string|NamedClassKey|NamedKey $key, Closure $callback): self
{
$key = self::keyToString($key);
$this->validateBinding($key);
$this->data->addContext(
@@ -145,11 +156,13 @@ class ContextualBinder
/**
* Bind an interface to a factory.
*
* @param string $key An interface or interface with a parameter name (`Interface $name`).
* @param class-string<Factory> $factoryClassName A factory class name.
* @template T of object
* @param class-string<T>|NamedClassKey<T> $key An interface or interface with a parameter name.
* @param class-string<Factory<T>> $factoryClassName A factory class name.
*/
public function bindFactory(string $key, string $factoryClassName): self
public function bindFactory(string|NamedClassKey $key, string $factoryClassName): self
{
$key = self::keyToString($key);
$this->validateBindingKeyNoParameterName($key);
$this->data->addContext(
@@ -181,8 +194,16 @@ class ContextualBinder
{
$this->validateBinding($key);
if (strpos($key, '$') === false) {
if (!str_contains($key, '$')) {
throw new LogicException("Can't bind w/o a parameter name.");
}
}
/**
* @param string|NamedKey|NamedClassKey<object> $key
*/
private static function keyToString(string|NamedKey|NamedClassKey $key): string
{
return is_string($key) ? $key : $key->toString();
}
}
@@ -29,7 +29,13 @@
namespace Espo\Core\Binding;
/**
* @template T of object
*/
interface Factory
{
/**
* @return T
*/
public function create(): object;
}
@@ -0,0 +1,62 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Binding\Key;
/**
* A key for a class-type-hinted constructor parameter with a parameter name.
*
* @template-covariant T of object
*/
class NamedClassKey
{
/**
* @param class-string<T> $className
*/
private function __construct(private string $className, private string $parameterName)
{}
/**
* Create.
*
* @template TC of object
* @param class-string<TC> $className An interface.
* @param string $parameterName A constructor parameter name (w/o '$').
* @return self<TC>
*/
public static function create(string $className, string $parameterName): self
{
return new self($className, $parameterName);
}
public function toString(): string
{
return $this->className . ' $' . $this->parameterName;
}
}
@@ -0,0 +1,54 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Binding\Key;
/**
* A parameter-name-only key.
*/
class NamedKey
{
private function __construct(private string $parameterName)
{}
/**
* Create.
*
* @param string $parameterName A constructor parameter name (w/o '$').
*/
public static function create(string $parameterName): self
{
return new self($parameterName);
}
public function toString(): string
{
return '$' . $this->parameterName;
}
}
+2 -2
View File
@@ -297,8 +297,8 @@ class InjectableFactory
}
if ($type === Binding::FACTORY_CLASS_NAME) {
/** @var class-string $value */
/** @var Factory $factory */
/** @var class-string<object> $value */
/** @var Factory<object> $factory */
$factory = $this->createInternal($value, null, $bindingContainer);
return $factory->create();
@@ -42,6 +42,9 @@ use Espo\Core\Mail\Account\StorageFactory;
use Espo\Core\Mail\Account\GroupAccount\StorageFactory as GroupAccountStorageFactory;
use Espo\Core\Mail\Account\Fetcher;
/**
* @implements Factory<Fetcher>
*/
class FetcherFactory implements Factory
{
private InjectableFactory $injectableFactory;
@@ -40,6 +40,9 @@ use Espo\Core\Mail\Account\Fetcher;
use Espo\Core\Mail\Account\StorageFactory;
use Espo\Core\Mail\Account\PersonalAccount\StorageFactory as PersonalAccountStorageFactory;
/**
* @implements Factory<Fetcher>
*/
class FetcherFactory implements Factory
{
private InjectableFactory $injectableFactory;
+3 -4
View File
@@ -30,19 +30,18 @@
namespace Espo\Core\Sms;
use Espo\Core\Binding\Factory;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Metadata;
use Espo\Core\InjectableFactory;
use RuntimeException;
/**
* @implements Factory<Sender>
*/
class SenderFactory implements Factory
{
private Config $config;
private Metadata $metadata;
private InjectableFactory $injectableFactory;
public function __construct(
@@ -36,12 +36,13 @@ use Espo\Core\Binding\Factory;
use RuntimeException;
/**
* @implements Factory<Sender>
*/
class SenderFactory implements Factory
{
private $injectableFactory;
private $config;
private $metadata;
private const DEFAULT_MESSAGER = 'ZeroMQ';
@@ -33,25 +33,20 @@ use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Metadata;
use Espo\Core\Binding\Factory;
use RuntimeException;
/**
* @implements Factory<Subscriber>
*/
class SubscriberFactory implements Factory
{
private $injectableFactory;
private $config;
private $metadata;
private const DEFAULT_MESSAGER = 'ZeroMQ';
public function __construct(InjectableFactory $injectableFactory, Config $config, Metadata $metadata)
{
$this->injectableFactory = $injectableFactory;
$this->config = $config;
$this->metadata = $metadata;
}
public function __construct(
private InjectableFactory $injectableFactory,
private Config $config,
private Metadata $metadata
) {}
public function create(): Subscriber
{
@@ -29,15 +29,15 @@
namespace tests\unit\Espo\Core\Binding;
use Espo\Core\{
Binding\BindingContainer,
Binding\BindingLoader,
Binding\BindingData,
Binding\Binder,
Binding\Binding,
Binding\BindingContainerBuilder,
Binding\ContextualBinder,
};
use Espo\Core\Binding\Binder;
use Espo\Core\Binding\Binding;
use Espo\Core\Binding\BindingContainer;
use Espo\Core\Binding\BindingContainerBuilder;
use Espo\Core\Binding\BindingData;
use Espo\Core\Binding\BindingLoader;
use Espo\Core\Binding\ContextualBinder;
use Espo\Core\Binding\Key\NamedClassKey;
use Espo\Core\Binding\Key\NamedKey;
use ReflectionClass;
use ReflectionParameter;
@@ -225,6 +225,35 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase
);
}
public function testHasContextTrue3()
{
$this->binder
->for('Espo\\Context')
->bindValue(NamedKey::create('test'), 'Test Value');
$class = $this->createClassMock('Espo\\Context');
$param = $this->createParamMock('test');
$this->assertTrue(
$this->createContainer()->has($class, $param)
);
}
public function testHasContextTrue4()
{
$this->binder
->for('Espo\\Context')
->bindService(NamedClassKey::create('Espo\\Test', 'test'), 'service');
$class = $this->createClassMock('Espo\\Context');
$param = $this->createParamMock('test', 'Espo\\Test');
$this->assertTrue(
$this->createContainer()->has($class, $param)
);
}
public function testHasContextFalse1()
{
$this->binder
@@ -232,7 +261,6 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase
->bindService('Espo\\Test', 'test');
$class = $this->createClassMock('Espo\\Context');
$param = $this->createParamMock('test', 'Espo\\Hello');
$this->assertFalse(
@@ -524,7 +552,7 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase
$this->assertEquals('Test Value', $binding->getValue());
}
public function testGetContextInterfaceValue()
public function testGetContextInterfaceValue1()
{
$instance = (object) [];
@@ -533,13 +561,28 @@ class BindingContainerTest extends \PHPUnit\Framework\TestCase
->bindValue('Espo\\SomeClass $test', $instance);
$class = $this->createClassMock('Espo\\Context');
$param = $this->createParamMock('test', 'Espo\\SomeClass');
$binding = $this->createContainer()->get($class, $param);
$this->assertEquals(Binding::VALUE, $binding->getType());
$this->assertEquals($instance, $binding->getValue());
}
public function testGetContextInterfaceValue2()
{
$instance = (object) [];
$this->binder
->for('Espo\\Context')
->bindValue(NamedClassKey::create('Espo\\SomeClass', 'test'), $instance);
$class = $this->createClassMock('Espo\\Context');
$param = $this->createParamMock('test', 'Espo\\SomeClass');
$binding = $this->createContainer()->get($class, $param);
$this->assertEquals(Binding::VALUE, $binding->getType());
$this->assertEquals($instance, $binding->getValue());
}