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
54 changes: 54 additions & 0 deletions features/doctrine/separated_resource.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
Feature: Use state options to use an entity that is not a resource
In order to work with resources and a doctrine entity
As a client software developer
I need to retrieve a CRUD by specifying an entity class

@!mongodb
@createSchema
Scenario: Get collection
Given there are 5 separated entities
When I send a "GET" request to "/separated_entities"
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"
Then the JSON should be valid according to this schema:
"""
{
"type": "object",
"properties": {
"@context": {"pattern": "^/contexts/SeparatedEntity"},
"@id": {"pattern": "^/separated_entities"},
"@type": {"pattern": "^hydra:Collection$"},
"hydra:member": {
"type": "array",
"items": {
"type": "object"
}
},
"hydra:totalItems": {"type":"number"},
"hydra:view": {
"type": "object"
}
}
}
"""

@!mongodb
@createSchema
Scenario: Get ordered collection
Given there are 5 separated entities
When I send a "GET" request to "/separated_entities?order[value]=desc"
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 node "hydra:member[0].value" should be equal to "5"

@!mongodb
@createSchema
Scenario: Get item
Given there are 5 separated entities
When I send a "GET" request to "/separated_entities/1"
Then print last JSON response
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"
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,6 @@ public function create(string $resourceClass, string $property, array $options =
}
}

if (null === $propertyMetadata->isIdentifier()) {
$propertyMetadata = $propertyMetadata->withIdentifier(false);
}

