Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/",
Expand Down
129 changes: 129 additions & 0 deletions src/Container/Definition/Helper/ContextualDefinitionHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php declare(strict_types=1);

namespace Tribe\Libs\Container\Definition\Helper;

use DI\Definition\AutowireDefinition;
use DI\Definition\Definition;
use DI\Definition\Exception\InvalidDefinition;
use DI\Definition\Helper\AutowireDefinitionHelper;
use DI\Definition\ObjectDefinition;
use DI\Definition\ObjectDefinition\MethodInjection;
use ReflectionClass;
use ReflectionException;
use ReflectionNamedType;

/**
* Helps define how to create a concrete instance based on a provided
* interface.
*/
class ContextualDefinitionHelper extends AutowireDefinitionHelper {

public const DEFINITION_CLASS = AutowireDefinition::class;

/**
* The class name.
*
* @var string|null
*/
protected $class;

/**
* The interface => concrete relationship.
*
* @var array<string, string|callable>
*/
protected $contextual = [];

/**
* 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 concrete instances.
*
* @param string $entryName
*
* @throws \DI\Definition\Exception\InvalidDefinition
*
* @return \DI\Definition\Definition
*/
public function getDefinition( string $entryName ): Definition {
$definition = parent::getDefinition( $entryName );

if ( ! empty( $this->contextual ) ) {
$parameters = $this->replaceParameters( $definition, $this->contextual );
$constructorInjection = MethodInjection::constructor( $parameters );

$definition->setConstructorInjection( $constructorInjection );

// We now perform all the constructor injection, make sure the parent doesn't.
unset( $this->constructor );
}

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 ) {
$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();

// Skip parameters that haven't been defined
if ( ! isset( $parameters[ $interface ] ) ) {
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 );
}

}
20 changes: 20 additions & 0 deletions src/Container/functions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php declare(strict_types=1);

namespace Tribe\Libs\Container;

use Tribe\Libs\Container\Definition\Helper\ContextualDefinitionHelper;

/**
* 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() );
*
* @param string|null $className
*
* @return \Tribe\Libs\Container\Definition\Helper\ContextualDefinitionHelper
*/
function autowire( ?string $className = null ): ContextualDefinitionHelper {
return new ContextualDefinitionHelper( $className );
}
213 changes: 213 additions & 0 deletions tests/integration/Tribe/Libs/Container/ContextualDefinitionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
<?php declare( strict_types=1 );

namespace Tribe\Libs\Container;

use Codeception\TestCase\WPTestCase;
use DI\ContainerBuilder;
use InvalidArgumentException;
use Psr\Container\ContainerInterface;
use Tribe\Libs\Container;
use Tribe\Libs\Queues\Contracts\Task;
use Tribe\Libs\Support\Contextual\MultipleDependencyClass;
use Tribe\Libs\Support\Contextual\Strategy\Color;
use Tribe\Libs\Support\Contextual\Strategy\ColorManager;
use Tribe\Libs\Support\Contextual\Strategy\Colors\Blue;
use Tribe\Libs\Support\Contextual\Strategy\Colors\Green;
use Tribe\Libs\Support\Contextual\Strategy\Colors\Red;
use Tribe\Libs\Support\Contextual\Strategy\Managers\BlueColorManager;
use Tribe\Libs\Support\Contextual\Strategy\Managers\GreenColorManager;
use Tribe\Libs\Support\Contextual\Strategy\Managers\RedColorManager;
use Tribe\Libs\Support\SampleTask;

final class ContextualDefinitionTest extends WPTestCase {

/**
* @var ContainerBuilder
*/
private $builder;

protected function setUp(): void {
parent::setUp();

$this->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() );
}

}
Loading