-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add documentation about doctrine query extensions #120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
92ddec1
ffef6c8
e768e62
a9b640e
46e0a5c
03b7153
4583492
f016600
2d805d7
7775488
c325bbc
7e25652
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| # Extensions | ||
|
|
||
| API Platform Core provides a system to extend queries on items and collections. | ||
|
|
||
| Extensions are specific to Doctrine, and therefore, the Doctrine ORM support must be enabled to use this feature. If you use custom providers it's up to you to implement your own extension system or not. | ||
|
|
||
| ## Custom Extension | ||
|
|
||
| Custom extensions must implement the `ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface` and / or the `ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface` interfaces, to be run when querying for a collection of items and when querying for an item respectively. | ||
|
|
||
| If you use [custom data providers](data-providers.md), they must support extensions and be aware of active extensions to work properly. | ||
|
|
||
| ## Example | ||
|
|
||
| In the following example, we will see how to always get the offers owned by the current user. We will set up an exception, whenever the user has the `ROLE_ADMIN`. | ||
|
|
||
| Given these two entities: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same concern about formatting here... (and many other places below) |
||
|
|
||
| ```php | ||
| <?php | ||
| // src/AppBundle/Entity/User.php | ||
|
|
||
| namespace AppBundle\Entity; | ||
|
|
||
| use ApiPlatform\Core\Annotation\ApiResource; | ||
|
|
||
| /** | ||
| * @ApiResource | ||
| */ | ||
| class User | ||
| { | ||
| // ... | ||
| } | ||
| ``` | ||
|
|
||
| ```php | ||
| <?php | ||
| // src/AppBundle/Entity/Offer.php | ||
|
|
||
| namespace AppBundle\Entity; | ||
|
|
||
| use ApiPlatform\Core\Annotation\ApiResource; | ||
|
|
||
| /** | ||
| * @ApiResource | ||
| */ | ||
| class Offer | ||
| { | ||
| /** | ||
| * @var User | ||
| * @ORM\ManyToOne(targetEntity="User") | ||
| */ | ||
| private $user; | ||
|
|
||
| //... | ||
| } | ||
| ``` | ||
|
|
||
| ```php | ||
| <?php | ||
| // src/AppBundle/Doctrine/ORM/Extension/CurrentUserExtension.php | ||
|
|
||
| namespace AppBundle\Doctrine\ORM\Extension; | ||
|
|
||
| use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface; | ||
| use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface; | ||
| use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; | ||
| use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; | ||
| use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; | ||
| use AppBundle\Entity\Offer; | ||
| use AppBundle\Entity\User; | ||
| use Doctrine\ORM\QueryBuilder; | ||
| use Doctrine\ORM\Query\Expr; | ||
| use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; | ||
| use Symfony\Component\Security\Core\Authorization\AuthorizationChecker; | ||
|
|
||
| final class CurrentUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @api-platform/core-team IIUC the docs should contain best practices, is it one to implements both in one class ?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's ok to me for this use case. Creating two classes with duplicated code isn't ok however. So I would keep it as is. |
||
| { | ||
| private $authorizationChecker; | ||
| private $propertyNameCollectionFactory; | ||
| private $propertyMetadataFactory; | ||
| private $token; | ||
|
|
||
| public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, TokenStorageInterface $token, AuthorizationChecker $checker) | ||
| { | ||
| $this->propertyMetadataFactory = $propertyMetadataFactory; | ||
| $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; | ||
| $this->token = $token; | ||
| $this->authorizationChecker = $checker; | ||
| } | ||
|
|
||
| /** | ||
| * {@inheritdoc} | ||
| */ | ||
| public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null) | ||
| { | ||
| $this->addWhere($queryBuilder, $resourceClass); | ||
| } | ||
|
|
||
| /** | ||
| * {@inheritdoc} | ||
| */ | ||
| public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null) | ||
| { | ||
| $this->addWhere($queryBuilder, $resourceClass); | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * @param QueryBuilder $queryBuilder | ||
| * @param string $resourceClass | ||
| */ | ||
| private function addWhere(QueryBuilder $queryBuilder, string $resourceClass) | ||
| { | ||
| $user = $this->token->getToken()->getUser(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you should test if
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alternatively you can add a firewall rule to check that the user is connected. |
||
| if ($user instanceof User && Offer::class === $resourceClass && !$this->authorizationChecker->isGranted('ROLE_ADMIN')) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You need to do something if |
||
| $rootAlias = $queryBuilder->getRootAliases()[0]; | ||
| $queryBuilder->andWhere(sprintf('%s.user = :current_user', $rootAlias)); | ||
| $queryBuilder->setParameter('current_user', $user->getId()); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| ``` | ||
|
|
||
| Finally register the custom extension: | ||
|
|
||
| ```yaml | ||
| # app/config/services.yml | ||
|
|
||
| services: | ||
| app.doctrine.orm.query_extension.current_user: | ||
| class: AppBundle\Doctrine\ORM\Extension\CurrentUserExtension | ||
| public: false | ||
| arguments: | ||
| - '@api_platform.metadata.property.name_collection_factory' | ||
| - '@api_platform.metadata.property.metadata_factory' | ||
| - '@security.token_storage' | ||
| - '@security.authorization_checker' | ||
| tags: | ||
| - { name: api_platform.doctrine.orm.query_extension.collection, priority: 9 } | ||
| - { name: api_platform.doctrine.orm.query_extension.item } | ||
| ``` | ||
|
|
||
| Thanks to the `api_platform.doctrine.orm.query_extension.collection` tag, API Platform will register this service as a collection extension. The `api_platform.doctrine.orm.query_extension.item` do the same thing for items. | ||
|
|
||
| Notice the priority level for the `api_platform.doctrine.orm.query_extension.collection` tag. When an extension implements the `ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultCollectionExtensionInterface` or the `ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultItemExtensionInterface` interface to return results by itself, any lower priority extension will not be executed. Because the pagination is enabled by default with a priority of 8, the priority of the `app.doctrine.orm.query_extension.current_user` service must be at least 9 to ensure its execution. | ||
|
|
||
| ### Blocking Anonymous Users | ||
|
|
||
| This example adds a `WHERE` clause condition only when a fully authenticated user without `ROLE_ADMIN` tries to access to a resource. It means that anonymous users will be able to access to all data. To prevent this potential security issue, the API must ensure that the current user is authenticated. | ||
|
|
||
| To secure the access to endpoints, use the following access control rule: | ||
|
|
||
| ```yaml | ||
| # app/config/security.yml | ||
|
|
||
| security: | ||
| # ... | ||
|
|
||
| access_control: | ||
| # ... | ||
| - { path: ^/offers, roles: IS_AUTHENTICATED_FULLY } | ||
| - { path: ^/users, roles: IS_AUTHENTICATED_FULLY } | ||
| ``` | ||
|
|
||
| Previous chapter: [Data Providers](data-providers.md) | ||
|
|
||
| Next chapter: [Security](security.md) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should really be a level 2 title?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What level would you use ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exemple
blabla
Exemple
blabla maybe ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, i prefer
### Example@SimperfitThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But if the title in the file
/index.mdisFilter upon the current userthen it is necessary to put## Filter upon the current userinstead of## Example.And change anchor in the file
/index.md.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Index.md has a "Filter upon the current user" chapter, because it is what it is, it suits better than "Example" when that what a user wants to do, he finds it instantly. that's why the "Filter upon the current user" is linked with the example anchor (By the way, it used to be a "filter upon the current user" anchor, but @dunglas made me change it for "Example". For the anchor level, since it appear on the summary, I thought it had to be the same level than the previous anchor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok