1: <?php declare(strict_types=1);
2:
3: namespace Salient\Collection;
4:
5: use Salient\Contract\Collection\ListInterface;
6: use Salient\Contract\Core\Arrayable;
7: use Salient\Utility\Exception\InvalidArgumentTypeException;
8: use InvalidArgumentException;
9:
10: /**
11: * Implements ListInterface
12: *
13: * Unless otherwise noted, {@see ListTrait} methods operate on one instance of
14: * the class. Immutable lists should use {@see ImmutableListTrait} instead.
15: *
16: * @see ListInterface
17: *
18: * @todo Remove `@template TKey of int` when PHPStan propagates trait templates
19: * properly
20: *
21: * @api
22: *
23: * @template TKey of int
24: * @template TValue
25: *
26: * @phpstan-require-implements ListInterface
27: */
28: trait ListTrait
29: {
30: /** @use CollectionTrait<int,TValue> */
31: use CollectionTrait {
32: getItems as private _getItems;
33: replaceItems as private _replaceItems;
34: }
35:
36: /**
37: * @inheritDoc
38: */
39: public function add($value)
40: {
41: $items = $this->Items;
42: $items[] = $value;
43: return $this->maybeReplaceItems($items);
44: }
45:
46: /**
47: * @inheritDoc
48: */
49: public function set($key, $value)
50: {
51: $this->checkKey($key);
52: $items = $this->Items;
53: $items[$key] = $value;
54: return $this->maybeReplaceItems($items);
55: }
56:
57: /**
58: * @inheritDoc
59: */
60: public function merge($items)
61: {
62: $_items = $this->getItems($items);
63: if (!$_items) {
64: return $this;
65: }
66: $items = array_merge($this->Items, $_items);
67: return $this->maybeReplaceItems($items);
68: }
69:
70: /**
71: * @inheritDoc
72: */
73: public function push(...$item)
74: {
75: if (!$item) {
76: return $this;
77: }
78: $clone = $this->maybeClone();
79: array_push($clone->Items, ...$item);
80: return $clone;
81: }
82:
83: /**
84: * @inheritDoc
85: */
86: public function unshift(...$item)
87: {
88: if (!$item) {
89: return $this;
90: }
91: $clone = $this->maybeClone();
92: array_unshift($clone->Items, ...$item);
93: return $clone;
94: }
95:
96: /**
97: * @param int|null $offset
98: * @param TValue $value
99: */
100: public function offsetSet($offset, $value): void
101: {
102: if ($offset === null) {
103: $this->Items[] = $value;
104: return;
105: }
106: $this->checkKey($offset, 'offset');
107: $this->Items[$offset] = $value;
108: }
109:
110: /**
111: * @param int $offset
112: */
113: public function offsetUnset($offset): void
114: {
115: if (
116: !array_key_exists($offset, $this->Items)
117: || $offset === array_key_last($this->Items)
118: ) {
119: unset($this->Items[$offset]);
120: return;
121: }
122: $items = $this->Items;
123: unset($items[$offset]);
124: $this->Items = array_values($items);
125: }
126:
127: /**
128: * @param Arrayable<array-key,TValue>|iterable<array-key,TValue> $items
129: * @return list<TValue>
130: */
131: protected function getItems($items): array
132: {
133: return array_values($this->_getItems($items));
134: }
135:
136: /**
137: * @param array<int,TValue> $items
138: * @return static
139: */
140: protected function replaceItems(array $items, ?bool $getClone = null)
141: {
142: $key = array_key_last($items);
143: if ($key === null || $key === count($items) - 1) {
144: return $this->_replaceItems($items, $getClone);
145: }
146: return $this->_replaceItems(array_values($items), $getClone);
147: }
148:
149: /**
150: * @param int $key
151: * @throws InvalidArgumentException if `$key` is not an integer, or does not
152: * resolve to an existing item and is not the next numeric key in the list.
153: */
154: private function checkKey($key, string $argument = 'key'): void
155: {
156: if (!is_int($key)) {
157: throw new InvalidArgumentTypeException(1, $argument, 'int', $key);
158: }
159:
160: if (
161: !array_key_exists($key, $this->Items)
162: && $key !== count($this->Items)
163: ) {
164: throw new InvalidArgumentException(sprintf(
165: 'Item cannot be added with key: %d',
166: $key,
167: ));
168: }
169: }
170: }
171: