Skip to content

Commit b799f14

Browse files
committed
Collection: Allow more flexible implementations of Arrayable
- Add optional `TArrayValue` template to `DictionaryInterface` and `CollectionInterface` for `Arrayable::toArray()` return type control - Move `toArray()` from `ReadOnlyCollectionTrait` to `RecursiveArrayableCollectionTrait` - Add non-recursive implementation of `toArray()` to `ArrayableCollectionTrait` - Update implementations as needed
1 parent 63e3f24 commit b799f14

15 files changed

+448
-367
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Salient\Collection;
4+
5+
/**
6+
* @api
7+
*
8+
* @template TKey of array-key
9+
* @template TValue
10+
*/
11+
trait ArrayableCollectionTrait
12+
{
13+
/** @use HasItems<TKey,TValue> */
14+
use HasItems;
15+
16+
/**
17+
* @inheritDoc
18+
*/
19+
public function toArray(bool $preserveKeys = true): array
20+
{
21+
return $preserveKeys
22+
? $this->Items
23+
: array_values($this->Items);
24+
}
25+
}

src/Toolkit/Collection/Collection.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111
* @template TKey of array-key
1212
* @template TValue
1313
*
14-
* @implements CollectionInterface<TKey,TValue>
14+
* @implements CollectionInterface<TKey,TValue,mixed[]>
1515
* @implements IteratorAggregate<TKey,TValue>
1616
*/
1717
class Collection implements CollectionInterface, IteratorAggregate
1818
{
1919
/** @use CollectionTrait<TKey,TValue,static<TKey|int,TValue>> */
2020
use CollectionTrait;
21+
/** @use RecursiveArrayableCollectionTrait<TKey,TValue> */
22+
use RecursiveArrayableCollectionTrait;
2123
}

src/Toolkit/Collection/Dictionary.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111
* @template TKey of array-key
1212
* @template TValue
1313
*
14-
* @implements DictionaryInterface<TKey,TValue>
14+
* @implements DictionaryInterface<TKey,TValue,mixed[]>
1515
* @implements IteratorAggregate<TKey,TValue>
1616
*/
1717
class Dictionary implements DictionaryInterface, IteratorAggregate
1818
{
1919
/** @use DictionaryTrait<TKey,TValue> */
2020
use DictionaryTrait;
21+
/** @use RecursiveArrayableCollectionTrait<TKey,TValue> */
22+
use RecursiveArrayableCollectionTrait;
2123
}

src/Toolkit/Collection/DictionaryTrait.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,12 @@
33
namespace Salient\Collection;
44

55
use Salient\Contract\Collection\CollectionInterface;
6-
use Salient\Contract\Collection\DictionaryInterface;
76

