Passed
Push — master ( b5a0a4...bee6a6 )
by Misha
01:58
created

DataBag::getIndexed()   B

Complexity

Conditions 9
Paths 40

Size

Total Lines 30
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 16
nc 40
nop 4
dl 0
loc 30
rs 8.0555
c 0
b 0
f 0
1
<?php
2
3
namespace Communibase;
4
5
/**
6
 * Class DataBag
7
 *
8
 * It's a bag, for CB data. If we need to create a CB object from CB data (array) we can use this dataBag object as a
9
 * private entity class property. The dataBag can contain one or more entities. For each entity we can get/set
10
 * properties by path. If we need to persist the entity back into CB use getState to fetch the (updated) data array.
11
 *
12
 * @package Communibase\DataBag
13
 * @author Kingsquare ([email protected])
14
 * @copyright Copyright (c) Kingsquare BV (http://www.kingsquare.nl)
15
 */
16
final class DataBag
17
{
18
    /**
19
     * The bag!
20
     *
21
     * @var array
22
     */
23
    private $data;
24
25
    /**
26
     * Original data hash for isDirty check
27
     *
28
     * @var string[]
29
     */
30
    private $hashes;
31
32
    /**
33
     * If we have multiple identical get calls in the same request use the cached result
34
     *
35
     * @var array
36
     */
37
    private $cache = [];
38
39
    /**
40
     * DataBag constructor.
41
     */
42
    private function __construct()
43
    {
44
    }
45
46
    /**
47
     * @return DataBag
48
     */
49
    public static function create()
50
    {
51
        return new self();
52
    }
53
54
    /**
55
     * Static constructor
56
     *
57
     * @param string $entityType
58
     * @param array $data
59
     *
60
     * @return DataBag
61
     */
62
    public static function fromEntityData($entityType, array $data)
63
    {
64
        $dataBag = new self();
65
        $dataBag->addEntityData($entityType, $data);
66
        return $dataBag;
67
    }
68
69
    /**
70
     * Add additional entities
71
     *
72
     * @param string $entityType
73
     * @param array $data
74
     *
75
     * @return DataBag
76
     */
77
    public function addEntityData($entityType, array $data)
78
    {
79
        $this->data[$entityType] = $data;
80
        $this->hashes[$entityType] = md5(serialize($data));
81
        return $this;
82
    }
83
84
    /**
85
     * Fetch a value from the databag.
86
     *
87
     * $path can be:
88
     * - person.firstName               direct property
89
     * - person.emailAddresses.0        indexed by numeric position
90
     * - person.addresses.visit         indexed by 'type' property
91
     * - person.addresses.visit.street  indexed by 'type' property + get specific property
92
     *
93
     * @param string $path path to the target
94
     * @param mixed $default return value if there's no data
95
     *
96
     * @return mixed
97
     * @throws InvalidDataBagPathException
98
     */
99
    private function getByPath($path, $default = null)
100
    {
101
        list($entityType, $path) = explode('.', $path, 2);
102
103
        // Direct property
104
        if (strpos($path, '.') === false) {
105
            return isset($this->data[$entityType][$path]) ? $this->data[$entityType][$path] : $default;
106
        }
107
108
        // Indexed
109
        list($path, $index) = explode('.', $path, 2);
110
111
        if (empty($this->data[$entityType][$path])) {
112
            return $default;
113
        }
114
115
        return $this->getIndexed($entityType, $path, $index, $default);
116
    }
117
118
    /**
119
     * @param string $entityType
120
     * @param string $path
121
     * @param string $index
122
     * @param mixed $default
123
     *
124
     * @return mixed
125
     */
126
    private function getIndexed($entityType, $path, $index, $default)
127
    {
128
        // Indexed with 'type' property
129
        $field = null;
130
        if (strpos($index, '.') > 0) {
131
            list($index, $field) = explode('.', $index, 2);
132
        }
133
134
        $nodes = $this->data[$entityType][$path];
135
136
        if (!is_numeric($index)) {
137
            $target = $index;
138
            $index = null;
139
            foreach ((array)$nodes as $nodeIndex => $node) {
140
                if ($node['type'] === $target) {
141
                    $index = $nodeIndex;
142
                    break;
143
                }
144
            }
145
        }
146
147
        if ($index === null) {
148
            return $default;
149
        }
150
151
        if ($field === null) {
152
            return isset($nodes[$index]) ? $nodes[$index] : $default;
153
        }
154
155
        return isset($nodes[$index][$field]) ? $nodes[$index][$field] : $default;
156
    }
157
158
    /**
159
     * Fetch a cached value from the databag.
160
     *
161
     * $path can be:
162
     * - person.firstName               direct property
163
     * - person.emailAddresses.0        indexed by numeric position
164
     * - person.addresses.visit         indexed by 'type' property
165
     * - person.addresses.visit.street  indexed by 'type' property + get specific property
166
     *
167
     * @param string $path path to the target
168
     * @param mixed $default return value if there's no data
169
     *
170
     * @return mixed|null
171
     * @throws InvalidDataBagPathException
172
     */
173
    public function get($path, $default = null)
174
    {
175
        $path = (string)$path;
176
        $this->guardAgainstInvalidPath($path);
177
178
        if (!array_key_exists($path, $this->cache)) {
179
            $this->cache[$path] = $this->getByPath($path, $default);
180
        }
181
        return $this->cache[$path];
182
    }
183
184
    /**
185
     * Set a value in the bag.
186
     *
187
     * @param string $path path to the target (see get() for examples)
188
     * @param mixed $value new value
189
     *
190
     * @throws InvalidDataBagPathException
191
     */
192
    public function set($path, $value)
193
    {
194
        $path = (string)$path;
195
        $this->guardAgainstInvalidPath($path);
196
197
        unset($this->cache[$path]);
198
199
        if ($value === null) {
200
            $this->remove($path);
201
            return;
202
        }
203
204
        $this->setByPath($path, $value);
205
    }
206
207
    /**
208
     * @param string $path
209
     * @param mixed $value
210
     */
211
    private function setByPath($path, $value)
212
    {
213
        list($entityType, $path) = explode('.', $path, 2);
214
215
        // Direct property
216
        if (strpos($path, '.') === false) {
217
            $this->data[$entityType][$path] = $value;
218
            return;
219
        }
220
221
        // Indexed
222
        $this->setIndexed($entityType, $path, $value);
223
    }
224
225
    /**
226
     * @param string $entityType
227
     * @param string $path
228
     * @param mixed $value
229
     */
230
    private function setIndexed($entityType, $path, $value)
231
    {
232
        list($path, $index) = explode('.', $path, 2);
233
234
        $field = null;
235
        if (strpos($index, '.') > 0) {
236
            list($index, $field) = explode('.', $index, 2);
237
        }
238
239
        $target = $index;
240
        if (!is_numeric($index)) {
241
            if (is_array($value)) {
242
                $value['type'] = $index;
243
            }
244
            $index = null;
245
            if (isset($this->data[$entityType][$path])) {
246
                foreach ((array)$this->data[$entityType][$path] as $nodeIndex => $node) {
247
                    if ($node['type'] === $target) {
248
                        $index = $nodeIndex;
249
                        break;
250
                    }
251
                }
252
            }
253
        }
254
255
        // No index found, new entry
256
        if ($index === null) {
257
            $this->addNewEntry($entityType, $path, $field, $target, $value);
258
            return;
259
        }
260
261
        // Use found index
262
        if ($field === null) {
263
            $this->data[$entityType][$path][$index] = $value;
264
            return;
265
        }
266
        $this->data[$entityType][$path][$index][$field] = $value;
267
    }
268
269
    /**
270
     * @param string $entityType
271
     * @param string $path
272
     * @param string $field
273
     * @param string $target
274
     * @param mixed $value
275
     */
276
    private function addNewEntry($entityType, $path, $field, $target, $value)
277
    {
278
        if ($field === null) {
0 ignored issues
show
introduced by
The condition $field === null is always false.
Loading history...
279
            $this->data[$entityType][$path][] = $value;
280
            return;
281
        }
282
        $value = [
283
            $field => $value
284
        ];
285
        if (!is_numeric($target)) {
286
            $value['type'] = $target;
287
        }
288
        $this->data[$entityType][$path][] = $value;
289
    }
290
291
    /**
292
     * Check if a certain entity type exists in the dataBag
293
     *
294
     * @param string $entityType
295
     *
296
     * @return bool true if the entity type exists
297
     */
298
    public function hasEntityData($entityType)
299
    {
300
        return isset($this->data[$entityType]);
301
    }
302
303
    /**
304
     * Remove a property from the bag.
305
     *
306
     * @param string $path path to the target (see get() for examples)
307
     * @param bool $removeAll remove all when the index is numeric (to prevent a new value after re-indexing)
308
     *
309
     * @throws InvalidDataBagPathException
310
     */
311
    public function remove($path, $removeAll = true)
312
    {
313
        $path = (string)$path;
314
        $this->guardAgainstInvalidPath($path);
315
316
        list($entityType, $path) = explode('.', $path, 2);
317
318
        // Direct property
319
        if (strpos($path, '.') === false) {
320
            if (!isset($this->data[$entityType][$path])) {
321
                return;
322
            }
323
            $this->data[$entityType][$path] = null;
324
            return;
325
        }
326
327
        $this->removeIndexed($path, $entityType, $removeAll);
328
    }
329
330
    /**
331
     * @param string $path
332
     * @param string $entityType
333
     * @param bool $removeAll
334
     */
335
    private function removeIndexed($path, $entityType, $removeAll)
336
    {
337
        list($path, $index) = explode('.', $path);
338
339
        // Target doesn't exist, nothing to remove
340
        if (empty($this->data[$entityType][$path])) {
341
            return;
342
        }
343
344
        if (is_numeric($index)) {
345
            $index = (int)$index;
346
            if ($removeAll) {
347
                // Remove all (higher) values to prevent a new value after re-indexing
348
                if ($index === 0) {
349
                    $this->data[$entityType][$path] = null;
350
                    return;
351
                }
352
                $this->data[$entityType][$path] = array_slice($this->data[$entityType][$path], 0, $index);
353
                return;
354
            }
355
            unset($this->data[$entityType][$path][$index]);
356
        } else {
357
            // Filter out all nodes of the specified type
358
            $this->data[$entityType][$path] = array_filter(
359
                $this->data[$entityType][$path],
360
                static function ($node) use ($index) {
361
                    return empty($node['type']) || $node['type'] !== $index;
362
                }
363
            );
364
        }
365
366
        // If we end up with an empty array make it NULL
367
        if (empty($this->data[$entityType][$path])) {
368
            $this->data[$entityType][$path] = null;
369
            return;
370
        }
371
372
        // Re-index
373
        $this->data[$entityType][$path] = array_values($this->data[$entityType][$path]);
374
    }
375
376
    /**
377
     * Check if the initial data has changed
378
     *
379
     * @param string $entityType entity type to check
380
     *
381
     * @return bool|null true if changed, false if not and null if the entity type is not set
382
     */
383
    public function isDirty($entityType)
384
    {
385
        if (!isset($this->data[$entityType])) {
386
            return null;
387
        }
388
        if (empty($this->hashes[$entityType])) {
389
            return true;
390
        }
391
        return $this->hashes[$entityType] !== md5(serialize($this->getState($entityType)));
392
    }
393
394
    /**
395
     * Get the raw data array
396
     *
397
     * @param string|null $entityType only get the specified type (optional)
398
     *
399
     * @return array
400
     */
401
    public function getState($entityType = null)
402
    {
403
        if ($entityType === null) {
404
            return $this->data;
405
        }
406
        return isset($this->data[$entityType]) ? $this->data[$entityType] : [];
407
    }
408
409
    /**
410
     * @param string $path
411
     */
412
    private function guardAgainstInvalidPath($path)
413
    {
414
        if ($path === '' // empty
415
            || strpos($path, '..') !== false // has .. somewhere
416
            || substr($path, -1) === '.' // ends with .
417
            || in_array(strpos($path, '.'), [false, 0], true) // starts with or doesnt have any .
418
        ) {
419
            throw new InvalidDataBagPathException('Invalid path provided: ' . $path);
420
        }
421
    }
422
423
}
424