From 01d79bbe8f4cfae8f255e3a8d3c269bd5493b7d6 Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 14 Sep 2023 16:50:49 +0200 Subject: [PATCH] temp --- docs/composer.json | 5 +- docs/config/packages/doctrine.yaml | 2 +- docs/config/packages/framework.yaml | 19 +- docs/guides/declare-a-resource.php | 1 - docs/guides/doctrine-entity-as-resource.php | 110 ++++++++ docs/guides/doctrine-orm-service-filter.php | 115 -------- ...-filter.php => doctrine-search-filter.php} | 31 ++- ...k-a-persistence-layer-with-a-processor.php | 45 ++- .../secure-a-resource-with-custom-voters.php | 78 ------ docs/guides/use-doctrine-orm-filters.php | 262 ------------------ docs/guides/use-validation-groups.php | 67 ----- docs/src/Kernel.php | 8 +- 12 files changed, 192 insertions(+), 551 deletions(-) create mode 100644 docs/guides/doctrine-entity-as-resource.php delete mode 100644 docs/guides/doctrine-orm-service-filter.php rename docs/guides/{use-doctrine-search-filter.php => doctrine-search-filter.php} (83%) delete mode 100644 docs/guides/secure-a-resource-with-custom-voters.php delete mode 100644 docs/guides/use-doctrine-orm-filters.php delete mode 100644 docs/guides/use-validation-groups.php diff --git a/docs/composer.json b/docs/composer.json index dc5f5eaa3f2..8321efc7f30 100644 --- a/docs/composer.json +++ b/docs/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "api-platform/core": "^3.1", + "api-platform/core": "dev-main", "symfony/expression-language": "6.2.*", "nelmio/cors-bundle": "^2.2", "phpstan/phpdoc-parser": "^1.15", @@ -43,5 +43,6 @@ }, "require-dev": { "phpunit/phpunit": "^10" - } + }, + "minimum-stability": "dev" } diff --git a/docs/config/packages/doctrine.yaml b/docs/config/packages/doctrine.yaml index 2e58fa97242..1d6e342f607 100644 --- a/docs/config/packages/doctrine.yaml +++ b/docs/config/packages/doctrine.yaml @@ -1,6 +1,6 @@ doctrine: dbal: - url: 'sqlite:///%kernel.project_dir%/var/data.db' + url: 'sqlite:///%kernel.project_dir%/var/%guide%.db' orm: auto_generate_proxy_classes: true naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware diff --git a/docs/config/packages/framework.yaml b/docs/config/packages/framework.yaml index 09d96bc3b74..fc2237ad0c0 100644 --- a/docs/config/packages/framework.yaml +++ b/docs/config/packages/framework.yaml @@ -1,10 +1,13 @@ when@test: - framework: - test: true + framework: + test: true api_platform: - formats: - jsonld: ['application/ld+json'] - json: ['application/json'] - defaults: - extraProperties: - standard_put: true + formats: + jsonld: ['application/ld+json'] + json: ['application/json'] + event_listeners_backward_compatibility_layer: false + keep_legacy_inflector: false + defaults: + extra_properties: + rfc_7807_compliant_errors: true + standard_put: true diff --git a/docs/guides/declare-a-resource.php b/docs/guides/declare-a-resource.php index f2a2a87bf9f..02a26c15d7d 100644 --- a/docs/guides/declare-a-resource.php +++ b/docs/guides/declare-a-resource.php @@ -5,7 +5,6 @@ // position: 1 // executable: true // tags: design -// homepage: true // --- // # Declare a Resource diff --git a/docs/guides/doctrine-entity-as-resource.php b/docs/guides/doctrine-entity-as-resource.php new file mode 100644 index 00000000000..d400695787a --- /dev/null +++ b/docs/guides/doctrine-entity-as-resource.php @@ -0,0 +1,110 @@ +id; + } + } +} + +namespace App\Playground { + + use Symfony\Component\HttpFoundation\Request; + + function request(): Request + { + // Persistence is automatic, you can try to create or read data: + return Request::create('/books?order[id]=desc', 'GET'); + return Request::create('/books/1', 'GET'); + return Request::create(uri: '/books', method: 'POST', server: ['CONTENT_TYPE' => 'application/ld+json'], content: json_encode(['id' => 1, 'title' => 'API Platform rocks.'])); + } +} + +namespace DoctrineMigrations { + + use Doctrine\DBAL\Schema\Schema; + use Doctrine\Migrations\AbstractMigration; + + final class Migration extends AbstractMigration + { + public function up(Schema $schema): void + { + $this->addSql('CREATE TABLE book (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, title VARCHAR(255) NOT NULL)'); + } + } +} + +namespace App\Fixtures { + + use App\Entity\Book; + use Doctrine\Bundle\FixturesBundle\Fixture; + use Doctrine\Persistence\ObjectManager; + use function Zenstruck\Foundry\anonymous; + use function Zenstruck\Foundry\repository; + + final class BookFixtures extends Fixture + { + public function load(ObjectManager $manager): void + { + $bookFactory = anonymous(Book::class); + if (repository(Book::class)->count()) { + return; + } + + $bookFactory->many(10)->create([ + 'title' => 'title' + ]); + } + } +} + +namespace App\Tests { + + use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; + use ApiPlatform\Playground\Test\TestGuideTrait; + + final class BookTest extends ApiTestCase + { + use TestGuideTrait; + + public function testGet(): void + { + static::createClient()->request('GET', '/books.jsonld'); + $this->assertResponseIsSuccessful(); + } + + public function testGetOne(): void + { + static::createClient()->request('GET', '/books/1.jsonld'); + $this->assertResponseIsSuccessful(); + } + } +} diff --git a/docs/guides/doctrine-orm-service-filter.php b/docs/guides/doctrine-orm-service-filter.php deleted file mode 100644 index fe46991dccf..00000000000 --- a/docs/guides/doctrine-orm-service-filter.php +++ /dev/null @@ -1,115 +0,0 @@ -services() - ->set('book.search_filter') - ->parent('api_platform.doctrine.orm.search_filter') - ->args([['title' => null]]) - ->tag('api_platform.filter') - ->autowire(false) - ->autoconfigure(false) - ->public(false) - ; - } -} - -namespace App\Playground { - use Symfony\Component\HttpFoundation\Request; - - function request(): Request - { - return Request::create('/books.jsonld', 'GET'); - } -} - -namespace DoctrineMigrations { - use Doctrine\DBAL\Schema\Schema; - use Doctrine\Migrations\AbstractMigration; - - final class Migration extends AbstractMigration - { - public function up(Schema $schema): void - { - $this->addSql('CREATE TABLE book (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, title VARCHAR(255) NOT NULL)'); - } - - public function down(Schema $schema): void - { - $this->addSql('DROP TABLE book'); - } - } -} - -namespace App\Tests { - use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; - use App\Entity\Book; - use ApiPlatform\Playground\Test\TestGuideTrait; - - final class BookTest extends ApiTestCase - { - use TestGuideTrait; - - public function testAsAnonymousICanAccessTheDocumentation(): void - { - static::createClient()->request('GET', '/books.jsonld'); - - $this->assertResponseIsSuccessful(); - $this->assertMatchesResourceCollectionJsonSchema(Book::class, '_api_/books{._format}_get_collection', 'jsonld'); - $this->assertJsonContains([ - 'hydra:search' => [ - '@type' => 'hydra:IriTemplate', - 'hydra:template' => '/books.jsonld{?title,title[]}', - 'hydra:variableRepresentation' => 'BasicRepresentation', - 'hydra:mapping' => [ - [ - '@type' => 'IriTemplateMapping', - 'variable' => 'title', - 'property' => 'title', - 'required' => false, - ], - [ - '@type' => 'IriTemplateMapping', - 'variable' => 'title[]', - 'property' => 'title', - 'required' => false, - ], - ], - ], - ]); - } - } -} diff --git a/docs/guides/use-doctrine-search-filter.php b/docs/guides/doctrine-search-filter.php similarity index 83% rename from docs/guides/use-doctrine-search-filter.php rename to docs/guides/doctrine-search-filter.php index d2a8c477b52..cc64965a11f 100644 --- a/docs/guides/use-doctrine-search-filter.php +++ b/docs/guides/doctrine-search-filter.php @@ -1,7 +1,7 @@ addSql('CREATE TABLE book (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, title VARCHAR(255) NOT NULL, author VARCHAR(255) NOT NULL)'); } + } +} - public function down(Schema $schema): void +namespace App\Fixtures { + + use App\Entity\Book; + use Doctrine\Bundle\FixturesBundle\Fixture; + use Doctrine\Persistence\ObjectManager; + use function Zenstruck\Foundry\anonymous; + use function Zenstruck\Foundry\repository; + use function Zenstruck\Foundry\faker; + + final class BookFixtures extends Fixture + { + public function load(ObjectManager $manager): void { - $this->addSql('DROP TABLE book'); + $bookFactory = anonymous(Book::class); + if (repository(Book::class)->count()) { + return; + } + + $bookFactory->many(10)->create(fn() => + [ + 'title' => faker()->name(), + 'author' => faker()->firstName(), + ] + ); } } } diff --git a/docs/guides/hook-a-persistence-layer-with-a-processor.php b/docs/guides/hook-a-persistence-layer-with-a-processor.php index 903e3fa8bcd..836478db5e1 100644 --- a/docs/guides/hook-a-persistence-layer-with-a-processor.php +++ b/docs/guides/hook-a-persistence-layer-with-a-processor.php @@ -1,28 +1,32 @@ id; + file_put_contents(sprintf('book-%s.json', $id), json_encode($data)); return $data; } } + + final class BookProvider implements ProviderInterface { + public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?Book + { + if ($operation instanceof CollectionInterface) { + throw new \RuntimeException('Not supported.'); + } + + $file = sprintf('book-%s.json', $uriVariables['id']); + if (!file_exists($file)) { + return null; + } + + $data = json_decode(file_get_contents($file)); + return new Book($data->id, $data->title); + } + } } +namespace App\Playground { + use Symfony\Component\HttpFoundation\Request; + function request(): Request + { + return Request::create(uri: '/books', method: 'POST', server: ['CONTENT_TYPE' => 'application/ld+json'], content: json_encode(['id' => '1', 'title' => 'API Platform rocks.'])); + } +} diff --git a/docs/guides/secure-a-resource-with-custom-voters.php b/docs/guides/secure-a-resource-with-custom-voters.php deleted file mode 100644 index 88b10d100d3..00000000000 --- a/docs/guides/secure-a-resource-with-custom-voters.php +++ /dev/null @@ -1,78 +0,0 @@ -security = $security; - } - - protected function supports($attribute, $subject): bool - { - // It supports several attributes related to our Resource access control. - $supportsAttribute = in_array($attribute, ['BOOK_CREATE', 'BOOK_READ', 'BOOK_EDIT', 'BOOK_DELETE']); - $supportsSubject = $subject instanceof Book; - - return $supportsAttribute && $supportsSubject; - } - - /** - * @param string $attribute - * @param Book $subject - * @param TokenInterface $token - * @return bool - */ - protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool - { - /** ... check if the user is anonymous ... **/ - - switch ($attribute) { - case 'BOOK_CREATE': - if ( $this->security->isGranted(Role::ADMIN) ) { return true; } // only admins can create books - break; - case 'BOOK_READ': - /** ... other autorization rules ... **/ - } - - return false; - } - } -} - -namespace App\ApiResource { - use ApiPlatform\Metadata\ApiResource; - use ApiPlatform\Metadata\Delete; - use ApiPlatform\Metadata\Get; - use ApiPlatform\Metadata\GetCollection; - use ApiPlatform\Metadata\Post; - use ApiPlatform\Metadata\Put; - - #[ApiResource(security: "is_granted('ROLE_USER')")] - // We can then use the `is_granted` expression with our access control attributes: - #[Get(security: "is_granted('BOOK_READ', object)")] - #[Put(security: "is_granted('BOOK_EDIT', object)")] - #[Delete(security: "is_granted('BOOK_DELETE', object)")] - // On a collection, you need to [implement a Provider](provide-the-resource-state) to filter the collection manually. - #[GetCollection] - // `object` is empty uppon creation, we use `securityPostDenormalize` to get the denormalized object. - #[Post(securityPostDenormalize: "is_granted('BOOK_CREATE', object)")] - class Book - { - // ... - } -} diff --git a/docs/guides/use-doctrine-orm-filters.php b/docs/guides/use-doctrine-orm-filters.php deleted file mode 100644 index 418da97eed4..00000000000 --- a/docs/guides/use-doctrine-orm-filters.php +++ /dev/null @@ -1,262 +0,0 @@ -id; - } - } - - /* - * Each Book is related to a User, supposedly allowed to authenticate. - */ - #[ApiResource] - #[ORM\Entity] - /* - * This entity is restricted by current user: only current user books will be shown (cf. UserFilter). - */ - #[UserAware(userFieldName: 'user_id')] - class Book - { - #[ORM\Id, ORM\Column, ORM\GeneratedValue] - private ?int $id = null; - - #[ORM\ManyToOne(User::class)] - #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id')] - public User $user; - - #[ORM\Column] - public ?string $title = null; - - public function getId(): ?int - { - return $this->id; - } - } -} - -namespace App\Attribute { - use Attribute; - - /* - * The UserAware attribute restricts entities to the current user. - */ - #[Attribute(Attribute::TARGET_CLASS)] - final class UserAware - { - public ?string $userFieldName = null; - } -} - -namespace App\Filter { - use App\Attribute\UserAware; - use Doctrine\ORM\Mapping\ClassMetadata; - use Doctrine\ORM\Query\Filter\SQLFilter; - - /* - * The UserFilter adds a `AND user_id = :user_id` in the SQL query. - */ - final class UserFilter extends SQLFilter - { - public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string - { - /* - * The Doctrine filter is called for any query on any entity. - * Check if the current entity is "user aware" (marked with an attribute). - */ - $userAware = $targetEntity->getReflectionClass()->getAttributes(UserAware::class)[0] ?? null; - - $fieldName = $userAware?->getArguments()['userFieldName'] ?? null; - if ('' === $fieldName || is_null($fieldName)) { - return ''; - } - - try { - /* - * Don't worry, getParameter automatically escapes parameters - */ - $userId = $this->getParameter('id'); - } catch (\InvalidArgumentException $e) { - /* - * No user ID has been defined - */ - return ''; - } - - if (empty($fieldName) || empty($userId)) { - return ''; - } - - return sprintf('%s.%s = %s', $targetTableAlias, $fieldName, $userId); - } - } -} - -namespace App\EventSubscriber { - use App\Entity\User; - use Doctrine\Persistence\ObjectManager; - use Symfony\Component\EventDispatcher\EventSubscriberInterface; - use Symfony\Component\HttpKernel\KernelEvents; - - /* - * Retrieve the current user id and set it as SQL query parameter. - */ - final class UserAwareEventSubscriber implements EventSubscriberInterface - { - public function __construct(private readonly ObjectManager $em) - { - } - - public static function getSubscribedEvents(): array - { - return [ - KernelEvents::REQUEST => 'onKernelRequest', - ]; - } - - public function onKernelRequest(): void - { - /* - * You should retrieve the current user using the TokenStorage service. - * In this example, the user is forced by username to keep this guide simple. - */ - $user = $this->em->getRepository(User::class)->findOneBy(['username' => 'jane.doe']); - $filter = $this->em->getFilters()->enable('user_filter'); - $filter->setParameter('id', $user->getId()); - } - } -} - - namespace App\DependencyInjection { - - use App\EventSubscriber\UserAwareEventSubscriber; - use App\Filter\UserFilter; - use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; - use function Symfony\Component\DependencyInjection\Loader\Configurator\service; - - function configure(ContainerConfigurator $configurator) { - $services = $configurator->services(); - $services->set(UserAwareEventSubscriber::class) - ->args([service('doctrine.orm.default_entity_manager')]) - ->tag('kernel.event_subscriber') - ; - $configurator->extension('doctrine', [ - 'orm' => [ - 'filters' => [ - 'user_filter' => [ - 'class' => UserFilter::class, - 'enabled' => true, - ], - ], - ], - ]); - - } - } - -namespace App\Playground { - use Symfony\Component\HttpFoundation\Request; - - function request(): Request - { - return Request::create('/books.jsonld', 'GET'); - } -} - -namespace DoctrineMigrations { - use Doctrine\DBAL\Schema\Schema; - use Doctrine\Migrations\AbstractMigration; - - final class Migration extends AbstractMigration - { - public function up(Schema $schema): void - { - $this->addSql('CREATE TABLE user (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, username VARCHAR(255) NOT NULL)'); - $this->addSql('CREATE TABLE book (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, title VARCHAR(255) NOT NULL, user_id INTEGER NOT NULL, FOREIGN KEY (user_id) REFERENCES user (id))'); - } - } -} - -namespace App\Fixtures { - use App\Entity\Book; - use App\Entity\User; - use Doctrine\Bundle\FixturesBundle\Fixture; - use Doctrine\Persistence\ObjectManager; - use function Zenstruck\Foundry\anonymous; - - final class BookFixtures extends Fixture - { - public function load(ObjectManager $manager): void - { - $userFactory = anonymous(User::class); - $johnDoe = $userFactory->create(['username' => 'john.doe']); - $janeDoe = $userFactory->create(['username' => 'jane.doe']); - - $bookFactory = anonymous(Book::class); - $bookFactory->many(10)->create([ - 'title' => 'title', - 'user' => $johnDoe - ]); - $bookFactory->many(10)->create([ - 'title' => 'title', - 'user' => $janeDoe - ]); - } - } -} - -namespace App\Tests { - use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; - use App\Entity\Book; - use ApiPlatform\Playground\Test\TestGuideTrait; - - final class BookTest extends ApiTestCase - { - use TestGuideTrait; - - public function testAsAnonymousICanAccessTheDocumentation(): void - { - $response = static::createClient()->request('GET', '/books.jsonld'); - - $this->assertResponseIsSuccessful(); - $this->assertMatchesResourceCollectionJsonSchema(Book::class, '_api_/books{._format}_get_collection', 'jsonld'); - $this->assertNotSame(0, $response->toArray(false)['hydra:totalItems'], 'The collection is empty.'); - $this->assertJsonContains([ - 'hydra:totalItems' => 10, - ]); - } - } -} diff --git a/docs/guides/use-validation-groups.php b/docs/guides/use-validation-groups.php deleted file mode 100644 index 8bbb2a051da..00000000000 --- a/docs/guides/use-validation-groups.php +++ /dev/null @@ -1,67 +0,0 @@ - ['a', 'b']], - operations: [ - // When configured on a specific operation the configuration takes precedence over the one declared on the ApiResource. - // You can use a [callable](https://www.php.net/manual/en/language.types.callable.php) instead of strings. - new Get(validationContext: ['groups' => [Book::class, 'validationGroups']]), - new GetCollection(), - // You sometimes want to specify in which order groups must be tested against. On the Post operation, we use a Symfony service - // to use a [group sequence](http://symfony.com/doc/current/validation/sequence_provider.html). - new Post(validationContext: ['groups' => MySequencedGroup::class]) - ] - )] - final class Book - { - #[Assert\NotBlank(groups: ['a'])] - public string $name; - - #[Assert\NotNull(groups: ['b'])] - public string $author; - - /** - * Return dynamic validation groups. - * - * @param self $book Contains the instance of Book to validate. - * - * @return string[] - */ - public static function validationGroups(self $book) - { - return ['a']; - } - } -} - -namespace App\Validator { - use Symfony\Component\Validator\Constraints\GroupSequence; - - final class MySequencedGroup - { - public function __invoke(): GroupSequence - { - return new GroupSequence(['a', 'b']); // now, no matter which is first in the class declaration, it will be tested in this order. - } - } -} - -// To go further, read the guide on [Validating data on a Delete operation](./validate-data-on-a-delete-operation) diff --git a/docs/src/Kernel.php b/docs/src/Kernel.php index f190515f4c3..f11e2472d0d 100644 --- a/docs/src/Kernel.php +++ b/docs/src/Kernel.php @@ -43,6 +43,7 @@ class Kernel extends BaseKernel { use MicroKernelTrait; private $declaredClasses = []; + private string $guide; public function __construct(string $environment, bool $debug, string $guide = null) { @@ -108,12 +109,9 @@ private function configureContainer(ContainerConfigurator $container, LoaderInte $builder->addCompilerPass(new AttributeFilterPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 101); $builder->addCompilerPass(new FilterPass()); - $container->parameters()->set( - 'database_url', - sprintf('sqlite:///%s/%s', $this->getCacheDir(), 'data.db') - ); + $container->parameters()->set('guide', $this->guide); - $services->set('doctrine.orm.default_metadata_driver', StaticMappingDriver::class)->args(['$classes' => $resources]); + $services->set('doctrine.orm.default_metadata_driver', StaticMappingDriver::class)->args(['$classes' => $entities]); if (\function_exists('App\DependencyInjection\configure')) { configure($container);