Passed
Push — master ( 866c3b...50a671 )
by y
08:13
created

Data::_select()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 2
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
     *
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|select|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: `selectField(callable $filter)`
155
     *
156
     * This can also be used to select from an arbitrary array.
157
     *
158
     * @see __call()
159
     *
160
     * @param string|array $subject
161
     * @param callable $filter `fn( Data $object ): bool`
162
     * @return array
163
     */
164
    protected function _select ($subject, callable $filter) {
165
        if (!is_array($subject)) {
166
            $subject = $this->_get($subject) ?? [];
167
        }
168
        return array_values(array_filter($subject, $filter));
169
    }
170
171
    /**
172
     * Magic method: `setField(mixed $value)`
173
     *
174
     * @see __call()
175
     *
176
     * @param string $field
177
     * @param mixed $value
178
     * @return $this
179
     */
180
    protected function _set (string $field, $value) {
181
        $this->data[$field] = $value;
182
        $this->diff[$field] = true;
183
        return $this;
184
    }
185
186
    /**
187
     * Clears all diffs and sets all data, hydrating mapped fields.
188
     *
189
     * @param array $data
190
     */
191
    protected function _setData (array $data): void {
192
        $this->data = $this->diff = [];
193
        foreach ($data as $field => $value) {
194
            $this->_setMapped($field, $value);
195
        }
196
    }
197
198
    /**
199
     * Sets a value, hydrating if mapped, and clears the diff.
200
     *
201
     * @param string $field
202
     * @param mixed $value
203
     */
204
    protected function _setMapped (string $field, $value): void {
205
        unset($this->diff[$field]);
206
207
        // use value as-is?
208
        if (
209
            !isset(static::$map[$field])        // unmapped
210
            or empty($value)                    // null or []
211
            or $value instanceof self           // Data-ish
212
            or is_array($value) && current($value) instanceof self  // Data[]-ish
213
        ) {
214
            $this->data[$field] = $value;
215
            return;
216
        }
217
218
        // otherwise the field is mapped.
219
        $class = static::$map[$field];
220
        $hydrate = function($data) use (&$class) {
221
            // hydrate AbstractEntity
222
            if (is_subclass_of($class, AbstractEntity::class)) {
223
                // convert gids to data stubs. a lazy-loader will be returned.
224
                if (is_scalar($data)) {
225
                    $data = ['gid' => $data];
226
                }
227
                return $this->api->getCache()->get($data['gid'], $this, function() use ($class, $data) {
228
                    return $this->factory($class, $data);
229
                });
230
            }
231
            // hydrate Data
232
            return $this->factory($class, $data);
233
        };
234
235
        // the value is non-empty and either: data, data[], gid, or gid[]
236
        if (is_array($class)) {
237
            $class = $class[0];
0 ignored issues
show
Unused Code introduced by
The assignment to $class is dead and can be removed.
Loading history...
238
            $this->data[$field] = array_map($hydrate, $value);
239
        }
240
        else {
241
            $this->data[$field] = $hydrate($value);
242
        }
243
    }
244
245
    /**
246
     * Helper, forwards to the API.
247
     *
248
     * @param string $class
249
     * @param array $data
250
     * @return mixed|Data|AbstractEntity
251
     */
252
    final protected function factory (string $class, array $data = []) {
253
        return $this->api->factory($class, $this, $data);
254
    }
255
256
    /**
257
     * Constructs and returns an array for Asana using diffs.
258
     *
259
     * @return array
260
     */
261
    public function getDiff (): array {
262
        $convert = function($each) use (&$convert) {
263
            // convert existing entities to gids
264
            if ($each instanceof AbstractEntity and $each->hasGid()) {
265
                return $each->getGid();
266
            }
267
            // convert data objects and new entities to arrays
268
            elseif ($each instanceof self) {
269
                return $each->getDiff();
270
            }
271
            // convert arrays
272
            elseif (is_array($each)) {
273
                return array_map($convert, $each);
274
            }
275
            // return as-is
276
            return $each;
277
        };
278
        return array_map($convert, array_intersect_key($this->data, $this->diff));
279
    }
280
281
    /**
282
     * Whether the instance has changes.
283
     *
284
     * @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...
285
     * @return bool
286
     */
287
    final public function isDiff (string $field = null): bool {
288
        if ($field) {
289
            return isset($this->diff[$field]);
290
        }
291
        return (bool)$this->diff;
292
    }
293
294
    /**
295
     * @see toArray()
296
     * @return array
297
     */
298
    public function jsonSerialize (): array {
299
        return $this->toArray();
300
    }
301
302
    /**
303
     * Helper, forwards to the API.
304
     *
305
     * @see Api::load()
306
     *
307
     * @param string $class
308
     * @param string $path
309
     * @param array $query
310
     * @return null|mixed|AbstractEntity
311
     */
312
    final protected function load (string $class, string $path, array $query = []) {
313
        return $this->api->load($class, $this, $path, $query);
314
    }
315
316
    /**
317
     * Helper, forwards to the API.
318
     *
319
     * @see Api::loadAll()
320
     *
321
     * @param string $class
322
     * @param string $path
323
     * @param array $query
324
     * @param int $pages
325
     * @return array|AbstractEntity[]
326
     */
327
    final protected function loadAll (string $class, string $path, array $query = [], int $pages = 0) {
328
        return $this->api->loadAll($class, $this, $path, $query, $pages);
329
    }
330
331
    /**
332
     * Returns a serialized representation of the instance's dehydrated data.
333
     *
334
     * @return string
335
     */
336
    public function serialize (): string {
337
        return json_encode($this);
338
    }
339
340
    /**
341
     * Returns a dehydrated copy of the instance's data.
342
     *
343
     * @return array
344
     */
345
    public function toArray (): array {
346
        return array_map($dehydrate = function($each) use (&$dehydrate) {
347
            if ($each instanceof AbstractEntity and $each->hasGid()) {
348
                return $each->getGid();
349
            }
350
            elseif ($each instanceof self) {
351
                return $each->toArray();
352
            }
353
            elseif (is_array($each)) {
354
                return array_map($dehydrate, $each);
355
            }
356
            return $each;
357
        }, $this->data);
358
    }
359
360
    /**
361
     * Sets the dehydrated data and uses the default {@see Api} instance.
362
     *
363
     * @param $serialized
364
     */
365
    public function unserialize ($serialized): void {
366
        $this->api = Api::getDefault();
367
        $data = json_decode($serialized, true);
368
        $this->_setData($data);
369
    }
370
}