Passed
Push — master ( 8668b0...3235fe )
by Jakub
01:40
created

TCollection::addDefaultCheckers()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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