Completed
Push — master ( 88b8c9...f7af16 )
by Michael
02:28
created

ManagesItemsTrait   C

Complexity

Total Complexity 58

Size/Duplication

Total Lines 455
Duplicated Lines 4.62 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 44
Bugs 2 Features 9
Metric Value
wmc 58
c 44
b 2
f 9
lcom 1
cbo 5
dl 21
loc 455
rs 6.3006

26 Methods

Rating   Name   Duplication   Size   Complexity  
B initManager() 0 20 5
A hydrate() 0 9 2
C add() 0 38 7
A set() 0 4 1
B push() 0 22 4
A get() 7 15 3
A getIfExists() 7 13 3
A getIfHas() 0 4 1
A getAll() 0 5 1
A all() 0 4 1
A exists() 7 13 3
A has() 0 4 1
A isEmpty() 0 5 1
A remove() 0 16 4
A clear() 0 6 1
A reset() 0 4 1
A protect() 0 5 1
A loadDefaults() 0 5 1
A getItemsName() 0 4 1
A setItemsName() 0 5 1
A toJson() 0 4 1
A __toString() 0 4 1
A prepareReturnedValue() 0 10 2
B mergeDefaults() 0 17 6
A checkIfProtected() 0 15 3
A performProtectedCheck() 0 6 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ManagesItemsTrait 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 ManagesItemsTrait, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Michaels\Manager\Traits;
3
4
use Michaels\Manager\Contracts\ManagesItemsInterface;
5
use Michaels\Manager\Exceptions\ItemNotFoundException;
6
use Michaels\Manager\Exceptions\ModifyingProtectedValueException;
7
use Michaels\Manager\Exceptions\NestingUnderNonArrayException;
8
use Michaels\Manager\Helpers;
9
use Michaels\Manager\Messages\NoItemFoundMessage;
10
use Traversable;
11
12
/**
13
 * Manages complex, nested data
14
 *
15
 * @implements Michaels\Manager\Contracts\ManagesItemsInterface
16
 * @package Michaels\Manager
17
 */
18
trait ManagesItemsTrait
19
{
20
    /**
21
     * The items stored in the manager
22
     * @var array $items Items governed by manager
23
     */
24
    protected $_items;
25
26
    /**
27
     * Name of the property to hold the data items. Internal use only
28
     * @var string
29
     */
30
    protected $nameOfItemsRepository = '_items';
31
32
    /** @var array Array of protected aliases */
33
    protected $protectedItems = [];
34
35
    /* The user may also set $dataItemsName */
36
37
    /**
38
     * Initializes a new manager instance.
39
     *
40
     * This is useful for implementations that have their own __construct method
41
     * This is an alias for reset()
42
     *
43
     * @param array $items
44
     * @return $this
45
     */
46
    public function initManager($items = null)
47
    {
48
        if (property_exists($this, 'dataItemsName')) {
49
            $this->setItemsName($this->dataItemsName);
1 ignored issue
show
Bug introduced by
The property dataItemsName does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
50
        }
51
52
        $repo = $this->getItemsName();
53
54
        if (!isset($this->$repo)) {
55
            $this->$repo = [];
56
        }
57
58
        if (is_null($items)) {
59
            return $this;
60
        }
61
62
        $this->$repo = is_array($items) ? $items : Helpers::getArrayableItems($items);
63
64
        return $this;
65
    }
66
67
    /**
68
     * Hydrate with external data, optionally append
69
     *
70
     * @param $data string     The data to be hydrated into the manager
71
     * @param bool $append When true, data will be appended to the current set
72
     * @return $this
73
     */
74
    public function hydrate($data, $append = false)
75
    {
76
        if ($append === false) {
77
            $this->reset($data);
78
        } else {
79
            $this->add($data);
80
        }
81
        return $this;
82
    }
83
84
    /**
85
     * Adds a single item.
86
     *
87
     * Allow for dot notation (one.two.three) and item nesting.
88
     *
89
     * @param string $alias Key to be stored
90
     * @param mixed $item Value to be stored
91
     * @return $this
92
     */
93
    public function add($alias, $item = null)
94
    {
95
        $this->checkIfProtected($alias);
96
97
        // Are we adding multiple items?
98
        if (is_array($alias)) {
99
            foreach ($alias as $key => $value) {
100
                $this->add($key, $value);
101
            }
102
            return $this;
103
        }
104
105
        // No, we are adding a single item
106
        $repo = $this->getItemsName();
107
        $loc = &$this->$repo;
108
109
        $pieces = explode('.', $alias);
110
        $currentLevel = 1;
111
        $nestLevels = count($pieces) - 1;
112
113
        foreach ($pieces as $step) {
114
            // Make sure we are not trying to nest under a non-array. This is gross
115
            // https://github.com/chrismichaels84/data-manager/issues/6
116
117
            // 1. Not at the last level (the one with the desired value),
118
            // 2. The nest level is already set,
119
            // 3. and is not an array
120
            if ($nestLevels > $currentLevel && isset($loc[$step]) && !is_array($loc[$step])) {
121
                throw new NestingUnderNonArrayException();
122
            }
123
124
            $loc = &$loc[$step];
125
            $currentLevel++;
126
        }
127
        $loc = $item;
128
129
        return $this;
130
    }
131
132
    /**
133
     * Updates an item
134
     *
135
     * @param string $alias
136
     * @param null $item
137
     *
138
     * @return $this
139
     */
140
    public function set($alias, $item = null)
141
    {
142
        return $this->add($alias, $item);
143
    }
144
145
    /**
146
     * Push a value or values onto the end of an array inside manager
147
     * @param string $alias The level of nested data
148
     * @param mixed $value The first value to append
149
     * @param null|mixed $_ Optional other values to apend
150
     * @return int Number of items pushed
151
     * @throws ItemNotFoundException If pushing to unset array
152
     */
153
    public function push($alias, $value, $_ = null)
0 ignored issues
show
Coding Style Naming introduced by
The parameter $_ is not named in camelCase.

This check marks parameter names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
154
    {
155
        if (isset($_)) {
156
            $values = func_get_args();
157
            array_shift($values);
158
        } else {
159
            $values = [$value];
160
        }
161
162
        $array = $this->get($alias);
163
164
        if (!is_array($array)) {
165
            throw new NestingUnderNonArrayException("You may only push items onto an array");
166
        }
167
168
        foreach ($values as $value) {
169
            array_push($array, $value);
170
        }
171
        $this->set($alias, $array);
1 ignored issue
show
Documentation introduced by
$array is of type array, but the function expects a null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
172
173
        return count($values);
174
    }
175
176
    /**
177
     * Get a single item
178
     *
179
     * @param string $alias
180
     * @param string $fallback Defaults to '_michaels_no_fallback' so null can be a fallback
181
     * @throws \Michaels\Manager\Exceptions\ItemNotFoundException If item not found
182
     * @return mixed
183
     */
184
    public function get($alias, $fallback = '_michaels_no_fallback')
185
    {
186
        $item = $this->getIfExists($alias);
187
188
        // The item was not found
189 View Code Duplication
        if ($item instanceof NoItemFoundMessage) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
190
            if ($fallback !== '_michaels_no_fallback') {
191
                $item = $fallback;
192
            } else {
193
                throw new ItemNotFoundException("$alias not found");
194
            }
195
        }
196
197
        return $this->prepareReturnedValue($item);
198
    }
