Skip to content
Merged
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased Changes

## [1.0.2](https://github.com/cspray/annotated-container/tree/v1.0.2) - 2022-07-06

### Fixed

- Fixed an oversight where Service properties were not marked as readonly
- Fixed a bug where caching a ContainerDefinition was not including the InjectDefinitions causing any Container to be created from the cached results to be invalid if an Inject Attribute is used.

## [1.0.1](https://github.com/cspray/annotated-container/tree/v1.0.1) - 2022-07-05

### Changed
Expand Down
6 changes: 3 additions & 3 deletions src/Attribute/Service.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ final class Service {
* FQCN.
*/
public function __construct(
public array $profiles = [],
public bool $primary = false,
public ?string $name = null
public readonly array $profiles = [],
public readonly bool $primary = false,
public readonly ?string $name = null
) {}

}
93 changes: 91 additions & 2 deletions src/JsonContainerDefinitionSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,23 @@

namespace Cspray\AnnotatedContainer;

use JsonSerializable;
use Cspray\Typiphy\Internal\NamedType;
use Cspray\Typiphy\Type;
use Cspray\Typiphy\TypeIntersect;
use Cspray\Typiphy\TypeUnion;
use function Cspray\Typiphy\arrayType;
use function Cspray\Typiphy\boolType;
use function Cspray\Typiphy\callableType;
use function Cspray\Typiphy\floatType;
use function Cspray\Typiphy\intType;
use function Cspray\Typiphy\iterableType;
use function Cspray\Typiphy\mixedType;
use function Cspray\Typiphy\nullType;
use function Cspray\Typiphy\objectType;
use function Cspray\Typiphy\stringType;
use function Cspray\Typiphy\typeIntersect;
use function Cspray\Typiphy\typeUnion;
use function Cspray\Typiphy\voidType;

