Skip to content
  • Sponsor api-platform/core

  • Notifications You must be signed in to change notification settings
  • Fork 900

feat(doctrine): new hook for entity/document transformation #6919

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

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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
37 changes: 37 additions & 0 deletions features/doctrine/transform_model.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
Feature: Use an entity or document transformer to return the correct ressource

@createSchema
@!mongodb
Scenario: Get collection from entities
Given there is a TransformedDummy for date '2025-01-01'
When I send a "GET" request to "/transformed_dummy_entity_ressources"
Then the response status code should be 200
And the response should be in JSON
And the JSON node "hydra:totalItems" should be equal to 1

@!mongodb
Scenario: Get item from entity
Given there is a TransformedDummy for date '2025-01-01'
When I send a "GET" request to "/transformed_dummy_entity_ressources/1"
Then the response status code should be 200
And the response should be in JSON
And the JSON node "year" should exist
And the JSON node year should be equal to "2025"

@createSchema
@mongodb
Scenario: Get collection from documents
Given there is a TransformedDummy for date '2025-01-01'
When I send a "GET" request to "/transformed_dummy_document_ressources"
Then the response status code should be 200
And the response should be in JSON
And the JSON node "hydra:totalItems" should be equal to 1

@mongodb
Scenario: Get item from document
Given there is a TransformedDummy for date '2025-01-01'
When I send a "GET" request to "/transformed_dummy_document_ressources/1"
Then the response status code should be 200
And the response should be in JSON
And the JSON node "year" should exist
And the JSON node year should be equal to "2025"
30 changes: 30 additions & 0 deletions src/Doctrine/Common/State/Options.php
Original file line number Diff line number Diff line change
@@ -19,9 +19,13 @@ class Options implements OptionsInterface
{
/**
* @param mixed $handleLinks experimental callable, typed mixed as we may want a service name in the future
* @param mixed $toResourceTransformer experimental callable, typed mixed as we may want a service name in the future
* @param mixed $fromResourceTransformer experimental callable, typed mixed as we may want a service name in the future
*/
public function __construct(
protected mixed $handleLinks = null,
protected mixed $toResourceTransformer = null,
protected mixed $fromResourceTransformer = null,
) {
}

@@ -37,4 +41,30 @@ public function withHandleLinks(mixed $handleLinks): self

return $self;
}

public function getToResourceTransformer(): mixed
{
return $this->toResourceTransformer;
}

public function withToResourceTransformer(mixed $toResourceTransformer): self
{
$self = clone $this;
$self->toResourceTransformer = $toResourceTransformer;

return $self;
}

public function getFromResourceTransformer(): mixed
{
return $this->fromResourceTransformer;
}

