| Total Complexity | 54 | 
| Total Lines | 358 | 
| Duplicated Lines | 0 % | 
| Changes | 0 | ||
Complex classes like DataBag often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use DataBag, and based on these observations, apply Extract Interface, too.
| 1 | <?php  | 
            ||
| 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()  | 
            ||
| 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)  | 
            ||
| 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)  | 
            ||
| 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 | $this->guardAgainstInvalidPath($path);  | 
            ||
| 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)  | 
            ||
| 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)  | 
            ||
| 357 | }  | 
            ||
| 358 | |||
| 359 | /**  | 
            ||
| 360 | * @param string $path  | 
            ||
| 361 | */  | 
            ||
| 362 | private function guardAgainstInvalidPath($path)  | 
            ||
| 374 | }  | 
            ||
| 375 | }  | 
            ||
| 376 | |||
| 378 |