Completed
Push — master ( abe227...316baf )
by Raffael
13:55 queued 08:01
created

AttributeMap::requireAttribute()   B

Complexity

Conditions 9
Paths 3

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 9

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 8
cts 8
cp 1
rs 8.0555
c 0
b 0
f 0
cc 9
nc 3
nop 3
crap 9
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * tubee.io
7
 *
8
 * @copyright   Copryright (c) 2017-2019 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Tubee;
13
14
use InvalidArgumentException;
15
use Psr\Log\LoggerInterface;
16
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
17
use Tubee\AttributeMap\AttributeMapInterface;
18
use Tubee\AttributeMap\Exception;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Tubee\Exception.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
19
use Tubee\AttributeMap\Transform;
20
21
class AttributeMap implements AttributeMapInterface
22
{
23
    /**
24
     * Attribute map.
25
     *
26
     * @var array
27
     */
28
    protected $map = [];
29
30
    /**
31
     * Logger.
32
     *
33
     * @var LoggerInterface
34
     */
35
    protected $logger;
36
37
    /**
38
     * Expression language.
39
     *
40
     * @var ExpressionLanguage
41
     */
42
    protected $expression;
43
44
    /**
45
     * Init attribute map.
46
     */
47 32
    public function __construct(array $map = [], ExpressionLanguage $expression, LoggerInterface $logger)
48
    {
49 32
        $this->map = $map;
50 32
        $this->logger = $logger;
51 32
        $this->expression = $expression;
52 32
    }
53
54
    /**
55
     * {@inheritdoc}
56
     */
57
    public function getMap(): array
58
    {
59
        return $this->map;
60
    }
61
62
    /**
63
     * {@inheritdoc}
64
     */
65
    public function getAttributes(): array
66
    {
67
        return array_keys($this->map);
68
    }
69
70
    /**
71
     * {@inheritdoc}
72
     */
73 29
    public function map(array $data): array
74
    {
75 29
        $result = [];
76 29
        foreach ($this->map as $attr => $value) {
77 29
            if (isset($attrv)) {
78
                unset($attrv);
79
            }
80
81 29
            if (isset($value['ensure'])) {
82 4
                if ($value['ensure'] === AttributeMapInterface::ENSURE_MERGE && isset($value['type']) && $value['type'] !== AttributeMapInterface::TYPE_ARRAY) {
83 1
                    throw new InvalidArgumentException('attribute '.$attr.' ensure is set to merge but type is not an array');
84
                }
85
86 3
                if ($value['ensure'] === AttributeMapInterface::ENSURE_ABSENT) {
87 1
                    continue;
88
                }
89
            }
90
91 27
            $mapped = $this->mapField($attr, $value, $data);
92
93 21
            if (isset($value['name'])) {
94 1
                $attr = $value['name'];
95
            }
96
97 21
            if ($mapped !== null) {
98 18
                $result[$attr] = $mapped;
99
100 18
                $this->logger->debug('mapped attribute ['.$attr.'] to [<'.gettype($result[$attr]).'> {value}]', [
101 18
                    'category' => get_class($this),
102 21
                    'value' => $result[$attr],
103
                ]);
104
            }
105
        }
106
107 22
        return $result;
108
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113 3
    public function getDiff(array $object, array $endpoint_object): array
114
    {
115 3
        $diff = [];
116 3
        foreach ($this->map as $attr => $value) {
117 3
            $exists = isset($endpoint_object[$attr]);
118
119 3
            if (isset($value['name'])) {
120 1
                $attr = $value['name'];
121
            }
122
123 3
            if ($value['ensure'] === AttributeMapInterface::ENSURE_EXISTS && ($exists === true || !isset($object[$attr]))) {
124
                continue;
125
            }
126 3
            if (($value['ensure'] === AttributeMapInterface::ENSURE_LAST || $value['ensure'] === AttributeMapInterface::ENSURE_EXISTS) && isset($object[$attr])) {
127 3
                if ($exists && is_array($object[$attr]) && is_array($endpoint_object[$attr]) && Helper::arrayEqual($endpoint_object[$attr], $object[$attr])) {
128
                    continue;
129
                }
130 3
                if ($exists && $object[$attr] === $endpoint_object[$attr]) {
131 1
                    continue;
132
                }
133
134 2
                $diff[$attr] = [
135 2
                    'action' => AttributeMapInterface::ACTION_REPLACE,
136 2
                    'value' => $object[$attr],
137
                ];
138
            } elseif ($value['ensure'] === AttributeMapInterface::ENSURE_ABSENT && isset($endpoint_object[$attr]) || isset($endpoint_object[$attr]) && !isset($object[$attr]) && $value['ensure'] !== AttributeMapInterface::ENSURE_MERGE) {
139
                $diff[$attr] = [
140
                    'action' => AttributeMapInterface::ACTION_REMOVE,
141
                ];
142
            } elseif ($value['ensure'] === AttributeMapInterface::ENSURE_MERGE && isset($object[$attr])) {
143
                $new_values = [];
144
145
                foreach ($object[$attr] as $val) {
146
                    if (!$exists) {
147
                        $new_values[] = $val;
148
                    } elseif (is_array($endpoint_object[$attr]) && in_array($val, $endpoint_object[$attr]) || $val === $endpoint_object[$attr]) {
149
                        continue;
150
                    } else {
151
                        $new_values[] = $val;
152
                    }
153
                }
154
155
                if (!empty($new_values)) {
156
                    $diff[$attr] = [
157
                        'action' => AttributeMapInterface::ACTION_ADD,
158 2
                        'value' => $new_values,
159
                    ];
160
                }
161
            }
162
        }
163
164 3
        return $diff;
165
    }
166
167
    /**
168
     * Map field.
169
     */
170 27
    protected function mapField($attr, $value, $data)
171
    {
172 27
        $attrv = $this->resolveValue($attr, $value, $data);
173 27
        $attrv = $this->transformAttribute($attr, $value, $attrv);
174
175 24
        if ($this->requireAttribute($attr, $value, $attrv) === null) {
176 3
            return;
177
        }
178
179 19
        if (isset($value['type'])) {
180 8
            $attrv = Transform::convertType($attrv, $attr, $value['type']);
181
        }
182
183 19
        if (isset($value['unwind'])) {
184 5
            $unwind = [];
185 5
            foreach ($attrv as $key => $element) {
186 5
                $result = $this->mapField($attr, $value['unwind'], [
187 5
                    'root' => $element,
188
                ]);
189
190 4
                if ($result !== null) {
191 4
                    $unwind[$key] = $result;
192
                }
193
            }
194
195 4
            $attrv = $unwind;
196
        }
197
198 18
        return $attrv;
199
    }
200
201
    /**
202
     * Check if attribute is required.
203
     */
204 24
    protected function requireAttribute(string $attr, array $value, $attrv)
205
    {
206 24
        if ($attrv === null || is_string($attrv) && strlen($attrv) === 0 || is_array($attrv) && count($attrv) === 0) {
207 5
            if (isset($value['required']) && $value['required'] === false || !isset($value['required'])) {
208 3
                $this->logger->debug('found attribute ['.$attr.'] but source attribute is empty, remove attribute from mapping', [
209 3
                     'category' => get_class($this),
210
                ]);
211
212 3
                return null;
213
            }
214
215 2
            throw new Exception\AttributeNotResolvable('required attribute '.$attr.' could not be resolved');
216
        }
217
218 19
        return $attrv;
219
    }
220
221
    /**
222
     * Transform attribute.
223
     */
224 27
    protected function transformAttribute(string $attr, array $value, $attrv)
225
    {
226 27
        if ($attrv === null) {
227 5
            return null;
228
        }
229
230 22
        if (isset($value['type']) && $value['type'] !== AttributeMapInterface::TYPE_ARRAY && is_array($attrv)) {
231 1
            $attrv = $this->firstArrayElement($attrv, $attr);
232
        }
233
234 22
        if (isset($value['rewrite'])) {
235 8
            $attrv = $this->rewrite($attrv, $value['rewrite']);
236
        }
237
238 20
        if (isset($value['require_regex'])) {
239 4
            if (!preg_match($value['require_regex'], $attrv)) {
240 2
                throw new Exception\AttributeRegexNotMatch('resolve attribute '.$attr.' value does not match require_regex');
241
            }
242
        }
243
244 19
        return $attrv;
245
    }
246
247
    /**
248
     * Check if attribute is required.
249
     */
250 27
    protected function resolveValue(string $attr, array $value, array $data)
251
    {
252 27
        $result = null;
253
254 27
        if (isset($value['value'])) {
255 2
            $result = $value['value'];
256
        }
257
258
        try {
259 27
            if (isset($value['from'])) {
260 27
                $result = Helper::getArrayValue($data, $value['from']);
261
            }
262 3
        } catch (\Exception $e) {
263 3
            $this->logger->warning('failed to resolve value of attribute ['.$attr.'] from ['.$value['from'].']', [
264 3
                'category' => get_class($this),
265 3
                'exception' => $e,
266
            ]);
267
        }
268
269
        try {
270 27
            if (isset($value['script'])) {
271 27
                $result = $this->expression->evaluate($value['script'], $data);
272
            }
273 2
        } catch (\Exception $e) {
274 2
            $this->logger->warning('failed to execute script ['.$value['script'].'] of attribute ['.$attr.']', [
275 2
                'category' => get_class($this),
276 2
                'exception' => $e,
277
            ]);
278
        }
279
280
        //if (isset($data[$attr])) {
281
        //    return $data[$attr];
282
        //}
283
284 27
        return $result;
285
    }
286
287
    /**
288
     * Shift first array element.
289
     */
290 1
    protected function firstArrayElement(Iterable $value, string $attribute)
291
    {
292 1
        if (empty($value)) {
293
            return $value;
294
        }
295
296 1
        $this->logger->debug('resolved value for attribute ['.$attribute.'] is an array but is not declared as an array, use first array element instead', [
297 1
             'category' => get_class($this),
298
        ]);
299
300 1
        return current($value);
301
    }
302
303
    /**
304
     * Process ruleset.
305
     */
306 8
    protected function rewrite($value, array $ruleset)
307
    {
308 8
        foreach ($ruleset as $rule) {
309 8
            if (!isset($rule['match'])) {
310 1
                throw new InvalidArgumentException('match in filter is not set');
311
            }
312
313 7
            if (!isset($rule['to'])) {
314 1
                throw new InvalidArgumentException('to in filter is not set');
315
            }
316
317 6
            if (isset($rule['regex']) && $rule['regex'] === false) {
318 2
                if ($value === $rule['match']) {
319 2
                    $value = $rule['to'];
320
321 2
                    return $value;
322
                }
323 4
            } elseif (isset($rule['regex']) && $rule['regex'] === true || !isset($rule['regex'])) {
324 4
                $value = preg_replace($rule['match'], $rule['to'], $value, -1, $count);
325 4
                if ($count > 0) {
326 5
                    return $value;
327
                }
328
            }
329
        }
330
331 1
        return $value;
332
    }
333
}
334