Completed
Push — master ( b64fc1...5651bd )
by Raffael
16:20 queued 08:39
created

AttributeMap   F

Complexity

Total Complexity 61

Size/Duplication

Total Lines 298
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 92%

Importance

Changes 0
Metric Value
wmc 61
lcom 1
cbo 6
dl 0
loc 298
ccs 115
cts 125
cp 0.92
rs 3.52
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A getMap() 0 4 1
A getAttributes() 0 4 1
B map() 0 41 11
A getDiff() 0 4 1
B mapField() 0 34 8
B requireAttribute() 0 16 9
B transformAttribute() 0 27 11
A getFilters() 0 9 2
B resolveValue() 0 51 8
A firstArrayElement() 0 12 2
A rewrite() 0 19 6

How to fix   Complexity   

Complex Class

Complex classes like AttributeMap often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AttributeMap, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * tubee
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 MongoDB\BSON\Binary;
16
use Psr\Log\LoggerInterface;
17
use Tubee\AttributeMap\AttributeMapInterface;
18
use Tubee\AttributeMap\Diff;
19
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...
20
use Tubee\AttributeMap\Transform;
21
use Tubee\V8\Engine as V8Engine;
22
use V8Js;
23
use Zend\Filter\FilterChain;
24
25
class AttributeMap implements AttributeMapInterface
26
{
27
    /**
28
     * Attribute map.
29
     *
30
     * @var array
31
     */
32
    protected $map = [];
33
34
    /**
35
     * Logger.
36
     *
37
     * @var LoggerInterface
38
     */
39
    protected $logger;
40
41
    /**
42
     * V8.
43
     *
44
     * @var V8Engine
45
     */
46
    protected $v8;
47
48
    /**
49
     * Init attribute map.
50
     */
51 28
    public function __construct(array $map = [], V8Engine $v8, LoggerInterface $logger)
52
    {
53 28
        $this->map = $map;
54 28
        $this->logger = $logger;
55 28
        $this->v8 = $v8;
56 28
    }
57
58
    /**
59
     * {@inheritdoc}
60
     */
61
    public function getMap(): array
62
    {
63
        return $this->map;
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69
    public function getAttributes(): array
70
    {
71
        return array_column($this->map, 'name');
72
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77 25
    public function map(array $data): array
78
    {
79 25
        $this->v8->object = $data;
80 25
        $attrv = null;
81
82 25
        $result = [];
83 25
        foreach ($this->map as $value) {
84 25
            if (isset($attrv)) {
85
                unset($attrv);
86
            }
87
88 25
            $attr = $value['name'];
89
90 25
            if (isset($value['ensure'])) {
91 4
                if ($value['ensure'] === AttributeMapInterface::ENSURE_MERGE && isset($value['type']) && $value['type'] !== AttributeMapInterface::TYPE_ARRAY) {
92 1
                    throw new InvalidArgumentException('attribute '.$attr.' ensure is set to merge but type is not an array');
93
                }
94
95 3
                if ($value['ensure'] === AttributeMapInterface::ENSURE_ABSENT) {
96 1
                    continue;
97
                }
98
            }
99
100 23
            $mapped = $this->mapField($attr, $value, $data);
101
102 19
            if (isset($value['name'])) {
103 19
                $attr = $value['name'];
104
            }
105
106 19
            if ($mapped !== null) {
107 16
                $result[$attr] = $mapped;
108
109 16
                $this->logger->debug('mapped attribute ['.$attr.'] to [<'.gettype($result[$attr]).'> {value}]', [
110 16
                    'category' => get_class($this),
111 19
                    'value' => ($result[$attr] instanceof Binary) ? '<bin '.mb_strlen($result[$attr]->getData()).'>' : $result[$attr],
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\Binary does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
112
                ]);
113
            }
114
        }
115
116 20
        return $result;
117
    }
118
119
    /**
120
     * {@inheritdoc}
121
     */
122 3
    public function getDiff(array $object, array $endpoint_object): array
123
    {
124 3
        return Diff::calculate($this->map, $object, $endpoint_object);
125
    }
126
127
    /**
128
     * Map field.
129
     */
130 23
    protected function mapField($attr, $value, $data)
131
    {
132 23
        $attrv = $this->resolveValue($attr, $value, $data);
133 23
        $attrv = $this->transformAttribute($attr, $value, $attrv);
134
135 22
        if ($this->requireAttribute($attr, $value, $attrv) === null) {
136 3
            return;
137
        }
138
139 17
        if (isset($value['type'])) {
140 8
            $attrv = Transform::convertType($attrv, $attr, $value['type']);
141
        }
142
143 17
        if (isset($value['unwind'])) {
144 5
            $unwind = [];
145 5
            foreach ($attrv as $key => $element) {
146 5
                $result = $this->mapField($attr, $value['unwind'], [
147 5
                    'root' => $element,
148
                ]);
149
150 4
                if ($result !== null) {
151 4
                    $unwind[$key] = $result;
152
                }
153
            }
154
155 4
            if (isset($value['type']) && $value['type'] === AttributeMapInterface::TYPE_ARRAY) {
156 4
                $unwind = array_values($unwind);
157
            }
158
159 4
            $attrv = $unwind;
160
        }
161
162 16
        return $attrv;
163
    }
164
165
    /**
166
     * Check if attribute is required.
167
     */
168 22
    protected function requireAttribute(string $attr, array $value, $attrv)
169
    {
170 22
        if ($attrv === null || is_string($attrv) && strlen($attrv) === 0 || is_array($attrv) && count($attrv) === 0) {
171 5
            if (isset($value['required']) && $value['required'] === false || !isset($value['required'])) {
172 3
                $this->logger->debug('found attribute ['.$attr.'] but source attribute is empty, remove attribute from mapping', [
173 3
                     'category' => get_class($this),
174
                ]);
175
176 3
                return null;
177
            }
178
179 2
            throw new Exception\AttributeNotResolvable('required attribute '.$attr.' could not be resolved');
180
        }
181
182 17
        return $attrv;
183
    }
184
185
    /**
186
     * Transform attribute.
187
     */
188 23
    protected function transformAttribute(string $attr, array $value, $attrv)
189
    {
190 23
        if ($attrv === null) {
191 5
            return null;
192
        }
193
194 18
        if (isset($value['type']) && $value['type'] !== AttributeMapInterface::TYPE_ARRAY && is_array($attrv)) {
195 1
            $attrv = $this->firstArrayElement($attrv, $attr);
196
        }
197
198 18
        if (isset($value['filter']) && count($value['filter']) > 0) {
199 1
            $chain = new FilterChain(['filters' => $this->getFilters($value['filter'])]);
200 1
            $attrv = $chain->filter($attrv);
201
        }
202
203 18
        if (isset($value['rewrite']) && count($value['rewrite']) > 0) {
204 6
            $attrv = $this->rewrite($attrv, $value['rewrite']);
205
        }
206
207 18
        if (isset($value['require_regex'])) {
208 4
            if (!preg_match($value['require_regex'], $attrv)) {
209 2
                throw new Exception\AttributeRegexNotMatch('resolve attribute '.$attr.' value does not match require_regex');
210
            }
211
        }
212
213 17
        return $attrv;
214
    }
215
216
    /**
217
     * Get filters.
218
     */
219 1
    protected function getFilters(array $filter = []): array
220
    {
221 1
        $result = [];
222 1
        foreach ($filter as $value) {
223 1
            $result[] = ['name' => $value];
224
        }
225
226 1
        return $result;
227
    }
228
229
    /**
230
     * Check if attribute is required.
231
     */
232 23
    protected function resolveValue(string $attr, array $value, array $data)
233
    {
234 23
        $result = null;
235
236 23
        switch ($value['kind']) {
237
            default:
238 23
            case AttributeMapInterface::KIND_MAP:
239
                try {
240 19
                    if (isset($value['value'])) {
241 19
                        $result = Helper::getArrayValue($data, $value['value']);
242
                    }
243 3
                } catch (\Exception $e) {
244 3
                    $this->logger->warning('failed to resolve value of map attribute ['.$attr.'] from ['.$value['value'].']', [
245 3
                        'category' => get_class($this),
246 3
                        'exception' => $e,
247
                    ]);
248
                }
249
250 19
            break;
251 4
            case AttributeMapInterface::KIND_STATIC:
252 2
                $result = $value['value'];
253
254 2
            break;
255 2
            case AttributeMapInterface::KIND_SCRIPT:
256
                try {
257 2
                    if (isset($value['value'])) {
258 2
                        $this->logger->debug('attribute ['.$attr.'] is scripted [{script}]', [
259 2
                            'category' => get_class($this),
260 2
                            'script' => $value['value'],
261
                        ]);
262
263 2
                        $this->v8->executeString($value['value'], '', V8Js::FLAG_FORCE_ARRAY);
264 2
                        $result = $this->v8->getLastResult();
265
266 2
                        $this->logger->debug('script result of ['.$attr.'] core.result() is [{value}]', [
267 2
                            'category' => get_class($this),
268 2
                            'value' => $value,
269
                        ]);
270
                    }
271
                } catch (\Exception $e) {
272
                    $this->logger->warning('failed to execute script ['.$value['value'].'] of attribute ['.$attr.']', [
273
                        'category' => get_class($this),
274
                        'exception' => $e,
275
                    ]);
276
                }
277
278 2
            break;
279
        }
280
281 23
        return $result;
282
    }
283
284
    /**
285
     * Shift first array element.
286
     */
287 1
    protected function firstArrayElement(iterable $value, string $attribute)
288
    {
289 1
        if (empty($value)) {
290
            return $value;
291
        }
292
293 1
        $this->logger->debug('resolved value for attribute ['.$attribute.'] is an array but is not declared as an array, use first array element instead', [
294 1
             'category' => get_class($this),
295
        ]);
296
297 1
        return current($value);
298
    }
299
300
    /**
301
     * Process ruleset.
302
     */
303 6
    protected function rewrite($value, array $ruleset)
304
    {
305 6
        foreach ($ruleset as $rule) {
306 6
            if (isset($rule['from'])) {
307 2
                if ($value === $rule['from']) {
308 2
                    $value = $rule['to'];
309
310 2
                    return $value;
311
                }
312 4
            } elseif (isset($rule['match'])) {
313 4
                $value = preg_replace($rule['match'], $rule['to'], $value, -1, $count);
314 4
                if ($count > 0) {
315 5
                    return $value;
316
                }
317
            }
318
        }
319
320 1
        return $value;
321
    }
322
}
323