Skip to content

Commit e9a8fa0

Browse files
committed
FEATURE: Extend DenormalizingObjectConverter to support fromBoolean() and fromInteger()
...in addition to their short version (`fromBool()` and `fromInt()`). This also adds a short documentation about Value Object property mapping. Related: #2763
1 parent 05e57cc commit e9a8fa0

File tree

5 files changed

+219
-4
lines changed

5 files changed

+219
-4
lines changed

Neos.Flow/Classes/Property/TypeConverter/DenormalizingObjectConverter.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,10 @@ public static function canConvertFromSourceType(string $sourceType, string $targ
8282
return method_exists($targetType, 'fromString');
8383
case 'bool':
8484
case 'boolean':
85-
return method_exists($targetType, 'fromBool');
85+
return method_exists($targetType, 'fromBool') || method_exists($targetType, 'fromBoolean');
8686
case 'int':
8787
case 'integer':
88-
return method_exists($targetType, 'fromInt');
88+
return method_exists($targetType, 'fromInt') || method_exists($targetType, 'fromInteger');
8989
case 'double':
9090
case 'float':
9191
return method_exists($targetType, 'fromFloat');
@@ -161,9 +161,9 @@ public static function convertFromSource($source, string $targetType)
161161
case 'string':
162162
return $targetType::fromString($source);
163163
case 'boolean':
164-
return $targetType::fromBool($source);
164+
return method_exists($targetType, 'fromBool') ? $targetType::fromBool($source) : $targetType::fromBoolean($source);
165165
case 'integer':
166-
return $targetType::fromInt($source);
166+
return method_exists($targetType, 'fromInt') ? $targetType::fromInt($source) : $targetType::fromInteger($source);
167167
case 'double':
168168
return $targetType::fromFloat($source);
169169
default:

Neos.Flow/Documentation/TheDefinitiveGuide/PartIII/PropertyMapping.rst

+52
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,58 @@ body is empty.
354354
achieve the same without an annotation, by calling ``$this->arguments['comment']->setMapRequestBody(true)`` inside the
355355
``initializeCreateAction()`` method.
356356

357+
Mapping Value Objects
358+
---------------------
359+
360+
Value objects are immutable classes that represent one or more values.
361+
362+
Starting with version 8, Flow can map simple types to the corresponding Value Object if they follow some basic rules:
363+
364+
* They have a *public static* method named ``from<Type>`` that expects exactly one parameter of the given simple type
365+
and returns an instance of the class itself
366+
* They have a private default constructor (this is not required, but encouraged)
367+
368+
Supported simple types and their corresponding named constructor signature:
369+
370+
* ``array`` => ``public static function fromArray(array $array): self``
371+
* ``boolean`` => ``public static function fromBool(bool $value): self`` (or ``public static function fromBoolean(bool $value): self``
372+
* ``double``/``float`` => ``public static function fromFloat(double $value): self``
373+
* ``integer`` => ``public static function fromInt(int $value): self`` (or ``public static function fromInteger(int $value): self``
374+
* ``string`` => ``public static function fromString(string $value): self``
375+
376+
Example Value Object representing an email address::
377+
378+
/**
379+
* @Flow\Proxy(false)
380+
*/
381+
final class EmailAddress
382+
{
383+
private function __construct(
384+
public readonly string $value,
385+
) {
386+
if (filter_var($value, FILTER_VALIDATE_EMAIL) === false) {
387+
throw new \InvalidArgumentException(sprintf('"%s" is not a valid email address', $this->value));
388+
}
389+
}
390+
391+
public static function fromString(string $value): self
392+
{
393+
return new self($value);
394+
}
395+
}
396+
397+
.. note::
398+
399+
It's encouraged to add a ``@Flow\Proxy(false)`` annotation to Value Objects because private constructors can't be used
400+
and ``new self()`` can't be used otherwise.
401+
402+
With the example above, a corresponding Command- or ActionController can work with the ``EmailAddress` Value Object directly::
403+
404+
public function someCommand(EmailAddress $email): void
405+
{
406+
// $email->value is a valid email address at this point!
407+
}
408+
357409
Security Considerations
358410
-----------------------
359411

Neos.Flow/Tests/Unit/Property/TypeConverter/DenormalizingObjectConverterTest.php

+57
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414
use Neos\Flow\Property\TypeConverter\DenormalizingObjectConverter;
1515
use Neos\Flow\Tests\Unit\Property\TypeConverter\Fixture\ArrayBasedValueObject;
1616
use Neos\Flow\Tests\Unit\Property\TypeConverter\Fixture\BooleanBasedValueObject;
17+
use Neos\Flow\Tests\Unit\Property\TypeConverter\Fixture\BooleanBasedValueObjectWithLongName;
1718
use Neos\Flow\Tests\Unit\Property\TypeConverter\Fixture\FloatBasedValueObject;
1819
use Neos\Flow\Tests\Unit\Property\TypeConverter\Fixture\IntegerBasedValueObject;
20+
use Neos\Flow\Tests\Unit\Property\TypeConverter\Fixture\IntegerBasedValueObjectWithLongName;
1921
use Neos\Flow\Tests\Unit\Property\TypeConverter\Fixture\StringBasedValueObject;
22+
use Neos\Flow\Tests\Unit\Property\TypeConverter\Fixture\StringBasedValueObjectWithPrivateConstructor;
2023
use Neos\Flow\Tests\UnitTestCase;
2124

2225
final class DenormalizingObjectConverterTest extends UnitTestCase
@@ -30,7 +33,9 @@ public function identifiesDenormalizableClasses(): void
3033
$this->assertTrue(DenormalizingObjectConverter::isDenormalizable(ArrayBasedValueObject::class));
3134
$this->assertTrue(DenormalizingObjectConverter::isDenormalizable(StringBasedValueObject::class));
3235
$this->assertTrue(DenormalizingObjectConverter::isDenormalizable(BooleanBasedValueObject::class));
36+
$this->assertTrue(DenormalizingObjectConverter::isDenormalizable(BooleanBasedValueObjectWithLongName::class));
3337
$this->assertTrue(DenormalizingObjectConverter::isDenormalizable(IntegerBasedValueObject::class));
38+
$this->assertTrue(DenormalizingObjectConverter::isDenormalizable(IntegerBasedValueObjectWithLongName::class));
3439
$this->assertTrue(DenormalizingObjectConverter::isDenormalizable(FloatBasedValueObject::class));
3540

3641
$this->assertFalse(DenormalizingObjectConverter::isDenormalizable(UnitTestCase::class));
@@ -113,6 +118,35 @@ public function convertsFromBoolean(): void
113118
$this->assertEquals(true, $resultTrue->getValue());
114119
}
115120

121+
/**
122+
* @test
123+
* @return void
124+
*/
125+
public function canConvertFromBooleanWithLongName(): void
126+
{
127+
$typeConverter = new DenormalizingObjectConverter();
128+
$this->assertTrue($typeConverter->canConvertFrom(true, BooleanBasedValueObjectWithLongName::class));
129+
}
130+
131+
/**
132+
* @test
133+
* @return void
134+
*/
135+
public function convertsFromBooleanWithLongName(): void
136+
{
137+
$typeConverter = new DenormalizingObjectConverter();
138+
$resultFalse = $typeConverter->convertFrom(false, BooleanBasedValueObjectWithLongName::class);
139+
140+
$this->assertInstanceOf(BooleanBasedValueObjectWithLongName::class, $resultFalse);
141+
$this->assertEquals(false, $resultFalse->getValue());
142+
143+
$resultTrue = $typeConverter->convertFrom(true, BooleanBasedValueObjectWithLongName::class);
144+
145+
$this->assertInstanceOf(BooleanBasedValueObjectWithLongName::class, $resultTrue);
146+
$this->assertEquals(true, $resultTrue->getValue());
147+
}
148+
149+
116150
/**
117151
* @test
118152
* @return void
@@ -136,6 +170,29 @@ public function convertsFromInteger(): void
136170
$this->assertEquals(12264, $result->getValue());
137171
}
138172

173+
/**
174+
* @test
175+
* @return void
176+
*/
177+
public function canConvertFromIntegerWithLongName(): void
178+
{
179+
$typeConverter = new DenormalizingObjectConverter();
180+
$this->assertTrue($typeConverter->canConvertFrom(42, IntegerBasedValueObjectWithLongName::class));
181+
}
182+
183+
/**
184+
* @test
185+
* @return void
186+
*/
187+
public function convertsFromIntegerWithLongName(): void
188+
{
189+
$typeConverter = new DenormalizingObjectConverter();
190+
$result = $typeConverter->convertFrom(12264, IntegerBasedValueObjectWithLongName::class);
191+
192+
$this->assertInstanceOf(IntegerBasedValueObjectWithLongName::class, $result);
193+
$this->assertEquals(12264, $result->getValue());
194+
}
195+
139196
/**
140197
* @test
141198
* @return void
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
namespace Neos\Flow\Tests\Unit\Property\TypeConverter\Fixture;
3+
4+
/*
5+
* This file is part of the Neos.Flow package.
6+
*
7+
* (c) Contributors of the Neos Project - www.neos.io
8+
*
9+
* This package is Open Source Software. For the full copyright and license
10+
* information, please view the LICENSE file which was distributed with this
11+
* source code.
12+
*/
13+
14+
final class BooleanBasedValueObjectWithLongName implements \JsonSerializable
15+
{
16+
/**
17+
* @var bool
18+
*/
19+
private $value;
20+
21+
/**
22+
* @param bool $value
23+
*/
24+
private function __construct(bool $value)
25+
{
26+
$this->value = $value;
27+
}
28+
29+
/**
30+
* @param bool $bool
31+
* @return self
32+
*/
33+
public static function fromBoolean(bool $bool): self
34+
{
35+
return new self($bool);
36+
}
37+
38+
/**
39+
* @return bool
40+
*/
41+
public function getValue(): bool
42+
{
43+
return $this->value;
44+
}
45+
46+
/**
47+
* @return boolean
48+
*/
49+
public function jsonSerialize()
50+
{
51+
return $this->value;
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
namespace Neos\Flow\Tests\Unit\Property\TypeConverter\Fixture;
3+
4+
/*
5+
* This file is part of the Neos.Flow package.
6+
*
7+
* (c) Contributors of the Neos Project - www.neos.io
8+
*
9+
* This package is Open Source Software. For the full copyright and license
10+
* information, please view the LICENSE file which was distributed with this
11+
* source code.
12+
*/
13+
14+
final class IntegerBasedValueObjectWithLongName implements \JsonSerializable
15+
{
16+
/**
17+
* @var int
18+
*/
19+
private $value;
20+
21+
/**
22+
* @param int $value
23+
*/
24+
private function __construct(int $value)
25+
{
26+
$this->value = $value;
27+
}
28+
29+
/**
30+
* @param int $int
31+
* @return self
32+
*/
33+
public static function fromInteger(int $int): self
34+
{
35+
return new self($int);
36+
}
37+
38+
/**
39+
* @return int
40+
*/
41+
public function getValue(): int
42+
{
43+
return $this->value;
44+
}
45+
46+
/**
47+
* @return int
48+
*/
49+
public function jsonSerialize()
50+
{
51+
return $this->value;
52+
}
53+
}

0 commit comments

Comments
 (0)