Passed
Push — master ( 82cce0...9ec6c5 )
by y
01:38
created

Data   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 398
Duplicated Lines 0 %

Importance

Changes 33
Bugs 1 Features 1
Metric Value
eloc 95
c 33
b 1
f 1
dl 0
loc 398
rs 8.4
wmc 50

22 Methods

Rating   Name   Duplication   Size   Complexity  
A _loadAll() 0 2 1
A toArray() 0 13 5
A _load() 0 2 1
C _setMapped() 0 44 12
A _loadEach() 0 2 1
A isDiff() 0 5 2
A _factory() 0 2 1
A __debugInfo() 0 2 1
A _is() 0 2 1
A getDiff() 0 18 5
A _has() 0 9 3
A __isset() 0 2 1
A __call() 0 10 2
A unserialize() 0 4 1
A _set() 0 4 1
A __get() 0 2 1
A _select() 0 11 4
A _get() 0 2 1
A serialize() 0 2 1
A __construct() 0 3 2
A _setData() 0 4 2
A jsonSerialize() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Data 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.

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 Data, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Helix\Asana\Base;
4
5
use Helix\Asana\Api;
6
use JsonSerializable;
7
use Serializable;
8
use Traversable;
9
10
/**
11
 * A data object with support for annotated magic methods.
12
 */
