Skip to content

Commit 3a5efe5

Browse files
committed
feat(symfony): object mapper with state options
1 parent fb47197 commit 3a5efe5

File tree

11 files changed

+341
-12
lines changed

11 files changed

+341
-12
lines changed

composer.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@
162162
"symfony/cache": "^6.4 || ^7.0",
163163
"symfony/config": "^6.4 || ^7.0",
164164
"symfony/console": "^6.4 || ^7.0",
165+
"symfony/object-mapper": "dev-feat/automapper-frameworkbundle",
165166
"symfony/css-selector": "^6.4 || ^7.0",
166167
"symfony/dependency-injection": "^6.4 || ^7.0",
167168
"symfony/doctrine-bridge": "^6.4.2 || ^7.0.2",
@@ -208,5 +209,10 @@
208209
"symfony/web-profiler-bundle": "To use the data collector.",
209210
"webonyx/graphql-php": "To support GraphQL."
210211
},
211-
"type": "library"
212+
"type": "library",
213+
"repositories": [
214+
{ "type": "path", "url": "../symfony/src/Symfony/Component/ObjectMapper" },
215+
{ "type": "path", "url": "../symfony/src/Symfony/Bundle/FrameworkBundle" }
216+
],
217+
"minimum-stability": "dev"
212218
}

src/Serializer/SerializerContextBuilder.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,9 @@ public function createFromRequest(Request $request, bool $normalization, ?array
8282
}
8383
}
8484

85-
if (null === $context['output'] && ($options = $operation?->getStateOptions()) && class_exists(Options::class) && $options instanceof Options && $options->getEntityClass()) {
86-
$context['force_resource_class'] = $operation->getClass();
87-
}
85+
// if (null === $context['output'] && ($options = $operation?->getStateOptions()) && class_exists(Options::class) && $options instanceof Options && $options->getEntityClass()) {
86+
// $context['force_resource_class'] = $operation->getClass();
87+
// }
8888

8989
if ($this->debug && isset($context['groups']) && $operation instanceof ErrorOperation) {
9090
if (!\is_array($context['groups'])) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\State\Processor;
15+
16+
use ApiPlatform\Doctrine\Odm\State\Options as OdmOptions;
17+
use ApiPlatform\Doctrine\Orm\State\Options;
18+
use ApiPlatform\Metadata\Operation;
19+
use ApiPlatform\State\ProcessorInterface;
20+
use ApiPlatform\State\ProviderInterface;
21+
use Symfony\Component\ObjectMapper\Attribute\Map;
22+
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
23+
24+
final class ObjectMapperProcessor implements ProcessorInterface
25+
{
26+
public function __construct(
27+
private readonly ObjectMapperInterface $objectMapper,
28+
private readonly ProcessorInterface $decorated,
29+
) {
30+
}
31+
32+
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): object|array|null
33+
{
34+
if (!$operation->canWrite()) {
35+
return $this->decorated->process($data, $operation, $uriVariables, $context);
36+
}
37+
38+
if (!(new \ReflectionClass($operation->getClass()))->getAttributes(Map::class)) {
39+
return $this->decorated->process($data, $operation, $uriVariables, $context);
40+
}
41+
42+
return $this->objectMapper->map($this->decorated->process($this->objectMapper->map($data), $operation, $uriVariables, $context));
43+
}
44+
}
45+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\State\Provider;
15+
16+
use ApiPlatform\Doctrine\Odm\State\Options as OdmOptions;
17+
use ApiPlatform\Doctrine\Orm\State\Options;
18+
use ApiPlatform\Metadata\Operation;
19+
use ApiPlatform\State\Pagination\ArrayPaginator;
20+
use ApiPlatform\State\Pagination\PaginatorInterface;
21+
use ApiPlatform\State\ProviderInterface;
22+
use Symfony\Component\ObjectMapper\Attribute\Map;
23+
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
24+
25+
final class ObjectMapperProvider implements ProviderInterface
26+
{
27+
public function __construct(
28+
private readonly ObjectMapperInterface $objectMapper,
29+
private readonly ProviderInterface $decorated,
30+
) {
31+
}
32+
33+
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
34+
{
35+
$data = $this->decorated->provide($operation, $uriVariables, $context);
36+
37+
if(!is_object($data)) {
38+
return $data;
39+
}
40+
41+
$entityClass = $operation->getClass();
42+
if (($options = $operation->getStateOptions()) && $options instanceof Options && $options->getEntityClass()) {
43+
$entityClass = $options->getEntityClass();
44+
}
45+
46+
if (($options = $operation->getStateOptions()) && $options instanceof OdmOptions && $options->getDocumentClass()) {
47+
$entityClass = $options->getDocumentClass();
48+
}
49+
50+
if (!(new \ReflectionClass($entityClass))->getAttributes(Map::class)) {
51+
return $data;
52+
}
53+
54+
if ($data instanceof PaginatorInterface) {
55+
return new ArrayPaginator(array_map(fn($v) => $this->objectMapper->map($v), iterator_to_array($data)), 0, \count($data));
56+
}
57+
58+
return $this->objectMapper->map($data);
59+
}
60+
}
61+

