Passed
Push — master ( 24e1a2...15b43b )
by y
01:43
created

Data::_setField()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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