199
200
    /**
201
     * Return an item if it exist
202
     * @param $alias
203
     * @return NoItemFoundMessage
204
     */
205
    public function getIfExists($alias)
206
    {
207
        $repo = $this->getItemsName();
208
        $loc = &$this->$repo;
209 View Code Duplication
        foreach (explode('.', $alias) as $step) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
210
            if (array_key_exists($step, $loc)) {
211
                $loc = &$loc[$step];
212
            } else {
213
                return new NoItemFoundMessage($alias);
214
            }
215
        }
216
        return $loc;
217
    }
218
219
    /**
220
     * Return an item if it exist
221
     * Alias of getIfExists()
222
     *
223
     * @param $alias
224
     * @return NoItemFoundMessage
225
     */
226
    public function getIfHas($alias)
227
    {
228
        return $this->getIfExists($alias);
229
    }
230
231
    /**
232
     * Return all items as array
233
     *
234
     * @return array
235
     */
236
    public function getAll()
237
    {
238
        $repo = $this->getItemsName();
239
        return $this->prepareReturnedValue($this->$repo);
240
    }
241
242
    /**
243
     * Return all items as array
244
     * Alias of getAll()
245
     * @return array
246
     */
247
    public function all()
248
    {
249
        return $this->getAll();
250
    }
251
252
    /**
253
     * Confirm or deny that an item exists
254
     *
255
     * @param $alias
256
     * @return bool
257
     */
258
    public function exists($alias)
259
    {
260
        $repo = $this->getItemsName();
261
        $loc = &$this->$repo;
262 View Code Duplication
        foreach (explode('.', $alias) as $step) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
263
            if (!isset($loc[$step])) {
264
                return false;
265
            } else {
266
                $loc = &$loc[$step];
267
            }
268
        }
269
        return true;
270
    }
271
272
    /**
273
     * Confirm or deny that an item exists
274
     * Alias of exists()
275
     *
276
     * @param $alias
277
     * @return bool
278
     */
279
    public function has($alias)
280
    {
281
        return $this->exists($alias);
282
    }
283
284
285
    /**
286
     * Confirm that manager has no items
287
     * @return boolean
288
     */
289
    public function isEmpty()
290
    {
291
        $repo = $this->getItemsName();
292
        return empty($this->$repo);
293
    }
