From 7526dc3a38dc8bd38ae6ccd24511cb48955ea4e5 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Thu, 10 Mar 2022 20:41:17 -0700 Subject: [PATCH 1/3] First pass at PHP-DI contextual binding --- composer.json | 3 +- .../Definition/ContextualDefinition.php | 31 +++++ .../Helper/ContextualDefinitionHelper.php | 121 ++++++++++++++++++ src/Container/functions.php | 19 +++ 4 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/Container/Definition/ContextualDefinition.php create mode 100644 src/Container/Definition/Helper/ContextualDefinitionHelper.php create mode 100644 src/Container/functions.php diff --git a/composer.json b/composer.json index 0b7eb69d..10eeaa39 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,8 @@ "autoload": { "files": [ "src/Assets/version.php", - "src/Cache/functions.php" + "src/Cache/functions.php", + "src/Container/functions.php" ], "psr-4": { "Tribe\\Libs\\ACF\\": "src/ACF/", diff --git a/src/Container/Definition/ContextualDefinition.php b/src/Container/Definition/ContextualDefinition.php new file mode 100644 index 00000000..cb8b62aa --- /dev/null +++ b/src/Container/Definition/ContextualDefinition.php @@ -0,0 +1,31 @@ + concrete relationship. + * + * @var array + */ + protected $contextual; + + /** + * Set the definition binding. + * + * @param array $contextual + * + * @return void + */ + public function setContextualBinding( array $contextual ) { + $this->contextual = $contextual; + } + +} diff --git a/src/Container/Definition/Helper/ContextualDefinitionHelper.php b/src/Container/Definition/Helper/ContextualDefinitionHelper.php new file mode 100644 index 00000000..f4de65f5 --- /dev/null +++ b/src/Container/Definition/Helper/ContextualDefinitionHelper.php @@ -0,0 +1,121 @@ + concrete relationship. + * + * @var array + */ + protected $contextual = []; + + /** + * @param string|null $className If null, will automatically use the FQCN in the container. + */ + public function __construct( ?string $className = null ) { + $this->className = $className; + } + + /** + * Map a concrete class/instance to an interface/abstract or even another concrete class. + * + * @param string $interface The fully qualified class name or interface. + * @param string|\Closure|callable $concrete The concrete fully qualified class name or closure that returns an instance. + * + * @return $this + */ + public function contextualParameter( string $interface, $concrete ): self { + $this->contextual[ $interface ] = $concrete; + + return $this; + } + + /** + * Create the definition with the replaced concretes. + * + * @param string $entryName + * + * @throws \DI\Definition\Exception\InvalidDefinition + * + * @return \DI\Definition\Definition + */ + public function getDefinition( string $entryName ): Definition { + $definition = new ContextualDefinition( $entryName, $this->className ); + $definition->setContextualBinding( $this->contextual ); + + $parameters = $this->replaceParameters( $definition, $this->contextual ); + $constructorInjection = MethodInjection::constructor( $parameters ); + + $definition->setConstructorInjection( $constructorInjection ); + + return $definition; + } + + /** + * Replace interfaces/abstract constructor parameters with their concrete implementation. + * + * @throws \DI\Definition\Exception\InvalidDefinition + */ + protected function replaceParameters( ObjectDefinition $definition, array $parameters ): array { + $replaced = []; + + try { + $constructorParameters = ( new ReflectionClass( $definition->getClassName() ) ) + ->getConstructor() + ->getParameters(); + } catch ( ReflectionException $e ) { + throw InvalidDefinition::create( + $definition, + sprintf( 'Could not get constructor ReflectionParameters from %s', $definition->getClassName() ) + ); + } + + // Map constructor parameters to their numbered index. The order must match what's in the constructor. + foreach ( $constructorParameters as $index => $parameter ) { + $interface = $parameter->getClass()->getName(); + + // Keep existing parameters if they haven't been specifically defined. + if ( ! isset( $parameters[ $interface ] ) ) { + $replaced[ $index ] = $parameter; + continue; + } + + // Create the definition based on the type provided, possibly replacing the interface with its concrete. + $replaced[ $index ] = $this->createDefinition( $interface, $parameters[ $interface ] ); + } + + return $replaced; + } + + /** + * @param string $interface + * @param string|callable $concrete + * + * @return callable|\DI\Definition\AutowireDefinition + */ + protected function createDefinition( string $interface, $concrete ) { + return is_callable( $concrete ) ? $concrete : new AutowireDefinition( $interface, $concrete ); + } + +} diff --git a/src/Container/functions.php b/src/Container/functions.php new file mode 100644 index 00000000..8030f30a --- /dev/null +++ b/src/Container/functions.php @@ -0,0 +1,19 @@ +contextualParameter( Interface::class, Concrete::class ); + * @example Tribe\Libs\Container\autowire()->contextualParameter( Interface::class, static fn () => new Concrete() ); + * + * @param string|null $className + * + * @return \Tribe\Libs\Container\Definition\Helper\ContextualDefinitionHelper + */ +function autowire( ?string $className = null ): ContextualDefinitionHelper { + return new ContextualDefinitionHelper( $className ); +} From e94c667d345a08738f63d45723c5bb3fda7809f3 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 11 Mar 2022 16:17:39 -0700 Subject: [PATCH 2/3] Add contextual definition tests, support all autowiring functionality --- .../Definition/ContextualDefinition.php | 31 --- .../Helper/ContextualDefinitionHelper.php | 43 ++-- src/Container/functions.php | 3 +- .../Container/ContextualDefinitionTest.php | 213 ++++++++++++++++++ .../Contextual/MultipleDependencyClass.php | 47 ++++ .../Support/Contextual/Strategy/Color.php | 14 ++ .../Contextual/Strategy/ColorManager.php | 20 ++ .../Contextual/Strategy/Colors/Blue.php | 13 ++ .../Contextual/Strategy/Colors/Green.php | 13 ++ .../Contextual/Strategy/Colors/Red.php | 13 ++ .../Strategy/Managers/BlueColorManager.php | 9 + .../Strategy/Managers/GreenColorManager.php | 9 + .../Strategy/Managers/RedColorManager.php | 9 + 13 files changed, 388 insertions(+), 49 deletions(-) delete mode 100644 src/Container/Definition/ContextualDefinition.php create mode 100644 tests/integration/Tribe/Libs/Container/ContextualDefinitionTest.php create mode 100644 tests/integration/Tribe/Libs/Support/Contextual/MultipleDependencyClass.php create mode 100644 tests/integration/Tribe/Libs/Support/Contextual/Strategy/Color.php create mode 100644 tests/integration/Tribe/Libs/Support/Contextual/Strategy/ColorManager.php create mode 100644 tests/integration/Tribe/Libs/Support/Contextual/Strategy/Colors/Blue.php create mode 100644 tests/integration/Tribe/Libs/Support/Contextual/Strategy/Colors/Green.php create mode 100644 tests/integration/Tribe/Libs/Support/Contextual/Strategy/Colors/Red.php create mode 100644 tests/integration/Tribe/Libs/Support/Contextual/Strategy/Managers/BlueColorManager.php create mode 100644 tests/integration/Tribe/Libs/Support/Contextual/Strategy/Managers/GreenColorManager.php create mode 100644 tests/integration/Tribe/Libs/Support/Contextual/Strategy/Managers/RedColorManager.php diff --git a/src/Container/Definition/ContextualDefinition.php b/src/Container/Definition/ContextualDefinition.php deleted file mode 100644 index cb8b62aa..00000000 --- a/src/Container/Definition/ContextualDefinition.php +++ /dev/null @@ -1,31 +0,0 @@ - concrete relationship. - * - * @var array - */ - protected $contextual; - - /** - * Set the definition binding. - * - * @param array $contextual - * - * @return void - */ - public function setContextualBinding( array $contextual ) { - $this->contextual = $contextual; - } - -} diff --git a/src/Container/Definition/Helper/ContextualDefinitionHelper.php b/src/Container/Definition/Helper/ContextualDefinitionHelper.php index f4de65f5..7bc0b4d4 100644 --- a/src/Container/Definition/Helper/ContextualDefinitionHelper.php +++ b/src/Container/Definition/Helper/ContextualDefinitionHelper.php @@ -5,23 +5,27 @@ use DI\Definition\AutowireDefinition; use DI\Definition\Definition; use DI\Definition\Exception\InvalidDefinition; -use DI\Definition\Helper\DefinitionHelper; +use DI\Definition\Helper\AutowireDefinitionHelper; use DI\Definition\ObjectDefinition; use DI\Definition\ObjectDefinition\MethodInjection; use ReflectionClass; use ReflectionException; -use Tribe\Libs\Container\Definition\ContextualDefinition; +use ReflectionNamedType; /** * Helps define how to create a concrete instance based on a provided * interface. */ -class ContextualDefinitionHelper implements DefinitionHelper { +class ContextualDefinitionHelper extends AutowireDefinitionHelper { + + public const DEFINITION_CLASS = AutowireDefinition::class; /** + * The class name. + * * @var string|null */ - protected $className; + protected $class; /** * The interface => concrete relationship. @@ -30,13 +34,6 @@ class ContextualDefinitionHelper implements DefinitionHelper { */ protected $contextual = []; - /** - * @param string|null $className If null, will automatically use the FQCN in the container. - */ - public function __construct( ?string $className = null ) { - $this->className = $className; - } - /** * Map a concrete class/instance to an interface/abstract or even another concrete class. * @@ -52,7 +49,7 @@ public function contextualParameter( string $interface, $concrete ): self { } /** - * Create the definition with the replaced concretes. + * Create the definition with the replaced concrete instances. * * @param string $entryName * @@ -61,13 +58,17 @@ public function contextualParameter( string $interface, $concrete ): self { * @return \DI\Definition\Definition */ public function getDefinition( string $entryName ): Definition { - $definition = new ContextualDefinition( $entryName, $this->className ); - $definition->setContextualBinding( $this->contextual ); + $definition = parent::getDefinition( $entryName ); - $parameters = $this->replaceParameters( $definition, $this->contextual ); - $constructorInjection = MethodInjection::constructor( $parameters ); + if ( ! empty( $this->contextual ) ) { + $parameters = $this->replaceParameters( $definition, $this->contextual ); + $constructorInjection = MethodInjection::constructor( $parameters ); - $definition->setConstructorInjection( $constructorInjection ); + $definition->setConstructorInjection( $constructorInjection ); + + // We now perform all the constructor injection, make sure the parent doesn't. + unset( $this->constructor ); + } return $definition; } @@ -93,6 +94,14 @@ protected function replaceParameters( ObjectDefinition $definition, array $param // Map constructor parameters to their numbered index. The order must match what's in the constructor. foreach ( $constructorParameters as $index => $parameter ) { + $type = $parameter->getType(); + + // Get built in type values from the constructor parameters + if ( $type instanceof ReflectionNamedType && $type->isBuiltin() ) { + $replaced[ $index ] = $this->constructor[ $parameter->getName() ]; + continue; + } + $interface = $parameter->getClass()->getName(); // Keep existing parameters if they haven't been specifically defined. diff --git a/src/Container/functions.php b/src/Container/functions.php index 8030f30a..0071c2a7 100644 --- a/src/Container/functions.php +++ b/src/Container/functions.php @@ -5,7 +5,8 @@ use Tribe\Libs\Container\Definition\Helper\ContextualDefinitionHelper; /** - * Extend the PHP-DI container to allow for contextual binding. + * Extend the PHP-DI container to allow for contextual binding with + * our own autowire() function. * * @example Tribe\Libs\Container\autowire()->contextualParameter( Interface::class, Concrete::class ); * @example Tribe\Libs\Container\autowire()->contextualParameter( Interface::class, static fn () => new Concrete() ); diff --git a/tests/integration/Tribe/Libs/Container/ContextualDefinitionTest.php b/tests/integration/Tribe/Libs/Container/ContextualDefinitionTest.php new file mode 100644 index 00000000..37731de0 --- /dev/null +++ b/tests/integration/Tribe/Libs/Container/ContextualDefinitionTest.php @@ -0,0 +1,213 @@ +builder = new ContainerBuilder(); + } + + public function test_it_maps_concrete_instance_to_an_interface(): void { + $this->builder->addDefinitions( [ + ColorManager::class => Container\autowire() + ->contextualParameter( Color::class, Red::class ), + ] ); + + $container = $this->builder->build(); + $color = $container->get( ColorManager::class )->get_color(); + + $this->assertSame( 'red', $color ); + } + + public function test_it_maps_a_callable_to_an_interface(): void { + $this->builder->addDefinitions( [ + ColorManager::class => Container\autowire() + ->contextualParameter( Color::class, static function () { + return new Blue(); + } ), + ] ); + + $container = $this->builder->build(); + $color = $container->get( ColorManager::class )->get_color(); + + $this->assertSame( 'blue', $color ); + } + + public function test_it_maps_a_callable_to_an_interface_via_parameter_injection(): void { + $this->builder->addDefinitions( [ + ColorManager::class => Container\autowire() + ->contextualParameter( Color::class, static function ( ContainerInterface $c ) { + return $c->get( Green::class ); + } ), + ] ); + + $container = $this->builder->build(); + $color = $container->get( ColorManager::class )->get_color(); + + $this->assertSame( 'green', $color ); + } + + public function test_it_maps_a_callable_to_an_interface_via_factory(): void { + $color_state = 'blue'; + + $this->builder->addDefinitions( [ + ColorManager::class => Container\autowire() + ->contextualParameter( Color::class, static function ( ContainerInterface $c ) use ( $color_state ) { + // Mimic a simple factory + switch ( $color_state ) { + case 'red': + return $c->get( Red::class ); + case 'blue': + return $c->get( Blue::class ); + case 'green': + return $c->get( Green::class ); + default: + throw new InvalidArgumentException( 'Invalid color' ); + } + } ), + ] ); + + $container = $this->builder->build(); + $color = $container->get( ColorManager::class )->get_color(); + + $this->assertSame( $color_state, $color ); + } + + public function test_maps_multiple_strategies_at_once_using_class_constants(): void { + $this->builder->addDefinitions( [ + RedColorManager::class => Container\autowire() + ->contextualParameter( Color::class, Red::class ), + + BlueColorManager::class => Container\autowire() + ->contextualParameter( Color::class, Blue::class ), + + GreenColorManager::class => Container\autowire() + ->contextualParameter( Color::class, Green::class ), + ] ); + + $container = $this->builder->build(); + + $red = $container->get( RedColorManager::class )->get_color(); + $blue = $container->get( BlueColorManager::class )->get_color(); + $green = $container->get( GreenColorManager::class )->get_color(); + + $this->assertSame( 'red', $red ); + $this->assertSame( 'blue', $blue ); + $this->assertSame( 'green', $green ); + } + + public function test_maps_multiple_strategies_at_once_using_callables(): void { + $this->builder->addDefinitions( [ + RedColorManager::class => Container\autowire() + ->contextualParameter( Color::class, static function () { + return new Red(); + } ), + BlueColorManager::class => Container\autowire() + ->contextualParameter( Color::class, static function () { + return new Blue(); + } ), + GreenColorManager::class => Container\autowire() + ->contextualParameter( Color::class, static function () { + return new Green(); + } ), + ] ); + + $container = $this->builder->build(); + + $red = $container->get( RedColorManager::class )->get_color(); + $blue = $container->get( BlueColorManager::class )->get_color(); + $green = $container->get( GreenColorManager::class )->get_color(); + + $this->assertSame( 'red', $red ); + $this->assertSame( 'blue', $blue ); + $this->assertSame( 'green', $green ); + } + + public function test_maps_multiple_strategies_at_once_using_callables_with_container_injection(): void { + $this->builder->addDefinitions( [ + RedColorManager::class => Container\autowire() + ->contextualParameter( Color::class, static function ( ContainerInterface $c ) { + return $c->get( Red::class ); + } ), + + BlueColorManager::class => Container\autowire() + ->contextualParameter( Color::class, static function ( ContainerInterface $c ) { + return $c->get( Blue::class ); + } ), + + GreenColorManager::class => Container\autowire() + ->contextualParameter( Color::class, static function ( ContainerInterface $c ) { + return $c->get( Green::class ); + } ), + ] ); + + $container = $this->builder->build(); + + $red = $container->get( RedColorManager::class )->get_color(); + $blue = $container->get( BlueColorManager::class )->get_color(); + $green = $container->get( GreenColorManager::class )->get_color(); + + $this->assertSame( 'red', $red ); + $this->assertSame( 'blue', $blue ); + $this->assertSame( 'green', $green ); + } + + public function test_it_maps_multiple_dependencies(): void { + $this->builder->addDefinitions( [ + MultipleDependencyClass::class => Container\autowire() + ->contextualParameter( Color::class, Red::class ) + ->contextualParameter( Task::class, SampleTask::class ) + ->constructorParameter( 'test_string', 'hello' ) + ] ); + + $container = $this->builder->build(); + $instance = $container->get( MultipleDependencyClass::class ); + + $this->assertInstanceOf( SampleTask::class, $instance->get_task() ); + $this->assertInstanceOf( Red::class, $instance->get_color() ); + $this->assertSame( 'hello', $instance->get_test_string() ); + } + + public function test_it_maps_multiple_dependencies_with_method_setting(): void { + $this->builder->addDefinitions( [ + MultipleDependencyClass::class => Container\autowire() + ->contextualParameter( Color::class, Red::class ) + ->contextualParameter( Task::class, SampleTask::class ) + ->constructorParameter( 'test_string', 'hello' ) + ->method( 'set_color', new Green() ), + ] ); + + $container = $this->builder->build(); + $instance = $container->get( MultipleDependencyClass::class ); + + $this->assertInstanceOf( SampleTask::class, $instance->get_task() ); + $this->assertInstanceOf( Green::class, $instance->get_color() ); + $this->assertSame( 'hello', $instance->get_test_string() ); + } + +} diff --git a/tests/integration/Tribe/Libs/Support/Contextual/MultipleDependencyClass.php b/tests/integration/Tribe/Libs/Support/Contextual/MultipleDependencyClass.php new file mode 100644 index 00000000..e9e83950 --- /dev/null +++ b/tests/integration/Tribe/Libs/Support/Contextual/MultipleDependencyClass.php @@ -0,0 +1,47 @@ +task = $task; + $this->color = $color; + $this->test_string = $test_string; + } + + public function get_task(): Task { + return $this->task; + } + + public function get_color(): Color { + return $this->color; + } + + public function get_test_string(): string { + return $this->test_string; + } + + public function set_color( Color $color ): void { + $this->color = $color; + } + +} diff --git a/tests/integration/Tribe/Libs/Support/Contextual/Strategy/Color.php b/tests/integration/Tribe/Libs/Support/Contextual/Strategy/Color.php new file mode 100644 index 00000000..a9d2315f --- /dev/null +++ b/tests/integration/Tribe/Libs/Support/Contextual/Strategy/Color.php @@ -0,0 +1,14 @@ +color = $color; + } + + public function get_color(): string { + return $this->color->get(); + } + +} diff --git a/tests/integration/Tribe/Libs/Support/Contextual/Strategy/Colors/Blue.php b/tests/integration/Tribe/Libs/Support/Contextual/Strategy/Colors/Blue.php new file mode 100644 index 00000000..03d22605 --- /dev/null +++ b/tests/integration/Tribe/Libs/Support/Contextual/Strategy/Colors/Blue.php @@ -0,0 +1,13 @@ + Date: Tue, 15 Mar 2022 07:20:08 -0600 Subject: [PATCH 3/3] Bugfix: don't keep parameters that aren't explicitly defined --- src/Container/Definition/Helper/ContextualDefinitionHelper.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Container/Definition/Helper/ContextualDefinitionHelper.php b/src/Container/Definition/Helper/ContextualDefinitionHelper.php index 7bc0b4d4..8e2044ac 100644 --- a/src/Container/Definition/Helper/ContextualDefinitionHelper.php +++ b/src/Container/Definition/Helper/ContextualDefinitionHelper.php @@ -104,9 +104,8 @@ protected function replaceParameters( ObjectDefinition $definition, array $param $interface = $parameter->getClass()->getName(); - // Keep existing parameters if they haven't been specifically defined. + // Skip parameters that haven't been defined if ( ! isset( $parameters[ $interface ] ) ) { - $replaced[ $index ] = $parameter; continue; }