Data::__isset()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
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
     *
20
     * The classes specified here should be the field's base class (identity) from this library.
21
     *
22
     * Do not use this to map to extensions. Extend and override {@link Api::factory()} to do that.
23
     *
24
     * @see _setField()
25
     *
26
     * @var array
27
     */
28
    protected const MAP = [];
29
30
    /**
31
     * @var Api
32
     */
33
    protected $api;
34
35
    /**
36
     * @var array|Data[]|AbstractEntity[]
37
     */
38
    protected $data = [];
39
40
    /**
41
     * @var bool[]
42
     */
43
    protected $diff = [];
44
45
    /**
46
     * @param Api|Data $caller
47
     * @param array $data
48
     */
49
    public function __construct ($caller, array $data = []) {
50
        if ($caller instanceof self) {
51
            $this->api = $caller->api;
52
        }
53
        else {
54
            assert($caller instanceof Api);
55
            /** @var Api $caller */
56
            $this->api = $caller;
57
        }
58
        $this->_setData($data);
59
    }
60
61
    /**
62
     * Magic method handler.
63
     *
64
     * @see _get()
65
     * @see _has()
66
     * @see _is()
67
     * @see _select()
68
     * @see _set()
69
     *
70
     * @param string $method
71
     * @param array $args
72
     * @return mixed
73
     */
74
    public function __call (string $method, array $args) {
75
        static $magic = [];
76
        if (!$call =& $magic[$method]) {
77
            preg_match('/^(get|has|is|select|set)(.+)$/', $method, $call);
78
            if ('_select' !== $call[1] = '_' . $call[1]) { // _select() calls getters
79
                $call[2] = preg_replace_callback('/[A-Z]/', function(array $match) {
80
                    return '_' . strtolower($match[0]);
81
                }, lcfirst($call[2]));
82
            }
83
        }
84
        return $this->{$call[1]}($call[2], ...$args);
85
    }
86
87
    /**
88
     * @return array
89
     * @internal pool, `var_export()`
90
     */
91
    final public function __debugInfo (): array {
92
        return $this->data;
93
    }
94
95
    /**
96
     * @param $field
97
     * @return null|Data|mixed
98
     * @internal for `array_column()`
99
     */
100
    final public function __get ($field) {
101
        return $this->_get($field);
102
    }
103
104
    /**
105
     * @param $field
106
     * @return bool
107
     * @internal for `array_column()`
108
     */
109
    final public function __isset ($field) {
110
        return true; // fields may be lazy-loaded or coalesced to null.
111
    }
112
113
    /**
114
     * Magic method: `getField()`
115
     *
116
     * @see __call()
117
     *
118
     * @param string $field
119
     * @return mixed
120
     */
121
    protected function _get (string $field) {
122
        return $this->data[$field] ?? null;
123
    }
124
125
    /**
126
     * Magic method: `hasField()`
127
     *
128
     * Whether a countable field has anything in it,
129
     * or casts a scalar field to boolean.
130
     *
131
     * @see __call()
132
     *
133
     * @param string $field
134
     * @return bool
135
     */
136
    protected function _has (string $field): bool {
137
        $value = $this->_get($field);
138
        if (isset($value)) {
139
            if (is_countable($value)) {
140
                return count($value) > 0;
141
            }
142
            return (bool)$value;
143
        }
144
        return false;
145
    }
146
147
    /**
148
     * A factory that also hydrates / caches entities.
149
     *
150
     * @param string $class
151
     * @param mixed $item
152
     * @return mixed
153
     */
154
    protected function _hydrate (string $class, $item) {
155
        if (!isset($item) or $item instanceof self) {
156
            return $item;
157
        }
158
        // hydrate entities
159
        if (is_subclass_of($class, AbstractEntity::class)) {
160
            if (is_string($item)) { // convert gids to lazy stubs
161
                $item = ['gid' => $item];
162
            }
163
            return $this->api->getPool()->get($item['gid'], $this, function() use ($class, $item) {
164
                return $this->api->factory($this, $class, $item);
165
            });
166
        }
167
        // hydrate simple
168
        return $this->api->factory($this, $class, $item);
169
    }
