Skip to content

Commit

Permalink
Add ReflectionBasedContainer
Browse files Browse the repository at this point in the history
  • Loading branch information
codeliner committed Dec 16, 2017
1 parent 778f841 commit 1cec9ea
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 1 deletion.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"justinrainbow/json-schema": "^5.2"
},
"require-dev": {
"phpunit/phpunit": "^6.0"
"phpunit/phpunit": "^6.0",
"codeliner/array-reader": "^1.2"
},
"autoload": {
"psr-4": {
Expand Down
84 changes: 84 additions & 0 deletions src/Container/ReflectionBasedContainer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

declare(strict_types=1);

namespace Prooph\EventMachine\Container;

use Psr\Container\ContainerInterface;

final class ReflectionBasedContainer implements ContainerInterface
{
/**
* @var array
*/
private $aliasMap;

/**
* @var array
*/
private $serviceFactoryMap;

private $serviceFactory;

public function __construct($serviceFactory, array $aliasMap = [], array $serviceFactoryMap = null)
{
if(null === $serviceFactoryMap) {
$serviceFactoryMap = $this->scanServiceFactory($serviceFactory);
}

$this->serviceFactory = $serviceFactory;
$this->aliasMap = $aliasMap;
$this->serviceFactoryMap = $serviceFactoryMap;
}

/**
* @inheritdoc
*/
public function get($id)
{
$id = $this->aliasMap[$id] ?? $id;

if(!$this->has($id)) {
throw ServiceNotFound::withServiceId($id);
}

return ([$this->serviceFactory, $this->serviceFactoryMap[$id]])();
}

/**
* @inheritdoc
*/
public function has($id)
{
$id = $this->aliasMap[$id] ?? $id;

return array_key_exists($id, $this->serviceFactoryMap);
}

/**
* Cache the array and pass it to constructor again to avoid scanning of service factory
*
* @return array
*/
public function getServiceFactoryMap(): array
{
return $this->serviceFactoryMap;
}

private function scanServiceFactory($serviceFactory): array
{
$serviceFactoryMap = [];

$ref = new \ReflectionClass($serviceFactory);

foreach ($ref->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
if($returnType = $method->getReturnType()) {
if(!$returnType->isBuiltin()) {
$serviceFactoryMap[$method->getReturnType()->getName()] = $method->getName();
}
}
}

return $serviceFactoryMap;
}
}
21 changes: 21 additions & 0 deletions src/Container/ServiceRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Prooph\EventMachine\Container;

trait ServiceRegistry
{
/**
* @var array
*/
private $serviceRegistry = [];

private function makeSingleton(string $serviceId, callable $factory) {
if(!isset($this->serviceRegistry[$serviceId])) {
$this->serviceRegistry[$serviceId] = $factory();
}

return $this->serviceRegistry[$serviceId];
}
}
164 changes: 164 additions & 0 deletions tests/Container/ReflectionBasedContainerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php

declare(strict_types=1);

namespace Prooph\EventMachineTest\Container;

use Codeliner\ArrayReader\ArrayReader;
use Prooph\Common\Messaging\MessageFactory;
use Prooph\EventMachine\Container\ReflectionBasedContainer;
use Prooph\EventMachine\Container\ServiceRegistry;
use Prooph\EventMachine\JsonSchema\JsonSchema;
use Prooph\EventMachine\JsonSchema\JsonSchemaAssertion;
use Prooph\EventMachine\JsonSchema\JustinRainbowJsonSchemaAssertion;
use Prooph\EventMachine\Messaging\GenericJsonSchemaMessageFactory;
use Prooph\EventMachineTest\BasicTestCase;
use ProophExample\Aggregate\UserDescription;
use ProophExample\Messaging\Command;

final class ReflectionBasedContainerTest extends BasicTestCase
{
private $serviceFactory;

/**
* @var ReflectionBasedContainer
*/
private $reflectionBasedContainer;

protected function setUp()
{
$this->serviceFactory = $this->buildServiceFactory([
'event_machine' => [
'command_map' => [
Command::REGISTER_USER => JsonSchema::object([
UserDescription::IDENTIFIER => ['type' => 'string', 'minLength' => 2]
])
],
'event_map' => [],
]
]);

$this->reflectionBasedContainer = new ReflectionBasedContainer($this->serviceFactory, [
'CommandFactory' => MessageFactory::class
]);
}

/**
* @test
*/
public function it_scans_service_factory_to_identify_factory_methods()
{
$serviceFactoryMap = $this->reflectionBasedContainer->getServiceFactoryMap();

$this->assertEquals([
JsonSchemaAssertion::class => 'jsonSchemaAssertion',
MessageFactory::class => 'messageFactory'
], $serviceFactoryMap);
}

/**
* @test
*/
public function it_uses_service_factory_method_to_get_service()
{
$this->assertTrue($this->reflectionBasedContainer->has(MessageFactory::class));

/** @var MessageFactory $messageFactory */
$messageFactory = $this->reflectionBasedContainer->get(MessageFactory::class);

$this->assertInstanceOf(GenericJsonSchemaMessageFactory::class, $messageFactory);

//Test if config is passed correctly to message factory by checking that validation fails!
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Payload validation of RegisterUser failed: [userId] Must be at least 2 characters long');

$command = $messageFactory->createMessageFromArray(Command::REGISTER_USER, [
'payload' => [UserDescription::IDENTIFIER => '1']
]);

}

/**
* @test
*/
public function it_uses_alias_to_get_service()
{
$this->assertTrue($this->reflectionBasedContainer->has('CommandFactory'));

/** @var MessageFactory $messageFactory */
$messageFactory = $this->reflectionBasedContainer->get('CommandFactory');

$this->assertInstanceOf(GenericJsonSchemaMessageFactory::class, $messageFactory);
}

/**
* @test
*/
public function it_does_not_scan_service_factory_if_service_factory_map_is_provided()
{
$container = new ReflectionBasedContainer($this->serviceFactory, [
'CommandFactory' => MessageFactory::class
], [
JsonSchemaAssertion::class => 'jsonSchemaAssertion',
]);

//Container should not see message factory factory method because it is missing in the cached map
$this->assertFalse($container->has('CommandMap'));
$this->assertFalse($container->has(MessageFactory::class));
$this->assertTrue($container->has(JsonSchemaAssertion::class));

$assertion = $container->get(JsonSchemaAssertion::class);

$this->assertInstanceOf(JsonSchemaAssertion::class, $assertion);
}

private function buildServiceFactory(array $appConfig)
{
return new class($appConfig) {

use ServiceRegistry;

/**
* @var ArrayReader
*/
private $appConfig;

public function __construct(array $appConfig)
{
$this->appConfig = new ArrayReader($appConfig);
}

public function jsonSchemaAssertion(): JsonSchemaAssertion {
return $this->makeSingleton(JsonSchemaAssertion::class, function () {
return new JustinRainbowJsonSchemaAssertion();
});
}

public function messageFactory(): MessageFactory
{
return $this->makeSingleton(MessageFactory::class, function () {
return new GenericJsonSchemaMessageFactory(
$this->appConfig->arrayValue('event_machine.command_map', []),
$this->appConfig->arrayValue('event_machine.event_map', []),
$this->jsonSchemaAssertion()
);
});
}

protected function nonFactoryHelperMethod()
{

}

public function methodReturningBuiltInType(): string
{
return "not a service";
}

public function methodWithoutReturnType()
{

}
};
}
}

0 comments on commit 1cec9ea

Please sign in to comment.