Passed
Push — master ( 7ef18e...f705ba )
by Misha
45s
created

DataBag::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 5
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Communibase;
6
7
use Communibase\DataBag\DataMutator;
8
use Communibase\DataBag\DataRemover;
9
use Communibase\DataBag\DataRetriever;
10
11
/**
12
 * 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
13
 * private entity class property. The dataBag can contain one or more entities. For each entity we can get/set
14
 * properties by path. If we need to persist the entity back into CB use getState to fetch the (updated) data array.
15
 *
16
 * @author Kingsquare ([email protected])
17
 * @copyright Copyright (c) Kingsquare BV (http://www.kingsquare.nl)
18
 */
19
final class DataBag
20
{
21
    /**
22
     * The bag!
23
     * @var array<string, array>
24
     */
25
    private $data = [];
26
27
    /**
28
     * @var DataMutator
29
     */
30
    private $dataMutator;
31
32
    /**
33
     * @var DataRetriever
34
     */
35
    private $dataRetriever;
36
37
    /**
38
     * @var DataRemover;
39
     */
40
    private $dataRemover;
41
42
    /**
43
     * Original data hash for isDirty check
44
     *
45
     * @var array<string,string>
46
     */
47
    private $hashes;
48
49
    /**
50
     * If we have multiple identical get calls in the same request use the cached result
51
     *
52
     * @var array<string, mixed>
53
     */
54
    private $cache = [];
55
56
    /**
57
     * Private constructor, use the named constructors below
58
     */
59
    private function __construct()
60
    {
61
        $this->dataMutator = new DataMutator();
62
        $this->dataRetriever = new DataRetriever();
63
        $this->dataRemover = new DataRemover();
64
    }
65
66
    public static function create(): DataBag
67
    {
68
        return new self();
69
    }
70
71
    /**
72
     * @param array<string, mixed> $data
73
     */
74
    public static function fromEntityData(string $entityType, array $data): DataBag
75
    {
76
        $dataBag = self::create();
77
        $dataBag->addEntityData($entityType, $data);
78
        return $dataBag;
79
    }
80
81
    /**
82
     * Add additional entities
83
     * @param array<string, mixed> $data
84
     */
85
    public function addEntityData(string $entityType, array $data): DataBag
86
    {
87
        $this->data[$entityType] = $data;
88
        $this->hashes[$entityType] = $this->generateHash($data);
89
        return $this;
90
    }
91
92
    /**
93
     * Fetch a value from the databag.
94
     *
95
     * $path can be:
96
     * - person.firstName               direct property
97
     * - person.emailAddresses.0        indexed by numeric position
98
     * - person.addresses.visit         indexed by 'type' property
99
     * - person.addresses.visit.street  indexed by 'type' property + get specific property
100
     *
101
     * @param string $path path to the target
102
     * @param mixed $default return value if there's no data
103
     *
104
     * @return mixed|null
105
     * @throws InvalidDataBagPathException
106
     */
107
    public function get(string $path, $default = null)
108
    {
109
        $this->guardAgainstInvalidPath($path);
110
111
        if (!\array_key_exists($path, $this->cache)) {
112
            $this->cache[$path] = $this->dataRetriever->getByPath($this->data, $path, $default);
113
        }
114
        return $this->cache[$path];
115
    }
116
117
    /**
118
     * Set a value in the bag.
119
     *
120
     * @param string $path path to the target (see get() for examples)
121
     * @param mixed $value new value
122
     *
123
     * @throws InvalidDataBagPathException
124
     */
125
    public function set(string $path, $value): void
126
    {
127
        $this->guardAgainstInvalidPath($path);
128
129
        unset($this->cache[$path]);
130
131
        if ($value === null) {
132
            $this->guardAgainstInvalidPath($path);
133
            $this->dataRemover->removeByPath($this->data, $path);
134
            return;
135
        }
136
137
        $this->dataMutator->setByPath($this->data, $path, $value);
138
    }
139
140
    /**
141
     * Check if a certain entity type exists in the dataBag
142
     *
143
     * @return bool true if the entity type exists
144
     */
145
    public function hasEntityData(string $entityType): bool
146
    {
147
        return isset($this->data[$entityType]);
148
    }
149
150
    /**
151
     * Check if the initial data has changed
152
     *
153
     * @param string $entityType entity type to check
154
     *
155
     * @return bool|null true if changed, false if not and null if the entity type is not set
156
     */
157
    public function isDirty(string $entityType): ?bool
158
    {
159
        if (!isset($this->data[$entityType])) {
160
            return null;
161
        }
162
        if (empty($this->hashes[$entityType])) {
163
            return true;
164
        }
165
        return $this->hashes[$entityType] !== $this->generateHash($this->getState($entityType));
166
    }
167
168
    /**
169
     * @param array<string,mixed> $data
170
     */
171
    private function generateHash(array $data): string
172
    {
173
        return md5(serialize($this->filterIds($data)));
174
    }
175
176
    /**
177
     * @param array<string,mixed> $data
178
     * @return array<string,mixed>
179
     */
180
    private function filterIds(array $data): array
181
    {
182
        array_walk(
183
            $data,
184
            function (&$value) {
185
                if (\is_array($value)) {
186
                    $value = $this->filterIds($value);
187
                }
188
            }
189
        );
190
        return array_diff_key($data, ['_id' => null]);
191
    }
192
193
    /**
194
     * Get the raw data array
195
     *
196
     * @param string|null $entityType only get the specified type (optional)
197
     * @return array<string,mixed>
198
     */
199
    public function getState(string $entityType = null): array
200
    {
201
        if ($entityType === null) {
202
            return $this->data;
203
        }
204
        return $this->data[$entityType] ?? [];
205
    }
206
207
    /**
208
     * @throws InvalidDataBagPathException
209
     */
210
    private function guardAgainstInvalidPath(string $path): void
211
    {
212
        if ($path === '' // empty
213
            || strpos($path, '..') !== false // has .. somewhere
214
            || substr($path, -1) === '.' // ends with .
215
            || \in_array(strpos($path, '.'), [false, 0], true) // starts with or doesnt have any .
216
        ) {
217
            throw new InvalidDataBagPathException('Invalid path provided: ' . $path);
218
        }
219
    }
220
}
221