/**
* A ContainerDefinitionSerializer that will format a ContainerDefinition into a JSON string.
Expand Down Expand Up @@ -67,12 +82,26 @@ public function serialize(ContainerDefinition $containerDefinition) : string {
];
}

$injectDefinitions = [];
foreach ($containerDefinition->getInjectDefinitions() as $injectDefinition) {
$injectDefinitions[] = [
'injectTargetType' => $injectDefinition->getTargetIdentifier()->getClass()->getName(),
'injectTargetMethod' => $injectDefinition->getTargetIdentifier()->getMethodName(),
'injectTargetName' => $injectDefinition->getTargetIdentifier()->getName(),
'type' => $injectDefinition->getType()->getName(),
'value' => $injectDefinition->getValue(),
'profiles' => $injectDefinition->getProfiles(),
'storeName' => $injectDefinition->getStoreName()
];
}

return json_encode([
'compiledServiceDefinitions' => $compiledServiceDefinitions,
'sharedServiceDefinitions' => $serviceDefinitions,
'aliasDefinitions' => $aliasDefinitions,
'servicePrepareDefinitions' => $servicePrepareDefinitions,
'serviceDelegateDefinitions' => $serviceDelegateDefinitions
'serviceDelegateDefinitions' => $serviceDelegateDefinitions,
'injectDefinitions' => $injectDefinitions
]);
}

Expand Down Expand Up @@ -124,6 +153,34 @@ public function deserialize(string $serializedDefinition) : ContainerDefinition
);
}

foreach ($data['injectDefinitions'] as $injectDefinition) {
$injectBuilder = InjectDefinitionBuilder::forService(objectType($injectDefinition['injectTargetType']));

$type = $this->convertStringToType($injectDefinition['type']);

if (is_null($injectDefinition['injectTargetMethod'])) {
$injectBuilder = $injectBuilder->withProperty(
$type,
$injectDefinition['injectTargetName']
);
} else {
$injectBuilder = $injectBuilder->withMethod(
$injectDefinition['injectTargetMethod'],
$type,
$injectDefinition['injectTargetName']
);
}

$injectBuilder = $injectBuilder->withValue($injectDefinition['value'])
->withProfiles(...$injectDefinition['profiles']);

if (!is_null($injectDefinition['storeName'])) {
$injectBuilder = $injectBuilder->withStore($injectDefinition['storeName']);
}

$containerDefinitionBuilder = $containerDefinitionBuilder->withInjectDefinition($injectBuilder->build());
}

return $containerDefinitionBuilder->build();
}

Expand All @@ -150,5 +207,37 @@ private function getDeserializeServiceDefinition(array $compiledServiceDefinitio
return $serviceDefinitionCacheMap[$serviceHash];
}

private function convertStringToType(string $rawType) : Type|TypeUnion|TypeIntersect {
if (str_contains($rawType, '|')) {
$types = [];
foreach (explode('|', $rawType) as $unionType) {
$types[] = $this->convertStringToType($unionType);
}
$type = typeUnion(...$types);
} else if (str_contains($rawType, '&')) {
$types = [];
foreach (explode('&', $rawType) as $intersectType) {
$types[] = $this->convertStringToType($intersectType);
}
$type = typeIntersect(...$types);
} else {
$type = match($rawType) {
'string' => stringType(),
'int' => intType(),
'float' => floatType(),
'bool' => boolType(),
'array' => arrayType(),
'mixed' => mixedType(),
'iterable' => iterableType(),
'null' => nullType(),
'void' => voidType(),
'callable' => callableType(),
default => objectType($rawType)
};
}

return $type;
}


}
47 changes: 47 additions & 0 deletions test/ContainerFactoryTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
use Cspray\AnnotatedContainerFixture;
use Cspray\AnnotatedContainer\Exception\ContainerException;
use Cspray\AnnotatedContainer\Exception\InvalidParameterException;
use Cspray\AnnotatedContainerFixture\Fixture;
use Cspray\AnnotatedContainerFixture\Fixtures;
use Cspray\AnnotatedTarget\PhpParserAnnotatedTargetParser;
use Cspray\Typiphy\Internal\NamedType;
use Cspray\Typiphy\ObjectType;
use Cspray\Typiphy\Type;
use Cspray\Typiphy\TypeIntersect;
Expand Down Expand Up @@ -418,4 +420,49 @@ public function testNamedServiceProfileNotActiveNotShared() : void {
$this->assertFalse($container->has('test-foo'));
}

public function deserializeContainerProvider() : array {
return [
[Fixtures::injectCustomStoreServices(), function(ContainerFactory $containerFactory, ContainerDefinition $deserialize) {
$store = $this->getMockBuilder(ParameterStore::class)->getMock();
$store->expects($this->once())
->method('getName')
->willReturn('test-store');

$store->expects($this->once())
->method('fetch')
->with($this->isInstanceOf(NamedType::class), 'key')
->willReturn('the store key value');
$containerFactory->addParameterStore($store);

$container = $containerFactory->createContainer($deserialize);
$service = $container->get(Fixtures::injectCustomStoreServices()->scalarInjector()->getName());

$this->assertSame('the store key value', $service->key);
}],
[Fixtures::injectConstructorServices(), function(ContainerFactory $containerFactory, ContainerDefinition $deserialize) {
$container = $containerFactory->createContainer($deserialize);

$service = $container->get(Fixtures::injectConstructorServices()->injectTypeUnionService()->getName());

$this->assertSame(4.20, $service->value);
}]
];
}

/**
* @dataProvider deserializeContainerProvider
*/
public function testDeserializingContainerWithInjectAllowsServiceCreation(Fixture $fixture, callable $assertions) {
$serializer = new JsonContainerDefinitionSerializer();
$containerDefinition = $this->getContainerDefinitionCompiler()->compile(
ContainerDefinitionCompileOptionsBuilder::scanDirectories($fixture->getPath())->build()
);

$serialized = $serializer->serialize($containerDefinition);
$deserialize = $serializer->deserialize($serialized);

$containerFactory = $this->getContainerFactory();

$assertions($containerFactory, $deserialize);
}
}
1 change: 1 addition & 0 deletions test/ContainerFactoryTests/AurynContainerFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
* @covers \Cspray\AnnotatedContainer\ConfigurationDefinitionBuilder
* @covers \Cspray\AnnotatedContainer\Internal\PropertyInjectTargetIdentifier
* @covers \Cspray\AnnotatedContainer\Attribute\Configuration
* @covers \Cspray\AnnotatedContainer\JsonContainerDefinitionSerializer
* @covers ::\Cspray\AnnotatedContainer\containerFactory
* @covers ::\Cspray\AnnotatedContainer\autowiredParams
* @covers ::\Cspray\AnnotatedContainer\rawParam
Expand Down
107 changes: 107 additions & 0 deletions test/JsonContainerDefinitionSerializerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@

use Cspray\AnnotatedContainerFixture\Fixtures;
use Cspray\AnnotatedTarget\PhpParserAnnotatedTargetParser;
use Cspray\Typiphy\TypeIntersect;
use Cspray\Typiphy\TypeUnion;
use PHPUnit\Framework\TestCase;
use function Cspray\Typiphy\floatType;
use function Cspray\Typiphy\intType;
use function Cspray\Typiphy\stringType;

class JsonContainerDefinitionSerializerTest extends TestCase {

Expand Down Expand Up @@ -171,6 +176,44 @@ public function testSerializeNamedServicesHasName() {
], $json['compiledServiceDefinitions']);
}

