Passed
Push — master ( 321f29...c517f3 )
by y
01:59
created

Data::isDiff()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 3
b 0
f 0
nc 2
nop 1
dl 0
loc 5
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
     * Magic method: `getField()`
117
     *
118
     * @see __call()
119
     *
120
     * @param string $field
121
     * @return mixed
122
     */
123
    protected function _get (string $field) {
124
        return $this->data[$field] ?? null;
125
    }
126
127
    /**
128
     * Magic method: `hasField()`
129
     *
130
     * Whether a countable field has anything in it,
131
     * or casts a scalar field to boolean.
132
     *
133
     * @see __call()
134
     *
135
     * @param string $field
136
     * @return bool
137
     */
138
    protected function _has (string $field): bool {
139
        $value = $this->_get($field);
140
        if (isset($value)) {
141
            if (is_countable($value)) {
142
                return count($value) > 0;
143
            }
144
            return (bool)$value;
145
        }
146
        return false;
147
    }
148
149
    /**
150
     * Magic method: `isField()`
151
     *
152
     * Boolean casts a scalar field.
153
     *
154
     * Do not use this for countable fields, use `hasField()` instead.
155
     *
156
     * @see __call()
157
     *
158
     * @param string $field
159
     * @return bool
160
     */
161
    protected function _is (string $field): bool {
162
        return (bool)$this->_get($field);
163
    }
164
165
    /**
166
     * Magic method: `selectField(callable $filter)`
167
     *
168
     * This can also be used to select from an arbitrary array.
169
     *
170
     * @see __call()
171
     *
172
     * @param string|iterable $subject
173
     * @param callable $filter `fn( Data $object ): bool`
174
     * @return array
175
     */
176
    protected function _select ($subject, callable $filter) {
177
        if (!is_iterable($subject)) {
178
            $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

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