Passed
Push — master ( 85fa92...c01de8 )
by y
02:23
created

Data::_load()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 3
dl 0
loc 2
rs 10
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
     * - `* => class` to wildcard all fields as nullable instances
20
     *
21
     * The map supports polymorphic entity hydration via `*` indirection.
22
     *
23
     * ```
24
     * field => * or [*],
25
     * field* => [
26
     *      entity::TYPE => entity::class,
27
     *      entity::TYPE => entity::class
28
     * ],
29
     * ```
30
     *
31
     * The classes specified here should be the field's base class (identity) from this library.
32
     *
33
     * Do not use this to map to extensions. Extend and override {@see Api::factory()} to do that.
34
     *
35
     * @see _setData()
36
     * @see _setMapped()
37
     *
38
     * @var array
39
     */
40
    protected const MAP = [];
41
42
    /**
43
     * @var Api
44
     */
45
    protected $api;
46
47
    /**
48
     * @var array|Data[]
49
     */
50
    protected $data = [];
51
52
    /**
53
     * @var bool[]
54
     */
55
    protected $diff = [];
56
57
    /**
58
     * @param Api|Data $caller
59
     * @param array $data
60
     */
61
    public function __construct ($caller, array $data = []) {
62
        $this->api = $caller instanceof self ? $caller->api : $caller;
63
        $this->_setData($data);
64
    }
65
66
    /**
67
     * Magic method handler.
68
     *
69
     * @see _get()
70
     * @see _has()
71
     * @see _is()
72
     * @see _set()
73
     * @param string $method
74
     * @param array $args
75
     * @return mixed
76
     */
77
    public function __call (string $method, array $args) {
78
        static $cache = [];
79
        if (!$call =& $cache[$method]) {
80
            preg_match('/^(get|has|is|select|set)(.+)$/', $method, $call);
81
            $call[1] = '_' . $call[1];
82
            $call[2] = preg_replace_callback('/[A-Z]/', function(array $match) {
83
                return '_' . strtolower($match[0]);
84
            }, lcfirst($call[2]));
85
        }
86
        return $this->{$call[1]}($call[2], ...$args);
87
    }
88
89
    public function __debugInfo (): array {
90
        return $this->data;
91
    }
92
93
    /**
94
     * This is only provided for use by functions like `array_column()`.
95
     *
96
     * @param $field
97
     * @return null|Data|mixed
98
     */
99
    final public function __get ($field) {
100
        return $this->_get($field);
101
    }
102
103
    /**
104
     * This is only provided for use by functions like `array_column()`.
105
     *
106
     * Always returns `true`, since fields that are not set can be lazy-loaded.
107
     *
108
     * @param $field
109
     * @return bool
110
     */
111
    final public function __isset ($field) {
112
        return true;
113
    }
114
115
    /**
116
     * Helper, forwards to the API.
117
     *
118
     * @param string $class
119
     * @param array $data
120
     * @return mixed|Data|AbstractEntity
121
     */
122
    final protected function _factory (string $class, array $data = []) {
123
        return $this->api->factory($class, $this, $data);
124
    }
125
126
    /**
127
     * Magic method: `getField()`
128
     *
129
     * @see __call()
130
     *
131
     * @param string $field
132
     * @return mixed
133
     */
134
    protected function _get (string $field) {
135
        return $this->data[$field] ?? null;
136
    }
137
138
    /**
139
     * Magic method: `hasField()`
140
     *
141
     * Whether a countable field has anything in it,
142
     * or casts a scalar field to boolean.
143
     *
144
     * @see __call()
145
     *
146
     * @param string $field
147
     * @return bool
148
     */
149
    protected function _has (string $field): bool {
150
        $value = $this->_get($field);
151
        if (isset($value)) {
152
            if (is_countable($value)) {
153
                return count($value) > 0;
154
            }
155
            return (bool)$value;
156
        }
157
        return false;
158
    }
159
160
    /**
161
     * Magic method: `isField()`
162
     *
163
     * Boolean casts a scalar field.
164
     *
165
     * Do not use this for countable fields, use `hasField()` instead.
166
     *
167
     * @see __call()
168
     *
169
     * @param string $field
170
     * @return bool
171
     */
172
    protected function _is (string $field): bool {
173
        return (bool)$this->_get($field);
174
    }
175
176
    /**
177
     * Magic method: `selectField(callable $filter)`
178
     *
179
     * This can also be used to select from an arbitrary array.
180
     *
181
     * @see __call()
182
     *
183
     * @param string|iterable $subject
184
     * @param callable $filter `fn( Data $object ): bool`
185
     * @return array
186
     */
187
    protected function _select ($subject, callable $filter) {
188
        if (!is_iterable($subject)) {
189
            $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

189
            $subject = $this->_get(/** @scrutinizer ignore-type */ $subject) ?? [];
Loading history...
190
        }
191
        $selected = [];
192
        foreach ($subject as $item) {
193
            if (call_user_func($filter, $item)) {
194
                $selected[] = $item;
195
            }
196
        }
197
        return $selected;
198
    }
