From 6d15e228611875a3c1a41a14ecf26a38bd67639a Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Sun, 21 Apr 2024 22:05:17 +0200 Subject: [PATCH] ci: guides documentation with doctrine search parameters (#6328) * docs: how to run a local guide * docs: document parameter with doctrine filter --- docs/README.md | 16 +++++ .../create-a-custom-doctrine-filter.php | 2 +- docs/guides/doctrine-search-filter.php | 59 ++++++++++--------- docs/guides/error-provider.php | 6 +- docs/guides/error-resource.php | 5 +- docs/guides/provide-the-resource-state.php | 4 +- .../Compiler/AttributeFilterPass.php | 2 +- .../Compiler/FilterPass.php | 2 +- 8 files changed, 58 insertions(+), 38 deletions(-) diff --git a/docs/README.md b/docs/README.md index 0c9ff0086d7..4838d9994cb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,6 +4,8 @@ A guide is a PHP executable file that will be transformed into documentation. It follows [Diataxis How-To Guides](https://diataxis.fr/how-to-guides/) practice which is a must read before writing a guide. +Read the "[How To Guide](./guides/how-to.php)" to understand how to write an API Platform guide. + Guides are transformed to Markdown using [php-documentation-generator](https://github.com/php-documentation-generator/php-documentation-generator) which is merely a version of [docco](https://ashkenas.com/docco/) in PHP adapted to output markdown. ## WASM @@ -15,3 +17,17 @@ docker run -v $(pwd):/src -v $(pwd)/public/php-wasm:/public -w /public php-wasm ``` A build of [php-wasm](https://github.com/soyuka/php-wasm) is needed in the `public/php-wasm` directory to try it out. + +## Local tests + +First run `composer update`. + +Then, get the [`pdg-phpunit`](https://github.com/php-documentation-generator/php-documentation-generator/tags) binary that allows to run single-file test. + +Use `KERNEL_CLASS` and `PDG_AUTOLOAD` to run a guide: + +``` +APP_DEBUG=0 \ +PDG_AUTOLOAD='vendor/autoload.php' \ +KERNEL_CLASS='\ApiPlatform\Playground\Kernel' pdg-phpunit guides/doctrine-search-filter.php +``` diff --git a/docs/guides/create-a-custom-doctrine-filter.php b/docs/guides/create-a-custom-doctrine-filter.php index 0d0a5f32ce5..32af2c2bf25 100644 --- a/docs/guides/create-a-custom-doctrine-filter.php +++ b/docs/guides/create-a-custom-doctrine-filter.php @@ -33,7 +33,7 @@ final class RegexpFilter extends AbstractFilter /* * Filtered properties is accessible through getProperties() method: property => strategy */ - protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void + protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void { /* * Otherwise this filter is applied to order and page as well. diff --git a/docs/guides/doctrine-search-filter.php b/docs/guides/doctrine-search-filter.php index 8f54016615d..d9f0cd21abb 100644 --- a/docs/guides/doctrine-search-filter.php +++ b/docs/guides/doctrine-search-filter.php @@ -12,18 +12,18 @@ // By default, all filters are disabled. They must be enabled explicitly. namespace App\Entity { - use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; - use ApiPlatform\Metadata\ApiFilter; - use ApiPlatform\Metadata\ApiResource; + use ApiPlatform\Metadata\GetCollection; + use ApiPlatform\Metadata\QueryParameter; use Doctrine\ORM\Mapping as ORM; - #[ApiResource] - // - // By using the `#[ApiFilter]` attribute, this attribute automatically declares the service, - // and you just have to use the filter class you want. - // - // If the filter is declared on the resource, you can specify on which properties it applies. - #[ApiFilter(SearchFilter::class, properties: ['title'])] + #[GetCollection( + uriTemplate: 'books{._format}', + parameters: [ + // Declare a QueryParameter with the :property pattern that matches the properties declared on the Filter. + // The filter is a service declared in the next class. + ':property' => new QueryParameter(filter: 'app.search_filter'), + ] + )] #[ORM\Entity] class Book { @@ -34,13 +34,25 @@ class Book public ?string $title = null; #[ORM\Column] - // We can also declare the filter attribute on a property and specify the strategy that should be used. - // For a list of availabe options [head to the documentation](/docs/core/filters/#search-filter) - #[ApiFilter(SearchFilter::class, strategy: 'partial')] public ?string $author = null; } } +namespace App\DependencyInjection { + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + + function configure(ContainerConfigurator $configurator): void + { + // This is the custom search filter we declare, if you prefer to use decoration, suffix the parent service with `.instance`. They implement the `PropertyAwareFilterInterface` that allows you to override a filter's property. + $services = $configurator->services(); + $services->set('app.search_filter') + ->parent('api_platform.doctrine.orm.search_filter') + // Search strategies may be defined here per properties, [read more](https://api-platform.com/docs/core/filters/) on the filter documentation. + ->args([['author' => 'partial', 'title' => 'partial']]) + ->tag('api_platform.filter'); + } +} + namespace App\Playground { use Symfony\Component\HttpFoundation\Request; @@ -85,8 +97,7 @@ public function load(ObjectManager $manager): void $bookFactory->many(10)->create(fn () => [ 'title' => faker()->name(), 'author' => faker()->firstName(), - ] - ); + ]); } } } @@ -100,36 +111,30 @@ final class BookTest extends ApiTestCase { use TestGuideTrait; - public function testAsAnonymousICanAccessTheDocumentation(): void + public function testGetDocumentation(): void { static::createClient()->request('GET', '/books.jsonld'); $this->assertResponseIsSuccessful(); - $this->assertMatchesResourceCollectionJsonSchema(Book::class, '_api_/books{._format}_get_collection', 'jsonld'); + $this->assertMatchesResourceCollectionJsonSchema(Book::class, '_api_books{._format}_get_collection', 'jsonld'); $this->assertJsonContains([ 'hydra:search' => [ '@type' => 'hydra:IriTemplate', - 'hydra:template' => '/books.jsonld{?title,title[],author}', + 'hydra:template' => '/books.jsonld{?author,title}', 'hydra:variableRepresentation' => 'BasicRepresentation', 'hydra:mapping' => [ [ '@type' => 'IriTemplateMapping', - 'variable' => 'title', - 'property' => 'title', + 'variable' => 'author', + 'property' => 'author', 'required' => false, ], [ '@type' => 'IriTemplateMapping', - 'variable' => 'title[]', + 'variable' => 'title', 'property' => 'title', 'required' => false, ], - [ - '@type' => 'IriTemplateMapping', - 'variable' => 'author', - 'property' => 'author', - 'required' => false, - ], ], ], ]); diff --git a/docs/guides/error-provider.php b/docs/guides/error-provider.php index b05890e48be..5a272ce4f4d 100644 --- a/docs/guides/error-provider.php +++ b/docs/guides/error-provider.php @@ -14,6 +14,7 @@ // rfc_7807_compliant_errors: true // ``` // To customize the API Platform response, replace the api_platform.state.error_provider with your own provider: + namespace App\ApiResource { use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; @@ -75,6 +76,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c // This is replacing the service, the "key" is important as this is the provider we // will look for when handling an exception. + namespace App\DependencyInjection { use App\State\ErrorProvider; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; @@ -86,10 +88,8 @@ function configure(ContainerConfigurator $configurator): void ->class(ErrorProvider::class) ->tag('api_platform.state_provider', ['key' => 'api_platform.state.error_provider']); } - } - namespace App\Tests { use ApiPlatform\Playground\Test\TestGuideTrait; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; @@ -103,7 +103,7 @@ public function testBookDoesNotExists(): void static::createClient()->request('GET', '/books/1', options: ['headers' => ['accept' => 'application/ld+json']]); $this->assertResponseStatusCodeSame(400); $this->assertJsonContains([ - 'detail' => 'les calculs ne sont pas bons' + 'detail' => 'les calculs ne sont pas bons', ]); } } diff --git a/docs/guides/error-resource.php b/docs/guides/error-resource.php index 0b5f34a5b45..8ce945df841 100644 --- a/docs/guides/error-resource.php +++ b/docs/guides/error-resource.php @@ -13,6 +13,7 @@ // defaults: // rfc_7807_compliant_errors: true // ``` + namespace App\ApiResource { use ApiPlatform\Metadata\ErrorResource; use ApiPlatform\Metadata\Exception\ProblemExceptionInterface; @@ -71,7 +72,6 @@ public static function provide(Operation $operation, array $uriVariables = [], a throw new MyDomainException('I am teapot'); } } - } namespace App\Tests { @@ -90,7 +90,7 @@ public function testBookDoesNotExists(): void // you can override this by looking at the [Error Provider guide](/docs/guides/error-provider). $this->assertResponseStatusCodeSame(418); $this->assertJsonContains([ - 'detail' => 'I am teapot' + 'detail' => 'I am teapot', ]); } } @@ -104,4 +104,3 @@ function request(): Request return Request::create('/books/1.jsonld', 'GET'); } } - diff --git a/docs/guides/provide-the-resource-state.php b/docs/guides/provide-the-resource-state.php index 5b0d21df27b..cfd1dd57ad1 100644 --- a/docs/guides/provide-the-resource-state.php +++ b/docs/guides/provide-the-resource-state.php @@ -38,8 +38,8 @@ public function provide(Operation $operation, array $uriVariables = [], array $c $book = new Book(); $book->id = '1'; - /** $book2 = new Book(); - $book2->id = '2'; */ + /* $book2 = new Book(); + * $book2->id = '2'; */ // As an exercise you can edit the code and add a second book in the collection. return [$book/* $book2 */]; } diff --git a/docs/src/DependencyInjection/Compiler/AttributeFilterPass.php b/docs/src/DependencyInjection/Compiler/AttributeFilterPass.php index 6e1c79167f5..caf62dce150 100644 --- a/docs/src/DependencyInjection/Compiler/AttributeFilterPass.php +++ b/docs/src/DependencyInjection/Compiler/AttributeFilterPass.php @@ -27,7 +27,7 @@ final class AttributeFilterPass implements CompilerPassInterface { use AttributeFilterExtractorTrait; - private const TAG_FILTER_NAME = 'api_platform.filter'; + private const TAG_FILTER_NAME = 'api_platform.playground.filter'; /** * {@inheritdoc} diff --git a/docs/src/DependencyInjection/Compiler/FilterPass.php b/docs/src/DependencyInjection/Compiler/FilterPass.php index 826e3db7938..c891745be29 100644 --- a/docs/src/DependencyInjection/Compiler/FilterPass.php +++ b/docs/src/DependencyInjection/Compiler/FilterPass.php @@ -34,6 +34,6 @@ public function process(ContainerBuilder $container): void { $container ->getDefinition('api_platform.filter_locator') - ->addArgument($this->findAndSortTaggedServices('api_platform.filter', $container)); + ->addArgument($this->findAndSortTaggedServices('api_platform.playground.filter', $container)); } }