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 @@ -143,6 +143,10 @@ private function getDefaultParameters(Operation $operation, string $resourceClas
continue;
}

if (!$parameter->getKey()) {
$parameter = $parameter->withKey($key);
}

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

Expand Down Expand Up @@ -170,7 +174,7 @@ private function getDefaultParameters(Operation $operation, string $resourceClas
$parameter = $parameter->withProvider($f->getParameterProvider());
}

$key = $parameter->getKey() ?? $key;
$key = $parameter->getKey();

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
namespace ApiPlatform\Metadata\Tests\Resource\Factory;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\FilterInterface;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Parameters;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
Expand All @@ -36,8 +38,12 @@ public function testParameterFactory(): void
$nameCollection->method('create')->willReturn(new PropertyNameCollection(['id', 'hydra', 'everywhere']));
$propertyMetadata = $this->createStub(PropertyMetadataFactoryInterface::class);
$propertyMetadata->method('create')->willReturnOnConsecutiveCalls(
new ApiProperty(identifier: true), new ApiProperty(readable: true), new ApiProperty(readable: true),
new ApiProperty(identifier: true), new ApiProperty(readable: true), new ApiProperty(readable: true)
new ApiProperty(identifier: true),
new ApiProperty(readable: true),
new ApiProperty(readable: true),
new ApiProperty(identifier: true),
new ApiProperty(readable: true),
new ApiProperty(readable: true)
);
$filterLocator = $this->createStub(ContainerInterface::class);
$filterLocator->method('has')->willReturn(true);
Expand Down Expand Up @@ -77,14 +83,71 @@ public function getDescription(string $resourceClass): array
$this->assertNull($everywhere->getOpenApi());
}

public function testQueryParameterWithPropertyPlaceholder(): void
{
$nameCollection = $this->createStub(PropertyNameCollectionFactoryInterface::class);
$nameCollection->method('create')->willReturn(new PropertyNameCollection(['id', 'name', 'description']));

$propertyMetadata = $this->createStub(PropertyMetadataFactoryInterface::class);
$propertyMetadata->method('create')->willReturn(
new ApiProperty(readable: true),
);

$filterLocator = $this->createStub(ContainerInterface::class);
$filterLocator->method('has')->willReturn(false); // No specific filter logic needed for this test

$parameterFactory = new ParameterResourceMetadataCollectionFactory(
$nameCollection,
$propertyMetadata,
new AttributesResourceMetadataCollectionFactory(),
$filterLocator
);

$resourceMetadataCollection = $parameterFactory->create(HasParameterAttribute::class);
$operation = $resourceMetadataCollection->getOperation(forceCollection: true);
$parameters = $operation->getParameters();

$this->assertInstanceOf(Parameters::class, $parameters);

// Assert that the original parameter with ':property' is removed
$this->assertFalse($parameters->has('search[:property]'));

// Assert that the new parameters are created and have the correct properties
$this->assertTrue($parameters->has('search[name]'));
$this->assertTrue($parameters->has('search[description]'));
$this->assertTrue($parameters->has('static_param'));

$searchNameParam = $parameters->get('search[name]');
$this->assertInstanceOf(QueryParameter::class, $searchNameParam);
$this->assertSame('Search by property', $searchNameParam->getDescription());
$this->assertSame('name', $searchNameParam->getProperty());
$this->assertSame('search[name]', $searchNameParam->getKey());

$searchDescriptionParam = $parameters->get('search[description]');
$this->assertInstanceOf(QueryParameter::class, $searchDescriptionParam);
$this->assertSame('Search by property', $searchDescriptionParam->getDescription());
$this->assertSame('description', $searchDescriptionParam->getProperty());
$this->assertSame('search[description]', $searchDescriptionParam->getKey());

$staticParam = $parameters->get('static_param');
$this->assertInstanceOf(QueryParameter::class, $staticParam);
$this->assertSame('A static parameter', $staticParam->getDescription());
$this->assertNull($staticParam->getProperty());
$this->assertSame('static_param', $staticParam->getKey());
}

