Skip to content

Commit 82464bf

Browse files
committed
SequentialArray: Add $ignoreKeys option
1 parent f9ea374 commit 82464bf

7 files changed

+200
-51
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ $this->assertArrayHasKeyWith('answer', 42, $data);
170170

171171
The [`SequentialArray` constraint](https://github.com/PhrozenByte/phpunit-array-asserts/blob/master/src/Constraint/SequentialArray.php) asserts that a value is like a sequential array, has a minimum and/or maximum number of items, and that all items pass another constraint.
172172

173-
Sequential arrays are defined as ordered lists with incrementing numeric keys starting from zero. This is especially true for native sequential arrays like `[ "foo", "bar" ]`. Empty arrays are considered valid, too. `Traversable` objects must have sequential keys to be considered valid. The expected minimum (parameter `$minItems`, defaults to `0`) and/or maximum (parameter `$maxItems`, defaults to `null`, meaning infinite) number of items, and the constraint to apply all items to (optional parameter `$constraint`), are passed in the constructor. The constraint can either be an arbitrary `Constraint` instance (e.g. `PHPUnit\Framework\Constraint\StringContains`), or any static value, requiring an exact match of the value.
173+
Sequential arrays are defined as ordered lists with incrementing numeric keys starting from zero. This is especially true for native sequential arrays like `[ "foo", "bar" ]`. Empty arrays are considered valid, too. `Traversable` objects must have sequential keys to be considered valid. The expected minimum (parameter `$minItems`, defaults to `0`) and/or maximum (parameter `$maxItems`, defaults to `null`, meaning infinite) number of items, and the constraint to apply all items to (optional parameter `$constraint`), are passed in the constructor. The constraint can either be an arbitrary `Constraint` instance (e.g. `PHPUnit\Framework\Constraint\StringContains`), or any static value, requiring an exact match of the value. Requiring sequential keys can be disabled by setting parameter `$ignoreKeys` to `true` (defaults to `false`), causing the constraint to check just for the required number of items and whether they match the given constraint.
174174

175175
This constraint will fully traverse any `Traversable` object given. This also means that any `Generator` will be fully exhausted. If possible, it will try to restore an `Iterator`'s pointer to its previous state.
176176

@@ -185,14 +185,16 @@ ArrayAssertsTrait::assertSequentialArray(
185185
int $minItems, // required minimum number of items
186186
int $maxItems = null, // required maximum number of items (pass null for infinite)
187187
Constraint|mixed $constraint = null, // optional constraint to apply all items to
188+
bool $ignoreKeys = false, // whether to ignore non-sequential keys
188189
string $message = '' // additional information about the test
189190
);
190191

191192
// using new instance of `\PhrozenByte\PHPUnitArrayAsserts\Constraint\SequentialArray`
192193
new SequentialArray(
193194
int $minItems = 0,
194195
int $maxItems = null,
195-
Constraint|mixed $constraint = null
196+
Constraint|mixed $constraint = null,
197+
bool $ignoreKeys = false
196198
);
197199
```
198200

src/ArrayAssertsTrait.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ public static function arrayHasKeyWith($key, $constraint): ArrayHasKeyWith
148148
* @param int $minItems required minimum number of items, defaults to 0
149149
* @param int|null $maxItems required maximum number of items, defaults to NULL (infinite)
150150
* @param Constraint|mixed|null $constraint optional constraint to apply all items to (defaults to NULL)
151+
* @param bool $ignoreKeys whether to ignore non-sequential keys (defaults to FALSE)
151152
* @param string $message additional information about the test
152153
*
153154
* @throws ExpectationFailedException
@@ -159,13 +160,14 @@ public static function assertSequentialArray(
159160
int $minItems,
160161
int $maxItems = null,
161162
$constraint = null,
163+
bool $ignoreKeys = false,
162164
string $message = ''
163165
): void {
164166
if (!(is_array($array) || ($array instanceof Traversable))) {
165167
throw InvalidArgumentException::create(1, 'array or Traversable');
166168
}
167169

168-
$itemConstraint = new SequentialArray($minItems, $maxItems, $constraint);
170+
$itemConstraint = new SequentialArray($minItems, $maxItems, $constraint, $ignoreKeys);
169171
PHPUnitAssert::assertThat($array, $itemConstraint, $message);
170172
}
171173

@@ -175,14 +177,19 @@ public static function assertSequentialArray(
175177
* @param int $minItems required minimum number of items, defaults to 0
176178
* @param int|null $maxItems required maximum number of items, defaults to NULL (infinite)
177179
* @param Constraint|mixed|null $constraint optional constraint to apply all items to (defaults to NULL)
180+
* @param bool $ignoreKeys whether to ignore non-sequential keys (defaults to FALSE)
178181
*
179182
* @return SequentialArray
180183
*
181184
* @throws InvalidArgumentException
182185
*/
183-
public static function sequentialArray(int $minItems, int $maxItems = null, $constraint = null): SequentialArray
184-
{
185-
return new SequentialArray($minItems, $maxItems, $constraint);
186+
public static function sequentialArray(
187+
int $minItems,
188+
int $maxItems = null,
189+
$constraint = null,
190+
bool $ignoreKeys = false
191+
): SequentialArray {
192+
return new SequentialArray($minItems, $maxItems, $constraint, $ignoreKeys);
186193
}
187194

188195
/**

src/Constraint/SequentialArray.php

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,19 @@
3939
* keys starting from zero. This is especially true for native sequential
4040
* arrays like `[ "foo", "bar" ]`. Empty arrays are considered valid, too.
4141
* Traversable objects must have sequential keys to be considered valid.
42+
* Requiring sequential keys can be disabled, causing this constraint to just
43+
* check for the required number of items an whether all of its items are
44+
* considered valid.
4245
*
4346
* This constraint will fully traverse any Traversable object given. This also
4447
* means that any Generator will be fully exhausted. If possible, it will try
4548
* to restore an Iterator's pointer to its previous state.
4649
*
4750
* The expected minimum and/or maximum number of items, as well as the
48-
* constraint to apply all items to, are passed in the constructor. The
49-
* constraint can either be an arbitrary `Constraint` instance (e.g.
50-
* `PHPUnit\Framework\Constraint\StringContains`), or any static value,
51-
* requiring an exact match of the value.
51+
* constraint to apply all items to and the option to disable key checking, are
52+
* passed in the constructor. The constraint can either be an arbitrary
53+
* `Constraint` instance (e.g. `PHPUnit\Framework\Constraint\StringContains`),
54+
* or any static value, requiring an exact match of the value.
5255
*/
5356
class SequentialArray extends Constraint
5457
{
@@ -61,16 +64,20 @@ class SequentialArray extends Constraint
6164
/** @var Constraint|null */
6265
protected $constraint;
6366

67+
/** @var bool */
68+
protected $ignoreKeys;
69+
6470
/**
6571
* SequentialArray constructor.
6672
*
6773
* @param int $minItems required minimum number of items, defaults to 0
6874
* @param int|null $maxItems required maximum number of items, defaults to NULL (infinite)
6975
* @param Constraint|mixed|null $constraint optional constraint to apply all items to (defaults to NULL)
76+
* @param bool $ignoreKeys whether to ignore non-sequential keys (defaults to FALSE)
7077
*
7178
* @throws InvalidArgumentException
7279
*/
73-
public function __construct(int $minItems = 0, int $maxItems = null, $constraint = null)
80+
public function __construct(int $minItems = 0, int $maxItems = null, $constraint = null, bool $ignoreKeys = false)
7481
{
7582
if ($minItems < 0) {
7683
throw InvalidArgumentException::create(1, 'non-negative integer');
@@ -86,6 +93,7 @@ public function __construct(int $minItems = 0, int $maxItems = null, $constraint
8693

8794
$this->minItems = $minItems;
8895
$this->maxItems = $maxItems;
96+
$this->ignoreKeys = $ignoreKeys;
8997

9098
if ($constraint !== null) {
9199
$this->constraint = !($constraint instanceof Constraint) ? new IsEqual($constraint) : $constraint;
@@ -103,11 +111,12 @@ public function toString(): string
103111
return 'is an empty array';
104112
}
105113

114+
$description = !$this->ignoreKeys ? 'sequential array' : 'list array';
106115
if (($this->minItems <= 1) && ($this->maxItems === null)) {
107-
$text = 'is a' . (($this->minItems > 0) ? ' non-empty' : '') . ' sequential array';
116+
$text = 'is a' . (($this->minItems > 0) ? ' non-empty' : '') . ' ' . $description;
108117
$text .= ($this->constraint !== null) ? ' whose items match' : '';
109118
} else {
110-
$text = 'is a sequential array';
119+
$text = 'is a ' . $description;
111120
if ($this->minItems && $this->maxItems) {
112121
if ($this->minItems === $this->maxItems) {
113122
$text .= ' with exactly ' . $this->minItems . ' ' . (($this->minItems > 1) ? 'items' : 'item');
@@ -207,7 +216,11 @@ protected function inspectData($other): array
207216
{
208217
if (is_array($other)) {
209218
$itemCount = count($other);
210-
$valid = (($itemCount === 0) || (isset($other[0]) && ($other === array_values($other))));
219+
220+
$valid = true;
221+
if (($itemCount > 0) && !$this->ignoreKeys) {
222+
$valid = (isset($other[0]) && ($other === array_values($other)));
223+
}
211224

212225
$itemsValid = true;
213226
if ($valid && ($this->constraint !== null)) {
@@ -250,7 +263,7 @@ protected function inspectData($other): array
250263
$itemCount = 0;
251264
$itemsValid = true;
252265
foreach ($other as $key => $item) {
253-
if ($key !== $itemCount++) {
266+
if (($key !== $itemCount++) && !$this->ignoreKeys) {
254267
$valid = false;
255268
}
256269

tests/Unit/ArrayAssertsTraitTest.php

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -260,26 +260,21 @@ public function dataProviderArrayHasKeyWithFail(): array
260260
/**
261261
* @dataProvider dataProviderSequentialArray
262262
*
263-
* @param int $minItems
264-
* @param int|null $maxItems
265-
* @param Constraint|null $constraint
263+
* @param int $minItems
264+
* @param int|null $maxItems
265+
* @param Constraint|null $constraint
266+
* @param bool $ignoreKeys
266267
*/
267268
public function testSequentialArray(
268269
int $minItems,
269270
?int $maxItems,
270-
?Constraint $constraint
271+
?Constraint $constraint,
272+
bool $ignoreKeys
271273
): void {
272-
$this->mockConstraintInstance(
273-
SequentialArray::class,
274-
[ $minItems, $maxItems, $constraint ]
275-
);
274+
$constraintArgs = [ $minItems, $maxItems, $constraint, $ignoreKeys ];
275+
$this->mockConstraintInstance(SequentialArray::class, $constraintArgs);
276276

277-
$callableProxy = new CachedCallableProxy(
278-
[ Assert::class, 'sequentialArray' ],
279-
$minItems,
280-
$maxItems,
281-
$constraint
282-
);
277+
$callableProxy = new CachedCallableProxy([ Assert::class, 'sequentialArray' ], ...$constraintArgs);
283278

284279
$this->assertCallableThrowsNot($callableProxy, InvalidArgumentException::class);
285280
$this->assertInstanceOf(SequentialArray::class, $callableProxy->getReturnValue());
@@ -291,22 +286,21 @@ public function testSequentialArray(
291286
* @param int $minItems
292287
* @param int|null $maxItems
293288
* @param Constraint|null $constraint
289+
* @param bool $ignoreKeys
294290
* @param array|Traversable $array
295291
*/
296292
public function testAssertSequentialArray(
297293
int $minItems,
298294
?int $maxItems,
299295
?Constraint $constraint,
296+
bool $ignoreKeys,
300297
$array
301298
): void {
302-
$this->mockConstraintInstance(
303-
SequentialArray::class,
304-
[ $minItems, $maxItems, $constraint ],
305-
[ $array, '' ]
306-
);
299+
$constraintArgs = [ $minItems, $maxItems, $constraint, $ignoreKeys ];
300+
$this->mockConstraintInstance(SequentialArray::class, $constraintArgs, [ $array, '' ]);
307301

308302
$this->assertCallableThrowsNot(
309-
$this->callableProxy([ Assert::class, 'assertSequentialArray' ], $array, $minItems, $maxItems, $constraint),
303+
$this->callableProxy([ Assert::class, 'assertSequentialArray' ], $array, ...$constraintArgs),
310304
InvalidArgumentException::class
311305
);
312306
}
@@ -325,6 +319,7 @@ public function dataProviderSequentialArray(): array
325319
* @param int $minItems
326320
* @param int|null $maxItems
327321
* @param Constraint|null $constraint
322+
* @param bool $ignoreKeys
328323
* @param array|Traversable $array
329324
* @param string $expectedException
330325
* @param string $expectedExceptionMessage
@@ -333,18 +328,16 @@ public function testAssertSequentialArrayFail(
333328
int $minItems,
334329
?int $maxItems,
335330
?Constraint $constraint,
331+
bool $ignoreKeys,
336332
$array,
337333
string $expectedException,
338334
string $expectedExceptionMessage
339335
): void {
340-
$this->mockConstraintInstance(
341-
SequentialArray::class,
342-
[ $minItems, $maxItems, $constraint ],
343-
[ $array, '' ]
344-
);
336+
$constraintArgs = [ $minItems, $maxItems, $constraint, $ignoreKeys ];
337+
$this->mockConstraintInstance(SequentialArray::class, $constraintArgs, [ $array, '' ]);
345338

346339
$this->assertCallableThrows(
347-
$this->callableProxy([ Assert::class, 'assertSequentialArray' ], $array, $minItems, $maxItems, $constraint),
340+
$this->callableProxy([ Assert::class, 'assertSequentialArray' ], $array, ...$constraintArgs),
348341
$expectedException,
349342
$expectedExceptionMessage
350343
);

0 commit comments

Comments
 (0)