Skip to content

Commit

Permalink
Merge pull request #224 from auraphp/improve_test_reflector_target_ge…
Browse files Browse the repository at this point in the history
…tter

improve reflector test, use specific method for attribute specification
  • Loading branch information
frederikbosch authored Aug 21, 2024
2 parents 78b180a + 6efb9e6 commit 00dc6d6
Show file tree
Hide file tree
Showing 15 changed files with 393 additions and 70 deletions.
12 changes: 7 additions & 5 deletions docs/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ will cause a `RealRoute` to be appended in the routes array.
use Aura\Di\Attribute\AttributeConfigFor;
use Aura\Di\ClassScanner\AttributeConfigInterface;
use Aura\Di\ClassScanner\AttributeSpecification;
use Aura\Di\ClassScanner\ClassSpecification;
use Aura\Di\Container;

#[\Attribute]
Expand All @@ -213,7 +214,7 @@ class Route implements AttributeConfigInterface {
public function __construct(private string $method, private string $uri) {
}

public static function define(Container $di, AttributeSpecification $specification): void
public static function define(Container $di, AttributeSpecification $specification, ClassSpecification $classSpecification): void
{
if ($specification->getAttributeTarget() === \Attribute::TARGET_METHOD) {
/** @var self $attribute */
Expand All @@ -226,7 +227,7 @@ class Route implements AttributeConfigInterface {
$container->lazyLazy(
$di->lazyCallable([
$di->lazyNew($specification->getClassName()),
$specification->getTargetConfig()['method']
$specification->getTargetMethod()
])
)
);
Expand Down Expand Up @@ -259,21 +260,22 @@ you can create an implementation of `AttributeConfigInterface` yourself, and ann
use Aura\Di\Attribute\AttributeConfigFor;
use Aura\Di\ClassScanner\AttributeConfigInterface;
use Aura\Di\ClassScanner\AttributeSpecification;
use Aura\Di\ClassScanner\ClassSpecification;
use Aura\Di\Container;
use Symfony\Component\Routing\Attribute\Route;

#[AttributeConfigFor(Route::class)]
class SymfonyRouteAttributeConfig implements AttributeConfigInterface
{
public static function define(Container $di, AttributeSpecification $specification): void
public static function define(Container $di, AttributeSpecification $specification, ClassSpecification $classSpecification): void
{
if ($specification->getAttributeTarget() === \Attribute::TARGET_METHOD) {
if ($specification->isMethodAttribute()) {
/** @var Route $attribute */
$attribute = $specification->getAttributeInstance();

$invokableRoute = $di->lazyCallable([
$container->lazyNew($annotatedClassName),
$specification->getTargetConfig()['method']
$specification->getTargetMethod()
]);

// these are not real parameters, but just examples
Expand Down
10 changes: 5 additions & 5 deletions src/ClassScanner/AnnotatedInjectAttributeConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@

final class AnnotatedInjectAttributeConfig implements AttributeConfigInterface
{
public static function define(Container $di, AttributeSpecification $specification): void
public static function define(Container $di, AttributeSpecification $attribute, ClassSpecification $class): void
{
/** @var AnnotatedInjectInterface $attribute */
$attribute = $specification->getAttributeInstance();
if ($specification->getAttributeTarget() === \Attribute::TARGET_PARAMETER) {
$di->params[$specification->getClassName()][$specification->getTargetConfig()['parameter']] = $attribute->inject();
if ($attribute->isConstructorParameterAttribute()) {
/** @var AnnotatedInjectInterface $annotatedInject */
$annotatedInject = $attribute->getAttributeInstance();
$di->params[$attribute->getClassName()][$attribute->getTargetParameter()] = $annotatedInject->inject();
}
}
}
2 changes: 1 addition & 1 deletion src/ClassScanner/AttributeConfigInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@

interface AttributeConfigInterface
{
public static function define(Container $di, AttributeSpecification $specification): void;
public static function define(Container $di, AttributeSpecification $attribute, ClassSpecification $class): void;
}
56 changes: 54 additions & 2 deletions src/ClassScanner/AttributeSpecification.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@

final class AttributeSpecification
{
private const CONSTRUCTOR_NAME = '__construct';
private object $attributeInstance;
private string $className;
private int $attributeTarget;
/**
* @param array{method?: string, parameter?: string, property?: string, constant?: string} $targetConfig
*/
private array $targetConfig;

/**
* @param array{method?: string, parameter?: string, property?: string, constant?: string} $targetConfig
*/
public function __construct(
object $attributeInstance,
string $className,
Expand Down Expand Up @@ -39,8 +46,53 @@ public function getAttributeTarget(): int
return $this->attributeTarget;
}

public function getTargetConfig(): array
public function getTargetMethod(): ?string
{
return $this->targetConfig;
return $this->targetConfig['method'] ?? null;
}

public function getTargetParameter(): ?string
{
return $this->targetConfig['parameter'] ?? null;
}

public function getTargetProperty(): ?string
{
return $this->targetConfig['property'] ?? null;
}

public function getTargetConstant(): ?string
{
return $this->targetConfig['constant'] ?? null;
}

public function isConstructorParameterAttribute(): bool
{
return $this->attributeTarget === \Attribute::TARGET_PARAMETER && ($this->targetConfig['method'] ?? '') === self::CONSTRUCTOR_NAME;
}

public function isMethodAttribute(): bool
{
return $this->attributeTarget === \Attribute::TARGET_METHOD;
}

public function isClassAttribute(): bool
{
return $this->attributeTarget === \Attribute::TARGET_CLASS;
}

public function isParameterAttribute(): bool
{
return $this->attributeTarget === \Attribute::TARGET_PARAMETER;
}

public function isPropertyAttribute(): bool
{
return $this->attributeTarget === \Attribute::TARGET_PROPERTY;
}

public function isClassConstantAttribute(): bool
{
return $this->attributeTarget === \Attribute::TARGET_CLASS_CONSTANT;
}
}
4 changes: 2 additions & 2 deletions src/ClassScanner/BlueprintAttributeConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@

final class BlueprintAttributeConfig implements AttributeConfigInterface
{
public static function define(Container $di, AttributeSpecification $specification): void
public static function define(Container $di, AttributeSpecification $attribute, ClassSpecification $class): void
{
$di->params[$specification->getClassName()] = $di->params[$specification->getClassName()] ?? [];
$di->params[$attribute->getClassName()] = $di->params[$attribute->getClassName()] ?? [];
}
}
60 changes: 34 additions & 26 deletions src/ClassScanner/ClassMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ final class ClassMap
private array $scanPaths;
/** @var array<string, class-string> */
private array $filesToClass = [];
/** @var array<class-string, array<int, AttributeSpecification>> */
/** @var array<class-string, ClassSpecification> */
private array $classesToAttributes = [];

/**
Expand All @@ -23,13 +23,10 @@ public function __construct(array $scanPaths, string $basePath)
$this->basePath = $basePath;
}

/**
* @param array<int, AttributeSpecification> $attributeSpecifications
*/
public function addClass(string $class, string $filename, array $attributeSpecifications): void
public function addClass(ClassSpecification $classSpecification): void
{
$this->filesToClass[$filename] = $class;
$this->classesToAttributes[$class] = $attributeSpecifications;
$this->filesToClass[$classSpecification->getFilename()] = $classSpecification->getClassName();
$this->classesToAttributes[$classSpecification->getClassName()] = $classSpecification;
}

public function remove(string $filename): void
Expand All @@ -54,20 +51,27 @@ public function getFiles(): array
return \array_keys($this->filesToClass);
}

/**
* @return array<int, AttributeSpecification>
*/
public function getAttributeSpecificationsFor(string $className): array
public function getClassSpecificationFor(string $className): ?ClassSpecification
{
return $this->classesToAttributes[$className] ?? [];
if (\array_key_exists($className, $this->classesToAttributes)) {
return $this->classesToAttributes[$className];
}

throw new \InvalidArgumentException('Cannot find class specification for: ' . $className);
}

/**
* @return array<int, AttributeSpecification>
*/
public function getAttributeSpecifications(): array
{
return \array_merge([], ...array_values($this->classesToAttributes));
return \array_merge(
[],
...\array_values(array_map(
fn (ClassSpecification $classSpecification) => $classSpecification->getAttributes(),
$this->classesToAttributes
))
);
}

public function isAttributeClassFile(string $filename): bool
Expand All @@ -81,14 +85,8 @@ public function isAttributeClassFile(string $filename): bool
return false;
}

$attributeSpecifications = $this->classesToAttributes[$class];
foreach ($attributeSpecifications as $specification) {
if ($specification->getAttributeInstance() instanceof \Attribute) {
return true;
}
}

return false;
return $this->classesToAttributes[$class]->isAttributeClass();
}

public function merge(ClassMap $other): ClassMap
Expand All @@ -110,7 +108,7 @@ public function saveToFileHandle($fileHandle): void
foreach ($this->filesToClass as $filename => $className) {
$classMapJson['files'][$filename] = $className;

if ($attributeSpecifications = $this->getAttributeSpecificationsFor($className)) {
if ($attributeSpecifications = $this->getClassSpecificationFor($className)->getAttributes()) {
$classMapJson['attributes'][$className] = \array_map(
fn (AttributeSpecification $attribute) => \serialize($attribute),
$attributeSpecifications
Expand All @@ -136,18 +134,28 @@ public static function fromFile(string $filename): self
*/
public static function fromFileHandle($fileHandle): ClassMap
{
\fseek($fileHandle, 0);
$cacheContents = \stream_get_contents($fileHandle);
if ($cacheContents === '') {
throw new \InvalidArgumentException('Cannot read empty file handle');
}

$cacheContentsJson = \json_decode($cacheContents, true, 512, \JSON_THROW_ON_ERROR);
if (!$cacheContentsJson) {
throw new \InvalidArgumentException('Cannot parse json from file handle');
}

$classMap = new ClassMap($cacheContentsJson['scanPaths'], $cacheContentsJson['basePath']);

foreach ($cacheContentsJson['files'] as $filename => $className) {
$classMap->addClass(
$className,
$filename,
\array_map(
fn (string $serializedAttributeSpecification) => \unserialize($serializedAttributeSpecification),
$cacheContentsJson['attributes'][$className] ?? []
new ClassSpecification(
$className,
$filename,
\array_map(
fn (string $serializedAttributeSpecification) => \unserialize($serializedAttributeSpecification),
$cacheContentsJson['attributes'][$className] ?? []
)
)
);
}
Expand Down
5 changes: 4 additions & 1 deletion src/ClassScanner/ClassScannerConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,15 @@ public function define(Container $di): void
}
}

foreach ($classMap->getAttributeSpecificationsFor($className) as $specification) {
$classSpecification = $classMap->getClassSpecificationFor($className);
foreach ($classSpecification->getAttributes() as $specification) {
$attribute = $specification->getAttributeInstance();
if (\array_key_exists($attribute::class, $attributeConfigs)) {
$configuredBy = $attributeConfigs[$attribute::class];
$configuredBy::define(
$di,
$specification,
$classSpecification
);
}

Expand All @@ -81,6 +83,7 @@ public function define(Container $di): void
$configuredBy::define(
$di,
$specification,
$classSpecification
);
}
}
Expand Down
84 changes: 84 additions & 0 deletions src/ClassScanner/ClassSpecification.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

declare(strict_types=1);

namespace Aura\Di\ClassScanner;

final class ClassSpecification
{
private string $className;
private string $filename;
/**
* @param array<int, AttributeSpecification> $attributes
*/
private array $attributes;

/**
* @param array<int, AttributeSpecification> $attributes
*/
public function __construct(
string $className,
string $filename,
array $attributes = [],
)
{
$this->className = $className;
$this->filename = $filename;
$this->attributes = $attributes;
}

public function getClassName(): string
{
return $this->className;
}

public function getFilename(): string
{
return $this->filename;
}

/**
* @return array<int, AttributeSpecification>
*/
public function getAttributes(): array
{
return $this->attributes;
}

public function isAttributeClass(): bool
{
foreach ($this->attributes as $specification) {
if ($specification->getAttributeInstance() instanceof \Attribute) {
return true;
}
}

return false;
}

/**
* @return array<int, AttributeSpecification>
*/
public function getClassAttributes(): array
{
return \array_filter(
$this->attributes,
function (AttributeSpecification $specification) {
return $specification->getAttributeTarget() === \Attribute::TARGET_CLASS;
}
);
}

/**
* @return array<int, AttributeSpecification>
*/
public function getParameterAttributesForMethod(string $methodName): array
{
return \array_filter(
$this->attributes,
function (AttributeSpecification $specification) use ($methodName) {
return $specification->getTargetMethod() === $methodName;
}
);
}
}
Loading

0 comments on commit 00dc6d6

Please sign in to comment.