Skip to content

Commit

Permalink
Merge 16524a0 into da82888
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelbigard committed Oct 7, 2021
2 parents da82888 + 16524a0 commit d90bc1c
Show file tree
Hide file tree
Showing 14 changed files with 636 additions and 64 deletions.
191 changes: 191 additions & 0 deletions features/main/crud_uri_variables.feature
@@ -0,0 +1,191 @@
Feature: Uri Variables

@createSchema
@php8
Scenario: Create a resource Company
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/companies" with body:
"""
{
"name": "Foo Company 1"
}
"""
Then the response status code should be 201
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the header "Content-Location" should be equal to "/companies/1"
And the header "Location" should be equal to "/companies/1"
And the JSON should be equal to:
"""
{
"@context": "/contexts/Company",
"@id": "/companies/1",
"@type": "Company",
"id": 1,
"name": "Foo Company 1",
"employees": null
}
"""

@php8
Scenario: Create a second resource Company
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/companies" with body:
"""
{
"name": "Foo Company 2"
}
"""
Then the response status code should be 201

@php8
Scenario: Create first Employee
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/employees" with body:
"""
{
"name": "foo",
"company": "/companies/1"
}
"""
Then the response status code should be 201
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the header "Content-Location" should be equal to "/companies/1/employees/1"
And the header "Location" should be equal to "/companies/1/employees/1"
And the JSON should be equal to:
"""
{
"@context": "/contexts/Employee",
"@id": "/companies/1/employees/1",
"@type": "Employee",
"id": 1,
"name": "foo",
"company": "/companies/1"
}
"""

@php8
Scenario: Create second Employee
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/employees" with body:
"""
{
"name": "foo2",
"company": "/companies/2"
}
"""
Then the response status code should be 201

@php8
Scenario: Create thirf Employee
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/employees" with body:
"""
{
"name": "foo3",
"company": "/companies/2"
}
"""
Then the response status code should be 201

@php8
Scenario: Create a resource Company
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/companies" with body:
"""
{
"name": "bar",
"company": "/companies/1"
}
"""
Then the response status code should be 201

@php8
Scenario: Create a second resource Company
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/companies" with body:
"""
{
"name": "toto",
"company": "/companies/2"
}
"""
Then the response status code should be 201

@php8
Scenario: Retrieve the collection of employees
When I add "Content-Type" header equal to "application/ld+json"
And I send a "GET" request to "/companies/2/employees"
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON should be equal to:
"""
{
"@context": "/contexts/Employee",
"@id": "/companies/2/employees",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "/companies/2/employees/2",
"@type": "Employee",
"id": 2,
"name": "foo2",
"company": "/companies/2"
},
{
"@id": "/companies/2/employees/3",
"@type": "Employee",
"id": 3,
"name": "foo3",
"company": "/companies/2"
}
],
"hydra:totalItems": 2
}
"""

@php8
Scenario: Retrieve the company of an employee
When I add "Content-Type" header equal to "application/ld+json"
And I send a "GET" request to "/employees/1/company"
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON should be equal to:
"""
{
"@context": "/contexts/Company",
"@id": "/companies/1",
"@type": "Company",
"id": 1,
"name": "Foo Company 1",
"employees": null
}
"""

@php8
Scenario: Retrieve an employee
When I add "Content-Type" header equal to "application/ld+json"
And I send a "GET" request to "/companies/1/employees/1"
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON should be equal to:
"""
{
"@context": "/contexts/Employee",
"@id": "/companies/1/employees/1",
"@type": "Employee",
"id": 1,
"name": "foo",
"company": "/companies/1"
}
"""

@php8
Scenario: Trying to get an employee of wrong company
When I add "Content-Type" header equal to "application/ld+json"
And I send a "GET" request to "/companies/1/employees/2"
Then the response status code should be 404
5 changes: 5 additions & 0 deletions src/Bridge/Doctrine/Orm/State/CollectionProvider.php
Expand Up @@ -30,6 +30,8 @@
*/
final class CollectionProvider implements ProviderInterface
{
use UriVariablesHandlerTrait;

private $resourceMetadataCollectionFactory;
private $managerRegistry;
private $collectionExtensions;
Expand All @@ -56,6 +58,9 @@ public function provide(string $resourceClass, array $identifiers = [], ?string

$queryBuilder = $repository->createQueryBuilder('o');
$queryNameGenerator = new QueryNameGenerator();

$this->handleUriVariables($queryBuilder, $identifiers, $queryNameGenerator, $context, $resourceClass, $operationName);

foreach ($this->collectionExtensions as $extension) {
$extension->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);

Expand Down
26 changes: 3 additions & 23 deletions src/Bridge/Doctrine/Orm/State/ItemProvider.php
Expand Up @@ -20,9 +20,7 @@
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\State\ProviderInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Persistence\Mapping\ClassMetadata;

/**
* Item state provider using the Doctrine ORM.
Expand All @@ -32,6 +30,8 @@
*/
final class ItemProvider implements ProviderInterface
{
use UriVariablesHandlerTrait;

private $resourceMetadataCollectionFactory;
private $managerRegistry;
private $itemExtensions;
Expand Down Expand Up @@ -63,9 +63,8 @@ public function provide(string $resourceClass, array $identifiers = [], ?string

$queryBuilder = $repository->createQueryBuilder('o');
$queryNameGenerator = new QueryNameGenerator();
$doctrineClassMetadata = $manager->getClassMetadata($resourceClass);

$this->addWhereForIdentifiers($identifiers, $queryBuilder, $doctrineClassMetadata);
$this->handleUriVariables($queryBuilder, $identifiers, $queryNameGenerator, $context, $resourceClass, $operationName);

foreach ($this->itemExtensions as $extension) {
$extension->applyToItem($queryBuilder, $queryNameGenerator, $resourceClass, $identifiers, $operationName, $context);
Expand All @@ -88,23 +87,4 @@ public function supports(string $resourceClass, array $identifiers = [], ?string

return !($operation->isCollection() ?? false);
}

/**
* Add WHERE conditions to the query for one or more identifiers (simple or composite).
*/
private function addWhereForIdentifiers(array $identifiers, QueryBuilder $queryBuilder, ClassMetadata $classMetadata)
{
$alias = $queryBuilder->getRootAliases()[0];
foreach ($identifiers as $identifier => $value) {
$placeholder = ':id_'.$identifier;
$expression = $queryBuilder->expr()->eq(
"{$alias}.{$identifier}",
$placeholder
);

$queryBuilder->andWhere($expression);

$queryBuilder->setParameter($placeholder, $value, $classMetadata->getTypeOfField($identifier));
}
}
}
73 changes: 73 additions & 0 deletions src/Bridge/Doctrine/Orm/State/UriVariablesHandlerTrait.php
@@ -0,0 +1,73 @@
<?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\Bridge\Doctrine\Orm\State;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator;
use Doctrine\ORM\QueryBuilder;

trait UriVariablesHandlerTrait
{
private function handleUriVariables(QueryBuilder $queryBuilder, array $identifiers, QueryNameGenerator $queryNameGenerator, array $context, string $resourceClass, ?string $operationName = null): void
{
$operation = $context['operation'] ?? $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation($operationName);
$uriVariables = $operation->getUriVariables();

if (null === $uriVariables) {
return;
}
$alias = $queryBuilder->getRootAliases()[0];

foreach ($identifiers as $identifier => $value) {
$uriVariable = $uriVariables[$identifier] ?? $uriVariables['id'];

$placeholder = ':id_'.$identifier;
if ($inverseProperty = $uriVariable->getInverseProperty()) {
$propertyIdentifier = $uriVariable->getIdentifiers()[0];
$joinAlias = $queryNameGenerator->generateJoinAlias($inverseProperty);

$queryBuilder->join(
$uriVariable->getTargetClass(),
$joinAlias,
'with',
"$alias.$propertyIdentifier = $joinAlias.$inverseProperty"
);

$expression = $queryBuilder->expr()->eq(
"{$joinAlias}.{$propertyIdentifier}",
$placeholder
);
} elseif ($property = $uriVariable->getProperty()) {
$propertyIdentifier = $uriVariable->getIdentifiers()[0];
$joinAlias = $queryNameGenerator->generateJoinAlias($property);

$queryBuilder->join(
"$alias.$property",
$joinAlias,
);

$expression = $queryBuilder->expr()->eq(
"{$joinAlias}.{$propertyIdentifier}",
$placeholder
);
} else {
$propertyIdentifier = $uriVariable->getIdentifiers()[0];
$expression = $queryBuilder->expr()->eq(
"{$alias}.{$propertyIdentifier}", $placeholder
);
}
$queryBuilder->andWhere($expression);
$queryBuilder->setParameter($placeholder, $value);
}
}
}
2 changes: 1 addition & 1 deletion src/Core/EventListener/RespondListener.php
Expand Up @@ -107,7 +107,7 @@ public function onKernelView(ViewEvent $event): void
$this->iriConverter &&
($operation->getExtraProperties()['is_alternate_resource_metadata'] ?? false) &&
!($operation->getExtraProperties()['legacy_subresource_behavior'] ?? false)
&& !$operation->getStatus()
&& 301 === $operation->getStatus()
) {
$status = 301;

Expand Down
7 changes: 6 additions & 1 deletion src/Core/HttpCache/EventListener/AddTagsListener.php
Expand Up @@ -14,8 +14,10 @@
namespace ApiPlatform\Core\HttpCache\EventListener;

use ApiPlatform\Api\IriConverterInterface;
use ApiPlatform\Api\UrlGeneratorInterface;
use ApiPlatform\Core\Util\RequestAttributesExtractor;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\State\UriVariablesResolverTrait;
use ApiPlatform\Util\OperationRequestInitiatorTrait;
use Symfony\Component\HttpKernel\Event\ResponseEvent;

Expand All @@ -36,6 +38,8 @@
final class AddTagsListener
{
use OperationRequestInitiatorTrait;
use UriVariablesResolverTrait;

private $iriConverter;
private $xkeyEnabled;
private $xkeyGlue;
Expand Down Expand Up @@ -70,7 +74,8 @@ public function onKernelResponse(ResponseEvent $event): void
$resources = $request->attributes->get('_resources');
if (isset($attributes['collection_operation_name']) || ($attributes['subresource_context']['collection'] ?? false) || ($operation && $operation->isCollection())) {
// Allows to purge collections
$iri = $this->iriConverter->getIriFromResourceClass($attributes['resource_class']);
$identifiers = $this->getOperationIdentifiers($operation, $request->attributes->all(), $attributes['resource_class']);
$iri = $this->iriConverter->getIriFromResourceClass($attributes['resource_class'], $attributes['operation_name'] ?? null, UrlGeneratorInterface::ABS_PATH, ['identifiers_values' => $identifiers]);
$resources[$iri] = $iri;
}

Expand Down

0 comments on commit d90bc1c

Please sign in to comment.