Passed
Push — master ( b66c10...181037 )
by y
02:15
created

Data::_setData()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 9
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 9
b 0
f 0
nc 2
nop 1
dl 0
loc 4
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 {@see Api::factory()} to do that.
23
     *
24
     * @see _setData()
25
     * @see _setMapped()
26
     *
27
     * @var array
28
     */
29
    protected static $map = [];
30
31
    /**
32
     * @var Api
33
     */
34
    protected $api;
35
36
    /**
37
     * @var array|Data[]
38
     */
39
    protected $data = [];
40
41
    /**
42
     * @var bool[]
43
     */
44
    protected $diff = [];
45
46
    /**
47
     * @param Api|Data $caller
48
     * @param array $data
49
     */
50
    public function __construct ($caller, array $data = []) {
51
        $this->api = $caller instanceof self ? $caller->api : $caller;
52
        $this->_setData($data);
53
    }
54
55
    /**
56
     * Magic method handler.
57
     *
58
     * @see _get()
59
     * @see _has()
60
     * @see _is()
61
     * @see _set()
62
     * @param string $method
63
     * @param array $args
64
     * @return mixed
65
     */
66
    public function __call (string $method, array $args) {
67
        static $cache = [];
68
        if (!$call =& $cache[$method]) {
69
            preg_match('/^(get|has|is|set)(.+)$/', $method, $call);
70
            $call[1] = '_' . $call[1];
71
            $call[2] = preg_replace_callback('/[A-Z]/', function(array $match) {
72
                return '_' . strtolower($match[0]);
73
            }, lcfirst($call[2]));
74
        }
75
        return $this->{$call[1]}($call[2], ...$args);
76
    }
77
78
    public function __debugInfo (): array {
79
        return $this->data;
80
    }
81
82
    /**
83
     * This is only provided for use by functions like `array_column()`.
84
     *
85
     * @param $field
86
     * @return null|Data|mixed
87
     */
88
    final public function __get ($field) {
89
        return $this->_get($field);
90
    }
91
92
    /**
93
     * This is only provided for use by functions like `array_column()`.
94
     *
95
     * Always returns `true`, since fields that are not set can be lazy-loaded.
96
     *
97
     * @param $field
98
     * @return bool
99
     */
100
    final public function __isset ($field) {
101
        return true;
102
    }
103
104
    /**
105
     * Magic method: `getField()`
106
     *
107
     * @see __call()
108
     *
109
     * @param string $field
110
     * @return mixed
111
     */
112
    protected function _get (string $field) {
113
        return $this->data[$field] ?? null;
114
    }
115
116
    /**
117
     * Magic method: `hasField()`
118
     *
119
     * Whether a scalar field is set, or a countable field has anything in it.
120
     *
121
     * @see __call()
122
     *
123
     * @param string $field
124
     * @return bool
125
     */
126
    protected function _has (string $field): bool {
127
        $value = $this->_get($field);
128
        if (isset($value)) {
129
            if (is_countable($value)) {
130
                return count($value) > 0;
131
            }
132
            return true;
133
        }
134
        return false;
135
    }
136
137
    /**
138
     * Magic method: `isField()`
139
     *
140
     * The boolean state of a scalar field.
141
     *
142
     * Do not use this for countable fields, use `hasField()` instead.
143
     *
144
     * @see __call()
145
     *
146
     * @param string $field
147
     * @return bool
148
     */
149
    protected function _is (string $field): bool {
150
        return !empty($this->_get($field));
151
    }
152
153
    /**
154
     * Magic method: `setField(mixed $value)`
155
     *
156
     * @see __call()
157
     *
158
     * @param string $field
159
     * @param mixed $value
160
     * @return $this
161
     */
162
    protected function _set (string $field, $value) {
163
        $this->data[$field] = $value;
164
        $this->diff[$field] = true;
165
        return $this;
166
    }
167
168
    /**
169
     * Clears all diffs and sets all data, hydrating mapped fields.
170
     *
171
     * @param array $data
172
     */
173
    protected function _setData (array $data): void {
174
        $this->data = $this->diff = [];
175
        foreach ($data as $field => $value) {
176
            $this->_setMapped($field, $value);
177
        }
178
    }
179
180
    /**
181
     * Sets a value, hydrating if mapped, and clears the diff.
182
     *
183
     * @param string $field
184
     * @param mixed $value
185
     */