public function withFromResourceTransformer(mixed $fromResourceTransformer): self
{
$self = clone $this;
$self->fromResourceTransformer = $fromResourceTransformer;

return $self;
}
}
1 change: 1 addition & 0 deletions src/Doctrine/Common/State/PersistProcessor.php
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ final class PersistProcessor implements ProcessorInterface
{
use ClassInfoTrait;
use LinksHandlerTrait;
use ResourceTransformerLocatorTrait;

public function __construct(private readonly ManagerRegistry $managerRegistry)
{
61 changes: 61 additions & 0 deletions src/Doctrine/Common/State/ResourceTransformerLocatorTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?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\Common\State;

use ApiPlatform\Metadata\Operation;
use Psr\Container\ContainerInterface;

/**
* Maybe merge this and LinksHandlerLocatorTrait into a OptionsHooksLocatorTrait or something similar?
*/
trait ResourceTransformerLocatorTrait
{
private ?ContainerInterface $resourceTransformerLocator;

protected function getToResourceTransformer(Operation $operation): ?callable
{
if (!($options = $operation->getStateOptions()) || !$options instanceof Options) {
return null;
}

$transformer = $options->getToResourceTransformer();
if (\is_callable($transformer)) {
return $transformer;
}

if ($this->resourceTransformerLocator && \is_string($transformer) && $this->resourceTransformerLocator->has($transformer)) {
return [$this->resourceTransformerLocator->get($transformer), 'toResource'];
}

return null;
}

protected function getFromResourceTransformer(Operation $operation): ?callable
{
if (!($options = $operation->getStateOptions()) || !$options instanceof Options) {
return null;
}

$transformer = $options->getFromResourceTransformer();
if (\is_callable($transformer)) {
return $transformer;
}

if ($this->resourceTransformerLocator && \is_string($transformer) && $this->resourceTransformerLocator->has($transformer)) {
return [$this->resourceTransformerLocator->get($transformer), 'fromResource'];
}

return null;
}
}
13 changes: 11 additions & 2 deletions src/Doctrine/Odm/State/CollectionProvider.php
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@
namespace ApiPlatform\Doctrine\Odm\State;

use ApiPlatform\Doctrine\Common\State\LinksHandlerLocatorTrait;
use ApiPlatform\Doctrine\Common\State\ResourceTransformerLocatorTrait;
use ApiPlatform\Doctrine\Odm\Extension\AggregationCollectionExtensionInterface;
use ApiPlatform\Doctrine\Odm\Extension\AggregationResultCollectionExtensionInterface;
use ApiPlatform\Metadata\Exception\RuntimeException;
@@ -32,6 +33,7 @@ final class CollectionProvider implements ProviderInterface
{
use LinksHandlerLocatorTrait;
use LinksHandlerTrait;
use ResourceTransformerLocatorTrait;

/**
* @param AggregationCollectionExtensionInterface[] $collectionExtensions
@@ -40,6 +42,7 @@ public function __construct(ResourceMetadataCollectionFactoryInterface $resource
{
$this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
$this->handleLinksLocator = $handleLinksLocator;
$this->resourceTransformerLocator = $handleLinksLocator;
$this->managerRegistry = $managerRegistry;
}

@@ -70,13 +73,19 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
$extension->applyToCollection($aggregationBuilder, $documentClass, $operation, $context);

if ($extension instanceof AggregationResultCollectionExtensionInterface && $extension->supportsResult($documentClass, $operation, $context)) {
return $extension->getResult($aggregationBuilder, $documentClass, $operation, $context);
$result = $extension->getResult($aggregationBuilder, $documentClass, $operation, $context);
break;
}
}

$attribute = $operation->getExtraProperties()['doctrine_mongodb'] ?? [];
$executeOptions = $attribute['execute_options'] ?? [];

return $aggregationBuilder->hydrate($documentClass)->execute($executeOptions);
$result = $result ?? $aggregationBuilder->hydrate($documentClass)->execute($executeOptions);

return match ($transformer = $this->getToResourceTransformer($operation)) {
null => $result,
default => array_map($transformer, iterator_to_array($result)),
};
}
}
13 changes: 11 additions & 2 deletions src/Doctrine/Odm/State/ItemProvider.php
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@
namespace ApiPlatform\Doctrine\Odm\State;

use ApiPlatform\Doctrine\Common\State\LinksHandlerLocatorTrait;
use ApiPlatform\Doctrine\Common\State\ResourceTransformerLocatorTrait;
use ApiPlatform\Doctrine\Odm\Extension\AggregationItemExtensionInterface;
use ApiPlatform\Doctrine\Odm\Extension\AggregationResultItemExtensionInterface;
use ApiPlatform\Metadata\Exception\RuntimeException;
@@ -35,6 +36,7 @@ final class ItemProvider implements ProviderInterface
{
use LinksHandlerLocatorTrait;
use LinksHandlerTrait;
use ResourceTransformerLocatorTrait;

/**
* @param AggregationItemExtensionInterface[] $itemExtensions
@@ -43,6 +45,7 @@ public function __construct(ResourceMetadataCollectionFactoryInterface $resource
{
$this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
$this->handleLinksLocator = $handleLinksLocator;
$this->resourceTransformerLocator = $handleLinksLocator;
$this->managerRegistry = $managerRegistry;
}

@@ -78,12 +81,18 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
$extension->applyToItem($aggregationBuilder, $documentClass, $uriVariables, $operation, $context);

if ($extension instanceof AggregationResultItemExtensionInterface && $extension->supportsResult($documentClass, $operation, $context)) {
return $extension->getResult($aggregationBuilder, $documentClass, $operation, $context);
$result = $extension->getResult($aggregationBuilder, $documentClass, $operation, $context);
break;
}
}

$executeOptions = $operation->getExtraProperties()['doctrine_mongodb']['execute_options'] ?? [];

return $aggregationBuilder->hydrate($documentClass)->execute($executeOptions)->current() ?: null;
$result = $result ?? ($aggregationBuilder->hydrate($documentClass)->execute($executeOptions)->current() ?: null);

return match ($transformer = $this->getToResourceTransformer($operation)) {
null => $result,
default => (null !== $result) ? $transformer($result) : null,
};
}
}
21 changes: 18 additions & 3 deletions src/Doctrine/Odm/State/Options.php
Original file line number Diff line number Diff line change
@@ -19,15 +19,17 @@
class Options extends CommonOptions implements OptionsInterface
{
/**
* @param mixed $handleLinks experimental callable, typed mixed as we may want a service name in the future
* @param mixed $handleLinks experimental callable, typed mixed as we may want a service name in the future
* @param mixed $transformFromDocument experimental callable, typed mixed as we may want a service name in the future
*
* @see LinksHandlerInterface
*/
public function __construct(
protected ?string $documentClass = null,
mixed $handleLinks = null,
mixed $handleLinks = null,
mixed $transformFromDocument = null,
) {
parent::__construct(handleLinks: $handleLinks);
parent::__construct(handleLinks: $handleLinks, toResourceTransformer: $transformFromDocument);
}

public function getDocumentClass(): ?string
@@ -42,4 +44,17 @@ public function withDocumentClass(?string $documentClass): self

return $self;
}

public function getTransformDocument(): mixed
{
return $this->getToResourceTransformer();
}

public function withTransformDocument(mixed $transformDocument): self
{
$self = clone $this;
$self->toResourceTransformer = $transformDocument;

return $self;
}
}
13 changes: 11 additions & 2 deletions src/Doctrine/Orm/State/CollectionProvider.php
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@
namespace ApiPlatform\Doctrine\Orm\State;

use ApiPlatform\Doctrine\Common\State\LinksHandlerLocatorTrait;
use ApiPlatform\Doctrine\Common\State\ResourceTransformerLocatorTrait;
use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Doctrine\Orm\Extension\QueryResultCollectionExtensionInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGenerator;
@@ -35,6 +36,7 @@ final class CollectionProvider implements ProviderInterface
{
use LinksHandlerLocatorTrait;
use LinksHandlerTrait;
use ResourceTransformerLocatorTrait;

/**
* @param QueryCollectionExtensionInterface[] $collectionExtensions
@@ -43,6 +45,7 @@ public function __construct(ResourceMetadataCollectionFactoryInterface $resource
{
$this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
$this->handleLinksLocator = $handleLinksLocator;
$this->resourceTransformerLocator = $handleLinksLocator;
$this->managerRegistry = $managerRegistry;
}

@@ -74,10 +77,16 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
$extension->applyToCollection($queryBuilder, $queryNameGenerator, $entityClass, $operation, $context);

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

return $queryBuilder->getQuery()->getResult();
$result = $result ?? $queryBuilder->getQuery()->getResult();

return match ($transformer = $this->getToResourceTransformer($operation)) {
null => $result,
default => array_map($transformer, iterator_to_array($result)),
};
}
}
13 changes: 11 additions & 2 deletions src/Doctrine/Orm/State/ItemProvider.php
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@
namespace ApiPlatform\Doctrine\Orm\State;

use ApiPlatform\Doctrine\Common\State\LinksHandlerLocatorTrait;
use ApiPlatform\Doctrine\Common\State\ResourceTransformerLocatorTrait;
use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface;
use ApiPlatform\Doctrine\Orm\Extension\QueryResultItemExtensionInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGenerator;
@@ -35,6 +36,7 @@ final class ItemProvider implements ProviderInterface
{
use LinksHandlerLocatorTrait;
use LinksHandlerTrait;
use ResourceTransformerLocatorTrait;

/**
* @param QueryItemExtensionInterface[] $itemExtensions
@@ -43,6 +45,7 @@ public function __construct(ResourceMetadataCollectionFactoryInterface $resource
{
$this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
$this->handleLinksLocator = $handleLinksLocator;
$this->resourceTransformerLocator = $handleLinksLocator;
$this->managerRegistry = $managerRegistry;
}

@@ -80,10 +83,16 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
$extension->applyToItem($queryBuilder, $queryNameGenerator, $entityClass, $uriVariables, $operation, $context);

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

return $queryBuilder->getQuery()->getOneOrNullResult();
$result = $result ?? $queryBuilder->getQuery()->getOneOrNullResult();

return match ($transformer = $this->getToResourceTransformer($operation)) {
null => $result,
default => $transformer($result),
};
}
}
Loading