Skip to content

Commit

Permalink
Merge pull request #95 from jolicode/feat/api-platform-integration
Browse files Browse the repository at this point in the history
Api platform integration
  • Loading branch information
joelwurtz committed Mar 27, 2024
2 parents 3bb785b + 45ac3ae commit b6ef622
Show file tree
Hide file tree
Showing 27 changed files with 605 additions and 25 deletions.
35 changes: 20 additions & 15 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,39 @@
"require": {
"php": "^8.2",
"nikic/php-parser": "^4.18 || ^5.0",
"symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0",
"symfony/expression-language": "^5.4 || ^6.0 || ^7.0",
"symfony/deprecation-contracts": "^2.0|^3.0",
"symfony/property-info": "^5.4.23 || ^6.2.10 || ^7.0"
"symfony/event-dispatcher": "^6.4 || ^7.0",
"symfony/expression-language": "^6.4 || ^7.0",
"symfony/deprecation-contracts": "^3.0",
"symfony/property-info": "^6.4 || ^7.0"
},
"require-dev": {
"api-platform/core": "^3.0.4",
"doctrine/annotations": "~1.0",
"doctrine/inflector": "^2.0",
"moneyphp/money": "^3.3.2",
"phpdocumentor/type-resolver": "^1.7",
"phpunit/phpunit": "^9.0",
"symfony/filesystem": "^5.4 || ^6.0 || ^7.0",
"symfony/browser-kit": "^7.0",
"symfony/filesystem": "^6.4 || ^7.0",
"symfony/framework-bundle": "*",
"symfony/http-kernel": "^5.4 || ^6.0 || ^7.0",
"symfony/property-access": "^5.4 || ^6.0 || ^7.0",
"symfony/http-client": "^6.4 || ^7.0",
"symfony/http-kernel": "^6.4 || ^7.0",
"symfony/property-access": "^6.4 || ^7.0",
"symfony/serializer": "*",
"symfony/stopwatch": "^7.0",
"symfony/twig-bundle": "^5.4 || ^6.0 || ^7.0",
"symfony/uid": "^5.4 || ^6.0 || ^7.0",
"symfony/web-profiler-bundle": "^5.4 || ^6.0 || ^7.0",
"symfony/yaml": "^5.4 || ^6.0 || ^7.0"
"symfony/stopwatch": "^6.4 || ^7.0",
"symfony/twig-bundle": "^6.4 || ^7.0",
"symfony/uid": "^6.4 || ^7.0",
"symfony/web-profiler-bundle": "^6.4 || ^7.0",
"symfony/yaml": "^6.4 || ^7.0",
"willdurand/negotiation": "^3.0"
},
"suggest": {
"symfony/serializer": "Allow to use symfony serializer attributes in mapping"
},
"conflict": {
"symfony/dependency-injection": "<5.4",
"symfony/framework-bundle": "<5.4",
"symfony/serializer": "<5.4"
"api-platform/core": "<3",
"symfony/framework-bundle": "<6.4",
"symfony/serializer": "<6.4"
},
"autoload": {
"psr-4": {
Expand Down
16 changes: 16 additions & 0 deletions docs/bundle/api-platform.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Api Platform integration

> [!WARNING]
> The api platform integration is in a experimental state, and may change in the future.
> Some behavior may not be handled correctly, and some features may not be implemented.
>
> If you find a bug or missing feature, please report it on the [issue tracker](https://github.com/jolicode/automapper/issues).
This bundle provides a way to integrate with [Api Platform](https://api-platform.com/) by generating the mappers for you.

It injects extra data in the mappers when we map a Resource class to or from an array.

You have to enable the `api_platform` option in the configuration to use this feature.

If you have custom normalizer with some logic inside you will have to convert this logic with our library way of doing things.
[See our migrate guide](migrate.md) for more information.
3 changes: 3 additions & 0 deletions docs/bundle/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ automapper:
only_registered_mapping: false
priority: 1000
serializer: true
api_platform: false
name_converter: null
cache_dir: "%kernel.cache_dir%/automapper"
mappings:
Expand Down Expand Up @@ -49,6 +50,8 @@ automapper:
* `serializer` (default: `true` if the symfony/serializer is available, false otherwise): A boolean which indicate
if we use the attribute of the symfony/serializer during the mapping, this only apply to the `#[Groups]`, `#[MaxDepth]`,
`#[Ignore]` and `#[DiscriminatorMap]` attributes;
* `api_platform` (default: `false`): A boolean which indicate if we use services from the api-platform/core package and
inject extra data (json ld) in the mappers when we map a Resource class to or from an array.
* `name_converter` (default: `null`): A service id which implement the `AdvancedNameConverterInterface` from the symfony/serializer,
this name converter will be used when mapping from an array to an object and vice versa;
* `cache_dir` (default: `%kernel.cache_dir%/automapper`): This setting allows you to customize the output directory
Expand Down
73 changes: 73 additions & 0 deletions src/EventListener/ApiPlatform/JsonLdListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

declare(strict_types=1);

namespace AutoMapper\EventListener\ApiPlatform;

use ApiPlatform\Api\ResourceClassResolverInterface;
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use AutoMapper\Event\GenerateMapperEvent;
use AutoMapper\Event\PropertyMetadataEvent;
use AutoMapper\Event\SourcePropertyMetadata;
use AutoMapper\Event\TargetPropertyMetadata;
use AutoMapper\Provider\ApiPlatform\IriProvider;
use AutoMapper\Transformer\ApiPlatform\JsonLdContextTransformer;
use AutoMapper\Transformer\ApiPlatform\JsonLdIdTransformer;
use AutoMapper\Transformer\FixedValueTransformer;
use AutoMapper\Transformer\PropertyTransformer\PropertyTransformer;

final readonly class JsonLdListener
{
public function __construct(
private ResourceClassResolverInterface $resourceClassResolver,
private ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
) {
}

public function __invoke(GenerateMapperEvent $event): void
{
if ($event->mapperMetadata->target === 'array' && $this->resourceClassResolver->isResourceClass($event->mapperMetadata->source)) {
$event->properties['@id'] = new PropertyMetadataEvent(
mapperMetadata: $event->mapperMetadata,
source: new SourcePropertyMetadata('@id'),
target: new TargetPropertyMetadata('@id'),
transformer: new PropertyTransformer(JsonLdIdTransformer::class),
if: "(context['normalizer_format'] ?? false) === 'jsonld'",
disableGroupsCheck: true,
);

$operation = $this->resourceMetadataCollectionFactory->create($event->mapperMetadata->source)->getOperation();

$types = $operation instanceof HttpOperation ? $operation->getTypes() : null;

if (null === $types) {
$types = [$operation->getShortName()];
}

$fixedTypes = 1 === \count($types) ? $types[0] : $types;

$event->properties['@type'] = new PropertyMetadataEvent(
mapperMetadata: $event->mapperMetadata,
source: new SourcePropertyMetadata('@type'),
target: new TargetPropertyMetadata('@type'),
transformer: new FixedValueTransformer($fixedTypes),
if: "(context['normalizer_format'] ?? false) === 'jsonld'",
disableGroupsCheck: true,
);

$event->properties['@context'] = new PropertyMetadataEvent(
mapperMetadata: $event->mapperMetadata,
source: new SourcePropertyMetadata('@context'),
target: new TargetPropertyMetadata('@context'),
transformer: new PropertyTransformer(JsonLdContextTransformer::class, ['forced_resource_class' => $event->mapperMetadata->source]),
if: "(context['normalizer_format'] ?? false) === 'jsonld' and (context['jsonld_has_context'] ?? false) === false and (context['depth'] ?? 0) <= 1",
disableGroupsCheck: true,
);
}

if ($event->mapperMetadata->source === 'array' && $this->resourceClassResolver->isResourceClass($event->mapperMetadata->target)) {
$event->provider = IriProvider::class;
}
}
}
11 changes: 10 additions & 1 deletion src/Generator/MapMethodStatementsGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,16 @@ private function initializeTargetFromProvider(GeneratorMetadata $metadata): arra
private function handleDependencies(GeneratorMetadata $metadata): array
{
if (!$metadata->getDependencies()) {
return [];
return [
new Stmt\Expression(
new Expr\Assign(
$metadata->variableRegistry->getContext(),
new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'withIncrementedDepth', [
new Arg($metadata->variableRegistry->getContext()),
])
)
),
];
}

$variableRegistry = $metadata->variableRegistry;
Expand Down
3 changes: 3 additions & 0 deletions src/Metadata/MetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ private function createGeneratorMetadata(MapperMetadata $mapperMetadata): Genera
$propertyEvents[$propertyEvent->target->name] = $propertyEvent;
}

// Sort transformations by property name, to ensure consistent order, and easier debugging
ksort($propertyEvents, SORT_NATURAL);

$propertiesMapping = [];

foreach ($propertyEvents as $propertyMappedEvent) {
Expand Down
43 changes: 43 additions & 0 deletions src/Provider/ApiPlatform/IriProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Provider\ApiPlatform;

use ApiPlatform\Api\IriConverterInterface;
use ApiPlatform\Api\ResourceClassResolverInterface;
use AutoMapper\MapperContext;
use AutoMapper\Provider\EarlyReturn;
use AutoMapper\Provider\ProviderInterface;

final readonly class IriProvider implements ProviderInterface
{
public function __construct(
private IriConverterInterface $iriConverter,
private ResourceClassResolverInterface $resourceClassResolver
) {
}

public function provide(string $targetType, mixed $source, array $context): object|array|null
{
if (($context[MapperContext::NORMALIZER_FORMAT] ?? false) !== 'jsonld') {
return null;
}

$isResource = $this->resourceClassResolver->isResourceClass($targetType);

if (!$isResource) {
return null;
}

if (\is_string($source)) {
return new EarlyReturn($this->iriConverter->getResourceFromIri($source));
}

if (!\is_array($source) || !\array_key_exists('@id', $source) || !\is_string($source['@id'])) {
return null;
}

return $this->iriConverter->getResourceFromIri($source['@id']);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ public function load(array $configs, ContainerBuilder $container): void
}
}

if ($config['api_platform']) {
$loader->load('api_platform.php');
}

if (null !== $config['name_converter']) {
$container
->getDefinition(AdvancedNameConverterListener::class)
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Bundle/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public function getConfigTreeBuilder(): TreeBuilder
->addDefaultsIfNotSet()
->end()
->booleanNode('serializer')->defaultValue(interface_exists(SerializerInterface::class))->end()
->booleanNode('api_platform')->defaultFalse()->end()
->scalarNode('name_converter')->defaultNull()->end()
->scalarNode('cache_dir')->defaultValue('%kernel.cache_dir%/automapper')->end()
->scalarNode('date_time_format')->defaultValue(\DateTimeInterface::RFC3339)->end()
Expand Down
40 changes: 40 additions & 0 deletions src/Symfony/Bundle/config/api_platform.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use AutoMapper\Event\GenerateMapperEvent;
use AutoMapper\EventListener\ApiPlatform\JsonLdListener;
use AutoMapper\Provider\ApiPlatform\IriProvider;
use AutoMapper\Transformer\ApiPlatform\JsonLdContextTransformer;
use AutoMapper\Transformer\ApiPlatform\JsonLdIdTransformer;

return static function (ContainerConfigurator $container) {
$container->services()
->set(JsonLdListener::class)
->args([
service('api_platform.resource_class_resolver'),
service('api_platform.metadata.resource.metadata_collection_factory'),
])
->tag('kernel.event_listener', ['event' => GenerateMapperEvent::class, 'priority' => 0])

->set(JsonLdIdTransformer::class)
->args([service('api_platform.iri_converter')])
->tag('automapper.property_transformer', ['priority' => 0])

->set(JsonLdContextTransformer::class)
->args([
service('api_platform.jsonld.context_builder'),
service('api_platform.resource_class_resolver'),
])
->tag('automapper.property_transformer', ['priority' => 0])

->set(IriProvider::class)
->args([
service('api_platform.iri_converter'),
service('api_platform.resource_class_resolver'),
])
->tag('automapper.provider', ['priority' => 0])
;
};
42 changes: 42 additions & 0 deletions src/Transformer/ApiPlatform/JsonLdContextTransformer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Transformer\ApiPlatform;

use ApiPlatform\Api\ResourceClassResolverInterface;
use ApiPlatform\JsonLd\AnonymousContextBuilderInterface;
use ApiPlatform\JsonLd\ContextBuilderInterface;
use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerInterface;

final readonly class JsonLdContextTransformer implements PropertyTransformerInterface
{
public function __construct(
private ContextBuilderInterface $contextBuilder,
private ResourceClassResolverInterface $resourceClassResolver,
) {
}

public function transform(mixed $value, object|array $source, array $context): mixed
{
if (!\is_object($source)) {
return null;
}

$resourceClass = $context['forced_resource_class'] ?? $this->resourceClassResolver->isResourceClass($source::class) ? $this->resourceClassResolver->getResourceClass($source) : null;

if (null === $resourceClass) {
if ($this->contextBuilder instanceof AnonymousContextBuilderInterface) {
return $this->contextBuilder->getAnonymousResourceContext($source, ($context['output'] ?? []) + ['api_resource' => $context['api_resource'] ?? null]);
}

return null;
}

if (isset($context['jsonld_embed_context'])) {
return $this->contextBuilder->getResourceContext($resourceClass);
}

return $this->contextBuilder->getResourceContextUri($resourceClass);
}
}
25 changes: 25 additions & 0 deletions src/Transformer/ApiPlatform/JsonLdIdTransformer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Transformer\ApiPlatform;

use ApiPlatform\Api\IriConverterInterface;
use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerInterface;

final readonly class JsonLdIdTransformer implements PropertyTransformerInterface
{
public function __construct(
private IriConverterInterface $iriConverter
) {
}

public function transform(mixed $value, object|array $source, array $context): mixed
{
if (\is_array($source)) {
return null;
}

return $this->iriConverter->getIriFromResource($source);
}
}
Loading

0 comments on commit b6ef622

Please sign in to comment.