src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

+4
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
use ApiPlatform\Symfony\Validator\ValidationGroupsGeneratorInterface;
4444
use ApiPlatform\Validator\Exception\ValidationException;
4545
use Doctrine\Persistence\ManagerRegistry;
46+
use Symfony\Component\ObjectMapper\Attribute\Map;
4647
use phpDocumentor\Reflection\DocBlockFactoryInterface;
4748
use PHPStan\PhpDocParser\Parser\PhpDocParser;
4849
use Ramsey\Uuid\Uuid;
@@ -158,6 +159,9 @@ public function load(array $configs, ContainerBuilder $container): void
158159
$this->registerArgumentResolverConfiguration($loader);
159160
$this->registerLinkSecurityConfiguration($loader, $config);
160161

162+
if (class_exists(Map::class)) {
163+
$loader->load('state/object_mapper.xml');
164+
}
161165
$container->registerForAutoconfiguration(FilterInterface::class)
162166
->addTag('api_platform.filter');
163167
$container->registerForAutoconfiguration(ProviderInterface::class)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
13+
14+
use Symfony\Component\ObjectMapper\Metadata\MapperMetadataFactoryInterface;
15+
use Symfony\Component\ObjectMapper\Metadata\ReflectionMapperMetadataFactory;
16+
use Symfony\Component\ObjectMapper\ObjectMapper;
17+
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
18+
19+
return static function (ContainerConfigurator $container) {
20+
$container->services()
21+
->set('object_mapper.metadata_factory', ReflectionMapperMetadataFactory::class)
22+
->alias(ReflectionMapperMetadataFactory::class, 'object_mapper.metadata_factory')
23+
->alias(MapperMetadataFactoryInterface::class, 'object_mapper.metadata_factory')
24+
25+
->set('object_mapper', ObjectMapper::class)
26+
->args([
27+
service('object_mapper.metadata_factory')->ignoreOnInvalid(),
28+
service('property_accessor')->ignoreOnInvalid(),
29+
tagged_locator('object_mapper.transform_callable'),
30+
tagged_locator('object_mapper.condition_callable'),
31+
])
32+
->alias(ObjectMapper::class, 'object_mapper')
33+
->alias(ObjectMapperInterface::class, 'object_mapper')
34+
;
35+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" ?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
6+
<services>
7+
<service id="api_platform.state_provider.object_mapper" class="ApiPlatform\State\Provider\ObjectMapperProvider" decorates="api_platform.state_provider.read">
8+
<argument type="service" id="object_mapper" />
9+
<argument type="service" id="api_platform.state_provider.object_mapper.inner" />
10+
</service>
11+
12+
<service id="api_platform.state_processor.object_mapper" class="ApiPlatform\State\Processor\ObjectMapperProcessor" decorates="api_platform.state_processor.locator">
13+
<argument type="service" id="object_mapper" />
14+
<argument type="service" id="api_platform.state_processor.object_mapper.inner" />
15+
</service>
16+
</services>
17+
</container>

src/Symfony/Controller/MainController.php

+8-8
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@ public function __invoke(Request $request): Response
8080
$operation = $operation->withDeserialize(\in_array($operation->getMethod(), ['POST', 'PUT', 'PATCH'], true));
8181
}
8282

