Skip to content
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

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

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 6 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"
43 changes: 43 additions & 0 deletions src/Doctrine/Common/State/ModelTransformerLocatorTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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 ModelTransformerLocatorTrait
{
private ?ContainerInterface $transformEntityLocator;

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

$transformModel = $options->getTransformModel();
if (\is_callable($transformModel)) {
return $transformModel;
}

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

return null;
}
}
14 changes: 14 additions & 0 deletions src/Doctrine/Common/State/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class Options implements OptionsInterface
*/
public function __construct(
protected mixed $handleLinks = null,
protected mixed $transformModel = null,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the feature, I think it'd be a great fit with #6801 best would be that when the mapper is installed, we add a default transformer that uses the object mapper!

I'm just not a fan of the naming but this one is hard haha. A few ideas:

  • transformEntityToResource
  • entityToResourceMapper
  • mapper
  • resourceTransformer

I think that it's missing a way to transform a resource to an entity when persisting data (POST/PATCH) WDYT?

Last but not least, you should work on an interface that'd be used to autoconfigure the "transformEntityLocator", it would surely help with naming things.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the feature, I think it'd be a great fit with #6801 best would be that when the mapper is installed, we add a default transformer that uses the object mapper!

I'm just not a fan of the naming but this one is hard haha. A few ideas:

* transformEntityToResource

* entityToResourceMapper

* mapper

* resourceTransformer

Yeah, i'm not a fan of it either - tried to find a name that works with both entities or documents, but model feels lame, and I didn't want to use mapper to avoid confusion with what you're preparing on that side.

How about resourceBuilder or something along those lines?

I think that it's missing a way to transform a resource to an entity when persisting data (POST/PATCH) WDYT?

Wouldn't it be redundant when we already have entityClass + handlelinks?

Last but not least, you should work on an interface that'd be used to autoconfigure the "transformEntityLocator", it would surely help with naming things.

I'll look into this. I was not sure about that part, actually i was wondering (and i actually left a comment in the code) if this locator trait is better by itself or if it should be merged with LinksHandlerLocatorTrait into a common OptionsHooksLocatorTrait of some sort...WDYT?

) {
}

Expand All @@ -37,4 +38,17 @@ public function withHandleLinks(mixed $handleLinks): self

return $self;
}

public function getTransformModel(): mixed
{
return $this->transformModel;
}

public function withTransformModel(mixed $transformModel): self
{
$self = clone $this;
$self->transformModel = $transformModel;

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

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

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

Expand Down Expand Up @@ -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->getEntityTransformer($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
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\Doctrine\Odm\State;

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

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

Expand Down Expand Up @@ -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->getEntityTransformer($operation)) {
null => $result,
default => (null !== $result) ? $transformer($result) : null,
};
}
}
19 changes: 17 additions & 2 deletions src/Doctrine/Odm/State/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 $transformDocument 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 $transformDocument = null,
) {
parent::__construct(handleLinks: $handleLinks);
parent::__construct(handleLinks: $handleLinks, transformModel: $transformDocument);
}

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

return $self;
}

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

public function withTransformDocument(mixed $transformDocument): self
{
$self = clone $this;
$self->transformModel = $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
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\Doctrine\Orm\State;

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

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

Expand Down Expand Up @@ -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->getEntityTransformer($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
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\Doctrine\Orm\State;

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

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

Expand Down Expand Up @@ -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->getEntityTransformer($operation)) {
null => $result,
default => $transformer($result),
};
}
}
19 changes: 17 additions & 2 deletions src/Doctrine/Orm/State/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@
class Options extends CommonOptions implements OptionsInterface
{
/**
* @param string|callable $handleLinks experimental callable, typed mixed as we may want a service name in the future
* @param string|callable $handleLinks experimental callable, typed mixed as we may want a service name in the future
* @param string|callable $transformEntity experimental callable, typed mixed as we may want a service name in the future
*
* @see LinksHandlerInterface
*/
public function __construct(
protected ?string $entityClass = null,
mixed $handleLinks = null,
mixed $transformEntity = null,
) {
parent::__construct(handleLinks: $handleLinks);
parent::__construct(handleLinks: $handleLinks, transformModel: $transformEntity);
}

public function getEntityClass(): ?string
Expand All @@ -42,4 +44,17 @@ public function withEntityClass(?string $entityClass): self

return $self;
}

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

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

return $self;
}
}
Loading
Loading