diff --git a/application/Espo/Binding.php b/application/Espo/Binding.php index 6a0895c8ad..ddb492b574 100644 --- a/application/Espo/Binding.php +++ b/application/Espo/Binding.php @@ -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' ); diff --git a/application/Espo/Core/Binding/Binder.php b/application/Espo/Core/Binding/Binder.php index 586b9fcf49..d0ddced3f2 100644 --- a/application/Espo/Core/Binding/Binder.php +++ b/application/Espo/Core/Binding/Binder.php @@ -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 $implementationClassName An implementation class name. + * @template T of object + * @param class-string|NamedClassKey $key An interface or interface with a parameter name. + * @param class-string $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|NamedClassKey $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|NamedClassKey $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|NamedClassKey $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 $factoryClassName A factory class name. + * @template T of object + * @param class-string|NamedClassKey $key An interface or interface with a parameter name. + * @param class-string> $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 $key + */ + private static function keyToString(string|NamedClassKey $key): string + { + return is_string($key) ? $key : $key->toString(); + } + private function validateBindingKey(string $key): void { if (!$key) { diff --git a/application/Espo/Core/Binding/Binding.php b/application/Espo/Core/Binding/Binding.php index 9427395a87..9360e02540 100644 --- a/application/Espo/Core/Binding/Binding.php +++ b/application/Espo/Core/Binding/Binding.php @@ -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 $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> $factoryClassName + */ public static function createFromFactoryClassName(string $factoryClassName): self { if (!$factoryClassName) { diff --git a/application/Espo/Core/Binding/BindingContainerBuilder.php b/application/Espo/Core/Binding/BindingContainerBuilder.php index cba1d0c4e7..006bc03756 100644 --- a/application/Espo/Core/Binding/BindingContainerBuilder.php +++ b/application/Espo/Core/Binding/BindingContainerBuilder.php @@ -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 $implementationClassName An implementation class name. + * @template T of object + * @param class-string|NamedClassKey $key An interface or interface with a parameter name. + * @param class-string $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|NamedClassKey $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|NamedClassKey $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|NamedClassKey $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 $factoryClassName A factory class name. + * @template T of object + * @param class-string|NamedClassKey $key An interface or interface with a parameter name. + * @param class-string> $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); diff --git a/application/Espo/Core/Binding/ContextualBinder.php b/application/Espo/Core/Binding/ContextualBinder.php index fc81162388..18e6814082 100644 --- a/application/Espo/Core/Binding/ContextualBinder.php +++ b/application/Espo/Core/Binding/ContextualBinder.php @@ -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 $implementationClassName An implementation class name. + * @template T of object + * @param class-string|NamedClassKey $key An interface or interface with a parameter name. + * @param class-string $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|NamedClassKey $key An interface or interface with a parameter name. + * @param class-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->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 $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|NamedClassKey $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|NamedClassKey|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 $factoryClassName A factory class name. + * @template T of object + * @param class-string|NamedClassKey $key An interface or interface with a parameter name. + * @param class-string> $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 $key + */ + private static function keyToString(string|NamedKey|NamedClassKey $key): string + { + return is_string($key) ? $key : $key->toString(); + } } diff --git a/application/Espo/Core/Binding/Factory.php b/application/Espo/Core/Binding/Factory.php index 6104cc3488..2f029816bc 100644 --- a/application/Espo/Core/Binding/Factory.php +++ b/application/Espo/Core/Binding/Factory.php @@ -29,7 +29,13 @@ namespace Espo\Core\Binding; +/** + * @template T of object + */ interface Factory { + /** + * @return T + */ public function create(): object; } diff --git a/application/Espo/Core/Binding/Key/NamedClassKey.php b/application/Espo/Core/Binding/Key/NamedClassKey.php new file mode 100644 index 0000000000..6bc70c0e19 --- /dev/null +++ b/application/Espo/Core/Binding/Key/NamedClassKey.php @@ -0,0 +1,62 @@ + $className + */ + private function __construct(private string $className, private string $parameterName) + {} + + /** + * Create. + * + * @template TC of object + * @param class-string $className An interface. + * @param string $parameterName A constructor parameter name (w/o '$'). + * @return self + */ + public static function create(string $className, string $parameterName): self + { + return new self($className, $parameterName); + } + + public function toString(): string + { + return $this->className . ' $' . $this->parameterName; + } +} diff --git a/application/Espo/Core/Binding/Key/NamedKey.php b/application/Espo/Core/Binding/Key/NamedKey.php new file mode 100644 index 0000000000..a85bf9d511 --- /dev/null +++ b/application/Espo/Core/Binding/Key/NamedKey.php @@ -0,0 +1,54 @@ +parameterName; + } +} diff --git a/application/Espo/Core/InjectableFactory.php b/application/Espo/Core/InjectableFactory.php index b2bb583789..7fc2c2b461 100644 --- a/application/Espo/Core/InjectableFactory.php +++ b/application/Espo/Core/InjectableFactory.php @@ -297,8 +297,8 @@ class InjectableFactory } if ($type === Binding::FACTORY_CLASS_NAME) { - /** @var class-string $value */ - /** @var Factory $factory */ + /** @var class-string $value */ + /** @var Factory $factory */ $factory = $this->createInternal($value, null, $bindingContainer); return $factory->create(); diff --git a/application/Espo/Core/Mail/Account/GroupAccount/FetcherFactory.php b/application/Espo/Core/Mail/Account/GroupAccount/FetcherFactory.php index a4e26b22d8..5d1b08dad1 100644 --- a/application/Espo/Core/Mail/Account/GroupAccount/FetcherFactory.php +++ b/application/Espo/Core/Mail/Account/GroupAccount/FetcherFactory.php @@ -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 + */ class FetcherFactory implements Factory { private InjectableFactory $injectableFactory; diff --git a/application/Espo/Core/Mail/Account/PersonalAccount/FetcherFactory.php b/application/Espo/Core/Mail/Account/PersonalAccount/FetcherFactory.php index 3866e845d0..2a6355a81c 100644 --- a/application/Espo/Core/Mail/Account/PersonalAccount/FetcherFactory.php +++ b/application/Espo/Core/Mail/Account/PersonalAccount/FetcherFactory.php @@ -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 + */ class FetcherFactory implements Factory { private InjectableFactory $injectableFactory; diff --git a/application/Espo/Core/Sms/SenderFactory.php b/application/Espo/Core/Sms/SenderFactory.php index bccc80d337..c8847e2bcb 100644 --- a/application/Espo/Core/Sms/SenderFactory.php +++ b/application/Espo/Core/Sms/SenderFactory.php @@ -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 + */ class SenderFactory implements Factory { private Config $config; - private Metadata $metadata; - private InjectableFactory $injectableFactory; public function __construct( diff --git a/application/Espo/Core/WebSocket/SenderFactory.php b/application/Espo/Core/WebSocket/SenderFactory.php index 9956ef6aca..56d2054fe8 100644 --- a/application/Espo/Core/WebSocket/SenderFactory.php +++ b/application/Espo/Core/WebSocket/SenderFactory.php @@ -36,12 +36,13 @@ use Espo\Core\Binding\Factory; use RuntimeException; +/** + * @implements Factory + */ class SenderFactory implements Factory { private $injectableFactory; - private $config; - private $metadata; private const DEFAULT_MESSAGER = 'ZeroMQ'; diff --git a/application/Espo/Core/WebSocket/SubscriberFactory.php b/application/Espo/Core/WebSocket/SubscriberFactory.php index 9bdfc8a2e9..66b1ee4cd0 100644 --- a/application/Espo/Core/WebSocket/SubscriberFactory.php +++ b/application/Espo/Core/WebSocket/SubscriberFactory.php @@ -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 + */ 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 { diff --git a/tests/unit/Espo/Core/Binding/BindingContainerTest.php b/tests/unit/Espo/Core/Binding/BindingContainerTest.php index f9f3c70638..6f876aac25 100644 --- a/tests/unit/Espo/Core/Binding/BindingContainerTest.php +++ b/tests/unit/Espo/Core/Binding/BindingContainerTest.php @@ -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()); }