Skip to content

Commit 280ee15

Browse files
committed
feat(symfony): object mapper with state options
1 parent c022b44 commit 280ee15

File tree

12 files changed

+325
-9
lines changed

12 files changed

+325
-9
lines changed

composer.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@
163163
"symfony/cache": "^6.4 || ^7.0",
164164
"symfony/config": "^6.4 || ^7.0",
165165
"symfony/console": "^6.4 || ^7.0",
166+
"symfony/object-mapper": "v7.3.0-RC1",
166167
"symfony/css-selector": "^6.4 || ^7.0",
167168
"symfony/dependency-injection": "^6.4 || ^7.0",
168169
"symfony/doctrine-bridge": "^6.4.2 || ^7.1",
@@ -172,7 +173,7 @@
172173
"symfony/expression-language": "^6.4 || ^7.0",
173174
"symfony/finder": "^6.4 || ^7.0",
174175
"symfony/form": "^6.4 || ^7.0",
175-
"symfony/framework-bundle": "^6.4 || ^7.0",
176+
"symfony/framework-bundle": "v7.3.0-RC1",
176177
"symfony/http-client": "^6.4 || ^7.0",
177178
"symfony/intl": "^6.4 || ^7.0",
178179
"symfony/maker-bundle": "^1.24",
@@ -212,5 +213,6 @@
212213
"type": "library",
213214
"repositories": [
214215
{"type": "vcs", "url": "https://github.com/soyuka/phpunit"}
215-
]
216+
],
217+
"minimum-stability": "RC"
216218
}

src/Serializer/SerializerContextBuilder.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ public function createFromRequest(Request $request, bool $normalization, ?array
8484
}
8585
}
8686

87-
if (null === $context['output'] && $this->getStateOptionsClass($operation)) {
88-
$context['force_resource_class'] = $operation->getClass();
89-
}
87+
// if (null === $context['output'] && $this->getStateOptionsClass($operation)) {
88+
// $context['force_resource_class'] = $operation->getClass();
89+
// }
9090

9191
if ($this->debug && isset($context['groups']) && $operation instanceof ErrorOperation) {
9292
if (!\is_array($context['groups'])) {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
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\Metadata\Operation;
17+
use ApiPlatform\State\ProcessorInterface;
18+
use Doctrine\Persistence\ManagerRegistry;
19+
use Symfony\Component\ObjectMapper\Attribute\Map;
20+
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
21+
22+
final class ObjectMapperProcessor implements ProcessorInterface
23+
{
24+
public function __construct(
25+
private readonly ObjectMapperInterface $objectMapper,
26+
private readonly ProcessorInterface $decorated,
27+
) {
28+
}
29+
30+
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): object|array|null
31+
{
32+
if (!(new \ReflectionClass($operation->getClass()))->getAttributes(Map::class)) {
33+
return $this->decorated->process($data, $operation, $uriVariables, $context);
34+
}
35+
36+
return $this->objectMapper->map($this->decorated->process($this->objectMapper->map($data, $context['data'] ?? null), $operation, $uriVariables, $context));
37+
}
38+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
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+
}

src/State/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
"php": ">=8.2",
3131
"api-platform/metadata": "^4.1.11",
3232
"psr/container": "^1.0 || ^2.0",
33-
"symfony/http-kernel": "^6.4 || ^7.0"
33+
"symfony/http-kernel": "^6.4 || ^7.0",
34+
"symfony/object-mapper": "^7.3.0-RC1"
3435
},
3536
"require-dev": {
3637
"phpunit/phpunit": "11.5.x-dev",

src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
use Symfony\Component\DependencyInjection\Reference;
6060
use Symfony\Component\Finder\Finder;
6161
use Symfony\Component\HttpClient\ScopingHttpClient;
62+
use Symfony\Component\ObjectMapper\Attribute\Map;
6263
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
6364
use Symfony\Component\Uid\AbstractUid;
6465
use Symfony\Component\Validator\Validator\ValidatorInterface;
@@ -169,6 +170,9 @@ public function load(array $configs, ContainerBuilder $container): void
169170
$this->registerArgumentResolverConfiguration($loader);
170171
$this->registerLinkSecurityConfiguration($loader, $config);
171172

173+
if (class_exists(Map::class)) {
174+
$loader->load('state/object_mapper.xml');
175+
}
172176
$container->registerForAutoconfiguration(FilterInterface::class)
173177
->addTag('api_platform.filter');
174178
$container->registerForAutoconfiguration(ProviderInterface::class)
Lines changed: 17 additions & 0 deletions
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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,6 @@ public function __invoke(Request $request): Response
109109
}
110110
}
111111

112-
$context['previous_data'] = $request->attributes->get('previous_data');
113-
$context['data'] = $request->attributes->get('data');
114-
115112
if (null === $operation->canWrite()) {
116113
$operation = $operation->withWrite(!$request->isMethodSafe());
117114
}
@@ -120,6 +117,9 @@ public function __invoke(Request $request): Response
120117
$operation = $operation->withSerialize(true);
121118
}
122119

120+
$context['previous_data'] = $request->attributes->get('previous_data');
121+
$context['data'] = $request->attributes->get('data');
122+
123123
return $this->processor->process($body, $operation, $uriVariables, $context);
124124
}
125125
}

src/Symfony/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"api-platform/parameter-validator": "^3.1",
5555
"phpspec/prophecy-phpunit": "^2.2",
5656
"phpunit/phpunit": "11.5.x-dev",
57+
"symfony/object-mapper": "v7.3.0-RC1",
5758
"symfony/expression-language": "^6.4 || ^7.0",
5859
"symfony/intl": "^6.4 || ^7.0",
5960
"symfony/mercure-bundle": "*",
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
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\ApiResource;
15+
16+
use ApiPlatform\Doctrine\Orm\State\Options;
17+
use ApiPlatform\JsonLd\ContextBuilder;
18+
use ApiPlatform\Metadata\ApiResource;
19+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MappedEntity;
20+
use Symfony\Component\ObjectMapper\Attribute\Map;
21+
22+
#[ApiResource(stateOptions: new Options(entityClass: MappedEntity::class), normalizationContext: [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false])]
23+
#[Map(target: MappedEntity::class)]
24+
final class MappedResource
25+
{
26+
#[Map(if: false)]
27+
public ?string $id = null;
28+
29+
#[Map(target: 'firstName', transform: [self::class, 'toFirstName'])]
30+
#[Map(target: 'lastName', transform: [self::class, 'toLastName'])]
31+
public string $username;
32+
33+
public static function toFirstName(string $v): string
34+
{
35+
return explode(' ', $v)[0] ?? null;
36+
}
37+
38+
public static function toLastName(string $v): string
39+
{
40+
return explode(' ', $v)[1] ?? null;
41+
}
42+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
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+
{
42+
return $object->getFirstName().' '.$object->getLastName();
43+
}
44+
45+
public function getId(): ?int
46+
{
47+
return $this->id;
48+
}
49+
50+
public function setLastName(string $name): void
51+
{
52+
$this->lastName = $name;
53+
}
54+
55+
public function getLastName(): string
56+
{
57+
return $this->lastName;
58+
}
59+
60+
public function setFirstName(string $name): void
61+
{
62+
$this->firstName = $name;
63+
}
64+
65+
public function getFirstName(): string
66+
{
67+
return $this->firstName;
68+
}
69+
}

0 commit comments

Comments
 (0)