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
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ public function create(string $resourceClass): ResourceMetadataCollection
continue;
}

$operations->add($operationName, $this->addDefaults($operation));
$operation = $this->addDefaults($operation);
$operation = $this->setParametersFilterClass($operation, $documentClass);
$operations->add($operationName, $operation);
}

$resourceMetadata = $resourceMetadata->withOperations($operations);
Expand All @@ -60,11 +62,14 @@ public function create(string $resourceClass): ResourceMetadataCollection

if ($graphQlOperations) {
foreach ($graphQlOperations as $operationName => $graphQlOperation) {
if (!$this->managerRegistry->getManagerForClass($graphQlOperation->getClass()) instanceof DocumentManager) {
$documentClass = $this->getStateOptionsClass($graphQlOperation, $graphQlOperation->getClass(), Options::class);
if (!$this->managerRegistry->getManagerForClass($documentClass) instanceof DocumentManager) {
continue;
}

$graphQlOperations[$operationName] = $this->addDefaults($graphQlOperation);
$graphQlOperation = $this->addDefaults($graphQlOperation);
$graphQlOperation = $this->setParametersFilterClass($graphQlOperation, $documentClass);
$graphQlOperations[$operationName] = $graphQlOperation;
}

$resourceMetadata = $resourceMetadata->withGraphQlOperations($graphQlOperations);
Expand Down Expand Up @@ -112,4 +117,20 @@ private function getProcessor(Operation $operation): string

return 'api_platform.doctrine_mongodb.odm.state.persist_processor';
}

private function setParametersFilterClass(Operation $operation, string $documentClass): Operation
{
$parameters = $operation->getParameters();
if (!$parameters) {
return $operation;
}

foreach ($parameters as $key => $parameter) {
if (null === $parameter->getFilterClass()) {
$parameters->add($key, $parameter->withFilterClass($documentClass));
}
}

return $operation->withParameters($parameters);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ public function create(string $resourceClass): ResourceMetadataCollection
continue;
}

$operations->add($operationName, $this->addDefaults($operation));
$operation = $this->addDefaults($operation);
$operation = $this->setParametersFilterClass($operation, $entityClass);
$operations->add($operationName, $operation);
}

$resourceMetadata = $resourceMetadata->withOperations($operations);
Expand All @@ -67,7 +69,9 @@ public function create(string $resourceClass): ResourceMetadataCollection
continue;
}

$graphQlOperations[$operationName] = $this->addDefaults($graphQlOperation);
$graphQlOperation = $this->addDefaults($graphQlOperation);
$graphQlOperation = $this->setParametersFilterClass($graphQlOperation, $entityClass);
$graphQlOperations[$operationName] = $graphQlOperation;
}

$resourceMetadata = $resourceMetadata->withGraphQlOperations($graphQlOperations);
Expand Down Expand Up @@ -115,4 +119,20 @@ private function getProcessor(Operation $operation): string

return 'api_platform.doctrine.orm.state.persist_processor';
}

private function setParametersFilterClass(Operation $operation, string $entityClass): Operation
{
$parameters = $operation->getParameters();
if (!$parameters) {
return $operation;
}

foreach ($parameters as $key => $parameter) {
if (null === $parameter->getFilterClass()) {
$parameters->add($key, $parameter->withFilterClass($entityClass));
}
}

return $operation->withParameters($parameters);
}
}
15 changes: 15 additions & 0 deletions src/Metadata/Parameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ abstract class Parameter
* @param Type $nativeType the PHP native type, we cast values to an array if its a CollectionType, if not and it's an array with a single value we use it (eg: HTTP Header)
* @param ?bool $castToNativeType whether API Platform should cast your parameter to the nativeType declared
* @param ?callable(mixed): mixed $castFn the closure used to cast your parameter, this gets called only when $castToNativeType is set
* @param ?string $filterClass the class to use when resolving filter properties (from stateOptions)
*
* @phpstan-param array<string, mixed>|null $schema
*
Expand All @@ -56,6 +57,7 @@ public function __construct(
protected ?bool $castToArray = null,
protected ?bool $castToNativeType = null,
protected mixed $castFn = null,
protected ?string $filterClass = null,
) {
}

