Skip to content

Commit 5be86da

Browse files
author
Yevhen Sentiabov
authored
Merge pull request #207 from phoenix128/dto-proposal
DTO proposal
2 parents f1b6e71 + 60dda31 commit 5be86da

File tree

1 file changed

+324
-0
lines changed

1 file changed

+324
-0
lines changed

design-documents/dto/proposal.md

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
# DTO Management proposal
2+
3+
The current proposal is working and implemented in a WIP branch: https://github.com/magento/magento2/pull/23265 .
4+
5+
## Current situation
6+
DTOs in Magento2 must be manually created as PHP classes and immutables are not currently supported by `\Magento\Framework\Api\DataObjectHelper::populateWithArray` method which is widely used in the core.
7+
8+
Most of the time, DTOs are simply used to transport immutable information, such as an API request or an API resonse. In this case and because of implementation limitatsion, each DTO must also declare setter methods even if it supposed to not change. This requirement can lead to unexpected conditions and can introduce mutations where they should not be allowed.
9+
10+
Another problem with the manual DTO creation is represented by the possibility for a non experienced developer to add side effects to getter methods.
11+
12+
## Proposal
13+
The aim of this proposal is to switch from a manual DTO creation to an XML declarative way and an autogeneration of DTO PHP classes.
14+
15+
Other than speeding up the developing process of new features, this approach could avoid most of the human errors such as typos or side effects in DTOs.
16+
17+
### XML Syntax
18+
19+
The XML declarative DTO definition should be base on the following model:
20+
21+
```
22+
<?xml version="1.0"?>
23+
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
24+
xsi:noNamespaceSchemaLocation="urn:magento:framework:Dto:etc/dto.xsd">
25+
26+
<dto
27+
id="test-dto"
28+
mutable="false"
29+
>
30+
<class>Test\DtoGenerator\Dto\Test</class>
31+
<interface>Test\DtoGenerator\Api\TestInterface</interface>
32+
<property name="test1" type="string" nullable="true" />
33+
<property name="test2" type="string" optional="true" />
34+
<property name="test3" type="string" />
35+
<property name="test_abc" type="string" />
36+
</dto>
37+
</config>
38+
39+
```
40+
41+
The class name and the interface name should be declaread as fully qualified class names to allow a valid code generation.
42+
43+
### DTO mutability
44+
45+
A DTO can be specified with a `mutable` attribute. Depending on its value, setters methods will be created or not.
46+
In case of `mutable` = `false`, a PSR-7 like interface will be applied by creating a `with` method for each property. BY calling a `with` method, a new instance of the object will be returned with the updated information.
47+
48+
Example:
49+
```
50+
$dto = $dto
51+
->withTest1('another value')
52+
->withTest2('something different');
53+
```
54+
55+
#### DTO mutators
56+
Since the PSR-7 like approach can lead to an high memory consumption in case of several fields update, an autogenerated `Mutator` class is created to help such process.
57+
Mutator classes follow the builder model to store all the changed properties and return an updated object only at the end of the mutation process.
58+
59+
Example:
60+
```
61+
/** @var \Test\DtoGenerator\Dto\TestMutator $mutator */
62+
$dto = $mutator
63+
->withTest1('another value')
64+
->withTest2('something different')
65+
->mutate($dto);
66+
67+
```
68+
69+
In this last case, **only a new instance** is created instead of 2.
70+
71+
**NOTE:**
72+
Developers shold be aware that mutating several properties of an immutable can mean that the object should be considered as a mutable. Maybe a warning in static testing should be considered.
73+
74+
### Implementation consequences introdcuing immutables
75+
The current core implementation deeply relies on `\Magento\Framework\Api\DataObjectHelper::populateWithArray` that requires an empty object to be created before hydrating it.
76+
77+
Example:
78+
```
79+
$address = $this->addressFactory->create();
80+
$this->dataObjectHelper->populateWithArray($address, $data, AddressInterface::class);
81+
```
82+
83+
This approach is of course not compatible with an immutable DTO since the DTO information should be vailable at the moment of its creation.
84+
85+
The solution proposed is to add a new method to `\Magento\Framework\Api\DataObjectHelper::createFromArray` and deprecate `populateWithArray`.
86+
87+
**\Magento\Framework\Api\DataObjectHelper::createFromArray:**
88+
```
89+
public function createFromArray(array $data, string $type)
90+
{
91+
if ($this->dtoConfig->isDto($type)) {
92+
return $this->dtoProcessor->createFromArray($data, $type);
93+
}
94+
95+
// Compatibility mode
96+
$dataObject = $this->objectFactory->create($type, []);
97+
$this->populateWithArray($dataObject, $data, $type);
98+
99+
return $dataObject;
100+
}
101+
```
102+
103+
This new method should be used by `ServiceInputProcessor` and used in the recursive operation of `\Magento\Framework\Api\DataObjectHelper::_setDataValues` allowing a full backward compatibility.
104+
105+
#### The hydration strategy
106+
107+
The `createFromArray` method should be supporting both mutable and immutable DTOs, so a values injection srategy should be defined to allow the compatibility.
108+
109+
The proposal is to create a class `\Magento\Framework\Dto\DtoProcessor\GetHydrationStrategy` able to define the attributes injection strategy depending on code reflection. In this way we know whenever an attribute should be injected via constructor or setter.
110+
111+
### The extension attributes problem in immutables
112+
113+
One of the most logical effects of introducing immutable DTOs is related to **Extension Attributes**. Since Extension Attributes are `setter` based by designed, they are not supposed to be immutable, but an immutable DTO cannot have a mutable part.
114+
115+
The proposal is to define a new interface `\Magento\Framework\Api\ImmutableExtensibleDataInterface` not defining setter methods while code generating the extensible data class.
116+
117+
The open point now is how to inject values inside the extension classes if no setters are provided and extension must be hydrated only after the DTO creation.
118+
119+
#### The extension attributes injectors
120+
121+
The proposal is to modify the `extension_attributes.xsd` by adding the following specifications:
122+
123+
```
124+
<xs:complexType name="injectorType">
125+
<xs:attribute type="xs:string" name="code" use="required"/>
126+
<xs:attribute type="xs:string" name="type" use="required"/>
127+
</xs:complexType>
128+
<xs:complexType name="extension_attributesType">
129+
<xs:sequence>
130+
<xs:element type="attributeType" name="attribute" minOccurs="0" maxOccurs="unbounded"/>
131+
<xs:element type="injectorType" name="injector" minOccurs="0" maxOccurs="unbounded"/>
132+
</xs:sequence>
133+
<xs:attribute type="xs:string" name="for" use="required"/>
134+
</xs:complexType>
135+
```
136+
137+
In this way, an `extension_attributes.xml` file may look like:
138+
139+
```
140+
<?xml version="1.0"?>
141+
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
142+
xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd">
143+
<extension_attributes for="Test\DtoGenerator\Api\TestInterface">
144+
<attribute code="test_attr" type="string" />
145+
<injector code="test_attr_injector" type="Test\DtoGenerator\Model\MyInjector" />
146+
</extension_attributes>
147+
</config>
148+
149+
```
150+
151+
The class `Test\DtoGenerator\Model\MyInjector` must implement `\Magento\Framework\Api\ExtensionAttribute\InjectorProcessorInterface` and declare an `execute` method with the DTO data as array and return an array with the extension attributes values.
152+
153+
Unfortunately, at this stage, only an associative array can be used to manupulate the information, since the object should be created only after its dataset is defined.
154+
155+
Example:
156+
```
157+
<?php
158+
declare(strict_types=1);
159+
160+
namespace Test\DtoGenerator\Model;
161+
162+
use Magento\Framework\Api\ExtensionAttribute\InjectorProcessorInterface;
163+
164+
class MyInjector implements InjectorProcessorInterface
165+
{
166+
/**
167+
* Process object for injections
168+
*
169+
* @param string $type
170+
* @param array $objectData
171+
* @return array
172+
*/
173+
public function execute(string $type, array $objectData): array
174+
{
175+
return ['test_attr' => 'Hello world'];
176+
}
177+
}
178+
179+
```
180+
181+
The attributes extension injector mechanism could also be used in mutable DTOs or existing classes.
182+
183+
## Generated code examples
184+
185+
```
186+
<?php
187+
declare(strict_types=1);
188+
189+
namespace Test\DtoGenerator\Dto;
190+
191+
class Test implements \Test\DtoGenerator\Api\TestInterface
192+
{
193+
/**
194+
* @var string
195+
*/
196+
private $test1 = null;
197+
198+
/**
199+
* @var string
200+
*/
201+
private $test2 = null;
202+
203+
/**
204+
* @var string
205+
*/
206+
private $test3 = null;
207+
208+
/**
209+
* @var string
210+
*/
211+
private $testAbc = null;
212+
213+
/**
214+
* @var \Test\DtoGenerator\Api\TestExtensionInterface
215+
*/
216+
private $extensionAttributes = null;
217+
218+
/**
219+
* @param string|null $test1
220+
* @param string $test2
221+
* @param string $test3
222+
* @param string $testAbc
223+
* @param \Test\DtoGenerator\Api\TestExtensionInterface|null $extensionAttributes
224+
*/
225+
public function __construct(?string $test1, string $test3, string $testAbc, ?string $test2 = null, ?\Test\DtoGenerator\Api\TestExtensionInterface $extensionAttributes = null)
226+
{
227+
$this->test1 = $test1;
228+
$this->test2 = $test2;
229+
$this->test3 = $test3;
230+
$this->testAbc = $testAbc;
231+
$this->extensionAttributes = $extensionAttributes;
232+
}
233+
234+
/**
235+
* @return string|null
236+
*/
237+
public function getTest1() : ?string
238+
{
239+
return $this->test1;
240+
}
241+
242+
/**
243+
* @return string|null
244+
*/
245+
public function getTest2() : ?string
246+
{
247+
return $this->test2;
248+
}
249+
250+
/**
251+
* @return string
252+
*/
253+
public function getTest3() : string
254+
{
255+
return $this->test3;
256+
}
257+
258+
/**
259+
* @return string
260+
*/
261+
public function getTestAbc() : string
262+
{
263+
return $this->testAbc;
264+
}
265+
266+
/**
267+
* @return \Test\DtoGenerator\Api\TestExtensionInterface|null
268+
*/
269+
public function getExtensionAttributes() : ?\Test\DtoGenerator\Api\TestExtensionInterface
270+
{
271+
return $this->extensionAttributes;
272+
}
273+
274+
/**
275+
* @param string|null $value
276+
* @return Test\DtoGenerator\Api\TestInterface
277+
*/
278+
public function withTest1(?string $value) : \Test\DtoGenerator\Api\TestInterface
279+
{
280+
$dtoProcessor = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Framework\Dto\DtoProcessor::class);
281+
return $dtoProcessor->createUpdatedObjectFromArray($this, ['test1' => $value]);
282+
}
283+
284+
/**
285+
* @param string $value
286+
* @return Test\DtoGenerator\Api\TestInterface
287+
*/
288+
public function withTest2(string $value) : \Test\DtoGenerator\Api\TestInterface
289+
{
290+
$dtoProcessor = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Framework\Dto\DtoProcessor::class);
291+
return $dtoProcessor->createUpdatedObjectFromArray($this, ['test2' => $value]);
292+
}
293+
294+
/**
295+
* @param string $value
296+
* @return Test\DtoGenerator\Api\TestInterface
297+
*/
298+
public function withTest3(string $value) : \Test\DtoGenerator\Api\TestInterface
299+
{
300+
$dtoProcessor = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Framework\Dto\DtoProcessor::class);
301+
return $dtoProcessor->createUpdatedObjectFromArray($this, ['test3' => $value]);
302+
}
303+
304+
/**
305+
* @param string $value
306+
* @return Test\DtoGenerator\Api\TestInterface
307+
*/
308+
public function withTestAbc(string $value) : \Test\DtoGenerator\Api\TestInterface
309+
{
310+
$dtoProcessor = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Framework\Dto\DtoProcessor::class);
311+
return $dtoProcessor->createUpdatedObjectFromArray($this, ['testAbc' => $value]);
312+
}
313+
314+
/**
315+
* @param \Test\DtoGenerator\Api\TestExtensionInterface|null $value
316+
* @return Test\DtoGenerator\Api\TestInterface
317+
*/
318+
public function withExtensionAttributes(?\Test\DtoGenerator\Api\TestExtensionInterface $value) : \Test\DtoGenerator\Api\TestInterface
319+
{
320+
$dtoProcessor = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Framework\Dto\DtoProcessor::class);
321+
return $dtoProcessor->createUpdatedObjectFromArray($this, ['extensionAttributes' => $value]);
322+
}
323+
}
324+
```

0 commit comments

Comments
 (0)