87
/**
98
* @api
109
*
1110
* @template TKey of array-key
1211
* @template TValue
13-
*
14-
* @phpstan-require-implements DictionaryInterface
1512
*/
1613
trait DictionaryTrait
1714
{
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Salient\Collection;
4+
5+
use Salient\Contract\Collection\CollectionInterface;
6+
use Salient\Contract\Collection\DictionaryInterface;
7+
use Salient\Contract\Core\Arrayable;
8+
use Salient\Contract\Core\Comparable;
9+
use Salient\Contract\Core\Jsonable;
10+
use Salient\Utility\Arr;
11+
use Salient\Utility\Json;
12+
use ArrayIterator;
13+
use InvalidArgumentException;
14+
use IteratorAggregate;
15+
use JsonSerializable;
16+
use OutOfRangeException;
17+
use ReturnTypeWillChange;
18+
use Traversable;
19+
20+
/**
21+
* @internal
22+
*
23+
* @template TKey of array-key
24+
* @template TValue
25+
*
26+
* @phpstan-require-implements DictionaryInterface
27+
* @phpstan-require-implements IteratorAggregate
28+
*/
29+
trait HasItems
30+
{
31+
/** @var array<TKey,TValue> */
32+
protected array $Items;
33+
34+
/**
35+
* @inheritDoc
36+
*/
37+
public function isEmpty(): bool
38+
{
39+
return !$this->Items;
40+
}
41+
42+
/**
43+
* @inheritDoc
44+
*/
45+
public function has($key): bool
46+
{
47+
return array_key_exists($key, $this->Items);
48+
}
49+
50+
/**
51+
* @inheritDoc
52+
*/
53+
public function get($key)
54+
{
55+
if (!array_key_exists($key, $this->Items)) {
56+
throw new OutOfRangeException(sprintf('Item not found: %s', $key));
57+
}
58+
return $this->Items[$key];
59+
}
60+
61+
/**
62+
* @inheritDoc
63+
*/
64+
public function forEach(callable $callback, int $mode = CollectionInterface::CALLBACK_USE_VALUE)
65+
{
66+
$prev = null;
67+
$item = null;
68+
69+
foreach ($this->Items as $nextKey => $nextValue) {
70+
$next = $this->getCallbackValue($mode, $nextKey, $nextValue);
71+
if ($item !== null) {
72+
$callback($item, $next, $prev);
73+
}
74+
$prev = $item;
75+
$item = $next;
76+
}
77+
if ($item !== null) {
78+
$callback($item, null, $prev);
79+
}
80+
81+
return $this;
82+
}
83+
84+
/**
85+
* @inheritDoc
86+
*/
87+
public function find(callable $callback, int $mode = CollectionInterface::CALLBACK_USE_VALUE | CollectionInterface::FIND_VALUE)
88+
{
89+
$prev = null;
90+
$item = null;
91+
$key = null;
92+
$value = null;
93+
94+
foreach ($this->Items as $nextKey => $nextValue) {
95+
$next = $this->getCallbackValue($mode, $nextKey, $nextValue);
96+
if ($item !== null && $callback($item, $next, $prev)) {
97+
/** @var TKey $key */
98+
/** @var TValue $value */
99+
// @phpstan-ignore return.type
100+
return $this->getReturnValue($mode, $key, $value);
101+
}
102+
$prev = $item;
103+
$item = $next;
104+
$key = $nextKey;
105+
$value = $nextValue;
106+
}
107+
if ($item !== null && $callback($item, null, $prev)) {
108+
/** @var TKey $key */
109+
/** @var TValue $value */
110+
// @phpstan-ignore return.type
111+
return $this->getReturnValue($mode, $key, $value);
112+
}
113+
114+
return null;
115+
}
116+
117+
/**
118+
* @inheritDoc
119+
*/
120+
public function hasValue($value, bool $strict = false): bool
121+
{
122+
if ($strict) {
123+
return in_array($value, $this->Items, true);
124+
}
125+
126+
foreach ($this->Items as $item) {
127+
if (!$this->compareItems($value, $item)) {
128+
return true;
129+
}
130+
}
131+
return false;
132+
}
133+
134+
/**
135+
* @inheritDoc
136+
*/
137+
public function keyOf($value, bool $strict = false)
138+
{
139+
if ($strict) {
140+
return Arr::search($this->Items, $value, true);
141+
}
142+
143+
foreach ($this->Items as $key => $item) {
144+
if (!$this->compareItems($value, $item)) {
145+
return $key;
146+
}
147+
}
148+
return null;
149+
}
150+
151+
/**
152+
* @inheritDoc
153+
*/
154+
public function firstOf($value)
155+
{
156+
foreach ($this->Items as $item) {
157+
if (!$this->compareItems($value, $item)) {
158+
return $item;
159+
}
160+
}
161+
return null;
162+
}
163+
164+
/**
165+
* @inheritDoc
166+
*/
167+
public function all(): array
168+
{
169+
return $this->Items;
170+
}
171+
172+
/**
173+
* @return array<TKey,mixed>
174+
*/
175+
public function jsonSerialize(): array
176+
{
177+
foreach ($this->Items as $key => $value) {
178+
if ($value instanceof JsonSerializable) {
179+
$array[$key] = $value->jsonSerialize();
180+
} elseif ($value instanceof Jsonable) {
181+
$array[$key] = Json::objectAsArray($value->toJson());
182+
} elseif ($value instanceof Arrayable) {
183+
$array[$key] = $value->toArray();
184+
} else {
185+
$array[$key] = $value;
186+
}
187+
}
188+
return $array ?? [];
189+
}
190+
191+
/**
192+
* @inheritDoc
193+
*/
194+
public function toJson(int $flags = 0): string
195+
{
196+
return Json::encode($this->jsonSerialize(), $flags);
197+
}
198+
199+
/**
200+
* @inheritDoc
201+
*/
202+
public function first()
203+
{
204+
return $this->Items ? reset($this->Items) : null;
205+
}
206+
207+
/**
208+
* @inheritDoc
209+
*/
210+
public function last()
211+
{
212+
return $this->Items ? end($this->Items) : null;
213+
}
214+
215+
/**
216+
* @inheritDoc
217+
*/
218+
public function nth(int $n)
219+
{
220+
if ($n === 0) {
221+
throw new InvalidArgumentException('Argument #1 ($n) is 1-based, 0 given');
222+
}
223+
224+
$keys = array_keys($this->Items);
225+
if ($n < 0) {
226+
$keys = array_reverse($keys);
227+
$n = -$n;
228+
}
229+
$key = $keys[$n - 1] ?? null;
230+
return $key === null
231+
? null
232+
: $this->Items[$key];
233+
}
234+
235+
/**
236+
* @return Traversable<TKey,TValue>
237+
*/
238+
public function getIterator(): Traversable
239+
{
240+
return new ArrayIterator($this->Items);
241+
}
242+
243+
/**
244+
* @param TKey $offset
245+
*/
246+
public function offsetExists($offset): bool
247+
{
248+
return array_key_exists($offset, $this->Items);
249+
}
250+
251+
/**
252+
* @param TKey $offset
253+
* @return TValue
254+
*/
255+
#[ReturnTypeWillChange]
256+
public function offsetGet($offset)
257+
{
258+
return $this->Items[$offset];
259+
}
260+
261+
public function count(): int
262+
{
263+
return count($this->Items);
264+
}
265+
266+
/**
267+
* Compare items using Comparable::compare() if implemented
268+
*
269+
* @param TValue $a
270+
* @param TValue $b
271+
*/
272+
protected function compareItems($a, $b): int
273+
{
274+
if (
275+
$a instanceof Comparable
276+
&& $b instanceof Comparable
277+
) {
278+
if ($b instanceof $a) {
279+
return $a->compare($a, $b);
280+
}
281+
if ($a instanceof $b) {
282+
return $b->compare($a, $b);
283+
}
284+
}
285+
return $a <=> $b;
286+
}
287+
288+
/**
289+
* @param int-mask-of<CollectionInterface::*> $mode
290+
* @param TKey $key
291+
* @param TValue $value
292+
* @return ($mode is 3|11|19 ? array{TKey,TValue} : ($mode is 2|10|18 ? TKey : TValue))
293+
*/
294+
private function getCallbackValue(int $mode, $key, $value)
295+
{
296+
$mode &= CollectionInterface::CALLBACK_USE_BOTH;
297+
return $mode === CollectionInterface::CALLBACK_USE_KEY
298+
? $key
299+
: ($mode === CollectionInterface::CALLBACK_USE_BOTH
300+
? [$key, $value]
301+
: $value);
302+
}
303+
304+
/**
305+
* @param int-mask-of<CollectionInterface::*> $mode
306+
* @param TKey $key
307+
* @param TValue $value
308+
* @return ($mode is 16|17|18|19 ? TKey : TValue)
309+
*/
310+
private function getReturnValue(int $mode, $key, $value)
311+
{
312+
return $mode & CollectionInterface::FIND_KEY
313+
&& !($mode & CollectionInterface::FIND_VALUE)
314+
? $key
315+
: $value;
316+
}
317+
}

0 commit comments

Comments
 (0)