294
295
    /**
296
     * Deletes an item
297
     *
298
     * @param $alias
299
     * @return $this
300
     */
301
    public function remove($alias)
302
    {
303
        $repo = $this->getItemsName();
304
        $loc = &$this->$repo;
305
        $parts = explode('.', $alias);
306
307
        while (count($parts) > 1) {
308
            $step = array_shift($parts);
309
            if (isset($loc[$step]) && is_array($loc[$step])) {
310
                $loc = &$loc[$step];
311
            }
312
        }
313
314
        unset($loc[array_shift($parts)]);
315
        return $this;
316
    }
317
318
    /**
319
     * Clear the manager
320
     * @return $this
321
     */
322
    public function clear()
323
    {
324
        $repo = $this->getItemsName();
325
        $this->$repo = [];
326
        return $this;
327
    }
328
329
    /**
330
     * Reset the manager with an array of items
331
     * Alias of initManager()
332
     *
333
     * @param array $items
334
     * @return mixed
335
     */
336
    public function reset($items)
337
    {
338
        $this->initManager($items);
339
    }
340
341
    /**
342
     * Guard an alias from being modified
343
     * @param $item
344
     * @return $this
345
     */
346
    public function protect($item)
347
    {
348
        array_push($this->protectedItems, $item);
349
        return $this;
350
    }
351
352
    /**
353
     * Merge a set of defaults with the current items
354
     * @param array $defaults
355
     * @return $this
356
     */
357
    public function loadDefaults(array $defaults)
358
    {
359
        $this->mergeDefaults($defaults);
360
        return $this;
361
    }
362
363
    /**
364
     * Returns the name of the property that holds data items
365
     * @return string
366
     */
367
    public function getItemsName()
368
    {
369
        return $this->nameOfItemsRepository;
370
    }
371
372
    /**
373
     * Sets the name of the property that holds data items
374
     * @param $nameOfItemsRepository
375
     * @return $this
376
     */
377
    public function setItemsName($nameOfItemsRepository)
378
    {
379
        $this->nameOfItemsRepository = $nameOfItemsRepository;
380
        return $this;
381
    }
382
383
    /**
384
     * Get the collection of items as JSON.
385
     *
386
     * @param  int $options
387
     * @return string
388
     */
389
    public function toJson($options = 0)
390
    {
391
        return json_encode($this->getAll(), $options);
392
    }
393
394
    /**
395
     * When manager instance is used as a string, return json of items
396
     * @return string
397
     */
398
    public function __toString()
399
    {
400
        return $this->toJson();
401
    }
402
403
    /**
404
     * Prepare the returned value
405
     * @param $value
406
     * @return mixed
407
     */
408
    protected function prepareReturnedValue($value)
409
    {
410
        // Are we looking for Collections?
411
        if (method_exists($this, 'toCollection')) {
412
            return $this->toCollection($value);
1 ignored issue
show
Bug introduced by
It seems like toCollection() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
413
        }
414
415
        // No? Just return the value
416
        return $value;
417
    }
418
419
    /**
420
     * Recursively merge defaults array and items array
421
     * @param array $defaults
422
     * @param string $level
423
     */
424
    protected function mergeDefaults(array $defaults, $level = '')
425
    {
426
        foreach ($defaults as $key => $value) {
427
            if (is_array($value)) {
428
                $original = $this->getIfExists(ltrim("$level.$key", "."));
429
                if (is_array($original)) {
430
                    $this->mergeDefaults($value, "$level.$key");
431
                } elseif ($original instanceof NoItemFoundMessage) {
432
                    $this->set(ltrim("$level.$key", "."), $value);
1 ignored issue
show
Documentation introduced by
$value is of type array, but the function expects a null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
433
                }
434
            } else {
435
                if (!$this->exists(ltrim("$level.$key", "."))) {
436
                    $this->set(ltrim("$level.$key", "."), $value);
437
                }
438
            }
439
        }
440
    }
441
442
    /**
443
     * Cycle through the nests to see if an item is protected
444
     * @param $item
445
     */
446
    protected function checkIfProtected($item)
447
    {
448
        $this->performProtectedCheck($item);
449
450
        if (!is_string($item)) {
451
            return;
452
        }
453
454
        $prefix = $item;
455
        while (false !== $pos = strrpos($prefix, '.')) {
456
            $prefix = substr($item, 0, $pos);
457
            $this->performProtectedCheck($prefix);
458
            $prefix = rtrim($prefix, '.');
459
        }
460
    }
461
462
    /**
463
     * Throws an exception if item is protected
464
     * @param $item
465
     */
466
    protected function performProtectedCheck($item)
467
    {
468
        if (in_array($item, $this->protectedItems)) {
469
            throw new ModifyingProtectedValueException("Cannot access $item because it is protected");
470
        }
471
    }
472
}
473