Skip to content

Commit 98a3a6f

Browse files
authored
fix: constructor with default values as objects (#188)
While working on #187 I noticed what you can't map to a class that has constructor with default values as objects if source missing these arguments. Seems like a complex issue to fix, so I'll start with the tests
2 parents b5703b2 + c0a831b commit 98a3a6f

File tree

3 files changed

+170
-119
lines changed

3 files changed

+170
-119
lines changed

src/Generator/CreateTargetStatementsGenerator.php

Lines changed: 130 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
namespace AutoMapper\Generator;
66

7-
use AutoMapper\Exception\CompileException;
87
use AutoMapper\Exception\MissingConstructorArgumentsException;
98
use AutoMapper\Generator\Shared\CachedReflectionStatementsGenerator;
109
use AutoMapper\Generator\Shared\DiscriminatorStatementsGenerator;
@@ -14,12 +13,9 @@
1413
use AutoMapper\Transformer\AllowNullValueTransformerInterface;
1514
use PhpParser\Node\Arg;
1615
use PhpParser\Node\Expr;
17-
use PhpParser\Node\Identifier;
1816
use PhpParser\Node\Name;
1917
use PhpParser\Node\Scalar;
2018
use PhpParser\Node\Stmt;
21-
use PhpParser\Parser;
22-
use PhpParser\ParserFactory;
2319

2420
use function AutoMapper\PhpParser\create_expr_array_item;
2521
use function AutoMapper\PhpParser\create_scalar_int;
@@ -29,16 +25,12 @@
2925
*/
3026
final readonly class CreateTargetStatementsGenerator
3127
{
32-
private Parser $parser;
33-
3428
public function __construct(
3529
private DiscriminatorStatementsGenerator $discriminatorStatementsGeneratorSource,
3630
private DiscriminatorStatementsGenerator $discriminatorStatementsGeneratorTarget,
3731
private CachedReflectionStatementsGenerator $cachedReflectionStatementsGenerator,
3832
private PropertyConditionsGenerator $propertyConditionsGenerator,
39-
?Parser $parser = null,
4033
) {
41-
$this->parser = $parser ?? (new ParserFactory())->createForHostVersion();
4234
}
4335

4436
/**
@@ -120,29 +112,35 @@ private function constructorArguments(GeneratorMetadata $metadata): array
120112
return [];
121113
}
122114

123-
$constructArguments = [];
124115
$createObjectStatements = [];
116+
$constructVar = $metadata->variableRegistry->getVariableWithUniqueName('constructArgs');
125117

126118
foreach ($targetConstructor->getParameters() as $constructorParameter) {
127119
// Find property for parameter
128120
$propertyMetadata = $metadata->getTargetPropertyWithConstructor($constructorParameter->getName());
129121

130122
$propertyStatements = null;
131-
$constructArgument = null;
132-
$constructorName = null;
123+
$assignVar = new Expr\ArrayDimFetch(
124+
$constructVar,
125+
new Scalar\String_($constructorParameter->getName())
126+
);
133127

134128
if (null !== $propertyMetadata) {
135-
[$propertyStatements, $constructArgument, $constructorName] = $this->constructorArgument($metadata, $propertyMetadata, $constructorParameter);
129+
$propertyStatements = $this->constructorArgument($assignVar, $metadata, $propertyMetadata, $constructorParameter);
136130
}
137131

138-
if (null === $propertyStatements || null === $constructArgument || null === $constructorName) {
139-
[$propertyStatements, $constructArgument, $constructorName] = $this->constructorArgumentWithoutSource($metadata, $constructorParameter);
132+
if (null === $propertyStatements) {
133+
$propertyStatements = $this->constructorArgumentWithoutSource($assignVar, $metadata, $constructorParameter);
140134
}
141135

142136
$createObjectStatements = [...$createObjectStatements, ...$propertyStatements];
143-
$constructArguments[$constructorName] = $constructArgument;
144137
}
145138

139+
$createObjectStatements = [
140+
new Stmt\Expression(new Expr\Assign($constructVar, new Expr\Array_())),
141+
...$createObjectStatements,
142+
];
143+
146144
/*
147145
* Create object with named constructor arguments
148146
*
@@ -151,7 +149,9 @@ private function constructorArguments(GeneratorMetadata $metadata): array
151149
$createObjectStatements[] = new Stmt\Expression(
152150
new Expr\Assign(
153151
$metadata->variableRegistry->getResult(),
154-
new Expr\New_(new Name\FullyQualified($metadata->mapperMetadata->target), $constructArguments)
152+
new Expr\New_(new Name\FullyQualified($metadata->mapperMetadata->target), [
153+
new Arg($constructVar, unpack: true),
154+
])
155155
)
156156
);
157157

@@ -162,94 +162,101 @@ private function constructorArguments(GeneratorMetadata $metadata): array
162162
* If source missing a constructor argument, check if there is a constructor argument in the context, otherwise we use the default value or throw exception.
163163
*
164164
* ```php
165-
*
166-
* if ($value not defined) {
167-
* $constructarg = MapperContext::hasConstructorArgument($context, $target, 'propertyName') ? MapperContext::getConstructorArgument($context, $target, 'propertyName') : {defaultValueExpr} // default value or throw exception
165+
* if ($value is defined) {
166+
* $constructarg['param'] = transformation of value
167+
* } elseif (MapperContext::hasConstructorArgument($context, $target, 'propertyName')) {
168+
* $constructarg['param'] = MapperContext::getConstructorArgument($context, $target, 'propertyName');
168169
* } else {
169-
* $constructarg = transformation of value
170+
* // throw exception if no default expression and no null allowed
170171
* }
171172
* ```
172173
*
173-
* @return array{Stmt[], Arg, string}|array{null, null, null}
174+
* @return list<Stmt>|null
174175
*/
175-
private function constructorArgument(GeneratorMetadata $metadata, PropertyMetadata $propertyMetadata, \ReflectionParameter $parameter): array
176+
private function constructorArgument(Expr\ArrayDimFetch $assignVar, GeneratorMetadata $metadata, PropertyMetadata $propertyMetadata, \ReflectionParameter $parameter): ?array
176177
{
177178
$variableRegistry = $metadata->variableRegistry;
178-
$constructVar = $variableRegistry->getVariableWithUniqueName('constructArg');
179179
$fieldValueExpr = $propertyMetadata->source->accessor?->getExpression($variableRegistry->getSourceInput());
180180

181-
$condition = $this->propertyConditionsGenerator->generate(
181+
$conditionDefined = $this->propertyConditionsGenerator->generate(
182182
$metadata,
183183
$propertyMetadata,
184184
true
185185
);
186186

187187
if (null === $fieldValueExpr) {
188188
if (!($propertyMetadata->transformer instanceof AllowNullValueTransformerInterface)) {
189-
return [null, null, null];
189+
return null;
190190
}
191191

192192
$fieldValueExpr = new Expr\ConstFetch(new Name('null'));
193193
}
194194

195-
$defaultValueExpr = new Expr\Throw_(
196-
new Expr\New_(new Name\FullyQualified(MissingConstructorArgumentsException::class), [
197-
new Arg(new Scalar\String_(sprintf('Cannot create an instance of "%s" from mapping data because its constructor requires the following parameters to be present : "$%s".', $metadata->mapperMetadata->target, $propertyMetadata->target->property))),
198-
new Arg(create_scalar_int(0)),
199-
new Arg(new Expr\ConstFetch(new Name('null'))),
200-
new Arg(new Expr\Array_([
201-
create_expr_array_item(new Scalar\String_($propertyMetadata->target->property)),
202-
])),
203-
new Arg(new Scalar\String_($metadata->mapperMetadata->target)),
204-
])
205-
);
206-
207-
if ($parameter->isDefaultValueAvailable()) {
208-
$defaultValueExpr = $this->getValueAsExpr($parameter->getDefaultValue());
209-
} elseif ($parameter->allowsNull()) {
210-
$defaultValueExpr = new Expr\ConstFetch(new Name('null'));
195+
$defaultValueExpr = null;
196+
197+
if (!$parameter->isDefaultValueAvailable()) {
198+
if ($parameter->allowsNull()) {
199+
$defaultValueExpr = new Expr\ConstFetch(new Name('null'));
200+
} else {
201+
$defaultValueExpr = new Expr\Throw_(new Expr\New_(new Name\FullyQualified(MissingConstructorArgumentsException::class), [
202+
new Arg(new Scalar\String_(sprintf('Cannot create an instance of "%s" from mapping data because its constructor requires the following parameters to be present : "$%s".', $metadata->mapperMetadata->target, $propertyMetadata->target->property))),
203+
new Arg(create_scalar_int(0)),
204+
new Arg(new Expr\ConstFetch(new Name('null'))),
205+
new Arg(new Expr\Array_([
206+
create_expr_array_item(new Scalar\String_($propertyMetadata->target->property)),
207+
])),
208+
new Arg(new Scalar\String_($metadata->mapperMetadata->target)),
209+
]));
210+
}
211211
}
212212

213213
/* Get extract and transform statements for this property */
214-
[$output, $propStatements] = $propertyMetadata->transformer->transform($fieldValueExpr, $constructVar, $propertyMetadata, $variableRegistry->getUniqueVariableScope(), $variableRegistry->getSourceInput());
214+
[$output, $propStatements] = $propertyMetadata->transformer->transform($fieldValueExpr, $assignVar, $propertyMetadata, $variableRegistry->getUniqueVariableScope(), $variableRegistry->getSourceInput());
215+
216+
$hasConstructorArgument = new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'hasConstructorArgument', [
217+
new Arg($variableRegistry->getContext()),
218+
new Arg(new Scalar\String_($metadata->mapperMetadata->target)),
219+
new Arg(new Scalar\String_($propertyMetadata->target->property)),
220+
]);
221+
$hasConstructorArgumentStmts = [
222+
new Stmt\Expression(new Expr\Assign($assignVar, new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'getConstructorArgument', [
223+
new Arg($variableRegistry->getContext()),
224+
new Arg(new Scalar\String_($metadata->mapperMetadata->target)),
225+
new Arg(new Scalar\String_($propertyMetadata->target->property)),
226+
]))),
227+
];
215228

216-
if (!$condition) {
229+
if (!$conditionDefined) {
217230
return [
218-
[
219-
...$propStatements,
220-
new Stmt\Expression(new Expr\Assign($constructVar, $output)),
221-
],
222-
new Arg($constructVar, name: new Identifier($parameter->getName())),
223-
$parameter->getName(),
231+
...$propStatements,
232+
new Stmt\Expression(new Expr\Assign($assignVar, $output)),
224233
];
225234
}
226235

227-
return [
236+
$if = new Stmt\If_(
237+
$conditionDefined,
228238
[
229-
new Stmt\If_($condition, [
230-
'stmts' => [
231-
...$propStatements,
232-
new Stmt\Expression(new Expr\Assign($constructVar, $output)),
233-
],
234-
'else' => new Stmt\Else_([
235-
new Stmt\Expression(new Expr\Assign($constructVar, new Expr\Ternary(
236-
new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'hasConstructorArgument', [
237-
new Arg($variableRegistry->getContext()),
238-
new Arg(new Scalar\String_($metadata->mapperMetadata->target)),
239-
new Arg(new Scalar\String_($propertyMetadata->target->property)),
240-
]),
241-
new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'getConstructorArgument', [
242-
new Arg($variableRegistry->getContext()),
243-
new Arg(new Scalar\String_($metadata->mapperMetadata->target)),
244-
new Arg(new Scalar\String_($propertyMetadata->target->property)),
245-
]),
246-
$defaultValueExpr,
247-
))),
248-
]),
249-
]),
239+
'stmts' => [
240+
...$propStatements,
241+
new Stmt\Expression(new Expr\Assign($assignVar, $output)),
242+
],
243+
'elseifs' => [
244+
new Stmt\ElseIf_(
245+
$hasConstructorArgument,
246+
$hasConstructorArgumentStmts,
247+
),
248+
],
250249
],
251-
new Arg($constructVar, name: new Identifier($parameter->getName())),
252-
$parameter->getName(),
250+
);
251+
252+
if ($defaultValueExpr) {
253+
$if->else = new Stmt\Else_([
254+
new Stmt\Expression(new Expr\Assign($assignVar, $defaultValueExpr)),
255+
]);
256+
}
257+
258+
return [
259+
$if,
253260
];
254261
}
255262

@@ -261,50 +268,67 @@ private function constructorArgument(GeneratorMetadata $metadata, PropertyMetada
261268
* ? MapperContext::getConstructorArgument($context, $target, 'propertyName')
262269
* : {defaultValueExpr} // default value or throw exception
263270
* ```
271+
* ```php
272+
* if (MapperContext::hasConstructorArgument($context, $target, 'propertyName')) {}
273+
* $constructArgs['paramName'] = MapperContext::getConstructorArgument($context, $target, 'propertyName');
274+
* } else {
275+
* // throw exception if no default expression and no null allowed
276+
* throw new MissingConstructorArgumentsException('Cannot create an instance of "AutoMapper\Tests\Fixtures\ConstructorWithDefaultValuesAsObjects" from mapping data because its constructor requires the following parameters to be present : "$baz".', 0, null, ['baz'], 'AutoMapper\Tests\Fixtures\ConstructorWithDefaultValuesAsObjects');
277+
* // set null if no default expression and null allowed
278+
* $constructArgs['paramName'] = null;
279+
* }
280+
* ```
264281
*
265-
* @return array{Stmt[], Arg, string}
282+
* @return list<Stmt>
266283
*/
267-
private function constructorArgumentWithoutSource(GeneratorMetadata $metadata, \ReflectionParameter $constructorParameter): array
284+
private function constructorArgumentWithoutSource(Expr\ArrayDimFetch $assignVar, GeneratorMetadata $metadata, \ReflectionParameter $constructorParameter): array
268285
{
269286
$variableRegistry = $metadata->variableRegistry;
270-
$constructVar = $variableRegistry->getVariableWithUniqueName('constructArg');
271-
272-
$defaultValueExpr = new Expr\Throw_(new Expr\New_(new Name\FullyQualified(MissingConstructorArgumentsException::class), [
273-
new Arg(new Scalar\String_(sprintf('Cannot create an instance of "%s" from mapping data because its constructor requires the following parameters to be present : "$%s".', $metadata->mapperMetadata->target, $constructorParameter->getName()))),
274-
new Arg(create_scalar_int(0)),
275-
new Arg(new Expr\ConstFetch(new Name('null'))),
276-
new Arg(new Expr\Array_([
277-
create_expr_array_item(new Scalar\String_($constructorParameter->getName())),
278-
])),
279-
new Arg(new Scalar\String_($constructorParameter->getName())),
280-
]));
281-
282-
if ($constructorParameter->isDefaultValueAvailable()) {
283-
$defaultValueExpr = $this->getValueAsExpr($constructorParameter->getDefaultValue());
284-
} elseif ($constructorParameter->allowsNull()) {
285-
$defaultValueExpr = new Expr\ConstFetch(new Name('null'));
287+
$defaultValueExpr = null;
288+
289+
if (!$constructorParameter->isDefaultValueAvailable()) {
290+
if ($constructorParameter->allowsNull()) {
291+
$defaultValueExpr = new Expr\ConstFetch(new Name('null'));
292+
} else {
293+
$defaultValueExpr = new Expr\Throw_(new Expr\New_(new Name\FullyQualified(MissingConstructorArgumentsException::class), [
294+
new Arg(new Scalar\String_(sprintf('Cannot create an instance of "%s" from mapping data because its constructor requires the following parameters to be present : "$%s".', $metadata->mapperMetadata->target, $constructorParameter->getName()))),
295+
new Arg(create_scalar_int(0)),
296+
new Arg(new Expr\ConstFetch(new Name('null'))),
297+
new Arg(new Expr\Array_([
298+
create_expr_array_item(new Scalar\String_($constructorParameter->getName())),
299+
])),
300+
new Arg(new Scalar\String_($constructorParameter->getName())),
301+
]));
302+
}
286303
}
287304

288-
return [
305+
$if = new Stmt\If_(
306+
new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'hasConstructorArgument', [
307+
new Arg($variableRegistry->getContext()),
308+
new Arg(new Scalar\String_($metadata->mapperMetadata->target)),
309+
new Arg(new Scalar\String_($constructorParameter->getName())),
310+
]),
289311
[
290-
new Stmt\Expression(new Expr\Assign($constructVar,
291-
new Expr\Ternary(
292-
new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'hasConstructorArgument', [
293-
new Arg($variableRegistry->getContext()),
294-
new Arg(new Scalar\String_($metadata->mapperMetadata->target)),
295-
new Arg(new Scalar\String_($constructorParameter->getName())),
296-
]),
312+
'stmts' => [
313+
new Stmt\Expression(new Expr\Assign($assignVar,
297314
new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'getConstructorArgument', [
298315
new Arg($variableRegistry->getContext()),
299316
new Arg(new Scalar\String_($metadata->mapperMetadata->target)),
300317
new Arg(new Scalar\String_($constructorParameter->getName())),
301-
]),
302-
$defaultValueExpr,
303-
))
304-
),
305-
],
306-
new Arg($constructVar, name: new Identifier($constructorParameter->getName())),
307-
$constructorParameter->getName(),
318+
])
319+
)),
320+
],
321+
]
322+
);
323+
324+
if ($defaultValueExpr !== null) {
325+
$if->else = new Stmt\Else_([
326+
new Stmt\Expression(new Expr\Assign($assignVar, $defaultValueExpr)),
327+
]);
328+
}
329+
330+
return [
331+
$if,
308332
];
309333
}
310334

@@ -330,15 +354,4 @@ private function constructorWithoutArgument(GeneratorMetadata $metadata): ?Stmt
330354

331355
return new Stmt\Expression(new Expr\Assign($metadata->variableRegistry->getResult(), new Expr\New_(new Name\FullyQualified($metadata->mapperMetadata->target))));
332356
}
333-
334-
private function getValueAsExpr(mixed $value): Expr
335-
{
336-
$expr = $this->parser->parse('<?php ' . var_export($value, true) . ';')[0] ?? null;
337-
338-
if ($expr instanceof Stmt\Expression) {
339-
return $expr->expr;
340-
}
341-
342-
throw new CompileException('Cannot extract expr from ' . var_export($value, true));
343-
}
344357
}

0 commit comments

Comments
 (0)