Passed
Push — master ( e5878d...3c0c31 )
by y
01:52
created

Data::_setMapped()   B

Complexity

Conditions 11
Paths 9

Size

Total Lines 44
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 11
eloc 24
nc 9
nop 2
dl 0
loc 44
rs 7.3166
c 2
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Helix\Asana\Base;
4
5
use Helix\Asana\Api;
6
use JsonSerializable;
7
use Serializable;
8
9
/**
10
 * A data object with support for annotated magic methods.
11
 */
12
class Data implements JsonSerializable, Serializable {
13
14
    /**
15
     * Sub-element hydration specs.
16
     *
17
     * - `field => class` for a nullable instance
18
     * - `field => [class]` for an array of instances
19
     *
20
     * The map supports polymorphic entity hydration via `*` indirection.
21
     *
22
     * ```
23
     * field => * or [*],
24
     * field* => [
25
     *      entity::TYPE => entity::class,
26
     *      entity::TYPE => entity::class
27
     * ],
28
     * ```
29
     *
30
     * The classes specified here should be the field's base class (identity) from this library.
31
     *
32
     * Do not use this to map to extensions. Extend and override {@link Api::factory()} to do that.
33
     *
34
     * @see _setMapped()
35
     *
36
     * @var array
37
     */
38
    protected const MAP = [];
39
40
    /**
41
     * @var Api
42
     */
43
    protected $api;
44
45
    /**
46
     * @var array|Data[]|AbstractEntity[]
47
     */
48
    protected $data = [];
49
50
    /**
51
     * @var bool[]
52
     */
53
    protected $diff = [];
54
55
    /**
56
     * @param Api|Data $caller
57
     * @param array $data
58
     */
59
    public function __construct ($caller, array $data = []) {
60
        if ($caller instanceof self) {
61
            $this->api = $caller->api;
62
        }
63
        else {
64
            $this->api = $caller;
65
        }
66
        $this->_setData($data);
67
    }
68
69
    /**
70
     * Magic method handler.
71
     *
72
     * @see _get()
73
     * @see _has()
74
     * @see _is()
75
     * @see _set()
76
     * @param string $method
77
     * @param array $args
78
     * @return mixed
79
     */
80
    public function __call (string $method, array $args) {
81
        static $magic = [];
82
        if (!$call =& $magic[$method]) {
83
            preg_match('/^(get|has|is|select|set)(.+)$/', $method, $call);
84
            $call[1] = '_' . $call[1];
85
            $call[2] = preg_replace_callback('/[A-Z]/', function(array $match) {
86
                return '_' . strtolower($match[0]);
87
            }, lcfirst($call[2]));
88
        }
89
        return $this->{$call[1]}($call[2], ...$args);
90
    }
91
92
    /**
93
     * @return array
94
     */
95
    public function __debugInfo (): array {
96
        return $this->data;
97
    }
98
99
    /**
100
     * @param $field
101
     * @return null|Data|mixed
102
     * @internal `array_column()`
103
     */
104
    final public function __get ($field) {
105
        return $this->_get($field);
106
    }
107
108
    /**
109
     * @param $field
110
     * @return bool
111
     * @internal `array_column()`
112
     */
113
    final public function __isset ($field) {
114
        return true;
115
    }
116
117
    /**
118
     * Magic method: `getField()`
119
     *
120
     * @see __call()
121
     *
122
     * @param string $field
123
     * @return mixed
124
     */
125
    protected function _get (string $field) {
126
        return $this->data[$field] ?? null;
127
    }
128
129
    /**
130
     * Magic method: `hasField()`
131
     *
132
     * Whether a countable field has anything in it,
133
     * or casts a scalar field to boolean.
134
     *
135
     * @see __call()
136
     *
137
     * @param string $field
138
     * @return bool
139
     */
140
    protected function _has (string $field): bool {
141
        $value = $this->_get($field);
142
        if (isset($value)) {
143
            if (is_countable($value)) {
144
                return count($value) > 0;
145
            }
146
            return (bool)$value;
147
        }
148
        return false;
149
    }
150
151
    /**
152
     * Magic method: `isField()`
153
     *
154
     * Boolean casts a scalar field.
155
     *
156
     * Do not use this for countable fields, use `hasField()` instead.
157
     *
158
     * @see __call()
159
     *
160
     * @param string $field
161
     * @return bool
162
     */
163
    protected function _is (string $field): bool {
164
        return (bool)$this->_get($field);
165
    }
166
167
    /**
168
     * Magic method: `selectField(callable $filter)`
169
     *
170
     * This can also be used to select from an arbitrary array.
171
     *
172
     * @see __call()
173
     *
174
     * @param string|iterable $subject
175
     * @param callable $filter `fn( Data $object ): bool`
176
     * @return array
177
     */
178
    protected function _select ($subject, callable $filter) {
179
        if (is_string($subject)) {
180
            $subject = $this->_get($subject) ?? [];
181
        }
182
        $selected = [];
183
        foreach ($subject as $item) {
184
            if (call_user_func($filter, $item)) {
185
                $selected[] = $item;
186
            }
187
        }
188
        return $selected;
189
    }
190
191
    /**
192
     * Magic method: `setField(mixed $value)`
193
     *
194
     * @see __call()
195
     *
196
     * @param string $field
197
     * @param mixed $value
198
     * @return $this
199
     */
200
    protected function _set (string $field, $value) {
201
        $this->data[$field] = $value;
202
        $this->diff[$field] = true;
203
        return $this;
204
    }
205
206
    /**
207
     * Clears all diffs and sets all data, hydrating mapped fields.
208
     *
209
     * @param array $data
210
     */
211
    protected function _setData (array $data): void {
212
        $this->data = $this->diff = [];
213
        foreach ($data as $field => $value) {
214
            $this->_setMapped($field, $value);
215
        }
216
    }
217
218
    /**
219
     * Sets a value, hydrating if mapped, and clears the diff.
220
     *
221
     * @param string $field
222
     * @param mixed $value
223
     */
224
    protected function _setMapped (string $field, $value): void {
225
        unset($this->diff[$field]);
226
227
        // use value as-is?
228
        if (
229
            !isset(static::MAP[$field])                             // unmapped
230
            or empty($value)                                        // null or []
231
            or $value instanceof self                               // Data-ish
232
            or is_array($value) && current($value) instanceof self  // Data[]-ish
233
        ) {
234
            $this->data[$field] = $value;
235
            return;
236
        }
237
238
        // otherwise the field is mapped.
239
        $class = static::MAP[$field];
240
        if ($isList = is_array($class)) {
241
            $class = $class[0];
242
        }
243
        // polymorphic
244
        if ($class === '*') {
245
            $class = static::MAP["{$field}*"][$value['resource_type']];
246
        }
247
248
        $hydrate = function($data) use ($class) {
249
            // hydrate entities
250
            if (is_subclass_of($class, AbstractEntity::class)) {
251
                // convert gids to lazy stubs
252
                if (is_string($data)) {
253
                    $data = ['gid' => $data];
254
                }
255
                return $this->api->getPool()->get($data['gid'], $this, function() use ($class, $data) {
256
                    return $this->api->factory($this, $class, $data);
257
                });
258
            }
259
            // hydrate Data
260
            return $this->api->factory($this, $class, $data);
261
        };
262
263
        if ($isList) {
264
            $this->data[$field] = array_map($hydrate, $value);
265
        }
266
        else {
267
            $this->data[$field] = $hydrate($value);
268
        }
269
    }
270
271
    /**
272
     * Constructs and returns an array for Asana using diffs.
273
     *
274
     * @return array
275
     */
276
    public function getDiff (): array {
277
        $convert = function($each) use (&$convert) {
278
            // convert existing entities to gids
279
            if ($each instanceof AbstractEntity and $each->hasGid()) {
280
                return $each->getGid();
281
            }
282
            // convert data objects and new entities to arrays
283
            elseif ($each instanceof self) {
284
                return $each->getDiff();
285
            }
286
            // convert arrays
287
            elseif (is_array($each)) {
288
                return array_map($convert, $each);
289
            }
290
            // return as-is
291
            return $each;
292
        };
293
        return array_map($convert, array_intersect_key($this->data, $this->diff));
294
    }
295
296
    /**
297
     * Whether the instance has changes.
298
     *
299
     * @param string $field
300
     * @return bool
301
     */
302
    final public function isDiff (string $field = null): bool {
303
        if ($field) {
304
            return isset($this->diff[$field]);
305
        }
306
        return (bool)$this->diff;
307
    }
308
309
    /**
310
     * @see toArray()
311
     * @return array
312
     */
313
    public function jsonSerialize (): array {
314
        $data = $this->toArray();
315
        ksort($data);
316
        return $data;
317
    }
318
319
    /**
320
     * Returns a serialized representation of the instance's dehydrated data.
321
     *
322
     * @return string
323
     */
324
    public function serialize (): string {
325
        return json_encode($this, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
326
    }
327
328
    /**
329
     * Returns a dehydrated copy of the instance's data.
330
     *
331
     * @return array
332
     */
333
    public function toArray (): array {
334
        if (!$this->api) {
335
            return $this->data;
336
        }
337
        return array_map($dehydrate = function($each) use (&$dehydrate) {
338
            // convert entities to their paths.
339
            if ($each instanceof AbstractEntity and $each->hasGid()) {
340
                return $each->getGid();
341
            }
342
            // convert other data to arrays.
343
            elseif ($each instanceof self) {
344
                return $each->toArray();
345
            }
346
            // dehydrate normal arrays.
347
            elseif (is_array($each)) {
348
                return array_map($dehydrate, $each);
349
            }
350
            // return as-is
351
            return $each;
352
        }, $this->data);
353
    }
354
355
    /**
356
     * @param $serialized
357
     */
358
    public function unserialize ($serialized): void {
359
        $this->data = json_decode($serialized, true);
360
    }
361
}