Expand Down Expand Up @@ -370,4 +372,17 @@ public function withCastFn(mixed $castFn): self

return $self;
}

public function getFilterClass(): ?string
{
return $this->filterClass;
}

public function withFilterClass(?string $filterClass): self
{
$self = clone $this;
$self->filterClass = $filterClass;

return $self;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,17 +94,23 @@ public function create(string $resourceClass): ResourceMetadataCollection
/**
* @return array{propertyNames: string[], properties: array<string, ApiProperty>}
*/
private function getProperties(string $resourceClass, ?Parameter $parameter = null): array
private function getProperties(string $resourceClass, ?Parameter $parameter = null, ?Operation $operation = null): array
{
$k = $resourceClass.($parameter?->getProperties() ? ($parameter->getKey() ?? '') : '').(\is_string($parameter->getFilter()) ? $parameter->getFilter() : '');
$filterClass = $parameter?->getFilterClass();
if (null === $filterClass && null !== $operation) {
$filterClass = $this->getStateOptionsClass($operation, $resourceClass);
}
$filterClass ??= $resourceClass;

$k = $resourceClass.($parameter?->getProperties() ? ($parameter->getKey() ?? '') : '').(\is_string($parameter->getFilter()) ? $parameter->getFilter() : '').$filterClass;
if (isset($this->localPropertyCache[$k])) {
return $this->localPropertyCache[$k];
}

$propertyNames = [];
$properties = [];
foreach ($parameter?->getProperties() ?? $this->propertyNameCollectionFactory->create($resourceClass) as $property) {
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property);
foreach ($parameter?->getProperties() ?? $this->propertyNameCollectionFactory->create($filterClass) as $property) {
$propertyMetadata = $this->propertyMetadataFactory->create($filterClass, $property);
if ($propertyMetadata->isReadable()) {
$propertyNames[] = $property;
$properties[$property] = $propertyMetadata;
Expand Down Expand Up @@ -147,7 +153,7 @@ private function getDefaultParameters(Operation $operation, string $resourceClas
$parameter = $parameter->withKey($key);
}

['propertyNames' => $propertyNames, 'properties' => $properties] = $this->getProperties($resourceClass, $parameter);
['propertyNames' => $propertyNames, 'properties' => $properties] = $this->getProperties($resourceClass, $parameter, $operation);
$parameter = $parameter->withProperties($propertyNames);

foreach ($propertyNames as $property) {
Expand Down Expand Up @@ -176,7 +182,7 @@ private function getDefaultParameters(Operation $operation, string $resourceClas

$key = $parameter->getKey();

['propertyNames' => $propertyNames, 'properties' => $properties] = $this->getProperties($resourceClass, $parameter);
['propertyNames' => $propertyNames, 'properties' => $properties] = $this->getProperties($resourceClass, $parameter, $operation);

if ($filter instanceof PropertiesAwareInterface) {
$parameter = $parameter->withProperties($propertyNames);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@
$services->alias('api_platform.state.item_provider', 'ApiPlatform\Doctrine\Odm\State\ItemProvider');

$services->set('api_platform.doctrine.odm.metadata.resource.metadata_collection_factory', DoctrineMongoDbOdmResourceCollectionMetadataFactory::class)
->decorate('api_platform.metadata.resource.metadata_collection_factory', null, 40)
->decorate('api_platform.metadata.resource.metadata_collection_factory', null, -50)
->args([
service('doctrine_mongodb'),
service('api_platform.doctrine.odm.metadata.resource.metadata_collection_factory.inner'),
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/Resources/config/doctrine_orm.php
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@
->args([[]]);

$services->set('api_platform.doctrine.orm.metadata.resource.metadata_collection_factory', DoctrineOrmResourceCollectionMetadataFactory::class)
->decorate('api_platform.metadata.resource.metadata_collection_factory', null, 40)
->decorate('api_platform.metadata.resource.metadata_collection_factory', null, -50)
->args([
service('doctrine'),
service('api_platform.doctrine.orm.metadata.resource.metadata_collection_factory.inner'),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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\Tests\Fixtures\TestBundle\ApiResource;

use ApiPlatform\Doctrine\Orm\Filter\PartialSearchFilter;
use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\QueryParameter;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FilterWithStateOptionsAndNoApiFilterEntity;

#[ApiResource(
stateOptions: new Options(entityClass: FilterWithStateOptionsAndNoApiFilterEntity::class),
operations: [
new GetCollection(
uriTemplate: '/filter_with_state_options_and_no_api_filters_api_resource',
parameters: [
'search[:property]' => new QueryParameter(
properties: ['name'],
filter: new PartialSearchFilter(),
),
],
),
]
)]
final class FilterWithStateOptionsAndNoApiFilter
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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\Tests\Fixtures\TestBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class FilterWithStateOptionsAndNoApiFilterEntity
{
public function __construct(
#[ORM\Column(type: 'integer')]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'AUTO')]
public ?int $id = null,
#[ORM\Column(type: 'string', nullable: true)]
public ?string $name = null,
) {
}
}
42 changes: 41 additions & 1 deletion tests/Functional/Parameters/DoctrineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@

use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\FilterWithStateOptions;
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\FilterWithStateOptionsAndNoApiFilter;
use ApiPlatform\Tests\Fixtures\TestBundle\Document\SearchFilterParameter as SearchFilterParameterDocument;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FilterWithStateOptionsAndNoApiFilterEntity;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FilterWithStateOptionsEntity;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ProductWithQueryParameter;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SearchFilterParameter;
Expand All @@ -35,7 +37,12 @@ final class DoctrineTest extends ApiTestCase
*/
public static function getResources(): array
{
return [SearchFilterParameter::class, FilterWithStateOptions::class, ProductWithQueryParameter::class];
return [
SearchFilterParameter::class,
FilterWithStateOptions::class,
FilterWithStateOptionsAndNoApiFilter::class,
ProductWithQueryParameter::class,
];
}

public function testDoctrineEntitySearchFilter(): void
Expand Down Expand Up @@ -147,6 +154,39 @@ public function testStateOptions(): void
$this->assertEquals('after', $a['hydra:member'][0]['name']);
}

public function testStateOptionsAndNoApiFilter(): void
{
if ($this->isMongoDB()) {
$this->markTestSkipped('Not tested with mongodb.');
}

static::bootKernel();
$container = static::$kernel->getContainer();
$this->recreateSchema([FilterWithStateOptionsAndNoApiFilterEntity::class]);

$manager = $container->get('doctrine')->getManager();
$manager->persist(new FilterWithStateOptionsAndNoApiFilterEntity(name: 'current'));
$manager->persist(new FilterWithStateOptionsAndNoApiFilterEntity(name: 'null'));
$manager->persist(new FilterWithStateOptionsAndNoApiFilterEntity(name: 'after'));
$manager->flush();

$uri = '/filter_with_state_options_and_no_api_filters_api_resource';

$response = self::createClient()->request('GET', $uri);
$this->assertResponseIsSuccessful();
$a = $response->toArray();
$this->assertSame('hydra:Collection', $a['@type']);
$this->assertSame(3, $a['hydra:totalItems']);
$this->assertCount(3, $a['hydra:member']);

$response = self::createClient()->request('GET', $uri.'?search[name]=aft');
$this->assertResponseIsSuccessful();
$a = $response->toArray();
$this->assertSame('hydra:Collection', $a['@type']);
$this->assertSame(1, $a['hydra:totalItems']);
$this->assertCount(1, $a['hydra:member']);
}

#[DataProvider('partialFilterParameterProviderForSearchFilterParameter')]
public function testPartialSearchFilterWithSearchFilterParameter(string $url, int $expectedCount, array $expectedFoos): void
{
Expand Down
Loading