public function testParameterFactoryNoFilter(): void
{
$nameCollection = $this->createStub(PropertyNameCollectionFactoryInterface::class);
$nameCollection->method('create')->willReturn(new PropertyNameCollection(['id', 'hydra', 'everywhere']));
$propertyMetadata = $this->createStub(PropertyMetadataFactoryInterface::class);
$propertyMetadata->method('create')->willReturnOnConsecutiveCalls(
new ApiProperty(identifier: true), new ApiProperty(readable: true), new ApiProperty(readable: true),
new ApiProperty(identifier: true), new ApiProperty(readable: true), new ApiProperty(readable: true)
new ApiProperty(identifier: true),
new ApiProperty(readable: true),
new ApiProperty(readable: true),
new ApiProperty(identifier: true),
new ApiProperty(readable: true),
new ApiProperty(readable: true)
);
$filterLocator = $this->createStub(ContainerInterface::class);
$filterLocator->method('has')->willReturn(false);
Expand Down Expand Up @@ -135,3 +198,25 @@ public function testParameterFactoryWithLimitedProperties(): void
$this->assertSame(['name'], $param->getProperties());
}
}

#[ApiResource(
operations: [
new GetCollection(
parameters: [
'search[:property]' => new QueryParameter(
description: 'Search by property',
properties: ['name', 'description']
),
'static_param' => new QueryParameter(
description: 'A static parameter'
),
]
),
]
)]
class HasParameterAttribute
{
public $id;
public $name;
public $description;
}
87 changes: 87 additions & 0 deletions tests/Fixtures/TestBundle/Entity/ProductWithQueryParameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?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\Tests\Fixtures\TestBundle\Entity;

use ApiPlatform\Doctrine\Orm\Filter\ExactFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\PartialSearchFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\QueryParameter;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ApiResource(
operations: [
new GetCollection(
parameters: [
'brand' => new QueryParameter(
filter: new ExactFilter(),
),
'search[:property]' => new QueryParameter(
filter: new PartialSearchFilter(),
properties: ['title', 'description']
),
'filter[:property]' => new QueryParameter(
filter: new ExactFilter(),
properties: ['category', 'brand'],
),
'order[:property]' => new QueryParameter(
filter: new OrderFilter(),
properties: ['rating']
),
]
),
]
)]
class ProductWithQueryParameter
{
#[ORM\Id]
#[ORM\Column()]
#[ORM\GeneratedValue]
private ?int $id = null;

#[ORM\Column(length: 255)]
public ?string $sku = null;

#[ORM\Column(length: 255)]
public ?string $title = null;

#[ORM\Column(nullable: true)]
public ?string $description = null;

#[ORM\Column(nullable: true)]
public ?string $category = null;

#[ORM\Column(nullable: true)]
public ?string $brand = null;

#[ORM\Column(nullable: true)]
public ?float $exactPrice = null;

#[ORM\Column()]
public int $rating = 0;

#[ORM\Column()]
public int $stock = 0;

#[ORM\Column(type: Types::JSON, nullable: true, options: ['jsonb' => true])]
public array $tags = [];

public function getId(): ?int
{
return $this->id;
}
}
90 changes: 88 additions & 2 deletions tests/Functional/Parameters/DoctrineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\FilterWithStateOptions;
use ApiPlatform\Tests\Fixtures\TestBundle\Document\SearchFilterParameter as SearchFilterParameterDocument;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FilterWithStateOptionsEntity;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ProductWithQueryParameter;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SearchFilterParameter;
use ApiPlatform\Tests\RecreateSchemaTrait;
use ApiPlatform\Tests\SetupClassResourcesTrait;
Expand All @@ -34,7 +35,7 @@ final class DoctrineTest extends ApiTestCase
*/
public static function getResources(): array
{
return [SearchFilterParameter::class, FilterWithStateOptions::class];
return [SearchFilterParameter::class, FilterWithStateOptions::class, ProductWithQueryParameter::class];
}

