Passed
Push — master ( 1a00b5...984447 )
by y
03:02
created

Data::toArray()   A

Complexity

Conditions 6
Paths 2

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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