Passed
Push — master ( 510bd1...28b336 )
by Jakub
01:29
created

TCollection::getIndex()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 3
nop 1
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 3
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
namespace Nexendrie\Utils;
5
6
/**
7
 * TCollection
8
 * Target class has to implement \ArrayAccess, \Countable, \IteratorAggregate interfaces
9
 *
10
 * @author Jakub Konečný
11
 */
12 1
trait TCollection {
13
  /** @var object[] */
14
  protected $items = [];
15
  /** @var string Type of items in the collection */
16
  protected $class;
17
  /** @var string|NULL */
18
  protected $uniqueProperty = null;
19
  /** @var int */
20
  protected $maxSize = 0;
21
  /** @var bool */
22
  protected $locked = false;
23
  /** @var callable[] */
24
  protected $checkers = [];
25
  
26
  public function __construct() {
27 1
    $this->addChecker([$this, "checkLock"]);
28 1
    $this->addChecker([$this, "checkType"]);
29 1
    $this->addChecker([$this, "checkUniqueness"]);
30 1
    $this->addChecker([$this, "checkSize"]);
31 1
  }
32
  
33
  public function isLocked(): bool {
34 1
    return $this->locked;
35
  }
36
  
37
  public function lock(): void {
38 1
    $this->locked = true;
39 1
  }
40
  
41
  public function count(): int {
42 1
    return count($this->items);
43
  }
44
  
45
  public function getIterator(): \ArrayIterator {
46 1
    return new \ArrayIterator($this->items);
47
  }
48
  
49
  /**
50
   * @param int $index
51
   */
52
  public function offsetExists($index): bool {
53 1
    return $index >= 0 AND $index < count($this->items);
54
  }
55
  
56
  /**
57
   * @param int|NULL $index
58
   * @return object
59
   * @throws \OutOfRangeException
60
   */
61
  public function offsetGet($index) {
62 1
    if($index < 0 OR $index >= count($this->items)) {
63 1
      throw new \OutOfRangeException("Offset invalid or out of range.");
64
    }
65 1
    return $this->items[$index];
66
  }
67
  
68
  public function addChecker(callable $checker): void {
69 1
    $this->checkers[] = $checker;
70 1
  }
71
72
  protected function checkLock(object $newItem, self $collection): void {
1 ignored issue
show
introduced by
The method parameter $newItem is never used
Loading history...
73 1
    if($collection->locked) {
74 1
      throw new \RuntimeException("Cannot add items to locked collection.");
75
    }
76 1
  }
77
78
  protected function checkType(object $newItem, self $collection): void {
79 1
    if(!$newItem instanceof $collection->class) {
80 1
      throw new \InvalidArgumentException("Argument must be of $this->class type.");
81
    }
82 1
  }
83
84
  protected function checkUniqueness(object $newItem, self $collection): void {
85 1
    $uniqueProperty = $collection->uniqueProperty;
86 1
    if(is_null($uniqueProperty)) {
87 1
      return;
88
    }
89 1
    if($this->hasItems([$uniqueProperty => $newItem->$uniqueProperty])) {
90 1
      throw new \RuntimeException("Duplicate $uniqueProperty {$newItem->$uniqueProperty}.");
91
    }
92 1
  }
93
94
  protected function checkSize(object $newItem, self $collection): void {
1 ignored issue
show
introduced by
The method parameter $newItem is never used
Loading history...
95 1
    if($collection->maxSize < 1) {
96 1
      return;
97
    }
98 1
    if($collection->count() + 1 > $collection->maxSize) {
99 1
      throw new \RuntimeException("Collection reached its max size. Cannot add more items.");
100
    }
101 1
  }
102
103
  protected function performChecks(object $item): void {
104 1
    foreach($this->checkers as $checker) {
105 1
      call_user_func($checker, $item, $this);
106
    }
107 1
  }
108
  
109
  /**
110
   * @param int|NULL $index
111
   * @param object $item
112
   * @throws \OutOfRangeException
113
   * @throws \InvalidArgumentException
114
   * @throws \RuntimeException
115
   */
116
  public function offsetSet($index, $item): void {
117 1
    $this->performChecks($item);
118 1
    if($index === null) {
119 1
      $this->items[] = & $item;
120 1
    } elseif($index < 0 OR $index >= count($this->items)) {
121 1
      throw new \OutOfRangeException("Offset invalid or out of range.");
122
    } else {
123 1
      $this->items[$index] = & $item;
124
    }
125 1
  }
126
  
127
  /**
128
   * @param int $index
129
   * @throws \RuntimeException
130
   * @throws \OutOfRangeException
131
   */
132
  public function offsetUnset($index): void {
133 1
    if($this->locked) {
134 1
      throw new \RuntimeException("Cannot remove items from locked collection.");
135 1
    } elseif($index < 0 OR $index >= count($this->items)) {
136 1
      throw new \OutOfRangeException("Offset invalid or out of range.");
137
    }
138 1
    array_splice($this->items, $index, 1);
139 1
  }
140
  
141
  public function toArray(): array {
142 1
    return $this->items;
143
  }
144
  
145
  /**
146
   * Create new collection from array
147
   */
148
  public static function fromArray(array $items, ...$args): self {
149 1
    $collection = new static(...$args);
1 ignored issue
show
Unused Code introduced by
The call to Nexendrie\Utils\TCollection::__construct() has too many arguments starting with $args. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

149
    $collection = /** @scrutinizer ignore-call */ new static(...$args);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
150 1
    foreach($items as $item) {
151 1
      $collection[] = $item;
152
    }
153 1
    return $collection;
154
  }
155
  
156
  /**
157
   * Check if the collection has at least 1 item matching the filter
158
   */
159
  public function hasItems(array $filter = []): bool {
160 1
    return (count($this->getItems($filter)) > 0);
161
  }
162
  
163
  /**
164
   * Get all items matching the filter
165
   */
166
  public function getItems(array $filter = []): array {
167 1
    return Filter::applyFilter($this->items, $filter);
168
  }
169
170
  /**
171
   * Get first item matching the filter
172
   */
173
  public function getItem(array $filter): ?object {
174 1
    $items = $this->getItems($filter);
175 1
    if(count($items) === 0) {
176 1
      return null;
177
    }
178 1
    return $items[0];
179
  }
180
181
  /**
182
   * Remove all items matching the filter
183
   *
184
   * @throws \RuntimeException
185
   * @throws \OutOfRangeException
186
   */
187
  public function removeByFilter(array $filter): void {
188 1
    foreach($this->items as $index => $item) {
189 1
      if(Filter::matches($item, $filter)) {
190 1
        $this->offsetUnset($index);
191
      }
192
    }
193 1
  }
194
195
  /**
196
   * Get index of first item matching the filter
197
   */
198
  public function getIndex(array $filter): ?int {
199 1
    foreach($this->items as $index => $item) {
200 1
      if(Filter::matches($item, $filter)) {
201 1
        return $index;
202
      }
203
    }
204 1
    return null;
205
  }
206
}
207
?>