if ($doctrineClassMetadata instanceof ClassMetadataInfo && \in_array($property, $doctrineClassMetadata->getFieldNames(), true)) {
/** @var mixed[] */
$fieldMapping = $doctrineClassMetadata->getFieldMapping($property);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use ApiPlatform\Doctrine\Orm\State\CollectionProvider;
use ApiPlatform\Doctrine\Orm\State\ItemProvider;
use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\CollectionOperationInterface;
use ApiPlatform\Metadata\DeleteOperationInterface;
Expand Down Expand Up @@ -44,7 +45,12 @@ public function create(string $resourceClass): ResourceMetadataCollection
if ($operations) {
/** @var Operation $operation */
foreach ($resourceMetadata->getOperations() as $operationName => $operation) {
if (!$this->managerRegistry->getManagerForClass($operation->getClass()) instanceof EntityManagerInterface) {
$entityClass = $operation->getClass();
if (($options = $operation->getStateOptions()) && $options instanceof Options && $options->getEntityClass()) {
$entityClass = $options->getEntityClass();
}

if (!$this->managerRegistry->getManagerForClass($entityClass) instanceof EntityManagerInterface) {
continue;
}

Expand All @@ -58,7 +64,12 @@ public function create(string $resourceClass): ResourceMetadataCollection

if ($graphQlOperations) {
foreach ($graphQlOperations as $operationName => $graphQlOperation) {
if (!$this->managerRegistry->getManagerForClass($graphQlOperation->getClass()) instanceof EntityManagerInterface) {
$entityClass = $graphQlOperation->getClass();
if (($options = $graphQlOperation->getStateOptions()) && $options instanceof Options && $options->getEntityClass()) {
$entityClass = $options->getEntityClass();
}

if (!$this->managerRegistry->getManagerForClass($entityClass) instanceof EntityManagerInterface) {
continue;
}

Expand Down
18 changes: 11 additions & 7 deletions src/Doctrine/Orm/State/CollectionProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,25 +43,29 @@ public function __construct(ResourceMetadataCollectionFactoryInterface $resource

public function provide(Operation $operation, array $uriVariables = [], array $context = []): iterable
{
$resourceClass = $operation->getClass();
$entityClass = $operation->getClass();
if (($options = $operation->getStateOptions()) && $options instanceof Options && $options->getEntityClass()) {
$entityClass = $options->getEntityClass();
}

/** @var EntityManagerInterface $manager */
$manager = $this->managerRegistry->getManagerForClass($resourceClass);
$manager = $this->managerRegistry->getManagerForClass($entityClass);

$repository = $manager->getRepository($resourceClass);
$repository = $manager->getRepository($entityClass);
if (!method_exists($repository, 'createQueryBuilder')) {
throw new RuntimeException('The repository class must have a "createQueryBuilder" method.');
}

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

$this->handleLinks($queryBuilder, $uriVariables, $queryNameGenerator, $context, $resourceClass, $operation);
$this->handleLinks($queryBuilder, $uriVariables, $queryNameGenerator, $context, $entityClass, $operation);

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

if ($extension instanceof QueryResultCollectionExtensionInterface && $extension->supportsResult($resourceClass, $operation, $context)) {
return $extension->getResult($queryBuilder, $resourceClass, $operation, $context);
if ($extension instanceof QueryResultCollectionExtensionInterface && $extension->supportsResult($entityClass, $operation, $context)) {
return $extension->getResult($queryBuilder, $entityClass, $operation, $context);
}
}

Expand Down
20 changes: 12 additions & 8 deletions src/Doctrine/Orm/State/ItemProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,30 +43,34 @@ public function __construct(ResourceMetadataCollectionFactoryInterface $resource

public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?object
{
$resourceClass = $operation->getClass();
$entityClass = $operation->getClass();
if (($options = $operation->getStateOptions()) && $options instanceof Options && $options->getEntityClass()) {
$entityClass = $options->getEntityClass();
}

/** @var EntityManagerInterface $manager */
$manager = $this->managerRegistry->getManagerForClass($resourceClass);
$manager = $this->managerRegistry->getManagerForClass($entityClass);

$fetchData = $context['fetch_data'] ?? true;
if (!$fetchData) {
return $manager->getReference($resourceClass, $uriVariables);
return $manager->getReference($entityClass, $uriVariables);
}

$repository = $manager->getRepository($resourceClass);
$repository = $manager->getRepository($entityClass);
if (!method_exists($repository, 'createQueryBuilder')) {
throw new RuntimeException('The repository class must have a "createQueryBuilder" method.');
}

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

$this->handleLinks($queryBuilder, $uriVariables, $queryNameGenerator, $context, $resourceClass, $operation);
$this->handleLinks($queryBuilder, $uriVariables, $queryNameGenerator, $context, $entityClass, $operation);

foreach ($this->itemExtensions as $extension) {
$extension->applyToItem($queryBuilder, $queryNameGenerator, $resourceClass, $uriVariables, $operation, $context);
$extension->applyToItem($queryBuilder, $queryNameGenerator, $entityClass, $uriVariables, $operation, $context);

if ($extension instanceof QueryResultItemExtensionInterface && $extension->supportsResult($resourceClass, $operation, $context)) {
return $extension->getResult($queryBuilder, $resourceClass, $operation, $context);
if ($extension instanceof QueryResultItemExtensionInterface && $extension->supportsResult($entityClass, $operation, $context)) {
return $extension->getResult($queryBuilder, $entityClass, $operation, $context);
}
}

Expand Down
10 changes: 5 additions & 5 deletions src/Doctrine/Orm/State/LinksHandlerTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,17 @@ trait LinksHandlerTrait
{
use CommonLinksHandlerTrait;

private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, QueryNameGenerator $queryNameGenerator, array $context, string $resourceClass, Operation $operation): void
private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, QueryNameGenerator $queryNameGenerator, array $context, string $entityClass, Operation $operation): void
{
if (!$identifiers) {
return;
}

$manager = $this->managerRegistry->getManagerForClass($resourceClass);
$doctrineClassMetadata = $manager->getClassMetadata($resourceClass);
$manager = $this->managerRegistry->getManagerForClass($entityClass);
$doctrineClassMetadata = $manager->getClassMetadata($entityClass);
$alias = $queryBuilder->getRootAliases()[0];

$links = $this->getLinks($resourceClass, $operation, $context);
$links = $this->getLinks($entityClass, $operation, $context);

if (!$links) {
return;
Expand All @@ -54,7 +54,7 @@ private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, Que

if (!$link->getFromProperty() && !$link->getToProperty()) {
$doctrineClassMetadata = $manager->getClassMetadata($link->getFromClass());
$currentAlias = $link->getFromClass() === $resourceClass ? $alias : $queryNameGenerator->generateJoinAlias($alias);
$currentAlias = $link->getFromClass() === $entityClass ? $alias : $queryNameGenerator->generateJoinAlias($alias);

foreach ($identifierProperties as $identifierProperty) {
$placeholder = $queryNameGenerator->generateParameterName($identifierProperty);
Expand Down
37 changes: 37 additions & 0 deletions src/Doctrine/Orm/State/Options.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?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\Doctrine\Orm\State;

use ApiPlatform\State\OptionsInterface;

class Options implements OptionsInterface
{
public function __construct(
protected ?string $entityClass = null,
) {
}

public function getEntityClass(): ?string
{
return $this->entityClass;
}

public function withEntityClass(?string $entityClass): self
{
$self = clone $this;
$self->entityClass = $entityClass;

return $self;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

use ApiPlatform\Elasticsearch\State\CollectionProvider;
use ApiPlatform\Elasticsearch\State\ItemProvider;
use ApiPlatform\Elasticsearch\State\OptionsInterface;
use ApiPlatform\Elasticsearch\State\Options;
use ApiPlatform\Metadata\CollectionOperationInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
Expand Down Expand Up @@ -48,7 +48,7 @@ public function create(string $resourceClass): ResourceMetadataCollection
foreach ($resourceMetadata->getOperations() as $operationName => $operation) {
if (null !== ($elasticsearch = $operation->getElasticsearch())) {
$solution = $elasticsearch
? sprintf('Pass an instance of %s to $stateOptions instead', OptionsInterface::class)
? sprintf('Pass an instance of %s to $stateOptions instead', Options::class)
: 'You will have to remove it when upgrading to v4';
trigger_deprecation('api-platform/core', '3.1', sprintf('Setting "elasticsearch" in Operation is deprecated. %s', $solution));
}
Expand All @@ -72,7 +72,7 @@ public function create(string $resourceClass): ResourceMetadataCollection
foreach ($graphQlOperations as $operationName => $graphQlOperation) {
if (null !== ($elasticsearch = $graphQlOperation->getElasticsearch())) {
$solution = $elasticsearch
? sprintf('Pass an instance of %s to $stateOptions instead', OptionsInterface::class)
? sprintf('Pass an instance of %s to $stateOptions instead', Options::class)
: 'You will have to remove it when upgrading to v4';
trigger_deprecation('api-platform/core', '3.1', sprintf('Setting "elasticsearch" in GraphQlOperation is deprecated. %s', $solution));
}
Expand Down
2 changes: 2 additions & 0 deletions src/Elasticsearch/State/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

namespace ApiPlatform\Elasticsearch\State;

use ApiPlatform\State\OptionsInterface;

class Options implements OptionsInterface
{
public function __construct(
Expand Down
23 changes: 0 additions & 23 deletions src/Elasticsearch/State/OptionsInterface.php

This file was deleted.

8 changes: 7 additions & 1 deletion src/GraphQl/Type/FieldsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\GraphQl\Type;

use ApiPlatform\Api\ResourceClassResolverInterface;
use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface;
use ApiPlatform\GraphQl\Type\Definition\TypeInterface;
use ApiPlatform\Metadata\GraphQl\Mutation;
Expand Down Expand Up @@ -399,7 +400,12 @@ private function getFilterArgs(array $args, ?string $resourceClass, string $root
continue;
}

foreach ($this->filterLocator->get($filterId)->getDescription($resourceClass) as $key => $value) {
$entityClass = $resourceClass;
if (($options = $resourceOperation->getStateOptions()) && $options instanceof Options && $options->getEntityClass()) {
$entityClass = $options->getEntityClass();
}

foreach ($this->filterLocator->get($filterId)->getDescription($entityClass) as $key => $value) {
$nullable = isset($value['required']) ? !$value['required'] : true;
$filterType = \in_array($value['type'], Type::$builtinTypes, true) ? new Type($value['type'], $nullable) : new Type('object', $nullable, $value['type']);
$graphqlFilterType = $this->convertType($filterType, false, $resourceOperation, $rootOperation, $resourceClass, $rootResource, $property, $depth);
Expand Down
6 changes: 6 additions & 0 deletions src/Hydra/Serializer/CollectionFiltersNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use ApiPlatform\Api\FilterInterface;
use ApiPlatform\Api\FilterLocatorTrait;
use ApiPlatform\Api\ResourceClassResolverInterface;
use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use Psr\Container\ContainerInterface;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
Expand Down Expand Up @@ -87,6 +88,11 @@ public function normalize(mixed $object, string $format = null, array $context =
$currentFilters[] = $filter;
}
}

if (($options = $operation?->getStateOptions()) && $options instanceof Options && $options->getEntityClass()) {
$resourceClass = $options->getEntityClass();
}

if ($currentFilters) {
$data['hydra:search'] = $this->getSearch($resourceClass, $requestParts, $currentFilters);
}
Expand Down
Loading