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

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