Passed
Push — master ( d91d77...b5a0a4 )
by Misha
01:55
created

DataBag::set()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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