Skip to content

Commit 7383d7c

Browse files
authored
Feature: Doctrine Provider (#253)
This allow automatically provide an entity from database instead of creating a new one, which will update values instead of generating a new entity This is rather a first simple implementation, but it should work for the 90% use case
2 parents 811de08 + 2baa68c commit 7383d7c

33 files changed

+670
-76
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ tools/phpstan/cache/
88
cache/
99
site/
1010
.build/
11-
.castor*
11+
.castor*
12+
tests/Doctrine/db.sqlite

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@
3030
},
3131
"require-dev": {
3232
"api-platform/core": "^3.0.4 || ^4",
33-
"doctrine/annotations": "~1.0",
33+
"doctrine/annotations": "^2.0",
34+
"doctrine/doctrine-bundle": "^2.15",
3435
"doctrine/collections": "^2.2",
3536
"doctrine/inflector": "^2.0",
37+
"doctrine/orm": "^2.0 || ^3.0",
3638
"matthiasnoback/symfony-dependency-injection-test": "^5.1",
3739
"moneyphp/money": "^3.3.2",
3840
"phpunit/phpunit": "^9.0",

src/Attribute/MapProvider.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
final readonly class MapProvider
99
{
1010
/**
11-
* @param ?string $source from which source type this provider should apply
12-
* @param string $provider the provider class name or service identifier
11+
* @param ?string $source from which source type this provider should apply
12+
* @param false|string $provider the provider class name or service identifier, set false to disable any provider
1313
*/
1414
public function __construct(
15-
public string $provider,
15+
public false|string $provider,
1616
public ?string $source = null,
1717
) {
1818
}

src/AutoMapper.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
use AutoMapper\Loader\FileLoader;
1313
use AutoMapper\Metadata\MetadataFactory;
1414
use AutoMapper\Metadata\MetadataRegistry;
15+
use AutoMapper\Provider\Doctrine\DoctrineProvider;
1516
use AutoMapper\Provider\ProviderInterface;
1617
use AutoMapper\Provider\ProviderRegistry;
1718
use AutoMapper\Symfony\ExpressionLanguageProvider;
1819
use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerInterface;
1920
use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerRegistry;
2021
use AutoMapper\Transformer\TransformerFactoryInterface;
2122
use Doctrine\Common\Annotations\AnnotationReader;
23+
use Doctrine\Persistence\ObjectManager;
2224
use Symfony\Component\EventDispatcher\EventDispatcher;
2325
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
2426
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
@@ -148,6 +150,7 @@ public static function create(
148150
EventDispatcherInterface $eventDispatcher = new EventDispatcher(),
149151
iterable $providers = [],
150152
bool $removeDefaultProperties = false,
153+
?ObjectManager $objectManager = null,
151154
): AutoMapperInterface {
152155
if (\count($transformerFactories) > 0) {
153156
trigger_deprecation('jolicode/automapper', '9.0', 'The "$transformerFactories" property will be removed in version 10.0, AST transformer factories must be included within AutoMapper.', __METHOD__);
@@ -176,6 +179,12 @@ public static function create(
176179
$classDiscriminatorFromClassMetadata = new ClassDiscriminatorFromClassMetadata($classMetadataFactory);
177180
}
178181

182+
$providers = iterator_to_array($providers);
183+
184+
if (null !== $objectManager) {
185+
$providers[] = new DoctrineProvider($objectManager);
186+
}
187+
179188
$customTransformerRegistry = new PropertyTransformerRegistry($propertyTransformers);
180189
$metadataRegistry = new MetadataRegistry($configuration);
181190
$providerRegistry = new ProviderRegistry($providers);
@@ -192,6 +201,7 @@ public static function create(
192201
$expressionLanguage,
193202
$eventDispatcher,
194203
$removeDefaultProperties,
204+
$objectManager,
195205
);
196206

197207
$mapperGenerator = new MapperGenerator(

src/EventListener/ApiPlatform/JsonLdListener.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public function __invoke(GenerateMapperEvent $event): void
6868
}
6969

7070
if ($event->mapperMetadata->source === 'array' && $this->resourceClassResolver->isResourceClass($event->mapperMetadata->target)) {
71-
$event->provider = IriProvider::class;
71+
$event->provider ??= IriProvider::class;
7272
}
7373
}
7474
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AutoMapper\EventListener\Doctrine;
6+
7+
use AutoMapper\Event\PropertyMetadataEvent;
8+
use Doctrine\Persistence\ObjectManager;
9+
10+
final readonly class DoctrineIdentifierListener
11+
{
12+
public function __construct(
13+
private ObjectManager $objectManager
14+
) {
15+
}
16+
17+
public function __invoke(PropertyMetadataEvent $event): void
18+
{
19+
if ($event->mapperMetadata->target === 'array' || !$this->objectManager->getMetadataFactory()->hasMetadataFor($event->mapperMetadata->target)) {
20+
return;
21+
}
22+
23+
$metadata = $this->objectManager->getClassMetadata($event->mapperMetadata->target);
24+
25+
if ($metadata->isIdentifier($event->target->property)) {
26+
$event->identifier = true;
27+
}
28+
}
29+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AutoMapper\EventListener\Doctrine;
6+
7+
use AutoMapper\Event\GenerateMapperEvent;
8+
use AutoMapper\Provider\Doctrine\DoctrineProvider;
9+
use Doctrine\Persistence\ObjectManager;
10+
11+
final readonly class DoctrineProviderListener
12+
{
13+
public function __construct(
14+
private ObjectManager $objectManager
15+
) {
16+
}
17+
18+
public function __invoke(GenerateMapperEvent $event): void
19+
{
20+
if ($event->mapperMetadata->target === 'array' || !$this->objectManager->getMetadataFactory()->hasMetadataFor($event->mapperMetadata->target)) {
21+
return;
22+
}
23+
24+
$event->provider ??= DoctrineProvider::class;
25+
}
26+
}

src/EventListener/MapProviderListener.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ public function __invoke(GenerateMapperEvent $event): void
5151
}
5252
}
5353

54-
$event->provider ??= $provider ?? $defaultMapProvider;
54+
$eventProvider = $provider ?? $defaultMapProvider;
55+
56+
if (null === $eventProvider) {
57+
return;
58+
}
59+
60+
if (false === $eventProvider) {
61+
$event->provider = null;
62+
} else {
63+
$event->provider = $eventProvider;
64+
}
5565
}
5666
}

