Test Setup Failed
Pull Request — master (#1)
by Robin
04:58 queued 19s
created

DataBag::guardAgainstInvalidPath()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 4
nc 2
nop 1
dl 0
loc 7
rs 10
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
        $this->guardAgainstInvalidPath($path);
102
103
        list($entityType, $path) = explode('.', $path, 2);
104
105
        // Direct property
106
        if (strpos($path, '.') === false) {
107
            return isset($this->data[$entityType][$path]) ? $this->data[$entityType][$path] : $default;
108
        }
109
110
        // Indexed
111
        list($path, $index) = explode('.', $path, 2);
112
113
        if (empty($this->data[$entityType][$path])) {
114
            return $default;
115
        }
116
117
        // Indexed with 'type' property
118
        $field = null;
119
        if (strpos($index, '.') > 0) {
120
            list($index, $field) = explode('.', $index, 2);
121
        }
122
123
        $nodes = $this->data[$entityType][$path];
124
125
        if (!is_numeric($index)) {
126
            $target = $index;
127
            $index = null;
128
            foreach ((array)$nodes as $nodeIndex => $node) {
129
                if ($node['type'] === $target) {
130
                    $index = $nodeIndex;
131
                    break;
132
                }
133
            }
134
        }
135
        if ($index === null) {
136
            return $default;
137
        }
138
139
        if ($field === null) {
140
            return isset($nodes[$index]) ? $nodes[$index] : $default;
141
        }
142
        return isset($nodes[$index][$field]) ? $nodes[$index][$field] : $default;
143
    }
144
145
    /**
146
     * Fetch a cached value from the databag.
147
     *
148
     * $path can be:
149
     * - person.firstName               direct property
150
     * - person.emailAddresses.0        indexed by numeric position
151
     * - person.addresses.visit         indexed by 'type' property
152
     * - person.addresses.visit.street  indexed by 'type' property + get specific property
153
     *
154
     * @param string $path path to the target
155
     * @param mixed $default return value if there's no data
156
     *
157
     * @return mixed|null
158
     * @throws InvalidDataBagPathException
159
     */
160
    public function get($path, $default = null)
161
    {
162
        if (!array_key_exists($path, $this->cache)) {
163
            $this->cache[$path] = $this->getByPath($path, $default);
164
        }
165
        return $this->cache[$path];
166
    }
167
168
    /**
169
     * Set a value in the bag.
170
     *
171
     * @param string $path path to the target (see get() for examples)
172
     * @param mixed $value new value
173
     *
174
     * @throws InvalidDataBagPathException
175
     */
176
    public function set($path, $value)
177
    {
178
        $this->guardAgainstInvalidPath($path);
179
180
        unset($this->cache[$path]);
181
        if ($value === null) {
182
            $this->remove($path);
183
            return;
184
        }
185
186
        list($entityType, $path) = explode('.', $path, 2);
187
188
        // Direct property
189
        if (strpos($path, '.') === false) {
190
            $this->data[$entityType][$path] = $value;
191
            return;
192
        }
193
194
        // Indexed
195
        list($path, $index) = explode('.', $path, 2);
196
197
        $field = null;
198
        if (strpos($index, '.') > 0) {
199
            list($index, $field) = explode('.', $index, 2);
200
        }
201
202
        $target = $index;
203
        if (!is_numeric($index)) {
204
            if (is_array($value)) {
205
                $value['type'] = $index;
206
            }
207
            $index = null;
208
            if (isset($this->data[$entityType][$path])) {
209
                foreach ((array)$this->data[$entityType][$path] as $nodeIndex => $node) {
210
                    if ($node['type'] === $target) {
211
                        $index = $nodeIndex;
212
                        break;
213
                    }
214
                }
215
            }
216
        }
217
218
        // No index found, new entry
219
        if ($index === null) {
220
            if ($field === null) {
221
                $this->data[$entityType][$path][] = $value;
222
                return;
223
            }
224
            $value = [
225
                $field => $value
226
            ];
227
            if (!is_numeric($target)) {
228
                $value['type'] = $target;
229
            }
230
            $this->data[$entityType][$path][] = $value;
231
            return;
232
        }
233
234
        // Use found index
235
        if ($field === null) {
236
            $this->data[$entityType][$path][$index] = $value;
237
            return;
238
        }
239
        $this->data[$entityType][$path][$index][$field] = $value;
240
    }