public function testDoctrineEntitySearchFilter(): void
Expand Down Expand Up @@ -195,7 +196,50 @@ public static function partialFilterParameterProviderForSearchFilterParameter():
];
}

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

$resource = ProductWithQueryParameter::class;
$this->recreateSchema([$resource]);
$this->loadProductFixtures($resource);

// Test search[:property] with 'title'
$response = self::createClient()->request('GET', '/product_with_query_parameters?search[title]=Awesome');
$this->assertResponseIsSuccessful();
$this->assertCount(1, $response->toArray()['hydra:member']);
$this->assertEquals('Awesome Widget', $response->toArray()['hydra:member'][0]['title']);

// Test search[:property] with 'description'
$response = self::createClient()->request('GET', '/product_with_query_parameters?search[description]=super');
$this->assertResponseIsSuccessful();
$this->assertCount(1, $response->toArray()['hydra:member']);
$this->assertEquals('Super Gadget', $response->toArray()['hydra:member'][0]['title']);

// Test filter[:property] with 'category'
$response = self::createClient()->request('GET', '/product_with_query_parameters?filter[category]=Electronics');
$this->assertResponseIsSuccessful();
$this->assertCount(2, $response->toArray()['hydra:member']);

// Test filter[:property] with 'brand'
$response = self::createClient()->request('GET', '/product_with_query_parameters?filter[brand]=BrandY');
$this->assertResponseIsSuccessful();
$this->assertCount(1, $response->toArray()['hydra:member']);
$this->assertEquals('Super Gadget', $response->toArray()['hydra:member'][0]['title']);

// Test order[:property] with 'rating'
$response = self::createClient()->request('GET', '/product_with_query_parameters?order[rating]=desc');
$this->assertResponseIsSuccessful();
$members = $response->toArray()['hydra:member'];
$this->assertCount(3, $members);
$this->assertEquals('Awesome Widget', $members[0]['title']);
$this->assertEquals('Super Gadget', $members[1]['title']);
$this->assertEquals('Mega Device', $members[2]['title']);
}

private function loadFixtures(string $resourceClass): void
{
$container = static::$kernel->getContainer();
$registry = $this->isMongoDB() ? $container->get('doctrine_mongodb') : $container->get('doctrine');
Expand All @@ -214,4 +258,46 @@ public function loadFixtures(string $resourceClass): void

$manager->flush();
}

private function loadProductFixtures(string $resourceClass): void
{
$container = static::$kernel->getContainer();
$registry = $this->isMongoDB() ? $container->get('doctrine_mongodb') : $container->get('doctrine');
$manager = $registry->getManager();

$product1 = new $resourceClass();
$product1->sku = 'SKU001';
$product1->title = 'Awesome Widget';
$product1->description = 'A really awesome widget.';
$product1->category = 'Electronics';
$product1->brand = 'BrandX';
$product1->rating = 5;
$product1->stock = 100;
$product1->tags = ['new', 'sale'];
$manager->persist($product1);

$product2 = new $resourceClass();
$product2->sku = 'SKU002';
$product2->title = 'Super Gadget';
$product2->description = 'A super cool gadget.';
$product2->category = 'Electronics';
$product2->brand = 'BrandY';
$product2->rating = 4;
$product2->stock = 50;
$product2->tags = ['popular'];
$manager->persist($product2);

$product3 = new $resourceClass();
$product3->sku = 'SKU003';
$product3->title = 'Mega Device';
$product3->description = 'A mega useful device.';
$product3->category = 'Home';
$product3->brand = 'BrandX';
$product3->rating = 3;
$product3->stock = 200;
$product3->tags = ['clearance'];
$manager->persist($product3);

$manager->flush();
}
}
Loading