Skip to content

Commit

Permalink
Add support for php attributes (#1932)
Browse files Browse the repository at this point in the history
* Add support for php attributes

* Fix tests for php 8.1

* Simplify the annotations

* Revert the changes to the tests

* CS

* Test FOSRest parsing of attributes

* CS

* typo

* CS

* Test fetchArticle php 8.1 attributes

* Fix namespaces

Co-authored-by: Guilhem Niot <guilhem@gniot.fr>
  • Loading branch information
akalineskou and GuilhemN committed Dec 21, 2021
1 parent 3d263a5 commit cc97b0b
Show file tree
Hide file tree
Showing 19 changed files with 624 additions and 270 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -9,3 +9,4 @@
/.phpunit.result.cache
/Tests/Functional/cache
/Tests/Functional/logs
.idea
1 change: 1 addition & 0 deletions Annotation/Areas.php
Expand Up @@ -14,6 +14,7 @@
/**
* @Annotation
*/
#[\Attribute(\Attribute::TARGET_METHOD)]
final class Areas
{
/** @var string[] */
Expand Down
20 changes: 20 additions & 0 deletions Annotation/Model.php
Expand Up @@ -13,10 +13,12 @@

use OpenApi\Annotations\AbstractAnnotation;
use OpenApi\Annotations\Parameter;
use OpenApi\Generator;

/**
* @Annotation
*/
#[\Attribute(\Attribute::TARGET_METHOD)]
final class Model extends AbstractAnnotation
{
/** {@inheritdoc} */
Expand Down Expand Up @@ -46,4 +48,22 @@ final class Model extends AbstractAnnotation
* @var mixed[]
*/
public $options;

/**
* @param mixed[] $properties
* @param string[] $groups
* @param mixed[] $options
*/
public function __construct(
array $properties = [],
string $type = Generator::UNDEFINED,
array $groups = null,
array $options = null
) {
parent::__construct($properties + [
'type' => $type,
'groups' => $groups,
'options' => $options,
]);
}
}
1 change: 1 addition & 0 deletions Annotation/Operation.php
Expand Up @@ -16,6 +16,7 @@
/**
* @Annotation
*/
#[\Attribute(\Attribute::TARGET_METHOD)]
class Operation extends BaseOperation
{
}
12 changes: 12 additions & 0 deletions Annotation/Security.php
Expand Up @@ -16,6 +16,7 @@
/**
* @Annotation
*/
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class Security extends AbstractAnnotation
{
/** {@inheritdoc} */
Expand All @@ -35,4 +36,15 @@ class Security extends AbstractAnnotation
* @var string[]
*/
public $scopes = [];

public function __construct(
array $properties = [],
string $name = null,
array $scopes = []
) {
parent::__construct($properties + [
'name' => $name,
'scopes' => $scopes,
]);
}
}
21 changes: 21 additions & 0 deletions Describer/OpenApiPhpDescriber.php
Expand Up @@ -67,12 +67,14 @@ public function describe(OA\OpenApi $api)
$classAnnotations = array_filter($this->annotationReader->getClassAnnotations($declaringClass), function ($v) {
return $v instanceof OA\AbstractAnnotation;
});
$classAnnotations = array_merge($classAnnotations, $this->getAttributesAsAnnotation($declaringClass, OA\AbstractAnnotation::class));
$classAnnotations[$declaringClass->getName()] = $classAnnotations;
}

$annotations = array_filter($this->annotationReader->getMethodAnnotations($method), function ($v) {
return $v instanceof OA\AbstractAnnotation;
});
$annotations = array_merge($annotations, $this->getAttributesAsAnnotation($method, OA\AbstractAnnotation::class));

if (0 === count($annotations) && 0 === count($classAnnotations[$declaringClass->getName()])) {
continue;
Expand Down Expand Up @@ -190,4 +192,23 @@ private function normalizePath(string $path): string

return $path;
}

/**
* @param \ReflectionClass|\ReflectionMethod $reflection
*
* @return OA\AbstractAnnotation[]
*/
private function getAttributesAsAnnotation($reflection, string $className): array
{
$annotations = [];
if (\PHP_VERSION_ID < 80100) {
return $annotations;
}

foreach ($reflection->getAttributes($className, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
$annotations[] = $attribute->newInstance();
}

return $annotations;
}
}
40 changes: 30 additions & 10 deletions ModelDescriber/Annotations/OpenApiAnnotationsReader.php
Expand Up @@ -39,8 +39,8 @@ public function __construct(Reader $annotationsReader, ModelRegistry $modelRegis

public function updateSchema(\ReflectionClass $reflectionClass, OA\Schema $schema): void
{
/** @var OA\Schema $oaSchema */
if (!$oaSchema = $this->annotationsReader->getClassAnnotation($reflectionClass, OA\Schema::class)) {
/** @var OA\Schema|null $oaSchema */
if (!$oaSchema = $this->getAnnotation($reflectionClass, OA\Schema::class)) {
return;
}

Expand All @@ -56,10 +56,8 @@ public function updateSchema(\ReflectionClass $reflectionClass, OA\Schema $schem

public function getPropertyName($reflection, string $default): string
{
/** @var OA\Property $oaProperty */
if ($reflection instanceof \ReflectionProperty && !$oaProperty = $this->annotationsReader->getPropertyAnnotation($reflection, OA\Property::class)) {
return $default;
} elseif ($reflection instanceof \ReflectionMethod && !$oaProperty = $this->annotationsReader->getMethodAnnotation($reflection, OA\Property::class)) {
/** @var OA\Property|null $oaProperty */
if (!$oaProperty = $this->getAnnotation($reflection, OA\Property::class)) {
return $default;
}

Expand All @@ -78,10 +76,8 @@ public function updateProperty($reflection, OA\Property $property, array $serial
'filename' => $declaringClass->getFileName(),
]));

/** @var OA\Property $oaProperty */
if ($reflection instanceof \ReflectionProperty && !$oaProperty = $this->annotationsReader->getPropertyAnnotation($reflection, OA\Property::class)) {
return;
} elseif ($reflection instanceof \ReflectionMethod && !$oaProperty = $this->annotationsReader->getMethodAnnotation($reflection, OA\Property::class)) {
/** @var OA\Property|null $oaProperty */
if (!$oaProperty = $this->getAnnotation($reflection, OA\Property::class)) {
return;
}
$this->setContext(null);
Expand All @@ -95,4 +91,28 @@ public function updateProperty($reflection, OA\Property $property, array $serial

$property->mergeProperties($oaProperty);
}

/**
* @param \ReflectionClass|\ReflectionProperty|\ReflectionMethod $reflection
*
* @return mixed
*/
private function getAnnotation($reflection, string $className)
{
if (\PHP_VERSION_ID >= 80100) {
if (null !== $attribute = $reflection->getAttributes($className, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) {
return $attribute->newInstance();
}
}

if ($reflection instanceof \ReflectionClass) {
return $this->annotationsReader->getClassAnnotation($reflection, $className);
} elseif ($reflection instanceof \ReflectionProperty) {
return $this->annotationsReader->getPropertyAnnotation($reflection, $className);
} elseif ($reflection instanceof \ReflectionMethod) {
return $this->annotationsReader->getMethodAnnotation($reflection, $className);
}

return null;
}
}
16 changes: 15 additions & 1 deletion ModelDescriber/ObjectModelDescriber.php
Expand Up @@ -72,7 +72,7 @@ public function describe(Model $model, OA\Schema $schema)
$annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry, $this->mediaTypes);
$annotationsReader->updateDefinition($reflClass, $schema);

$discriminatorMap = $this->doctrineReader->getClassAnnotation($reflClass, DiscriminatorMap::class);
$discriminatorMap = $this->getAnnotation($reflClass, DiscriminatorMap::class);
if ($discriminatorMap && Generator::UNDEFINED === $schema->discriminator) {
$this->applyOpenApiDiscriminator(
$model,
Expand Down Expand Up @@ -183,6 +183,20 @@ private function describeProperty(array $types, Model $model, OA\Schema $propert
throw new \Exception(sprintf('Type "%s" is not supported in %s::$%s. You may use the `@OA\Property(type="")` annotation to specify it manually.', $types[0]->getBuiltinType(), $model->getType()->getClassName(), $propertyName));
}

/**
* @return mixed
*/
private function getAnnotation(\ReflectionClass $reflection, string $className)
{
if (\PHP_VERSION_ID >= 80000) {
if (null !== $attribute = $reflection->getAttributes($className, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) {
return $attribute->newInstance();
}
}

return $this->doctrineReader->getClassAnnotation($reflection, $className);
}

public function supports(Model $model): bool
{
return Type::BUILTIN_TYPE_OBJECT === $model->getType()->getBuiltinType() && class_exists($model->getType()->getClassName());
Expand Down
19 changes: 19 additions & 0 deletions RouteDescriber/FosRestDescriber.php
Expand Up @@ -44,6 +44,8 @@ public function describe(OA\OpenApi $api, Route $route, \ReflectionMethod $refle
$annotations = array_filter($annotations, static function ($value) {
return $value instanceof RequestParam || $value instanceof QueryParam;
});
$annotations = array_merge($annotations, $this->getAttributesAsAnnotation($reflectionMethod, RequestParam::class));
$annotations = array_merge($annotations, $this->getAttributesAsAnnotation($reflectionMethod, QueryParam::class));

foreach ($this->getOperations($api, $route) as $operation) {
foreach ($annotations as $annotation) {
Expand Down Expand Up @@ -185,4 +187,21 @@ private function describeCommonSchemaFromAnnotation(OA\Schema $schema, $annotati
$schema->format = $format;
}
}

/**
* @return OA\AbstractAnnotation[]
*/
private function getAttributesAsAnnotation(\ReflectionMethod $reflection, string $className): array
{
$annotations = [];
if (\PHP_VERSION_ID < 80100) {
return $annotations;
}

foreach ($reflection->getAttributes($className, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
$annotations[] = $attribute->newInstance();
}

return $annotations;
}
}
39 changes: 34 additions & 5 deletions Routing/FilteredRouteCollectionBuilder.php
Expand Up @@ -130,13 +130,23 @@ private function matchAnnotation(Route $route): bool
}

/** @var Areas|null $areas */
$areas = $this->annotationReader->getMethodAnnotation(
$reflectionMethod,
Areas::class
);
$areas = $this->getAttributesAsAnnotation($reflectionMethod, Areas::class)[0] ?? null;

if (null === $areas) {
$areas = $this->annotationReader->getClassAnnotation($reflectionMethod->getDeclaringClass(), Areas::class);
/** @var Areas|null $areas */
$areas = $this->getAttributesAsAnnotation($reflectionMethod->getDeclaringClass(), Areas::class)[0] ?? null;

if (null === $areas) {
/** @var Areas|null $areas */
$areas = $this->annotationReader->getMethodAnnotation(
$reflectionMethod,
Areas::class
);

if (null === $areas) {
$areas = $this->annotationReader->getClassAnnotation($reflectionMethod->getDeclaringClass(), Areas::class);
}
}
}

return (null !== $areas) ? $areas->has($this->area) : false;
Expand Down Expand Up @@ -168,4 +178,23 @@ private function defaultRouteDisabled(Route $route): bool

return false;
}

/**
* @param \ReflectionClass|\ReflectionMethod $reflection
*
* @return Areas[]
*/
private function getAttributesAsAnnotation($reflection, string $className): array
{
$annotations = [];
if (\PHP_VERSION_ID < 80100) {
return $annotations;
}

foreach ($reflection->getAttributes($className, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
$annotations[] = $attribute->newInstance();
}

return $annotations;
}
}

0 comments on commit cc97b0b

Please sign in to comment.