. * * 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 Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ namespace tests\unit\Espo\Core\Binding; 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 Espo\Core\Binding\Key\QualifiedClassKey; use PHPUnit\Framework\TestCase; use ReflectionClass; use ReflectionParameter; use ReflectionNamedType; use tests\unit\testClasses\Core\Binding\Class0; use tests\unit\testClasses\Core\Binding\Class1; use tests\unit\testClasses\Core\Binding\SomeClass1Alt; use tests\unit\testClasses\Core\Binding\SomeClass2Alt; use tests\unit\testClasses\Core\Binding\SomeInterface1; use tests\unit\testClasses\Core\Binding\SomeInterface2; use tests\unit\testClasses\Core\Binding\SomeClass1; use tests\unit\testClasses\Core\Binding\SomeClass2; class BindingContainerTest extends TestCase { private ?Binder $binder = null; private ?BindingLoader $loader = null; protected function setUp(): void { $this->loader = $this->createMock(BindingLoader::class); $data = new BindingData(); $this->binder = new Binder($data); $this->loader ->expects($this->any()) ->method('load') ->willReturn($data); } private function createClassMock(string $className): ReflectionClass { $class = $this->createMock(ReflectionClass::class); $class ->expects($this->any()) ->method('getName') ->willReturn($className); return $class; } private function createParamMock(string $name, ?string $className = null, ?string $qualifier = null): ReflectionParameter { $param = $this->createMock(ReflectionParameter::class); $class = null; $type = $this->createMock(ReflectionNamedType::class); if ($className) { $class = $this->createClassMock($className); $type ->expects($this->any()) ->method('isBuiltin') ->willReturn(false); $type ->expects($this->any()) ->method('getName') ->willReturn($className); } $type ->expects($this->any()) ->method('isBuiltin') ->willReturn(true); $param ->expects($this->any()) ->method('getType') ->willReturn($type); $param ->expects($this->any()) ->method('getName') ->willReturn($name); $param ->expects($this->any()) ->method('getClass') ->willReturn($class); if ($qualifier) { $attribute = $this->createMock(ReflectionClass::class); $param ->method('getAttributes') ->willReturn([$attribute]); $attribute->method('newInstance') ->willReturn((object) ['qualifier' => $qualifier]); } return $param; } protected function createContainer(): BindingContainer { return new BindingContainer($this->loader->load()); } public function testHasTrue(): void { $this->binder->bindService('Espo\\Test', 'test'); $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test', 'Espo\\Test'); $this->assertTrue( $this->createContainer()->hasByParam($class, $param) ); } public function testHasNoContextTrue(): void { $this->binder->bindService('Espo\\Test', 'test'); $param = $this->createParamMock('test', 'Espo\\Test'); $this->assertTrue( $this->createContainer()->hasByParam(null, $param) ); } public function testHasFalse(): void { $this->binder->bindService('Espo\\Test', 'test'); $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test', 'Espo\\Hello'); $this->assertFalse( $this->createContainer()->hasByParam($class, $param) ); } public function testHasNoContextFalse(): void { $this->binder->bindService('Espo\\Test', 'test'); $param = $this->createParamMock('test', 'Espo\\Hello'); $this->assertFalse( $this->createContainer()->hasByParam(null, $param) ); } public function testHasContextTrue0(): void { $this->binder ->inContext('Espo\\Context', function (ContextualBinder $binder): void { $binder->bindService('Espo\\Test', 'test'); }); $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test', 'Espo\\Test'); $this->assertTrue( $this->createContainer()->hasByParam($class, $param) ); } public function testHasContextTrue1(): void { $this->binder ->for('Espo\\Context') ->bindService('Espo\\Test', 'test'); $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test', 'Espo\\Test'); $this->assertTrue( $this->createContainer()->hasByParam($class, $param) ); } public function testHasContextTrue2(): void { $this->binder ->for('Espo\\Context') ->bindValue('$test', 'Test Value'); $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test'); $this->assertTrue( $this->createContainer()->hasByParam($class, $param) ); } public function testHasContextTrue3(): void { $this->binder ->for('Espo\\Context') ->bindValue(NamedKey::create('test'), 'Test Value'); $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test'); $this->assertTrue( $this->createContainer()->hasByParam($class, $param) ); } public function testHasContextTrue4(): void { $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()->hasByParam($class, $param) ); } public function testHasContextFalse1(): void { $this->binder ->for('Espo\\Context') ->bindService('Espo\\Test', 'test'); $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test', 'Espo\\Hello'); $this->assertFalse( $this->createContainer()->hasByParam($class, $param) ); } public function testHasContextFalse2(): void { $this->binder ->for('Espo\\Context') ->bindService('Espo\\Test', 'test'); $class = $this->createClassMock('Espo\\ContextOther'); $param = $this->createParamMock('test', 'Espo\\Test'); $this->assertFalse( $this->createContainer()->hasByParam($class, $param) ); } public function testHasContextFalse3(): void { $this->binder ->for('Espo\\Context') ->bindValue('$test', 'Test Value'); $class = $this->createClassMock('Espo\\Other'); $param = $this->createParamMock('test'); $this->assertFalse( $this->createContainer()->hasByParam($class, $param) ); } public function testGetClassNameImplementation(): void { $this->binder->bindImplementation('Espo\\Test', 'Espo\\ImplTest'); $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test', 'Espo\\Test'); $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::IMPLEMENTATION_CLASS_NAME, $binding->getType()); $this->assertEquals('Espo\\ImplTest', $binding->getValue()); } public function testGetClassNameFactory(): void { $this->binder->bindFactory('Espo\\Test', 'Espo\\TestFactory'); $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test', 'Espo\\Test'); $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::FACTORY_CLASS_NAME, $binding->getType()); $this->assertEquals('Espo\\TestFactory', $binding->getValue()); } public function testGetService(): void { $this->binder->bindService('Espo\\Test', 'test'); $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test', 'Espo\\Test'); $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::CONTAINER_SERVICE, $binding->getType()); $this->assertEquals('test', $binding->getValue()); } public function testGetCallback(): void { $this->binder->bindCallback( 'Espo\\Test', function () { return 'test'; } ); $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test', 'Espo\\Test'); $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::CALLBACK, $binding->getType()); $this->assertIsCallable($binding->getValue()); } public function testBindInstance(): void { $className = 'Espo\\Core\\Application'; $instance = $this->createMock($className); $this->binder->bindInstance($className, $instance); $param = $this->createParamMock('test', $className); $binding = $this->createContainer()->getByParam(null, $param); $this->assertEquals(Binding::VALUE, $binding->getType()); $this->assertSame($instance, $binding->getValue()); } public function testContextBindInstance(): void { $className = 'Espo\\Core\\Application'; $instance = $this->createMock($className); $this->binder ->for('Espo\\Context') ->bindInstance($className, $instance); $param = $this->createParamMock('test', $className); $class = $this->createClassMock('Espo\\Context'); $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::VALUE, $binding->getType()); $this->assertSame($instance, $binding->getValue()); } public function testContextGetCallback(): void { $this->binder ->for('Espo\\Context') ->bindCallback( 'Espo\\Test', function () { return 'test'; } ); $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test', 'Espo\\Test'); $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::CALLBACK, $binding->getType()); $this->assertIsCallable($binding->getValue()); } public function testRebindGlobal(): void { $this->binder->bindService('Espo\\Test', 'test'); $this->binder->bindService('Espo\\Test', 'testHello'); $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test', 'Espo\\Test'); $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::CONTAINER_SERVICE, $binding->getType()); $this->assertEquals('testHello', $binding->getValue()); } public function testBindInterfaceWithParamNameGlobal(): void { $this->binder->bindService('Espo\\Test $name', 'testName'); $this->binder->bindService('Espo\\Test', 'test'); $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('name', 'Espo\\Test'); $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::CONTAINER_SERVICE, $binding->getType()); $this->assertEquals('testName', $binding->getValue()); $param = $this->createParamMock('hello', 'Espo\\Test'); $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::CONTAINER_SERVICE, $binding->getType()); $this->assertEquals('test', $binding->getValue()); } public function testContextGetClassNameImplementation(): void { $this->binder ->for('Espo\\Context') ->bindImplementation('Espo\\Test', 'Espo\\ImplTest'); $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test', 'Espo\\Test'); $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::IMPLEMENTATION_CLASS_NAME, $binding->getType()); $this->assertEquals('Espo\\ImplTest', $binding->getValue()); } public function testContextGetClassNameFactory(): void { $this->binder ->for('Espo\\Context') ->bindFactory('Espo\\Test', 'Espo\\TestFactory'); $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test', 'Espo\\Test'); $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::FACTORY_CLASS_NAME, $binding->getType()); $this->assertEquals('Espo\\TestFactory', $binding->getValue()); } public function testNoContextClassName(): void { $this->binder ->for('Espo\\Context') ->bindImplementation('Espo\\Test', 'Espo\\ImplTest'); $class = $this->createClassMock('Espo\\AnotherContext'); $param = $this->createParamMock('test', 'Espo\\Test'); $this->assertFalse( $this->createContainer()->hasByParam($class, $param) ); } public function testBindContextInterfaceWithParamNameGlobal(): void { $this->binder ->for('Espo\\Context') ->bindService('Espo\\Test $name', 'testName'); $this->binder->bindService('Espo\\Test', 'test'); $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('name', 'Espo\\Test'); $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::CONTAINER_SERVICE, $binding->getType()); $this->assertEquals('testName', $binding->getValue()); $param = $this->createParamMock('hello', 'Espo\\Test'); $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::CONTAINER_SERVICE, $binding->getType()); $this->assertEquals('test', $binding->getValue()); } public function testGetContextParamValue(): void { $this->binder ->for('Espo\\Context') ->bindValue('$test', 'Test Value'); $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test'); $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::VALUE, $binding->getType()); $this->assertEquals('Test Value', $binding->getValue()); } public function testGetContextInterfaceValue1(): void { $instance = (object) []; $this->binder ->for('Espo\\Context') ->bindValue('Espo\\SomeClass $test', $instance); $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test', 'Espo\\SomeClass'); $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::VALUE, $binding->getType()); $this->assertEquals($instance, $binding->getValue()); } public function testGetContextInterfaceValue2(): void { $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()->getByParam($class, $param); $this->assertEquals(Binding::VALUE, $binding->getType()); $this->assertEquals($instance, $binding->getValue()); } public function testGetContextService(): void { $this->binder ->for('Espo\\Context') ->bindService('Espo\\Test', 'test'); $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test', 'Espo\\Test'); $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::CONTAINER_SERVICE, $binding->getType()); $this->assertEquals('test', $binding->getValue()); } public function testRebindContextService(): void { $this->binder ->for('Espo\\Context') ->bindService('Espo\\Test', 'test'); $this->binder ->for('Espo\\Context') ->bindService('Espo\\Test', 'testHello'); $class = $this->createClassMock('Espo\\Context'); $param = $this->createParamMock('test', 'Espo\\Test'); $binding = $this->createContainer()->getByParam($class, $param); $this->assertEquals(Binding::CONTAINER_SERVICE, $binding->getType()); $this->assertEquals('testHello', $binding->getValue()); } public function testBindingContainerBuilder1(): void { $container = BindingContainerBuilder::create() ->bindImplementation(SomeInterface1::class, SomeClass1::class) ->inContext(SomeClass1::class, function (ContextualBinder $binder): void { $binder->bindImplementation(SomeInterface2::class, SomeClass2::class); }) ->build(); $param1 = $this->createParamMock('test', SomeInterface1::class); $this->assertTrue($container->hasByParam(null, $param1)); $param2 = $this->createParamMock('test', SomeInterface2::class); $this->assertFalse($container->hasByParam(null, $param2)); $class3 = $this->createClassMock(SomeClass1::class); $param3 = $this->createParamMock('test', SomeInterface2::class); $this->assertTrue($container->hasByParam($class3, $param3)); } public function testTypedParamWithScalarBound1(): void { $container = BindingContainerBuilder::create() ->inContext(Class0::class, function (ContextualBinder $binder): void { $binder->bindValue('$dep', 'test'); }) ->build(); $class = new ReflectionClass(Class0::class); $constructor = $class->getConstructor(); $params = $constructor->getParameters(); $this->assertFalse($container->hasByParam($class, $params[0])); } public function testTypedParamWithScalarBound2(): void { $container = BindingContainerBuilder::create() ->inContext(Class0::class, function (ContextualBinder $binder): void { $binder->bindValue(NamedKey::create('dep'), new Class1()); }) ->build(); $class = new ReflectionClass(Class0::class); $constructor = $class->getConstructor(); $params = $constructor->getParameters(); $binding = $container->getByParam($class, $params[0]); $this->assertEquals(Binding::VALUE, $binding->getType()); $this->assertInstanceOf(Class1::class, $binding->getValue()); } public function testBindingQualified(): void { $container = BindingContainerBuilder::create() ->bindImplementation(SomeInterface1::class, SomeClass1::class) ->bindImplementation(QualifiedClassKey::create(SomeInterface1::class, 'q1'), SomeClass1Alt::class) ->bindImplementation(QualifiedClassKey::create(SomeInterface2::class, 'q3'), SomeClass2Alt::class) ->inContext(SomeClass1::class, function (ContextualBinder $binder): void { $binder->bindImplementation(SomeInterface2::class, SomeClass2::class); $binder->bindImplementation(QualifiedClassKey::create(SomeInterface2::class, 'q2'), SomeClass2Alt::class); }) ->build(); $param1 = $this->createParamMock('test', SomeInterface1::class, 'q1'); $this->assertTrue($container->hasByParam(null, $param1)); $binding = $container->getByParam(null, $param1); $this->assertEquals(SomeClass1Alt::class, $binding->getValue()); // $param2 = $this->createParamMock('test', SomeInterface1::class); $this->assertTrue($container->hasByParam(null, $param2)); $binding = $container->getByParam(null, $param2); $this->assertEquals(SomeClass1::class, $binding->getValue()); // $contextClass = $this->createClassMock(SomeClass1::class); // $param3 = $this->createParamMock('test', SomeInterface2::class, 'q2'); $this->assertTrue($container->hasByParam($contextClass, $param3)); $binding = $container->getByParam($contextClass, $param3); $this->assertEquals(SomeClass2Alt::class, $binding->getValue()); // $param4 = $this->createParamMock('test', SomeInterface2::class); $this->assertTrue($container->hasByParam($contextClass, $param4)); $binding = $container->getByParam($contextClass, $param4); $this->assertEquals(SomeClass2::class, $binding->getValue()); // $param3 = $this->createParamMock('test', SomeInterface2::class, 'q3'); $this->assertTrue($container->hasByParam($contextClass, $param3)); $binding = $container->getByParam($contextClass, $param3); $this->assertEquals(SomeClass2Alt::class, $binding->getValue()); } }