src/GeneratedMapper.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ public function registerMappers(AutoMapperRegistryInterface $registry): void
3636
{
3737
}
3838

39+
public function getTargetIdentifiers(mixed $value): mixed
40+
{
41+
return null;
42+
}
43+
3944
public function getSourceHash(mixed $value): ?string
4045
{
4146
return null;

src/Generator/IdentifierHashGenerator.php

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44

55
namespace AutoMapper\Generator;
66

7+
use AutoMapper\Extractor\ReadAccessor;
78
use AutoMapper\Metadata\GeneratorMetadata;
9+
use AutoMapper\Transformer\IdentifierHashInterface;
10+
use PhpParser\Builder;
811
use PhpParser\Node\Arg;
912
use PhpParser\Node\Expr;
1013
use PhpParser\Node\Name;
14+
use PhpParser\Node\Param;
1115
use PhpParser\Node\Scalar;
1216
use PhpParser\Node\Stmt;
1317

@@ -16,7 +20,7 @@
1620
/**
1721
* @return list<Stmt>
1822
*/
19-
public function getStatements(GeneratorMetadata $metadata, bool $fromSource): array
23+
private function getStatements(GeneratorMetadata $metadata, bool $fromSource): array
2024
{
2125
$identifiers = [];
2226

@@ -97,4 +101,146 @@ public function getStatements(GeneratorMetadata $metadata, bool $fromSource): ar
97101

98102
return $statements;
99103
}
104+
105+
/**
106+
* Create the getSourceHash method for this mapper.
107+
*
108+
* ```php
109+
* public function getSourceHash(mixed $source, mixed $target): ?string {
110+
* ... // statements
111+
* }
112+
* ```
113+
*/
114+
public function getSourceHashMethod(GeneratorMetadata $metadata): ?Stmt\ClassMethod
115+
{
116+
$stmts = $this->getStatements($metadata, true);
117+
118+
if (empty($stmts)) {
119+
return null;
120+
}
121+
122+
return (new Builder\Method('getSourceHash'))
123+
->makePublic()
124+
->setReturnType('?string')
125+
->addParam(new Param(
126+
var: new Expr\Variable('value'),
127+
type: new Name('mixed'))
128+
)
129+
->addStmts($stmts)
130+
->getNode();
131+
}
132+
133+
/**
134+
* Create the getTargetHash method for this mapper.
135+
*
136+
* ```php
137+
* public function getTargetHash(mixed $source, mixed $target): ?string {
138+
* ... // statements
139+
* }
140+
* ```
141+
*/
142+
public function getTargetHashMethod(GeneratorMetadata $metadata): ?Stmt\ClassMethod
143+
{
144+
$stmts = $this->getStatements($metadata, false);
145+
146+
if (empty($stmts)) {
147+
return null;
148+
}
149+
150+
return (new Builder\Method('getTargetHash'))
151+
->makePublic()
152+
->setReturnType('?string')
153+
->addParam(new Param(
154+
var: new Expr\Variable('value'),
155+
type: new Name('mixed'))
156+
)
157+
->addStmts($stmts)
158+
->getNode();
159+
}
160+
161+
/**
162+
* Create the getTargetIdentifiers method for this mapper.
163+
*
164+
* ```php
165+
* public function getTargetIdentifiers(mixed $source): mixed {
166+
* ... // statements
167+
* }
168+
* ```
169+
*/
170+
public function getTargetIdentifiersMethod(GeneratorMetadata $metadata): ?Stmt\ClassMethod
171+
{
172+
$identifiers = [];
173+
174+
foreach ($metadata->propertiesMetadata as $propertyMetadata) {
175+
if (!$propertyMetadata->identifier) {
176+
continue;
177+
}
178+
179+
if (null === $propertyMetadata->source->accessor) {
180+
continue;
181+
}
182+
183+
$identifiers[] = $propertyMetadata;
184+
}
185+
186+
if (empty($identifiers)) {
187+
return null;
188+
}
189+
190+
$isUnique = \count($identifiers) === 1;
191+
192+
$identifiersVariable = new Expr\Variable('identifiers');
193+
$valueVariable = new Expr\Variable('value');
194+
$statements = [];
195+
196+
if (!$isUnique) {
197+
$statements[] = new Stmt\Expression(new Expr\Assign($identifiersVariable, new Expr\Array_()));
198+
}
199+
200+
// foreach property we check
201+
foreach ($identifiers as $property) {
202+
/** @var ReadAccessor $accessor */
203+
$accessor = $property->source->accessor;
204+
205+
// check if the source is defined
206+
if ($property->source->checkExists) {
207+
$statements[] = new Stmt\If_($accessor->getIsUndefinedExpression($valueVariable), [
208+
'stmts' => [
209+
new Stmt\Return_(new Expr\ConstFetch(new Name('null'))),
210+
],
211+
]);
212+
}
213+
214+
$fieldValueExpr = $accessor->getExpression($valueVariable);
215+
$transformer = $property->transformer;
216+
217+
if ($transformer instanceof IdentifierHashInterface) {
218+
$fieldValueExpr = $transformer->getIdentifierExpression($fieldValueExpr);
219+
}
220+
221+
if ($isUnique) {
222+
$statements[] = new Stmt\Return_($fieldValueExpr);
223+
} else {
224+
$statements[] = new Stmt\Expression(new Expr\Assign(
225+
new Expr\ArrayDimFetch($identifiersVariable, new Scalar\String_($property->target->property)),
226+
$fieldValueExpr
227+
));
228+
}
229+
}
230+
231+
// return hash as string
232+
if (!$isUnique) {
233+
$statements[] = new Stmt\Return_($identifiersVariable);
234+
}
235+
236+
return (new Builder\Method('getTargetIdentifiers'))
237+
->makePublic()
238+
->setReturnType('mixed')
239+
->addParam(new Param(
240+
var: new Expr\Variable('value'),
241+
type: new Name('mixed'))
242+
)
243+
->addStmts($statements)
244+
->getNode();
245+
}
100246
}

0 commit comments

Comments
 (0)