83+
if (null === $operation->canWrite()) {
84+
$operation = $operation->withWrite(!$request->isMethodSafe());
85+
}
86+
87+
if (null === $operation->canSerialize()) {
88+
$operation = $operation->withSerialize(true);
89+
}
90+
8391
$body = $this->provider->provide($operation, $uriVariables, $context);
8492

8593
// The provider can change the Operation, extract it again from the Request attributes
@@ -101,14 +109,6 @@ public function __invoke(Request $request): Response
101109
$context['previous_data'] = $request->attributes->get('previous_data');
102110
$context['data'] = $request->attributes->get('data');
103111

104-
if (null === $operation->canWrite()) {
105-
$operation = $operation->withWrite(!$request->isMethodSafe());
106-
}
107-
108-
if (null === $operation->canSerialize()) {
109-
$operation = $operation->withSerialize(true);
110-
}
111-
112112
return $this->processor->process($body, $operation, $uriVariables, $context);
113113
}
114114
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource;
4+
5+
use ApiPlatform\Doctrine\Orm\State\Options;
6+
use ApiPlatform\JsonLd\ContextBuilder;
7+
use ApiPlatform\Metadata\ApiResource;
8+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MappedEntity;
9+
use Symfony\Component\ObjectMapper\Attribute\Map;
10+
11+
#[ApiResource(stateOptions: new Options(entityClass: MappedEntity::class), normalizationContext: [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false])]
12+
#[Map(target: MappedEntity::class)]
13+
final class MappedResource
14+
{
15+
#[Map(if: false)]
16+
public ?string $id = null;
17+
18+
#[Map(target: 'firstName', transform: [self::class, 'toFirstName'])]
19+
#[Map(target: 'lastName', transform: [self::class, 'toLastName'])]
20+
public string $username;
21+
22+
public static function toFirstName(string $v): string {
23+
return explode(' ', $v)[0] ?? null;
24+
}
25+
26+
public static function toLastName(string $v): string {
27+
return explode(' ', $v)[1] ?? null;
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\MappedResource;
17+
use Doctrine\ORM\Mapping as ORM;
18+
use Symfony\Component\ObjectMapper\Attribute\Map;
19+
20+
/**
21+
* MappedEntity to MappedResource.
22+
*/
23+
#[ORM\Entity]
24+
#[Map(target: MappedResource::class)]
25+
class MappedEntity
26+
{
27+
#[ORM\Column(type: 'integer')]
28+
#[ORM\Id]
29+
#[ORM\GeneratedValue(strategy: 'AUTO')]
30+
private ?int $id = null;
31+
32+
#[ORM\Column]
33+
#[Map(if: false)]
34+
private string $firstName;
35+
36+
#[Map(target: 'username', transform: [self::class, 'toUsername'])]
37+
#[ORM\Column]
38+
private string $lastName;
39+
40+
public static function toUsername($value, $object): string {
41+
return $object->getFirstName() . ' ' . $object->getLastName();
42+
}
43+
44+
public function getId(): ?int
45+
{
46+
return $this->id;
47+
}
48+
49+
public function setLastName(string $name): void
50+
{
51+
$this->lastName = $name;
52+
}
53+
54+
public function getLastName(): string
55+
{
56+
return $this->lastName;
57+
}
58+
59+
public function setFirstName(string $name): void
60+
{
61+
$this->firstName = $name;
62+
}
63+
64+
public function getFirstName(): string
65+
{
66+
return $this->firstName;
67+
}
68+
}

0 commit comments

Comments
 (0)