You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -12,12 +12,12 @@ Mapper transforms arrays from one format to another using an object composition
12
12
$mappedData = (new Mapper)->map($data, new MyMapping);
13
13
```
14
14
15
-
This supposes we already created a mapping, `MyMapping`, to convert the data.
15
+
This supposes we already created a mapping, `MyMapping`, to convert `$data` into `$mappedData`.
16
16
17
17
Mappings
18
18
--------
19
19
20
-
Mappings are data transformation descriptions that describe how to convert data from one format to another. Mappings are an object wrapper for an array that describes the output format with instructions (hereafter known as *strategies*) that fetch or augment input data. To write a mapping we must know the input data format so we can then write an array that represents the desired output format and decorate it with strategies.
20
+
Mappings are data transformation descriptions that describe how to convert data from one format to another. Mappings are an object wrapper for an array, which describes the output format, with [expressions](#expressions) that can fetch and augment input data. To write a mapping we must know the input data format so we can write an array that represents the desired output format and decorate it with expressions to transform the input data.
21
21
22
22
### Example
23
23
@@ -41,7 +41,7 @@ $barData = (new Mapper)->map($fooData, new FooToBarMapping);
41
41
42
42
In this example we declare a mapping, `FooToBarMapping`, and pass it to the `Mapper::map` method to transform `$fooData` into `$barData`.
43
43
44
-
This mapping introduces the `Copy` strategy that copies a value from the input data to the output. Strategies are just one type of *expression* we can specify in a mapping.
44
+
This mapping introduces the `Copy` strategy that copies a value from the input data to the output. Strategies are just one type of *expression* we can specify as mapping values.
45
45
46
46
### Expressions
47
47
@@ -57,12 +57,37 @@ An expression is a pseudo-type representing the list of valid mapping value type
57
57
58
58
### Writing a mapping
59
59
60
-
To write a mapping create a new class that extends `Mapping` and implement its abstract method, `createMapping()`, that returns an array describing the output format with any combination of valid[expressions](#expressions).
60
+
To write a mapping create a new class that extends `Mapping` and implement its abstract method, `createMapping()`, that returns a strategy or an array describing the output format with any combination of [expressions](#expressions).
61
61
62
62
For prototyping purposes we can avoid writing a new mapping class and instead create an `AnonymousMapping`, passing the mapping definition to its constructor, which can be quicker than writing a new class. However, the recommended way to write mappings is to write new classes so mappings have meaningful names to identify them.
63
63
64
64
It is recommended to name mapping classes *XToYMapping* where *X* is the name of the input format and *Y* is the name of the output format.
65
65
66
+
### Strategy-based mappings
67
+
68
+
*Strategy-based* mappings are created by specifying a strategy at the top level. Usually mappings are *array-based*, and although such mappings may contain other expressions, including strategies, at the top level they are an array.
69
+
70
+
Some problems can only be solved with strategy-based mappings. For example, suppose we want to create a mapping that combines two other mappings at the top level. With array-based mappings the best we can do is something like the following.
71
+
72
+
```php
73
+
protected function createMapping()
74
+
{
75
+
return [
76
+
'foo' => new FooMapping,
77
+
'bar' => new BarMapping,
78
+
]
79
+
}
80
+
```
81
+
82
+
This composes `FooMapping` and `BarMapping` in our mapping but each mapping will be mapped under new `foo` and `bar` keys respectively. What we really want is to combine the keys of each mapping together at the top level of our mapping but there is no way to express a solution to this problem with array-based mappings. If we use the [`Merge`](#merge) strategy as the basis of our mapping we can solve this problem.
83
+
84
+
```php
85
+
protected function createMapping()
86
+
{
87
+
return new Merge(new FooMapping, new BarMapping);
88
+
}
89
+
```
90
+
66
91
Strategies
67
92
----------
68
93
@@ -72,6 +97,151 @@ Strategies are basic building blocks from which complex data manipulation chains
72
97
73
98
For a complete list of strategies please see the [strategy reference](#strategy-reference).
74
99
100
+
### Writing strategies
101
+
102
+
Strategies must implement the `Strategy` interface but it is common to extend `Delegate` or `Decorator` because we usually write augmenters which expect another strategy injected into them to provide data. `Delegate` and `Decorator` provide the `delegate()` method, which allows a strategy to evaluate an expression using Mapper, and is usually needed to evaluate the injected strategy. `Delegate` can delegate any expression to Mapper whereas `Decorator` only accepts `Strategy` objects.
103
+
104
+
It is recommended to name custom strategies with a *Strategy* suffix to help distinguish them from stock strategies.
105
+
106
+
## Practical example
107
+
108
+
Suppose we receive two different address formats from two different third-party providers. The first provider, FooBook, provides a single UK addresses. The second provider, BarBucket, provides a collection of US addresses. We are tasked with converting both types to the same uniform address format for our application using mappings. Sample data from each provider is shown below.
109
+
110
+
The address format for our application must be a flat array with the following fields.
111
+
112
+
* line1
113
+
* line2 (if applicable)
114
+
* city
115
+
* postcode
116
+
* country
117
+
118
+
### FooBook
119
+
120
+
A sample of the data we receive from FooBook is shown below.
121
+
122
+
```php
123
+
$fooBookAddress = [
124
+
'address' => [
125
+
'name' => 'Mr A Smith',
126
+
'address_line1' => '3 High Street',
127
+
'address_line2' => 'Hedge End',
128
+
'city' => 'SOUTHAMPTON',
129
+
'post_code' => 'SO31 4NG',
130
+
],
131
+
'country' => 'UK',
132
+
];
133
+
```
134
+
135
+
Before continuing, consider attempting to create the mapping on your own, consulting the [reference](#strategy-reference) if unsure which strategies to use. The following code shows how we can create a mapping to convert this address format to our application's format.
136
+
137
+
```php
138
+
class FooBookAddressToAddresesMapping extends Mapping
139
+
{
140
+
protected function createMapping()
141
+
{
142
+
return [
143
+
'line1' => new Copy('address->address_line1'),
144
+
'line2' => new Copy('address->address_line2'),
145
+
'city' => new Copy('address->city'),
146
+
'postcode' => new Copy('address->post_code'),
147
+
'country' => new Copy('country'),
148
+
];
149
+
}
150
+
}
151
+
```
152
+
153
+
Since the input data already has the values we want we only need to effectively rename the fields using `Copy` strategies. We do not need the name field so it is left unmapped.
154
+
155
+
The result of mapping the input data is shown below.
156
+
157
+
```php
158
+
$address = (new Mapper)->map($fooBookAddress, new FooBookAddressToAddresesMapping);
159
+
160
+
// Output.
161
+
[
162
+
'line1' => '3 High Street',
163
+
'line2' => 'Hedge End',
164
+
'city' => 'SOUTHAMPTON',
165
+
'postcode' => 'SO31 4NG',
166
+
'country' => 'UK',
167
+
]
168
+
```
169
+
170
+
### BarBucket
171
+
172
+
A sample of the data we receive from BarBucket is show below.
173
+
174
+
```php
175
+
$barBucketAddress = [
176
+
'Addresses' => [
177
+
[
178
+
'Jeremy Martinson, Jr.',
179
+
'455 Larkspur Dr.',
180
+
'Baviera, CA 92908',
181
+
],
182
+
],
183
+
];
184
+
```
185
+
186
+
This format is a lot less similar to our application's format. In particular, BarBucket's format supports multiple addresses but we're only interested in mapping one so we'll assume the first will suffice and discard any others. Their format also omits the country but we know BarBucket only supplies US addresses so we can assume the country is always "US". Once again, consider attempting to create the mapping on your own before observing the solution below.
187
+
188
+
```php
189
+
class BarBucketAddressToAddresesMapping extends Mapping
if (preg_match('[.*\b(\d{5})]', $line, $matches)) {
217
+
return $matches[1];
218
+
}
219
+
}
220
+
}
221
+
```
222
+
223
+
*Line1* can be copied straight from the input data and *country* can be hard-coded with a constant value because we assume it does not change.
224
+
225
+
City and postcode must be extracted from the last line of the address. For this we use `Callback` strategies that indirectly point to private methods of our mapping. Callbacks are only necessary because there are currently no included strategies to perform string splitting or regular expression matching.
226
+
227
+
The anonymous function wrapper picks the relevant part of the input data to pass to our methods. The weakness of this solution is dereferencing non-existent values will cause PHP to generate *undefined index* notices whereas injecting `Copy` strategies would gracefully resolve to `null` if any part of the path does not exist. Therefore, the most elegant solution would be to create custom strategies to promote code reuse and avoid errors, but is beyond the scope of this demonstration. For more information see [writing strategies](#writing-strategies).
228
+
229
+
The result of mapping the input data is shown below.
230
+
231
+
```php
232
+
$address = (new Mapper)->map($barBucketAddress, new BarBucketAddressToAddresesMapping);
233
+
234
+
// Output.
235
+
[
236
+
'line1' => '455 Larkspur Dr.',
237
+
'city' => 'Baviera',
238
+
'postcode' => '92908',
239
+
'country' => 'US',
240
+
],
241
+
```
242
+
243
+
Note that *line2* is not included in our output because it is was declared optional in the requirements. If it was required we could simply add `'line2' => null,` to our mapping, to hard-code its value to `null`, since it is never present in the input data from this provider.
Strategies must implement the `Strategy` interface but it is common to extend `Delegate` or `Decorator` because we usually write augmenters which expect another strategy injected into them to provide data. `Delegate` and `Decorator` provide the `delegate()` method, which allows a strategy to evaluate an expression using Mapper, and is usually needed to evaluate the injected strategy. `Delegate` can delegate any expression to Mapper whereas `Decorator` only accepts `Strategy` objects.
532
-
533
-
It is recommended to name custom strategies with a *Strategy* suffix to help distinguish them from stock strategies.
0 commit comments