199
200
    /**
201
     * Magic method: `setField(mixed $value)`
202
     *
203
     * @see __call()
204
     *
205
     * @param string $field
206
     * @param mixed $value
207
     * @return $this
208
     */
209
    protected function _set (string $field, $value) {
210
        $this->data[$field] = $value;
211
        $this->diff[$field] = true;
212
        return $this;
213
    }
214
215
    /**
216
     * Clears all diffs and sets all data, hydrating mapped fields.
217
     *
218
     * @param array $data
219
     */
220
    protected function _setData (array $data): void {
221
        $this->data = $this->diff = [];
222
        foreach ($data as $field => $value) {
223
            $this->_setMapped($field, $value);
224
        }
225
    }
226
227
    /**
228
     * Sets a value, hydrating if mapped, and clears the diff.
229
     *
230
     * @param string $field
231
     * @param mixed $value
232
     */
233
    protected function _setMapped (string $field, $value): void {
234
        unset($this->diff[$field]);
235
236
        // use value as-is?
237
        if (
238
            !isset(static::MAP[$field]) && !isset(static::MAP['*']) // unmapped
239
            or empty($value)                                        // null or []
240
            or $value instanceof self                               // Data-ish
241
            or is_array($value) && current($value) instanceof self  // Data[]-ish
242
        ) {
243
            $this->data[$field] = $value;
244
            return;
245
        }
246
247
        // otherwise the field is mapped.
248
        $class = static::MAP[$field] ?? static::MAP['*'];
249
        if ($isList = is_array($class)) {
250
            $class = $class[0];
251
        }
252
        // polymorphic
253
        if ($class === '*') {
254
            $class = static::MAP["{$field}*"][$value['resource_type']];
255
        }
256
257
        $hydrate = function($data) use ($class) {
258
            // hydrate AbstractEntity
259
            if (is_subclass_of($class, AbstractEntity::class)) {
260
                // convert gids to data stubs. a lazy-loader will be returned.
261
                if (is_scalar($data)) {
262
                    $data = ['gid' => $data];
263
                }
264
                return $this->api->getCache()->get($data['gid'], $this, function() use ($class, $data) {
265
                    return $this->_factory($class, $data);
266
                });
267
            }
268
            // hydrate Data
269
            return $this->_factory($class, $data);
270
        };
271
272
        if ($isList) {
273
            $this->data[$field] = array_map($hydrate, $value);
274
        }
275
        else {
276
            $this->data[$field] = $hydrate($value);
277
        }
278
    }
279
280
    /**
281
     * Constructs and returns an array for Asana using diffs.
282
     *
283
     * @return array
284
     */
285
    public function getDiff (): array {
286
        $convert = function($each) use (&$convert) {
287
            // convert existing entities to gids
288
            if ($each instanceof AbstractEntity and $each->hasGid()) {
289
                return $each->getGid();
290
            }
291
            // convert data objects and new entities to arrays
292
            elseif ($each instanceof self) {
293
                return $each->getDiff();
294
            }
295
            // convert arrays
296
            elseif (is_array($each)) {
297
                return array_map($convert, $each);
298
            }
299
            // return as-is
300
            return $each;
301
        };
302
        return array_map($convert, array_intersect_key($this->data, $this->diff));
303
    }
304
305
    /**
306
     * Whether the instance has changes.
307
     *
308
     * @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...
309
     * @return bool
310
     */
311
    final public function isDiff (string $field = null): bool {
312
        if ($field) {
313
            return isset($this->diff[$field]);
314
        }
315
        return (bool)$this->diff;
316
    }
317
318
    /**
319
     * @see toArray()
320
     * @return array
321
     */
322
    public function jsonSerialize (): array {
323
        $data = $this->toArray();
324
        ksort($data);
325
        return $data;
326
    }
327
328
    /**
329
     * Returns a serialized representation of the instance's dehydrated data.
330
     *
331
     * @return string
332
     */
333
    public function serialize (): string {
334
        return json_encode($this, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
335
    }
336
337
    /**
338
     * Returns a dehydrated copy of the instance's data.
339
     *
340
     * @return array
341
     */
342
    public function toArray (): array {
343
        return array_map($dehydrate = function($each) use (&$dehydrate) {
344
            if ($each instanceof AbstractEntity and $each->hasGid()) {
345
                return $each->getGid();
346
            }
347
            elseif ($each instanceof self) {
348
                return $each->toArray();
349
            }
350
            elseif (is_array($each)) {
351
                return array_map($dehydrate, $each);
352
            }
353
            return $each;
354
        }, $this->data);
355
    }
356
357
    /**
358
     * Sets the dehydrated data and uses the default {@see Api} instance.
359
     *
360
     * @param $serialized
361
     */
362
    public function unserialize ($serialized): void {
363
        $this->api = Api::getDefault();
364
        $data = json_decode($serialized, true);
365
        $this->_setData($data);
366
    }
367
}