forked from api-platform/core
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(symfony): object mapper with state options
- Loading branch information
Showing
10 changed files
with
306 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
<?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\State\Processor; | ||
|
||
use ApiPlatform\Doctrine\Odm\State\Options as OdmOptions; | ||
use ApiPlatform\Doctrine\Orm\State\Options; | ||
use ApiPlatform\Metadata\Operation; | ||
use ApiPlatform\State\ProcessorInterface; | ||
use ApiPlatform\State\ProviderInterface; | ||
use Symfony\Component\ObjectMapper\Attribute\Map; | ||
use Symfony\Component\ObjectMapper\ObjectMapperInterface; | ||
|
||
final class ObjectMapperProcessor implements ProcessorInterface | ||
{ | ||
public function __construct( | ||
private readonly ObjectMapperInterface $objectMapper, | ||
private readonly ProcessorInterface $decorated, | ||
) { | ||
} | ||
|
||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): object|array|null | ||
{ | ||
if (!$operation->canWrite()) { | ||
return $this->decorated->process($data, $operation, $uriVariables, $context); | ||
} | ||
|
||
if (!(new \ReflectionClass($operation->getClass()))->getAttributes(Map::class)) { | ||
return $this->decorated->process($data, $operation, $uriVariables, $context); | ||
} | ||
|
||
return $this->objectMapper->map($this->decorated->process($this->objectMapper->map($data), $operation, $uriVariables, $context)); | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 <[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\State\Provider; | ||
|
||
use ApiPlatform\Doctrine\Odm\State\Options as OdmOptions; | ||
use ApiPlatform\Doctrine\Orm\State\Options; | ||
use ApiPlatform\Metadata\Operation; | ||
use ApiPlatform\State\Pagination\ArrayPaginator; | ||
use ApiPlatform\State\Pagination\PaginatorInterface; | ||
use ApiPlatform\State\ProviderInterface; | ||
use Symfony\Component\ObjectMapper\Attribute\Map; | ||
use Symfony\Component\ObjectMapper\ObjectMapperInterface; | ||
|
||
final class ObjectMapperProvider implements ProviderInterface | ||
{ | ||
public function __construct( | ||
private readonly ObjectMapperInterface $objectMapper, | ||
private readonly ProviderInterface $decorated, | ||
) { | ||
} | ||
|
||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null | ||
{ | ||
$data = $this->decorated->provide($operation, $uriVariables, $context); | ||
|
||
if(!is_object($data)) { | ||
return $data; | ||
} | ||
|
||
$entityClass = $operation->getClass(); | ||
if (($options = $operation->getStateOptions()) && $options instanceof Options && $options->getEntityClass()) { | ||
$entityClass = $options->getEntityClass(); | ||
} | ||
|
||
if (($options = $operation->getStateOptions()) && $options instanceof OdmOptions && $options->getDocumentClass()) { | ||
$entityClass = $options->getDocumentClass(); | ||
} | ||
|
||
if (!(new \ReflectionClass($entityClass))->getAttributes(Map::class)) { | ||
return $data; | ||
} | ||
|
||
if ($data instanceof PaginatorInterface) { | ||
return new ArrayPaginator(array_map(fn($v) => $this->objectMapper->map($v), iterator_to_array($data)), 0, \count($data)); | ||
} | ||
|
||
return $this->objectMapper->map($data); | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
17 changes: 17 additions & 0 deletions
17
src/Symfony/Bundle/Resources/config/state/object_mapper.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?xml version="1.0" ?> | ||
|
||
<container xmlns="http://symfony.com/schema/dic/services" | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> | ||
<services> | ||
<service id="api_platform.state_provider.object_mapper" class="ApiPlatform\State\Provider\ObjectMapperProvider" decorates="api_platform.state_provider.read"> | ||
<argument type="service" id="object_mapper" /> | ||
<argument type="service" id="api_platform.state_provider.object_mapper.inner" /> | ||
</service> | ||
|
||
<service id="api_platform.state_processor.object_mapper" class="ApiPlatform\State\Processor\ObjectMapperProcessor" decorates="api_platform.state_processor.locator"> | ||
<argument type="service" id="object_mapper" /> | ||
<argument type="service" id="api_platform.state_processor.object_mapper.inner" /> | ||
</service> | ||
</services> | ||
</container> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
<?php | ||
|
||
namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource; | ||
|
||
use ApiPlatform\Doctrine\Orm\State\Options; | ||
use ApiPlatform\JsonLd\ContextBuilder; | ||
use ApiPlatform\Metadata\ApiResource; | ||
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MappedEntity; | ||
use Symfony\Component\ObjectMapper\Attribute\Map; | ||
|
||
#[ApiResource(stateOptions: new Options(entityClass: MappedEntity::class), normalizationContext: [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false])] | ||
#[Map(target: MappedEntity::class)] | ||
final class MappedResource | ||
{ | ||
#[Map(if: false)] | ||
public ?string $id = null; | ||
|
||
#[Map(target: 'firstName', transform: [self::class, 'toFirstName'])] | ||
#[Map(target: 'lastName', transform: [self::class, 'toLastName'])] | ||
public string $username; | ||
|
||
public static function toFirstName(string $v): string { | ||
return explode(' ', $v)[0] ?? null; | ||
} | ||
|
||
public static function toLastName(string $v): string { | ||
return explode(' ', $v)[1] ?? null; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
<?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\Tests\Fixtures\TestBundle\Entity; | ||
|
||
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\MappedResource; | ||
use Doctrine\ORM\Mapping as ORM; | ||
use Symfony\Component\ObjectMapper\Attribute\Map; | ||
|
||
/** | ||
* MappedEntity to MappedResource. | ||
*/ | ||
#[ORM\Entity] | ||
#[Map(target: MappedResource::class)] | ||
class MappedEntity | ||
{ | ||
#[ORM\Column(type: 'integer')] | ||
#[ORM\Id] | ||
#[ORM\GeneratedValue(strategy: 'AUTO')] | ||
private ?int $id = null; | ||
|
||
#[ORM\Column] | ||
#[Map(if: false)] | ||
private string $firstName; | ||
|
||
#[Map(target: 'username', transform: [self::class, 'toUsername'])] | ||
#[ORM\Column] | ||
private string $lastName; | ||
|
||
public static function toUsername($value, $object): string { | ||
return $object->getFirstName() . ' ' . $object->getLastName(); | ||
} | ||
|
||
public function getId(): ?int | ||
{ | ||
return $this->id; | ||
} | ||
|
||
public function setLastName(string $name): void | ||
{ | ||
$this->lastName = $name; | ||
} | ||
|
||
public function getLastName(): string | ||
{ | ||
return $this->lastName; | ||
} | ||
|
||
public function setFirstName(string $name): void | ||
{ | ||
$this->firstName = $name; | ||
} | ||
|
||
public function getFirstName(): string | ||
{ | ||
return $this->firstName; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
<?php | ||
|
||
namespace ApiPlatform\Tests\Functional; | ||
|
||
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; | ||
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\MappedResource; | ||
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MappedEntity; | ||
use ApiPlatform\Tests\RecreateSchemaTrait; | ||
use ApiPlatform\Tests\SetupClassResourcesTrait; | ||
|
||
final class MappingTest extends ApiTestCase | ||
{ | ||
use SetupClassResourcesTrait; | ||
use RecreateSchemaTrait; | ||
|
||
/** | ||
* @return class-string[] | ||
*/ | ||
public static function getResources(): array | ||
{ | ||
return [MappedResource::class]; | ||
} | ||
|
||
public function testShouldMapBetweenResourceAndEntity(): void | ||
{ | ||
$this->recreateSchema([MappedEntity::class]); | ||
$this->loadFixtures(); | ||
self::createClient()->request('GET', 'mapped_resources'); | ||
$this->assertJsonContains(['member' => [ | ||
['username' => 'B0 A0'], | ||
['username' => 'B1 A1'], | ||
['username' => 'B2 A2'], | ||
]]); | ||
|
||
$r = self::createClient()->request('POST', 'mapped_resources', ['json' => ['username' => 'so yuka']]); | ||
$this->assertJsonContains(['username' => 'so yuka']); | ||
|
||
$manager = $this->getManager(); | ||
$repo = $manager->getRepository(MappedEntity::class); | ||
$persisted = $repo->findOneBy(['id' => $r->toArray()['id']]); | ||
$this->assertSame('so', $persisted->getFirstName()); | ||
$this->assertSame('yuka', $persisted->getLastName()); | ||
|
||
$uri = $r->toArray()['@id']; | ||
self::createClient()->request('GET', $uri); | ||
$this->assertJsonContains(['username' => 'so yuka']); | ||
|
||
$r = self::createClient()->request('PATCH', $uri, ['json' => ['username' => 'ba zar'], 'headers' => ['content-type' => 'application/merge-patch+json']]); | ||
$this->assertJsonContains(['username' => 'ba zar']); | ||
} | ||
|
||
private function loadFixtures(): void { | ||
$manager = $this->getManager(); | ||
|
||
for ($i=0; $i < 10; $i++) { | ||
$e = new MappedEntity; | ||
$e->setLastName('A'.$i); | ||
$e->setFirstName('B'.$i); | ||
$manager->persist($e); | ||
} | ||
|
||
$manager->flush(); | ||
} | ||
} |