Passed
Push — master ( 22dd1f...d25a5e )
by y
02:00
created

Data::_getPatch()   A

Complexity

Conditions 5
Paths 1

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 5
eloc 9
nc 1
nop 0
dl 0
loc 18
rs 9.6111
c 4
b 0
f 0

1 Method

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