Skip to content

Commit

Permalink
upgrade filters
Browse files Browse the repository at this point in the history
  • Loading branch information
nawel-les-tilleuls committed Jul 22, 2022
1 parent de239e5 commit e2ed94b
Show file tree
Hide file tree
Showing 4 changed files with 279 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use ApiPlatform\Core\Upgrade\ColorConsoleDiffFormatter;
use ApiPlatform\Core\Upgrade\SubresourceTransformer;
use ApiPlatform\Core\Upgrade\UpgradeApiResourceVisitor;
use ApiPlatform\Core\Upgrade\UpgradeApiFilterVisitor;
use ApiPlatform\Core\Upgrade\UpgradeApiSubresourceVisitor;
use ApiPlatform\Exception\ResourceClassNotFoundException;
use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
Expand Down Expand Up @@ -106,6 +107,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$traverser = new NodeTraverser();
[$attribute, $isAnnotation] = $this->readApiResource($resourceClass);

$traverser->addVisitor(new UpgradeApiFilterVisitor($this->reader, $resourceClass, $attribute));

if (!$attribute) {
continue;
Expand Down
256 changes: 256 additions & 0 deletions src/Core/Upgrade/UpgradeApiFilterVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\Upgrade;

use ApiPlatform\Core\Annotation\ApiFilter as LegacyApiFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter as LegacySearchFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\ExistsFilter as LegacyExistsFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter as LegacyDateFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Doctrine\Orm\Filter\ExistsFilter;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\Resource\DeprecationMetadataTrait;
use Doctrine\Common\Annotations\AnnotationReader;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;

final class UpgradeApiFilterVisitor extends NodeVisitorAbstract
{
use DeprecationMetadataTrait;
use RemoveAnnotationTrait;

private ?AnnotationReader $reader;
private \ReflectionClass $reflectionClass;

public function __construct(?AnnotationReader $reader, string $resourceClass)
{
$this->reader = $reader;
$this->reflectionClass = new \ReflectionClass($resourceClass);
}

/**
* @return int|Node|null
*/
public function enterNode(Node $node)
{
if ($node instanceof Node\Stmt\Namespace_) {
$namespaces = [
ApiFilter::class,
SearchFilter::class,
ExistsFilter::class,
DateFilter::class,
];

foreach ($node->stmts as $k => $stmt) {
if (!$stmt instanceof Node\Stmt\Use_) {
break;
}

$useStatement = implode('\\', $stmt->uses[0]->name->parts);

if (LegacyApiFilter::class === $useStatement) {
unset($node->stmts[$k]);
continue;
}
if (LegacySearchFilter::class === $useStatement) {
unset($node->stmts[$k]);
continue;
}
if (LegacyExistsFilter::class === $useStatement) {
unset($node->stmts[$k]);
continue;
}
if (LegacyDateFilter::class === $useStatement) {
unset($node->stmts[$k]);
continue;
}




if (false !== ($key = array_search($useStatement, $namespaces, true))) {
unset($namespaces[$key]);
}
}

foreach ($namespaces as $namespace) {
array_unshift($node->stmts, new Node\Stmt\Use_([
new Node\Stmt\UseUse(
new Node\Name(
$namespace
)
),
]));
}
}

if ($node instanceof Node\Stmt\Property || $node instanceof Node\Stmt\Class_) {
if ($node instanceof Node\Stmt\Property) {
$reflection = $this->reflectionClass->getProperty($node->props[0]->name->__toString());
} else {
$reflection = $this->reflectionClass;
}

// filter annotation : array
$filterAnnotations = $this->readApiFilters($reflection);

foreach ($this->readApiFilters($reflection) as $annotation) {
[$filterAnnotation, $isAnnotation] = $annotation;
if ($isAnnotation) {
$this->removeAnnotation($node);
} else {
$this->removeAttribute($node);
}

$arguments = [];

foreach ([
'strategy',
'filterClass',
'properties',
'arguments',
] as $key) {
$value = $filterAnnotation->{$key};
if (null === $value || [] === $value) {
continue;
}
$arguments[$key] = $this->valueToNode($value);

}
foreach ($filterAnnotation->attributes ?? [] as $key => $value) {
if (null === $value || [] === $value) {
continue;
}

[$key, $value] = $this->getKeyValue($key, $value);
$arguments[$key] = $this->valueToNode($value);
}

array_unshift($node->attrGroups, new Node\AttributeGroup([
new Node\Attribute(
new Node\Name('ApiFilter'),
$this->arrayToArguments($arguments),
),
]));
}
}

}

private function readApiFilters(\ReflectionProperty|\ReflectionClass $reflection): ?\Generator
{
if (\PHP_VERSION_ID >= 80000 && $attributes = $reflection->getAttributes(LegacyApiFilter::class)) {
yield from array_map(function($attribute) {
return $attribute->newInstance();
} , $attributes);
}

if (null === $this->reader) {
throw new \RuntimeException(sprintf('Resource "%s" not found.', $reflection->getDeclaringClass()->getName()));
}

if ($reflection instanceof \ReflectionProperty) {
$annotations = $this->reader->getPropertyAnnotations($reflection);
} else {
$annotations = $this->reader->getClassAnnotations($reflection);

}

foreach ($annotations as $annotation) {
if ($annotation instanceof LegacyApiFilter) {
yield [$annotation, true];
}
}
}

private function valueToNode(mixed $value)
{
if (\is_string($value)) {
if (class_exists($value)) {
return new Node\Expr\ClassConstFetch(new Node\Name($this->getShortName($value)), 'class');
}

return new Node\Scalar\String_($value);
}

if (\is_bool($value)) {
return new Node\Expr\ConstFetch(new Node\Name($value ? 'true' : 'false'));
}

if (is_numeric($value)) {
return \is_int($value) ? new Node\Scalar\LNumber($value) : new Node\Scalar\DNumber($value);
}

if (\is_array($value)) {
return new Node\Expr\Array_(
array_map(function ($key, $value) {
return new Node\Expr\ArrayItem(
$this->valueToNode($value),
\is_string($key) ? $this->valueToNode($key) : null,
);
}, array_keys($value), array_values($value)),
[
'kind' => Node\Expr\Array_::KIND_SHORT,
]
);
}
}

private function removeAnnotation(Node\Stmt\Property|Node\Stmt\Class_ $node)
{
$comment = $node->getDocComment();

if (preg_match('/@ApiFilter/', $comment->getText())) {
$node->setDocComment($this->removeAnnotationByTag($comment, 'ApiFilter'));
}
}

private function removeAttribute(Node\Stmt\Property|Node\Stmt\Class_ $node)
{
foreach ($node->attrGroups as $k => $attrGroupNode) {
foreach ($attrGroupNode->attrs as $i => $attribute) {
if (str_ends_with(implode('\\', $attribute->name->parts), 'ApiFilter')) {
unset($node->attrGroups[$k]);
break;
}
}
}
}

/**
* @return Node\Arg[]
*/
private function arrayToArguments(array $arguments)
{
$args = [];
foreach ($arguments as $key => $value) {
if ($value)
$args[] = new Node\Arg($value, false, false, [], new Node\Identifier($key));
}

return $args;
}


private function getShortName(string $class): string
{
if (false !== $pos = strrpos($class, '\\')) {
return substr($class, $pos + 1);
}

return $class;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,22 @@ public function testDebugResource()
$expectedStrings = [
'-use ApiPlatform\\Core\\Annotation\\ApiSubresource',
'-use ApiPlatform\\Core\\Annotation\\ApiResource',
'-use ApiPlatform\\Core\\Annotation\\ApiFilter',
'-use ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\SearchFilter;',
'-use ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\ExistsFilter;',
'-use ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\DateFilter;',
'+use ApiPlatform\\Metadata\\ApiResource',
'+use ApiPlatform\\Metadata\\ApiFilter',
'+use ApiPlatform\Doctrine\Orm\Filter\SearchFilter',
'+use ApiPlatform\Doctrine\Orm\Filter\ExistsFilter',
'+use ApiPlatform\Doctrine\Orm\Filter\DateFilter',
'+use ApiPlatform\\Metadata\\Get',
"+#[ApiResource(graphQlOperations: [new Query(name: 'item_query'), new Mutation(name: 'update', normalizationContext: ['groups' => ['chicago', 'fakemanytomany']], denormalizationContext: ['groups' => ['friends']])], types: ['https://schema.org/Product'], normalizationContext: ['groups' => ['friends']], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'])]",
"#[ApiResource(uriTemplate: '/related_dummies/{id}/id.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])]",
"+#[ApiFilter(filterClass: SearchFilter::class, properties: ['id', 'name'])]",
"+ #[ApiFilter(filterClass: SearchFilter::class)]",
"+ #[ApiFilter(filterClass: ExistsFilter::class)]",
"+ #[ApiFilter(filterClass: DateFilter::class)]",
];

$display = $commandTester->getDisplay();
Expand Down
8 changes: 8 additions & 0 deletions tests/Fixtures/TestBundle/Entity/RelatedDummy.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@

use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiSubresource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\ExistsFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
Expand All @@ -29,6 +33,7 @@
*
* @ApiResource(graphql={"item_query", "update"={"normalization_context"={"groups"={"chicago", "fakemanytomany"}}, "denormalization_context"={"groups"={"friends"}}}}, iri="https://schema.org/Product", attributes={"normalization_context"={"groups"={"friends"}}, "filters"={"related_dummy.friends", "related_dummy.complex_sub_query"}})
* @ORM\Entity
* @ApiFilter(SearchFilter::class, properties={"id", "name"})
*/
class RelatedDummy extends ParentDummy
{
Expand All @@ -53,6 +58,8 @@ class RelatedDummy extends ParentDummy
/**
* @ORM\Column
* @Groups({"barcelona", "chicago", "friends"})
* @ApiFilter(SearchFilter::class)
* @ApiFilter(ExistsFilter::class)
*/
protected $symfony = 'symfony';

Expand All @@ -62,6 +69,7 @@ class RelatedDummy extends ParentDummy
* @ORM\Column(type="datetime", nullable=true)
* @Assert\DateTime
* @Groups({"friends"})
* @ApiFilter(DateFilter::class)
*/
public $dummyDate;

Expand Down

0 comments on commit e2ed94b

Please sign in to comment.