241
242
    /**
243
     * Check if a certain entity type exists in the dataBag
244
     *
245
     * @param string $entityType
246
     *
247
     * @return bool true if the entity type exists
248
     */
249
    public function hasEntityData($entityType)
250
    {
251
        return isset($this->data[$entityType]);
252
    }
253
254
    /**
255
     * Remove a property from the bag.
256
     *
257
     * @param string $path path to the target (see get() for examples)
258
     * @param bool $removeAll remove all when the index is numeric (to prevent a new value after re-indexing)
259
     *
260
     * @throws InvalidDataBagPathException
261
     */
262
    public function remove($path, $removeAll = true)
263
    {
264
        $this->guardAgainstInvalidPath($path);
265
266
        list($entityType, $path) = explode('.', $path, 2);
267
268
        // Direct property
269
        if (strpos($path, '.') === false) {
270
            if (!isset($this->data[$entityType][$path])) {
271
                return;
272
            }
273
            $this->data[$entityType][$path] = null;
274
            return;
275
        }
276
277
        $this->removeIndexed($path, $entityType, $removeAll);
278
    }
279
280
    /**
281
     * @param string $path
282
     * @param string $entityType
283
     * @param bool $removeAll
284
     */
285
    private function removeIndexed($path, $entityType, $removeAll)
286
    {
287
        list($path, $index) = explode('.', $path);
288
289
        // Target doesn't exist, nothing to remove
290
        if (empty($this->data[$entityType][$path])) {
291
            return;
292
        }
293
294
        if (is_numeric($index)) {
295
            $index = (int)$index;
296
            if ($removeAll) {
297
                // Remove all (higher) values to prevent a new value after re-indexing
298
                if ($index === 0) {
299
                    $this->data[$entityType][$path] = null;
300
                    return;
301
                }
302
                $this->data[$entityType][$path] = array_slice($this->data[$entityType][$path], 0, $index);
303
                return;
304
            }
305
            unset($this->data[$entityType][$path][$index]);
306
        } else {
307
            // Filter out all nodes of the specified type
308
            $this->data[$entityType][$path] = array_filter(
309
                $this->data[$entityType][$path],
310
                static function ($node) use ($index) {
311
                    return empty($node['type']) || $node['type'] !== $index;
312
                }
313
            );
314
        }
315
316
        // If we end up with an empty array make it NULL
317
        if (empty($this->data[$entityType][$path])) {
318
            $this->data[$entityType][$path] = null;
319
            return;
320
        }
321
322
        // Re-index
323
        $this->data[$entityType][$path] = array_values($this->data[$entityType][$path]);
324
    }
325
326
    /**
327
     * Check if the initial data has changed
328
     *
329
     * @param string $entityType entity type to check
330
     *
331
     * @return bool|null true if changed, false if not and null if the entity type is not set
332
     */
333
    public function isDirty($entityType)
334
    {
335
        if (!isset($this->data[$entityType])) {
336
            return null;
337
        }
338
        if (empty($this->hashes[$entityType])) {
339
            return true;
340
        }
341
        return $this->hashes[$entityType] !== md5(serialize($this->getState($entityType)));
342
    }
343
344
    /**
345
     * Get the raw data array
346
     *
347
     * @param string|null $entityType only get the specified type (optional)
348
     *
349
     * @return array
350
     */
351
    public function getState($entityType = null)
352
    {
353
        if ($entityType === null) {
354
            return $this->data;
355
        }
356
        return isset($this->data[$entityType]) ? $this->data[$entityType] : [];
357
    }
358
359
    /**
360
     * @param string $path
361
     */
362
    private function guardAgainstInvalidPath($path)
363
    {
364
        if (strpos($path, '..') !== false // has .. somewhere
365
            || substr_compare($path, '.', -1) === 0 // ends with .
366
            || in_array(strpos($path, '.'), [false, 0], true) // starts with or doesnt have any .
367
        ) {
368
            throw new InvalidDataBagPathException('Invalid path provided: ' . $path);
369
        }
370
    }
371
372
}
373