170
171
    /**
172
     * Magic method: `isField()`
173
     *
174
     * Boolean casts a scalar field.
175
     *
176
     * Do not use this for countable fields, use `hasField()` instead.
177
     *
178
     * @see __call()
179
     *
180
     * @param string $field
181
     * @return bool
182
     */
183
    protected function _is (string $field): bool {
184
        return (bool)$this->_get($field);
185
    }
186
187
    /**
188
     * Magic method: `selectField(callable $filter)`
189
     *
190
     * Where `Field` has an accessor at `getField()`, either real or magic.
191
     *
192
     * This can also be used to select from an arbitrary iterable.
193
     *
194
     * @see __call()
195
     *
196
     * @param string|iterable $subject
197
     * @param callable $filter `fn( Data $object ): bool`
198
     * @param array $args
199
     * @return array
200
     */
201
    protected function _select ($subject, callable $filter, ...$args) {
202
        if (is_string($subject)) {
203
            $subject = $this->{'get' . $subject}(...$args) ?? [];
204
        }
205
        $selected = [];
206
        foreach ($subject as $item) {
207
            if (call_user_func($filter, $item)) {
208
                $selected[] = $item;
209
            }
210
        }
211
        return $selected;
212
    }
213
214
    /**
215
     * Magic method: `setField(mixed $value)`
216
     *
217
     * @see __call()
218
     *
219
     * @param string $field
220
     * @param mixed $value
221
     * @return $this
222
     */
223
    protected function _set (string $field, $value) {
224
        $this->data[$field] = $value;
225
        $this->diff[$field] = true;
226
        return $this;
227
    }
228
229
    /**
230
     * Clears all diffs and sets all data, hydrating mapped fields.
231
     *
232
     * @param array $data
233
     */
234
    protected function _setData (array $data): void {
235
        $this->data = $this->diff = [];
236
        foreach ($data as $field => $value) {
237
            $this->_setField($field, $value);
238
        }
239
    }
240
241
    /**
242
     * Sets a value, hydrating if mapped, and clears the diff.
243
     *
244
     * @param string $field
245
     * @param mixed $value
246
     */
247
    protected function _setField (string $field, $value): void {
248
        if (isset(static::MAP[$field])) {
249
            $class = static::MAP[$field];
250
            if (is_array($class)) {
251
                $value = array_map(function($each) use ($class) {
252
                    return $this->_hydrate($class[0], $each);
253
                }, $value);
254
            }
255
            elseif (isset($value)) {
256
                $value = $this->_hydrate($class, $value);
257
            }
258
        }
259
        $this->data[$field] = $value;
260
        unset($this->diff[$field]);
261
    }
262
263
    /**
264
     * Whether the instance has changes.
265
     *
266
     * @return bool
267
     */
268
    final public function isDiff (): bool {
269
        return (bool)$this->diff;
270
    }
271
272
    /**
273
     * @return array
274
     */
275
    public function jsonSerialize (): array {
276
        $data = $this->toArray();
277
        ksort($data);
278
        return $data;
279
    }
280
281
    /**
282
     * Dehydrated JSON encode.
283
     *
284
     * @return string
285
     */
286
    public function serialize (): string {
287
        return json_encode($this, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
288
    }
289
290
    /**
291
     * Dehydrated data.
292
     *
293
     * @param bool $diff
294
     * @return array
295
     */
296
    public function toArray (bool $diff = false): array {
297
        $dehydrate = function($each) use (&$dehydrate, $diff) {
298
            // convert entities to gids
299
            if ($each instanceof AbstractEntity and $each->hasGid()) {
300
                return $each->getGid();
301
            }
302
            // convert other data to arrays.
303
            elseif ($each instanceof self) {
304
                return $each->toArray($diff);
305
            }
306
            // dehydrate normal arrays.
307
            elseif (is_array($each)) {
308
                return array_map($dehydrate, $each);
309
            }
310
            // return as-is
311
            return $each;
312
        };
313
        if ($diff) {
314
            return array_map($dehydrate, array_intersect_key($this->data, $this->diff));
315
        }
316
        return array_map($dehydrate, $this->data);
317
    }
318
319
    /**
320
     * @param $serialized
321
     */
322
    public function unserialize ($serialized): void {
323
        $this->data = json_decode($serialized, true);
324
    }
325
}