TCollection::getItem()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 2
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
trait TCollection {
13
  /** @var object[] */
14
  protected array $items = [];
15
  /** @var string Type of items in the collection */
16
  protected string $class;
17
  protected ?string $uniqueProperty = null;
18
  protected int $maxSize = 0;
19
  protected bool $locked = false;
20
  /** @var callable[] */
21
  protected array $checkers = [];
22
  
23
  public function __construct() {
24 1
    $this->addDefaultCheckers();
25 1
  }
26
27
  protected function addDefaultCheckers(): void {
28 1
    $this->addChecker([$this, "checkLock"]);
29 1
    $this->addChecker([$this, "checkType"]);
30 1
    $this->addChecker([$this, "checkUniqueness"]);
31 1
    $this->addChecker([$this, "checkSize"]);
32 1
  }
33
  
34
  public function isLocked(): bool {
35 1
    return $this->locked;
36
  }
37
  
38
  public function lock(): void {
39 1
    $this->locked = true;
40 1
  }
41
  
42
  public function count(): int {
43 1
    return count($this->items);
44
  }
45
  
46
  public function getIterator(): \ArrayIterator {
47 1
    return new \ArrayIterator($this->items);
48
  }
49
  
50
  /**
51
   * @param int $index
52
   */
53
  public function offsetExists($index): bool {
54 1
    return $index >= 0 && $index < count($this->items);
55
  }
56
  
57
  /**
58
   * @param int|NULL $index
59
   * @return object
60
   * @throws \OutOfRangeException
61
   */
62
  public function offsetGet($index) {
63 1
    if($index < 0 || $index >= count($this->items)) {
64 1
      throw new \OutOfRangeException("Offset invalid or out of range.");
65
    }
66 1
    return $this->items[$index];
67
  }
68
  
69
  public function addChecker(callable $checker): void {
70 1
    $this->checkers[] = $checker;
71 1
  }
72
73
  protected function checkLock(object $newItem, self $collection): void {
74 1
    if($collection->isLocked()) {
75 1
      throw new \RuntimeException("Cannot add items to locked collection.");
76
    }
77 1
  }
78
79
  protected function checkType(object $newItem, self $collection): void {
80 1
    if(!$newItem instanceof $collection->class) {
81 1
      throw new \InvalidArgumentException("Argument must be of $this->class type.");
82
    }
83 1
  }
84
85
  protected function checkUniqueness(object $newItem, self $collection): void {
86 1
    $uniqueProperty = $collection->uniqueProperty;
87 1
    if($uniqueProperty === null) {
88 1
      return;
89
    }
90 1
    if($this->hasItems([$uniqueProperty => $newItem->$uniqueProperty])) {
91 1
      throw new \RuntimeException("Duplicate $uniqueProperty {$newItem->$uniqueProperty}.");
92
    }
93 1
  }
94
95
  protected function checkSize(object $newItem, self $collection): void {
96 1
    if($collection->maxSize < 1) {
97 1
      return;
98
    }
99 1
    if($collection->count() + 1 > $collection->maxSize) {
100 1
      throw new \RuntimeException("Collection reached its max size. Cannot add more items.");
101
    }
102 1
  }
103
104
  protected function performChecks(object $item): void {
105 1
    foreach($this->checkers as $checker) {
106 1
      $checker($item, $this);
107
    }
108 1
  }
109
  
110
  /**
111
   * @param int|NULL $index
112
   * @param object $item
113
   * @throws \OutOfRangeException
114
   * @throws \InvalidArgumentException
115
   * @throws \RuntimeException
116
   */
117
  public function offsetSet($index, $item): void {
118 1
    $this->performChecks($item);
119 1
    if($index === null) {
120 1
      $this->items[] = & $item;
121 1
    } elseif($index < 0 || $index >= count($this->items)) {
122 1
      throw new \OutOfRangeException("Offset invalid or out of range.");
123
    } else {
124 1
      $this->items[$index] = & $item;
125
    }
126 1
  }
127
  
128
  /**
129
   * @param int $index
130
   * @throws \RuntimeException
131
   * @throws \OutOfRangeException
132
   */
133
  public function offsetUnset($index): void {
134 1
    if($this->locked) {
135 1
      throw new \RuntimeException("Cannot remove items from locked collection.");
136 1
    } elseif($index < 0 || $index >= count($this->items)) {
137 1
      throw new \OutOfRangeException("Offset invalid or out of range.");
138
    }
139 1
    array_splice($this->items, $index, 1);
140 1
  }
141
  
142
  public function toArray(): array {
143 1
    return $this->items;
144
  }
145
146
  /**
147
   * Create new collection from array
148
   *
149
   * @param object[] $items
150
   * @param mixed ...$args
151
   * @return static
152
   */
153
  public static function fromArray(array $items, ...$args): self {
154 1
    $collection = new static(...$args);
155 1
    foreach($items as $item) {
156 1
      $collection[] = $item;
157
    }
158 1
    return $collection;
159
  }
160
  
161
  /**
162
   * Check if the collection has at least $count items matching the filter
163
   */
164
  public function hasItems(array $filter = [], int $count = 1): bool {
165 1
    return (count($this->getItems($filter)) >= $count);
166
  }
167
  
168
  /**
169
   * Get all items matching the filter
170
   */
171
  public function getItems(array $filter = []): array {
172 1
    return Filter::applyFilter($this->items, $filter);
173
  }
174
175
  /**
176
   * Get first item matching the filter
177
   */
178
  public function getItem(array $filter): ?object {
179 1
    $items = $this->getItems($filter);
180 1
    if(count($items) === 0) {
181 1
      return null;
182
    }
183 1
    return $items[0];
184
  }
185
186
  /**
187
   * Remove all items matching the filter
188
   *
189
   * @throws \RuntimeException
190
   * @throws \OutOfRangeException
191
   */
192
  public function removeByFilter(array $filter): void {
193 1
    foreach($this->items as $item) {
194 1
      if(Filter::matches($item, $filter)) {
195
        /** @var int $index */
196 1
        $index = $this->getIndex($filter);
197 1
        $this->offsetUnset($index);
198
      }
199
    }
200 1
  }
201
202
  /**
203
   * Get index of first item matching the filter
204
   */
205
  public function getIndex(array $filter): ?int {
206 1
    foreach($this->items as $index => $item) {
207 1
      if(Filter::matches($item, $filter)) {
208 1
        return $index;
209
      }
210
    }
211 1
    return null;
212
  }
213
}
214
?>