13
class Data implements JsonSerializable, Serializable {
14
15
    /**
16
     * Sub-element hydration specs.
17
     *
18
     * - `field => class` for a nullable instance
19
     * - `field => [class]` for an array of instances
20
     * - `* => class` to wildcard all fields as nullable instances
21
     *
22
     * The map supports polymorphic entity hydration via `*` indirection.
23
     *
24
     * ```
25
     * field => * or [*],
26
     * field* => [
27
     *      entity::TYPE => entity::class,
28
     *      entity::TYPE => entity::class
29
     * ],
30
     * ```
31
     *
32
     * The classes specified here should be the field's base class (identity) from this library.
33
     *
34
     * Do not use this to map to extensions. Extend and override {@see Api::factory()} to do that.
35
     *
36
     * @see _setData()
37
     * @see _setMapped()
38
     *
39
     * @var array
40
     */
41
    protected const MAP = [];
42
43
    /**
44
     * @var Api
45
     */
46
    protected $api;
47
48
    /**
49
     * @var array|Data[]
50
     */
51
    protected $data = [];
52
53
    /**
54
     * @var bool[]
55
     */
56
    protected $diff = [];
57
58
    /**
59
     * @param Api|Data $caller
60
     * @param array $data
61
     */
62
    public function __construct ($caller, array $data = []) {
63
        $this->api = $caller instanceof self ? $caller->api : $caller;
64
        $this->_setData($data);
65
    }
66
67
    /**
68
     * Magic method handler.
69
     *
70
     * @see _get()
71
     * @see _has()
72
     * @see _is()
73
     * @see _set()
74
     * @param string $method
75
     * @param array $args
76
     * @return mixed
77
     */
78
    public function __call (string $method, array $args) {
79
        static $cache = [];
80
        if (!$call =& $cache[$method]) {
81
            preg_match('/^(get|has|is|select|set)(.+)$/', $method, $call);
82
            $call[1] = '_' . $call[1];
83
            $call[2] = preg_replace_callback('/[A-Z]/', function(array $match) {
84
                return '_' . strtolower($match[0]);
85
            }, lcfirst($call[2]));
86
        }
87
        return $this->{$call[1]}($call[2], ...$args);
88
    }
89
90
    public function __debugInfo (): array {
91
        return $this->data;
92
    }
93
94
    /**
95
     * This is only provided for use by functions like `array_column()`.
96
     *
97
     * @param $field
98
     * @return null|Data|mixed
99
     */
100
    final public function __get ($field) {
101
        return $this->_get($field);
102
    }
103
104
    /**
105
     * This is only provided for use by functions like `array_column()`.
106
     *
107
     * Always returns `true`, since fields that are not set can be lazy-loaded.
108
     *
109
     * @param $field
110
     * @return bool
111
     */
112
    final public function __isset ($field) {
113
        return true;
114
    }
115
116
    /**
117
     * Helper, forwards to the API.
118
     *
119
     * @param string $class
120
     * @param array $data
121
     * @return mixed|Data|AbstractEntity
122
     */
123
    final protected function _factory (string $class, array $data = []) {
124
        return $this->api->factory($class, $this, $data);
125
    }
126
127
    /**
128
     * Magic method: `getField()`
129
     *
130
     * @see __call()
131
     *
132
     * @param string $field
133
     * @return mixed
134
     */
135
    protected function _get (string $field) {
136
        return $this->data[$field] ?? null;
137
    }
138
139
    /**
140
     * Magic method: `hasField()`
141
     *
142
     * Whether a countable field has anything in it,
143
     * or casts a scalar field to boolean.
144
     *
145
     * @see __call()
146
     *
147
     * @param string $field
148
     * @return bool
149
     */
150
    protected function _has (string $field): bool {
151
        $value = $this->_get($field);
152
        if (isset($value)) {
153
            if (is_countable($value)) {
154
                return count($value) > 0;
155
            }
156
            return (bool)$value;
157
        }
158
        return false;
159
    }
160
161
    /**
162
     * Magic method: `isField()`
163
     *
164
     * Boolean casts a scalar field.
165
     *
166
     * Do not use this for countable fields, use `hasField()` instead.
167
     *
168
     * @see __call()
169
     *
170
     * @param string $field
171
     * @return bool
172
     */
173
    protected function _is (string $field): bool {
174
        return (bool)$this->_get($field);
175
    }
176
177
    /**
178
     * Helper, forwards to the API.
179
     *
180
     * @see Api::load()
181
     *
182
     * @param string $class
183
     * @param string $path
184
     * @param array $query
185
     * @return null|mixed|AbstractEntity
186
     */
187
    final protected function _load (string $class, string $path, array $query = []) {
188
        return $this->api->load($class, $this, $path, $query);
189
    }
190
191
    /**
192
     * Helper, forwards to the API.
193
     *
194
     * @see Api::loadAll()
195
     *
196
     * @param string $class
197
     * @param string $path
198
     * @param array $query
199
     * @param int $pages
200
     * @return array|AbstractEntity[]
201
     */
202
    final protected function _loadAll (string $class, string $path, array $query = [], int $pages = 0) {
203
        return $this->api->loadAll($class, $this, $path, $query, $pages);
204
    }
205
206
    /**
207
     * Helper, forwards to the API.
208
     *
209
     * @see Api::loadEach()
210
     *
211
     * @param string $class
212
     * @param string $path
213
     * @param array $query
214
     * @param int $pages
215
     * @return Traversable|AbstractEntity[]
216
     */
217
    final protected function _loadEach (string $class, string $path, array $query = [], int $pages = 0) {
218
        return $this->api->loadEach($class, $this, $path, $query, $pages);
219
    }
220
221
    /**
222
     * Magic method: `selectField(callable $filter)`
223
     *
224
     * This can also be used to select from an arbitrary array.
225
     *
226
     * @see __call()
227
     *
228
     * @param string|iterable $subject
229
     * @param callable $filter `fn( Data $object ): bool`
230
     * @return array
231
     */
232
    protected function _select ($subject, callable $filter) {
233
        if (!is_iterable($subject)) {
234
            $subject = $this->_get($subject) ?? [];
1 ignored issue
show
Bug introduced by
It seems like $subject can also be of type iterable; however, parameter $field of Helix\Asana\Base\Data::_get() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

234
            $subject = $this->_get(/** @scrutinizer ignore-type */ $subject) ?? [];
Loading history...
235
        }
236
        $selected = [];
237
        foreach ($subject as $item) {
238
            if (call_user_func($filter, $item)) {
239
                $selected[] = $item;
240
            }
241
        }
242
        return $selected;
243
    }
244
245
    /**
246
     * Magic method: `setField(mixed $value)`
247
     *
248
     * @see __call()
249
     *
250
     * @param string $field
251
     * @param mixed $value
252
     * @return $this
253
     */
254
    protected function _set (string $field, $value) {
255
        $this->data[$field] = $value;
256
        $this->diff[$field] = true;
257
        return $this;
258
    }
259
260
    /**
261
     * Clears all diffs and sets all data, hydrating mapped fields.
262
     *
263
     * @param array $data
264
     */
265
    protected function _setData (array $data): void {
266
        $this->data = $this->diff = [];
267
        foreach ($data as $field => $value) {
268
            $this->_setMapped($field, $value);
269
        }
270
    }
271
272
    /**
273
     * Sets a value, hydrating if mapped, and clears the diff.
274
     *
275
     * @param string $field
276
     * @param mixed $value
277
     */
278
    protected function _setMapped (string $field, $value): void {
279
        unset($this->diff[$field]);
280
281
        // use value as-is?
282
        if (
283
            !isset(static::MAP[$field]) && !isset(static::MAP['*']) // unmapped
284
            or empty($value)                                        // null or []
285
            or $value instanceof self                               // Data-ish
286
            or is_array($value) && current($value) instanceof self  // Data[]-ish
287
        ) {
288
            $this->data[$field] = $value;
289
            return;
290
        }
291
292
        // otherwise the field is mapped.
293
        $class = static::MAP[$field] ?? static::MAP['*'];
294
        if ($isList = is_array($class)) {
295
            $class = $class[0];
296
        }
297
        // polymorphic
298
        if ($class === '*') {
299
            $class = static::MAP["{$field}*"][$value['resource_type']];
300
        }
301
302
        $hydrate = function($data) use ($class) {
303
            // hydrate AbstractEntity
304
            if (is_subclass_of($class, AbstractEntity::class)) {
305
                // convert gids to data stubs. a lazy-loader will be returned.
306
                if (is_scalar($data)) {
307
                    $data = ['gid' => $data];
308
                }
309
                return $this->api->getCache()->get($data['gid'], $this, function() use ($class, $data) {
310
                    return $this->_factory($class, $data);
311
                });
312
            }
313
            // hydrate Data
314
            return $this->_factory($class, $data);
315
        };
316
317
        if ($isList) {
318
            $this->data[$field] = array_map($hydrate, $value);
319
        }
320
        else {
321
            $this->data[$field] = $hydrate($value);
322
        }
323
    }
324
325
    /**
326
     * Constructs and returns an array for Asana using diffs.
327
     *
328
     * @return array
329
     */
330
    public function getDiff (): array {
331
        $convert = function($each) use (&$convert) {
332
            // convert existing entities to gids
333
            if ($each instanceof AbstractEntity and $each->hasGid()) {
334
                return $each->getGid();
335
            }
336
            // convert data objects and new entities to arrays
337
            elseif ($each instanceof self) {
338
                return $each->getDiff();
339
            }
340
            // convert arrays
341
            elseif (is_array($each)) {
342
                return array_map($convert, $each);
343
            }
344
            // return as-is
345
            return $each;
346
        };
347
        return array_map($convert, array_intersect_key($this->data, $this->diff));
348
    }
349
350
    /**
351
     * Whether the instance has changes.
352
     *
353
     * @param string null $field
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $string is correct as it would always require null to be passed?
Loading history...
354
     * @return bool
355
     */
356
    final public function isDiff (string $field = null): bool {
357
        if ($field) {
358
            return isset($this->diff[$field]);
359
        }
360
        return (bool)$this->diff;
361
    }
362
363
    /**
364
     * @see toArray()
365
     * @return array
366
     */
367
    public function jsonSerialize (): array {
368
        $data = $this->toArray();
369
        ksort($data);
370
        return $data;
371
    }
372
373
    /**
374
     * Returns a serialized representation of the instance's dehydrated data.
375
     *
376
     * @return string
377
     */
378
    public function serialize (): string {
379
        return json_encode($this, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
380
    }
381
382
    /**
383
     * Returns a dehydrated copy of the instance's data.
384
     *
385
     * @return array
386
     */
387
    public function toArray (): array {
388
        return array_map($dehydrate = function($each) use (&$dehydrate) {
389
            if ($each instanceof AbstractEntity and $each->hasGid()) {
390
                return $each->getGid();
391
            }
392
            elseif ($each instanceof self) {
393
                return $each->toArray();
394
            }
395
            elseif (is_array($each)) {
396
                return array_map($dehydrate, $each);
397
            }
398
            return $each;
399
        }, $this->data);
400
    }
401
402
    /**
403
     * Sets the dehydrated data and uses the default {@see Api} instance.
404
     *
405
     * @param $serialized
406
     */
407
    public function unserialize ($serialized): void {
408
        $this->api = Api::getDefault();
409
        $data = json_decode($serialized, true);
410
        $this->_setData($data);
411
    }
412
}