Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce definition factory #395

Draft
wants to merge 3 commits into
base: release-3.x
Choose a base branch
from
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
1 change: 1 addition & 0 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
with:
php-version: 8.2
extensions: xdebug
ini-file: development
tools: composer:2
- name: Setup problem matchers for PHP
run: echo "::add-matcher::${{ runner.tool_cache }}/php.json"
Expand Down
2 changes: 1 addition & 1 deletion docs/how-to/01-add-third-party-services.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class ThirdPartyServicesProvider implements DefinitionProvider {
public function consume(DefinitionProviderContext $context) : void {
$context->addServiceDefinition(service($loggerType = objectType(LoggerInterface::class)));
$context->addServiceDelegateDefinition(
serviceDelegate($loggerType, objectType(MonologLoggerFactory::class), 'createLogger')
serviceDelegate(objectType(MonologLoggerFactory::class), 'createLogger')
);
$context->addServicePrepareDefinition(
servicePrepare(
Expand Down
1 change: 0 additions & 1 deletion docs/references/02-functional-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ This document lists the functions for each purpose.
) : \Cspray\AnnotatedContainer\Definition\AliasDefinition;

\Cspray\AnnotatedContainer\serviceDelegate(
\Cspray\Typiphy\ObjectType $service,
\Cspray\Typiphy\ObjectType $factoryClass,
string $factoryMethod
) : \Cspray\AnnotatedContainer\Definition\ServiceDelegateDefinition;
Expand Down
2 changes: 1 addition & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
</testsuites>
<coverage>
<report>
<text outputFile="php://stdout" showOnlySummary="true"/>
<!--
<text outputFile="php://stdout" showOnlySummary="true"/>
<html outputDirectory="build/code-coverage/html" />
-->
</report>
Expand Down
339 changes: 339 additions & 0 deletions src/Definition/DefinitionFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
<?php declare(strict_types=1);

namespace Cspray\AnnotatedContainer\Definition;

use Cspray\AnnotatedContainer\Attribute\InjectAttribute;
use Cspray\AnnotatedContainer\Attribute\ServiceAttribute;
use Cspray\AnnotatedContainer\Attribute\ServiceDelegateAttribute;
use Cspray\AnnotatedContainer\Attribute\ServicePrepareAttribute;
use Cspray\AnnotatedContainer\Exception\InjectAttributeRequired;
use Cspray\AnnotatedContainer\Exception\ServiceAttributeRequired;
use Cspray\AnnotatedContainer\Exception\ServiceDelegateAttributeRequired;
use Cspray\AnnotatedContainer\Exception\ServiceDelegateReturnsIntersectionType;
use Cspray\AnnotatedContainer\Exception\ServiceDelegateReturnsScalarType;
use Cspray\AnnotatedContainer\Exception\ServiceDelegateReturnsUnionType;
use Cspray\AnnotatedContainer\Exception\ServiceDelegateReturnsUnknownType;
use Cspray\AnnotatedContainer\Exception\ServicePrepareAttributeRequired;
use Cspray\AnnotatedContainer\Exception\WrongTargetForInjectAttribute;
use Cspray\AnnotatedContainer\Exception\WrongTargetForServiceAttribute;
use Cspray\AnnotatedContainer\Exception\WrongTargetForServiceDelegateAttribute;
use Cspray\AnnotatedContainer\Exception\WrongTargetForServicePrepareAttribute;
use Cspray\AnnotatedTarget\AnnotatedTarget;
use Cspray\Typiphy\ObjectType;
use Cspray\Typiphy\Type;
use Cspray\Typiphy\TypeIntersect;
use Cspray\Typiphy\TypeUnion;
use ReflectionClass;
use ReflectionIntersectionType;
use ReflectionMethod;
use ReflectionNamedType;
use ReflectionParameter;
use ReflectionUnionType;
use function Cspray\Typiphy\arrayType;
use function Cspray\Typiphy\boolType;
use function Cspray\Typiphy\floatType;
use function Cspray\Typiphy\intType;
use function Cspray\Typiphy\objectType;
use function Cspray\Typiphy\mixedType;
use function Cspray\Typiphy\stringType;

final class DefinitionFactory {

public function serviceDefinitionFromAnnotatedTarget(AnnotatedTarget $annotatedTarget) : ServiceDefinition {
$attribute = $annotatedTarget->attributeInstance();
if (!$attribute instanceof ServiceAttribute) {
throw ServiceAttributeRequired::fromNotServiceAttributeProvidedInAnnotatedTarget(
$attribute::class
);
}

$reflection = $annotatedTarget->targetReflection();
if (!$reflection instanceof ReflectionClass) {
throw WrongTargetForServiceAttribute::fromServiceAttributeNotTargetingClass($reflection);
}

return $this->serviceDefinitionFromReflectionClassAndAttribute($reflection, $attribute);
}

public function serviceDefinitionFromObjectTypeAndAttribute(
ObjectType $objectType,
ServiceAttribute $serviceAttribute
) : ServiceDefinition {
$reflection = new ReflectionClass($objectType->name());

return $this->serviceDefinitionFromReflectionClassAndAttribute($reflection, $serviceAttribute);
}

private function serviceDefinitionFromReflectionClassAndAttribute(
ReflectionClass $reflection,
ServiceAttribute $attribute,
) : ServiceDefinition {
$objectType = objectType($reflection->getName());
$isAbstract = $reflection->isInterface() || $reflection->isAbstract();
return new class($objectType, $attribute, $isAbstract) implements ServiceDefinition {

public function __construct(
private readonly ObjectType $type,
private readonly ServiceAttribute $attribute,
private readonly bool $isAbstract,
) {
}

public function type() : ObjectType {
return $this->type;
}

public function name() : ?string {
return $this->attribute->name();
}

public function profiles() : array {
$profiles = $this->attribute->profiles();
if ($profiles === []) {
$profiles = ['default'];
}

return $profiles;
}

public function isPrimary() : bool {
return $this->attribute->isPrimary();
}

public function isConcrete() : bool {
return $this->isAbstract === false;
}

public function isAbstract() : bool {
return $this->isAbstract;
}

public function attribute() : ?ServiceAttribute {
return $this->attribute;
}
};
}

public function servicePrepareDefinitionFromAnnotatedTarget(AnnotatedTarget $annotatedTarget) : ServicePrepareDefinition {
$attribute = $annotatedTarget->attributeInstance();
if (!$attribute instanceof ServicePrepareAttribute) {
throw ServicePrepareAttributeRequired::fromNotServicePrepareAttributeInAnnotatedTarget($attribute::class);
}

$reflection = $annotatedTarget->targetReflection();
if (!$reflection instanceof ReflectionMethod) {
throw WrongTargetForServicePrepareAttribute::fromServicePrepareAttributeNotTargetingMethod($reflection);
}

return $this->servicePrepareDefinitionFromReflectionMethodAndAttribute($reflection, $attribute);
}

public function servicePrepareDefinitionFromClassMethodAndAttribute(
ObjectType $objectType,
string $method,
ServicePrepareAttribute $attribute,
) : ServicePrepareDefinition {
return $this->servicePrepareDefinitionFromReflectionMethodAndAttribute(
new ReflectionMethod($objectType->name(), $method),
$attribute
);
}

private function servicePrepareDefinitionFromReflectionMethodAndAttribute(
ReflectionMethod $reflection,
ServicePrepareAttribute $attribute
) : ServicePrepareDefinition {
return new class(objectType($reflection->getDeclaringClass()->getName()), $reflection->getName(), $attribute) implements ServicePrepareDefinition {
public function __construct(
private readonly ObjectType $service,
private readonly string $method,
private readonly ServicePrepareAttribute $attribute,
) {
}

public function service() : ObjectType {
return $this->service;
}

public function methodName() : string {

Check failure on line 158 in src/Definition/DefinitionFactory.php

View workflow job for this annotation

GitHub Actions / static-analysis

MoreSpecificReturnType

src/Definition/DefinitionFactory.php:158:44: MoreSpecificReturnType: The declared return type 'non-empty-string' for Cspray\AnnotatedContainer\Definition\_home_runner_work_annotated_container_annotated_container_src_Definition_DefinitionFactory_php_146_5819::methodName is more specific than the inferred return type 'string' (see https://psalm.dev/070)
return $this->method;

Check failure on line 159 in src/Definition/DefinitionFactory.php

View workflow job for this annotation

GitHub Actions / static-analysis

LessSpecificReturnStatement

src/Definition/DefinitionFactory.php:159:24: LessSpecificReturnStatement: The type 'string' is more general than the declared return type 'non-empty-string' for Cspray\AnnotatedContainer\Definition\_home_runner_work_annotated_container_annotated_container_src_Definition_DefinitionFactory_php_146_5819::methodName (see https://psalm.dev/129)
}

public function attribute() : ?ServicePrepareAttribute {
return $this->attribute;
}
};
}

public function serviceDelegateDefinitionFromAnnotatedTarget(AnnotatedTarget $target) : ServiceDelegateDefinition {
$attribute = $target->attributeInstance();
if (!$attribute instanceof ServiceDelegateAttribute) {
throw ServiceDelegateAttributeRequired::fromNotServiceDelegateAttributeInAnnotatedTarget($attribute::class);
}

$reflection = $target->targetReflection();
if (!$reflection instanceof ReflectionMethod) {
throw WrongTargetForServiceDelegateAttribute::fromServiceDelegateAttributeNotTargetingMethod($reflection);
}

return $this->serviceDelegateDefinitionFromReflectionMethodAndAttribute($reflection, $attribute);
}

public function serviceDelegateDefinitionFromClassMethodAndAttribute(
ObjectType $delegateType,
string $delegateMethod,
ServiceDelegateAttribute $attribute,
) : ServiceDelegateDefinition {
$reflection = new ReflectionMethod($delegateType->name(), $delegateMethod);

return $this->serviceDelegateDefinitionFromReflectionMethodAndAttribute($reflection, $attribute);
}

private function serviceDelegateDefinitionFromReflectionMethodAndAttribute(
ReflectionMethod $reflection,
ServiceDelegateAttribute $attribute
) : ServiceDelegateDefinition {
$delegateType = objectType($reflection->getDeclaringClass()->getName());
$delegateMethod = $reflection->getName();
$returnType = $reflection->getReturnType();

if ($returnType === null) {
throw ServiceDelegateReturnsUnknownType::fromServiceDelegateHasNoReturnType($delegateType);
}

if ($returnType instanceof ReflectionIntersectionType) {
throw ServiceDelegateReturnsIntersectionType::fromServiceDelegateCreatesIntersectionType($delegateType);
}

if ($returnType instanceof ReflectionUnionType) {
throw ServiceDelegateReturnsUnionType::fromServiceDelegateReturnsUnionType($delegateType);
}

$returnTypeName = $returnType->getName();

Check failure on line 212 in src/Definition/DefinitionFactory.php

View workflow job for this annotation

GitHub Actions / static-analysis

UndefinedMethod

src/Definition/DefinitionFactory.php:212:40: UndefinedMethod: Method ReflectionType::getName does not exist (see https://psalm.dev/022)
if ($returnTypeName === 'self') {
$returnTypeName = $delegateType->name();
}

if (!interface_exists($returnTypeName) && !class_exists($returnTypeName)) {

Check failure on line 217 in src/Definition/DefinitionFactory.php

View workflow job for this annotation

GitHub Actions / static-analysis

MixedArgument

src/Definition/DefinitionFactory.php:217:31: MixedArgument: Argument 1 of interface_exists cannot be class-string|mixed, expecting string (see https://psalm.dev/030)

Check failure on line 217 in src/Definition/DefinitionFactory.php

View workflow job for this annotation

GitHub Actions / static-analysis

MixedArgument

src/Definition/DefinitionFactory.php:217:65: MixedArgument: Argument 1 of class_exists cannot be class-string|mixed|string, expecting string (see https://psalm.dev/030)
throw ServiceDelegateReturnsScalarType::fromServiceDelegateCreatesScalarType($delegateType);
}

$serviceType = objectType($returnTypeName);

return new class(
$delegateType,
$delegateMethod,
$serviceType,
$attribute
) implements ServiceDelegateDefinition {

public function __construct(
private readonly ObjectType $delegateType,
private readonly string $delegateMethod,
private readonly ObjectType $serviceType,
private readonly ServiceDelegateAttribute $attribute,
) {
}

public function delegateType() : ObjectType {
return $this->delegateType;
}

public function delegateMethod() : string {

Check failure on line 242 in src/Definition/DefinitionFactory.php

View workflow job for this annotation

GitHub Actions / static-analysis

MoreSpecificReturnType

src/Definition/DefinitionFactory.php:242:48: MoreSpecificReturnType: The declared return type 'non-empty-string' for Cspray\AnnotatedContainer\Definition\_home_runner_work_annotated_container_annotated_container_src_Definition_DefinitionFactory_php_223_9047::delegateMethod is more specific than the inferred return type 'string' (see https://psalm.dev/070)
return $this->delegateMethod;

Check failure on line 243 in src/Definition/DefinitionFactory.php

View workflow job for this annotation

GitHub Actions / static-analysis

LessSpecificReturnStatement

src/Definition/DefinitionFactory.php:243:24: LessSpecificReturnStatement: The type 'string' is more general than the declared return type 'non-empty-string' for Cspray\AnnotatedContainer\Definition\_home_runner_work_annotated_container_annotated_container_src_Definition_DefinitionFactory_php_223_9047::delegateMethod (see https://psalm.dev/129)
}

public function serviceType() : ObjectType {
return $this->serviceType;
}

public function attribute() : ?ServiceDelegateAttribute {
return $this->attribute;
}
};
}

public function injectDefinitionFromAnnotatedTarget(AnnotatedTarget $target) : InjectDefinition {
$attribute = $target->attributeInstance();
if (!$attribute instanceof InjectAttribute) {
throw InjectAttributeRequired::fromNotInjectAttributeProvidedInAnnotatedTarget($attribute::class);
}

$reflection = $target->targetReflection();
if (!$reflection instanceof ReflectionParameter) {
throw WrongTargetForInjectAttribute::fromInjectAttributeNotTargetMethodParameter($reflection);
}

$class = objectType($reflection->getDeclaringClass()->getName());

Check failure on line 267 in src/Definition/DefinitionFactory.php

View workflow job for this annotation

GitHub Actions / static-analysis

PossiblyNullReference

src/Definition/DefinitionFactory.php:267:63: PossiblyNullReference: Cannot call method getName on possibly null value (see https://psalm.dev/083)
$method = $reflection->getDeclaringFunction()->getName();
$type = $this->typiphyTypeFromReflectionNamedType($reflection->getType());

Check failure on line 269 in src/Definition/DefinitionFactory.php

View workflow job for this annotation

GitHub Actions / static-analysis

ArgumentTypeCoercion

src/Definition/DefinitionFactory.php:269:59: ArgumentTypeCoercion: Argument 1 of Cspray\AnnotatedContainer\Definition\DefinitionFactory::typiphyTypeFromReflectionNamedType expects ReflectionNamedType|null, but parent type ReflectionType|null provided (see https://psalm.dev/193)
$parameter = $reflection->getName();
$value = $attribute->value();

Check failure on line 271 in src/Definition/DefinitionFactory.php

View workflow job for this annotation

GitHub Actions / static-analysis

MixedAssignment

src/Definition/DefinitionFactory.php:271:9: MixedAssignment: Unable to determine the type that $value is being assigned to (see https://psalm.dev/032)
$store = $attribute->from();
$profiles = $attribute->profiles();

return new class(
$class,
$method,
$type,
$parameter,
$value,
$attribute
) implements InjectDefinition {

public function __construct(
private readonly ObjectType $class,
private readonly string $method,
private readonly Type $type,
private readonly string $parameter,
private readonly mixed $value,
private readonly InjectAttribute $attribute,
) {}

Check failure on line 291 in src/Definition/DefinitionFactory.php

View workflow job for this annotation

GitHub Actions / code-linting

Closing brace must be on a line by itself

public function class() : ObjectType {
return $this->class;
}

public function methodName() : string {
return $this->method;
}

public function type() : Type|TypeUnion|TypeIntersect {
return $this->type;
}

public function parameterName() : string {
return $this->parameter;
}

public function value() : mixed {
return $this->value;
}

public function profiles() : array {
return ['default'];
}

public function storeName() : ?string {
return null;
}

public function attribute() : ?InjectAttribute {
return $this->attribute;
}
};
}

private function typiphyTypeFromReflectionNamedType(?ReflectionNamedType $type) : Type {
$name = $type?->getName();
return match ($name) {
'array' => arrayType(),
'bool' => boolType(),
'int' => intType(),
'float' => floatType(),
'mixed', null => mixedType(),
'string' => stringType(),
default => objectType($name)
};
}
}
Loading
Loading