Skip to content

Commit a42034d

Browse files
authored
feat(symfony): object mapper with state options (#6801)
1 parent 5db1a32 commit a42034d

File tree

10 files changed

+456
-0
lines changed

10 files changed

+456
-0
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@
180180
"symfony/maker-bundle": "^1.24",
181181
"symfony/mercure-bundle": "*",
182182
"symfony/messenger": "^6.4 || ^7.0",
183+
"symfony/object-mapper": "^7.3",
183184
"symfony/routing": "^6.4 || ^7.0",
184185
"symfony/security-bundle": "^6.4 || ^7.0",
185186
"symfony/security-core": "^6.4 || ^7.0",
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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\Metadata\Operation;
17+
use ApiPlatform\State\ProcessorInterface;
18+
use Symfony\Component\ObjectMapper\Attribute\Map;
19+
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
20+
21+
/**
22+
* @implements ProcessorInterface<mixed,mixed>
23+
*/
24+
final class ObjectMapperProcessor implements ProcessorInterface
25+
{
26+
/**
27+
* @param ProcessorInterface<mixed,mixed> $decorated
28+
*/
29+
public function __construct(
30+
private readonly ?ObjectMapperInterface $objectMapper,
31+
private readonly ProcessorInterface $decorated,
32+
) {
33+
}
34+
35+
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): object|array|null
36+
{
37+
if (!$this->objectMapper || !$operation->canWrite()) {
38+
return $this->decorated->process($data, $operation, $uriVariables, $context);
39+
}
40+
41+
if (!(new \ReflectionClass($operation->getClass()))->getAttributes(Map::class)) {
42+
return $this->decorated->process($data, $operation, $uriVariables, $context);
43+
}
44+
45+
return $this->objectMapper->map($this->decorated->process($this->objectMapper->map($data), $operation, $uriVariables, $context));
46+
}
47+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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\Metadata\Util\CloneTrait;
20+
use ApiPlatform\State\Pagination\ArrayPaginator;
21+
use ApiPlatform\State\Pagination\PaginatorInterface;
22+
use ApiPlatform\State\ProviderInterface;
23+
use Symfony\Component\ObjectMapper\Attribute\Map;
24+
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
25+
26+
/**
27+
* @implements ProviderInterface<object>
28+
*/
29+
final class ObjectMapperProvider implements ProviderInterface
30+
{
31+
use CloneTrait;
32+
33+
/**
34+
* @param ProviderInterface<object> $decorated
35+
*/
36+
public function __construct(
37+
private readonly ?ObjectMapperInterface $objectMapper,
38+
private readonly ProviderInterface $decorated,
39+
) {
40+
}
41+
42+
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
43+
{
44+
$data = $this->decorated->provide($operation, $uriVariables, $context);
45+
46+
if (!$this->objectMapper || !\is_object($data)) {
47+
return $data;
48+
}
49+
50+
$request = $context['request'] ?? null;
51+
$entityClass = null;
52+
if (($options = $operation->getStateOptions()) && $options instanceof Options && $options->getEntityClass()) {
53+
$entityClass = $options->getEntityClass();
54+
}
55+
56+
if (($options = $operation->getStateOptions()) && $options instanceof OdmOptions && $options->getDocumentClass()) {
57+
$entityClass = $options->getDocumentClass();
58+
}
59+
60+
$entityClass ??= $data::class;
61+
62+
if (!(new \ReflectionClass($entityClass))->getAttributes(Map::class)) {
63+
return $data;
64+
}
65+
66+
if ($data instanceof PaginatorInterface) {
67+
$data = new ArrayPaginator(array_map(fn ($v) => $this->objectMapper->map($v), iterator_to_array($data)), 0, \count($data));
68+
} else {
69+
$data = $this->objectMapper->map($data);
70+
}
71+
72+
$request?->attributes->set('data', $data);
73+
$request?->attributes->set('previous_data', $this->clone($data));
74+
75+
return $data;
76+
}
77+
}

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\ObjectMapper;
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(ObjectMapper::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" on-invalid="null" />
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" on-invalid="null" />
14+
<argument type="service" id="api_platform.state_processor.object_mapper.inner" />
15+
</service>
16+
</services>
17+
</container>
Lines changed: 45 additions & 0 deletions
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\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(
23+
stateOptions: new Options(entityClass: MappedEntity::class),
24+
normalizationContext: [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false],
25+
)]
26+
#[Map(target: MappedEntity::class)]
27+
final class MappedResource
28+
{
29+
#[Map(if: false)]
30+
public ?string $id = null;
31+
32+
#[Map(target: 'firstName', transform: [self::class, 'toFirstName'])]
33+
#[Map(target: 'lastName', transform: [self::class, 'toLastName'])]
34+
public string $username;
35+
36+
public static function toFirstName(string $v): string
37+
{
38+
return explode(' ', $v)[0];
39+
}
40+
41+
public static function toLastName(string $v): string
42+
{
43+
return explode(' ', $v)[1];
44+
}
45+
}
Lines changed: 45 additions & 0 deletions
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\Tests\Fixtures\TestBundle\ApiResource;
15+
16+
use ApiPlatform\Doctrine\Odm\State\Options;
17+
use ApiPlatform\JsonLd\ContextBuilder;
18+
use ApiPlatform\Metadata\ApiResource;
19+
use ApiPlatform\Tests\Fixtures\TestBundle\Document\MappedDocument;
20+
use Symfony\Component\ObjectMapper\Attribute\Map;
21+
22+
#[ApiResource(
23+
stateOptions: new Options(documentClass: MappedDocument::class),
24+
normalizationContext: [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false],
25+
)]
26+
#[Map(target: MappedDocument::class)]
27+
final class MappedResourceOdm
28+
{
29+
#[Map(if: false)]
30+
public ?string $id = null;
31+
32+
#[Map(target: 'firstName', transform: [self::class, 'toFirstName'])]
33+
#[Map(target: 'lastName', transform: [self::class, 'toLastName'])]
34+
public string $username;
35+
36+
public static function toFirstName(string $v): string
37+
{
38+
return explode(' ', $v)[0];
39+
}
40+
41+
public static function toLastName(string $v): string
42+
{
43+
return explode(' ', $v)[1];
44+
}
45+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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\Document;
15+
16+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\MappedResourceOdm;
17+
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
18+
use Symfony\Component\ObjectMapper\Attribute\Map;
19+
20+
/**
21+
* MappedEntity to MappedResource.
22+
*/
23+
#[ODM\Document]
24+
#[Map(target: MappedResourceOdm::class)]
25+
class MappedDocument
26+
{
27+
#[ODM\Id(strategy: 'INCREMENT', type: 'int')]
28+
private ?int $id = null;
29+
30+
#[ODM\Field(type: 'string')]
31+
#[Map(if: false)]
32+
private string $firstName;
33+
34+
#[Map(target: 'username', transform: [self::class, 'toUsername'])]
35+
#[ODM\Field(type: 'string')]
36+
private string $lastName;
37+
38+
public static function toUsername($value, $object): string
39+
{
40+
return $object->getFirstName().' '.$object->getLastName();
41+
}
42+
43+
public function getId(): ?int
44+
{
45+
return $this->id;
46+
}
47+
48+
public function setLastName(string $name): void
49+
{
50+
$this->lastName = $name;
51+
}
52+
53+
public function getLastName(): string
54+
{
55+
return $this->lastName;
56+
}
57+
58+
public function setFirstName(string $name): void
59+
{
60+
$this->firstName = $name;
61+
}
62+
63+
public function getFirstName(): string
64+
{
65+
return $this->firstName;
66+
}
67+
}
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 <[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+
{
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)