186
    protected function _setMapped (string $field, $value): void {
187
        unset($this->diff[$field]);
188
189
        // use value as-is?
190
        if (
191
            !isset(static::$map[$field])        // unmapped
192
            or empty($value)                    // null or []
193
            or $value instanceof self           // Data-ish
194
            or is_array($value) && current($value) instanceof self  // Data[]-ish
195
        ) {
196
            $this->data[$field] = $value;
197
            return;
198
        }
199
200
        // otherwise the field is mapped.
201
        $class = static::$map[$field];
202
        $hydrate = function($data) use (&$class) {
203
            // hydrate AbstractEntity
204
            if (is_subclass_of($class, AbstractEntity::class)) {
205
                // convert gids to data stubs. a lazy-loader will be returned.
206
                if (is_scalar($data)) {
207
                    $data = ['gid' => $data];
208
                }
209
                return $this->api->getCache()->get($data['gid'], $this, function() use ($class, $data) {
210
                    return $this->factory($class, $data);
211
                });
212
            }
213
            // hydrate Data
214
            return $this->factory($class, $data);
215
        };
216
217
        // the value is non-empty and either: data, data[], gid, or gid[]
218
        if (is_array($class)) {
219
            $class = $class[0];
0 ignored issues
show
Unused Code introduced by
The assignment to $class is dead and can be removed.
Loading history...
220
            $this->data[$field] = array_map($hydrate, $value);
221
        }
222
        else {
223
            $this->data[$field] = $hydrate($value);
224
        }
225
    }
226
227
    /**
228
     * Helper, forwards to the API.
229
     *
230
     * @param string $class
231
     * @param array $data
232
     * @return mixed|Data|AbstractEntity
233
     */
234
    final protected function factory (string $class, array $data = []) {
235
        return $this->api->factory($class, $this, $data);
236
    }
237
238
    /**
239
     * Constructs and returns an array for Asana using diffs.
240
     *
241
     * @return array
242
     */
243
    public function getDiff (): array {
244
        $convert = function($each) use (&$convert) {
245
            // convert existing entities to gids
246
            if ($each instanceof AbstractEntity and $each->hasGid()) {
247
                return $each->getGid();
248
            }
249
            // convert data objects and new entities to arrays
250
            elseif ($each instanceof self) {
251
                return $each->getDiff();
252
            }
253
            // convert arrays
254
            elseif (is_array($each)) {
255
                return array_map($convert, $each);
256
            }
257
            // return as-is
258
            return $each;
259
        };
260
        return array_map($convert, array_intersect_key($this->data, $this->diff));
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
     * @see toArray()
274
     * @return array
275
     */
276
    public function jsonSerialize (): array {
277
        return $this->toArray();
278
    }
279
280
    /**
281
     * Helper, forwards to the API.
282
     *
283
     * @see Api::load()
284
     *
285
     * @param string $class
286
     * @param string $path
287
     * @param array $query
288
     * @return null|mixed|AbstractEntity
289
     */
290
    final protected function load (string $class, string $path, array $query = []) {
291
        return $this->api->load($class, $this, $path, $query);
292
    }
293
294
    /**
295
     * Helper, forwards to the API.
296
     *
297
     * @see Api::loadAll()
298
     *
299
     * @param string $class
300
     * @param string $path
301
     * @param array $query
302
     * @param int $pages
303
     * @return array|AbstractEntity[]
304
     */
305
    final protected function loadAll (string $class, string $path, array $query = [], int $pages = 0) {
306
        return $this->api->loadAll($class, $this, $path, $query, $pages);
307
    }
308
309
    /**
310
     * Returns a serialized representation of the instance's dehydrated data.
311
     *
312
     * @return string
313
     */
314
    public function serialize (): string {
315
        return json_encode($this);
316
    }
317
318
    /**
319
     * Returns a dehydrated copy of the instance's data.
320
     *
321
     * @return array
322
     */
323
    public function toArray (): array {
324
        return array_map($dehydrate = function($each) use (&$dehydrate) {
325
            if ($each instanceof AbstractEntity and $each->hasGid()) {
326
                return $each->getGid();
327
            }
328
            elseif ($each instanceof self) {
329
                return $each->toArray();
330
            }
331
            elseif (is_array($each)) {
332
                return array_map($dehydrate, $each);
333
            }
334
            return $each;
335
        }, $this->data);
336
    }
337
338
    /**
339
     * Sets the dehydrated data and uses the default {@see Api} instance.
340
     *
341
     * @param $serialized
342
     */
343
    public function unserialize ($serialized): void {
344
        $this->api = Api::getDefault();
345
        $data = json_decode($serialized, true);
346
        $this->_setData($data);
347
    }
348
}