public function testSerializeInjectDefinitionMethod() {
$serializer = new JsonContainerDefinitionSerializer();
$containerDefinition = $this->containerDefinitionCompiler->compile(
ContainerDefinitionCompileOptionsBuilder::scanDirectories(Fixtures::injectConstructorServices()->getPath())->build()
);

$json = json_decode($serializer->serialize($containerDefinition), true);
$this->assertArrayHasKey('injectDefinitions', $json);
$this->assertContains([
'injectTargetType' => Fixtures::injectConstructorServices()->injectStringService()->getName(),
'injectTargetMethod' => '__construct',
'injectTargetName' => 'val',
'type' => 'string',
'value' => 'foobar',
'profiles' => ['default'],
'storeName' => null
], $json['injectDefinitions']);
}

public function testSerializeInjectDefinitionProperty() {
$serializer = new JsonContainerDefinitionSerializer();
$containerDefinition = $this->containerDefinitionCompiler->compile(
ContainerDefinitionCompileOptionsBuilder::scanDirectories(Fixtures::configurationServices()->getPath())->build()
);

$json = json_decode($serializer->serialize($containerDefinition), true);
$this->assertArrayHasKey('injectDefinitions', $json);
$this->assertContains([
'injectTargetType' => Fixtures::configurationServices()->myConfig()->getName(),
'injectTargetMethod' => null,
'injectTargetName' => 'key',
'type' => 'string',
'value' => 'my-api-key',
'profiles' => ['default'],
'storeName' => null
], $json['injectDefinitions']);
}

/** ======================================== Deserialization Testing ==============================================*/

public function serializeDeserializeSerializeDirs() : array {
Expand All @@ -181,6 +224,7 @@ public function serializeDeserializeSerializeDirs() : array {
[Fixtures::profileResolvedServices()->getPath()],
[Fixtures::abstractClassAliasedService()->getPath()],
[Fixtures::namedServices()->getPath()],
[Fixtures::injectConstructorServices()->getPath()]
];
}

Expand All @@ -205,6 +249,69 @@ public function testSerializingDeserializedContainerDefinitionIsCompatible(strin
);
}

public function testDeserializeInjectWithCorrectTypeUnion() {
$serializer = new JsonContainerDefinitionSerializer();
$containerDefinition = $this->containerDefinitionCompiler->compile(
ContainerDefinitionCompileOptionsBuilder::scanDirectories(Fixtures::injectConstructorServices()->getPath())->build()
);

$serialized = $serializer->serialize($containerDefinition);
$subjectDefinition = $serializer->deserialize($serialized);

$injectDefinitions = $subjectDefinition->getInjectDefinitions();

/** @var InjectDefinition[] $typeUnionInjects */
$typeUnionInjects = array_values(
array_filter(
$injectDefinitions,
fn(InjectDefinition $injectDefinition) =>
$injectDefinition->getTargetIdentifier()->getClass() === Fixtures::injectConstructorServices()->injectTypeUnionService()
)
);

$this->assertCount(1, $typeUnionInjects);
$this->assertInstanceOf(TypeUnion::class, $typeUnionInjects[0]->getType());

/** @var TypeUnion $type */
$type = $typeUnionInjects[0]->getType();

$this->assertSame([
stringType(),
intType(),
floatType()
], $type->getTypes());
}

public function testDeserializeInjectWithCorrectTypeIntersect() {
$serializer = new JsonContainerDefinitionSerializer();
$containerDefinition = $this->containerDefinitionCompiler->compile(
ContainerDefinitionCompileOptionsBuilder::scanDirectories(Fixtures::injectIntersectCustomStoreServices()->getPath())->build()
);

$serialized = $serializer->serialize($containerDefinition);
$subjectDefinition = $serializer->deserialize($serialized);

$injectDefinitions = $subjectDefinition->getInjectDefinitions();

/** @var InjectDefinition[] $typeIntersectInjects */
$typeIntersectInjects = array_values(
array_filter(
$injectDefinitions,
fn(InjectDefinition $injectDefinition) =>
$injectDefinition->getTargetIdentifier()->getClass() === Fixtures::injectIntersectCustomStoreServices()->intersectInjector()
)
);

$this->assertCount(1, $typeIntersectInjects);
$this->assertInstanceOf(TypeIntersect::class, $typeIntersectInjects[0]->getType());

$type = $typeIntersectInjects[0]->getType();

$this->assertSame([
Fixtures::injectIntersectCustomStoreServices()->fooInterface(),
Fixtures::injectIntersectCustomStoreServices()->barInterface()
], $type->getTypes());
}

}

Expand Down