Skip to content

Commit 31b6653

Browse files
Introduce StrictArrayFilter, which allows for declarations like:
* array<int, string> * array<string, callable>
1 parent 453140f commit 31b6653

File tree

3 files changed

+243
-0
lines changed

3 files changed

+243
-0
lines changed

src/Filter/StrictArrayFilter.php

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
declare(strict_types=1);
3+
namespace ParagonIE\Ionizer\Filter;
4+
5+
use ParagonIE\Ionizer\Contract\FilterInterface;
6+
use ParagonIE\Ionizer\Util;
7+
8+
/**
9+
* Class StrictArrayFilter
10+
* @package ParagonIE\Ionizer\Filter
11+
*/
12+
class StrictArrayFilter extends ArrayFilter implements FilterInterface
13+
{
14+
/** @var string */
15+
protected $keyType;
16+
17+
/** @var string */
18+
protected $valueType;
19+
20+
public function __construct(string $keyType, string $valueType)
21+
{
22+
if (!\in_array($keyType, ['int', 'string'], true)) {
23+
throw new \RuntimeException('Cannot accept key types other than "int" or "string".');
24+
}
25+
$this->keyType = $keyType;
26+
$this->valueType = $valueType;
27+
}
28+
29+
/**
30+
* Apply all of the callbacks for this filter.
31+
*
32+
* @param array|null $data
33+
* @param int $offset
34+
* @return mixed
35+
* @throws \TypeError
36+
* @psalm-suppress MixedArrayOffset
37+
* @psalm-suppress RedundantCondition
38+
*/
39+
public function applyCallbacks($data = null, int $offset = 0)
40+
{
41+
if ($offset === 0) {
42+
if (\is_null($data)) {
43+
return parent::applyCallbacks([], 0);
44+
} elseif (!\is_array($data)) {
45+
throw new \TypeError(
46+
\sprintf('Expected an array (%s).', $this->index)
47+
);
48+
}
49+
/** @var array<mixed, mixed> $data */
50+
foreach ($data as $key => $value) {
51+
$keyType = Util::getType($key);
52+
$valType = Util::getType($value);
53+
if ($keyType !== $this->keyType || $valType !== $this->valueType) {
54+
throw new \TypeError(
55+
\sprintf(
56+
'Expected an array<%s, %s>. At least one element of <%s, %s> was found (%s[%s] == %s).',
57+
$this->keyType,
58+
$this->valueType,
59+
$keyType,
60+
$valType,
61+
$this->index,
62+
\json_encode($key),
63+
\json_encode($value)
64+
)
65+
);
66+
}
67+
}
68+
69+
return parent::applyCallbacks($data, 0);
70+
}
71+
return parent::applyCallbacks($data, $offset);
72+
}
73+
}

src/Util.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,40 @@ public static function chunk(string $str, string $token = '/'): array
2323
);
2424
}
2525

26+
/**
27+
* @param mixed $input
28+
* @return string
29+
* @throws \TypeError
30+
*/
31+
public static function getType($input): string
32+
{
33+
if (\is_null($input)) {
34+
return 'null';
35+
}
36+
if (\is_callable($input)) {
37+
return 'callable';
38+
}
39+
if (\is_resource($input)) {
40+
return 'resource';
41+
}
42+
if (\is_object($input)) {
43+
return \get_class($input);
44+
}
45+
if (\is_string($input)) {
46+
return 'string';
47+
}
48+
$type = \gettype($input);
49+
switch ($type) {
50+
case 'boolean':
51+
return 'bool';
52+
case 'double':
53+
return 'float';
54+
case 'integer':
55+
return 'int';
56+
}
57+
throw new \TypeError('Unknown type');
58+
}
59+
2660
/**
2761
* Returns true if every member of an array is NOT another array
2862
*

tests/FilterTest.php

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
IntFilter,
99
IntArrayFilter,
1010
StringFilter,
11+
StrictArrayFilter,
1112
StringArrayFilter,
1213
WhiteList
1314
};
@@ -348,6 +349,141 @@ public function testStringFilter()
348349
}
349350
}
350351

352+
/**
353+
* @throws Error
354+
* @throws Exception
355+
*/
356+
public function testStrictArrayFilter()
357+
{
358+
try {
359+
(new GeneralFilterContainer())
360+
->addFilter('test', new StrictArrayFilter('float', 'string'));
361+
} catch (\RuntimeException $ex) {
362+
$this->assertSame(
363+
'Cannot accept key types other than "int" or "string".',
364+
$ex->getMessage()
365+
);
366+
}
367+
368+
$filter = (new GeneralFilterContainer())
369+
->addFilter('test', new StrictArrayFilter('int', 'string'));
370+
371+
if (!($filter instanceof GeneralFilterContainer)) {
372+
$this->fail('Type error');
373+
}
374+
$filter([
375+
'test' => [
376+
'abc',
377+
'def'
378+
]
379+
]);
380+
381+
try {
382+
$filter([
383+
'test' => [
384+
1,
385+
'abc',
386+
'def'
387+
]
388+
]);
389+
$this->fail('Uncaught value mismatch');
390+
} catch (\TypeError $ex) {
391+
$this->assertSame(
392+
'Expected an array<int, string>. At least one element of <int, int> was found (test[0] == 1).',
393+
$ex->getMessage()
394+
);
395+
}
396+
$filter([
397+
'test' => [
398+
1 => 'abc',
399+
2 => 'def'
400+
]
401+
]);
402+
try {
403+
$filter([
404+
'test' => [
405+
1 => 'abc',
406+
'1a' => 'def'
407+
]
408+
]);
409+
$this->fail('Uncaught value mismatch');
410+
} catch (\TypeError $ex) {
411+
$this->assertSame(
412+
'Expected an array<int, string>. At least one element of <string, string> was found (test["1a"] == "def").',
413+
$ex->getMessage()
414+
);
415+
}
416+
417+
$filter->addFilter('second', new StrictArrayFilter('string', \stdClass::class));
418+
$filter([
419+
'test' => ['abc', 'def'],
420+
'second' => [
421+
'test' => (object)['test']
422+
]
423+
]);
424+
$filter([
425+
'test' => ['abc', 'def'],
426+
'second' => [
427+
'1234a' => (object)['test']
428+
]
429+
]);
430+
try {
431+
$filter([
432+
'test' => ['abc', 'def'],
433+
'second' => [
434+
'test' => (object)['test'],
435+
123 => (object)['test2'],
436+
]
437+
]);
438+
$this->fail('Invalid key accepted');
439+
} catch (\TypeError $ex) {
440+
$this->assertSame(
441+
'Expected an array<string, stdClass>. At least one element of <int, stdClass> was found (second[123] == {"0":"test2"}).',
442+
$ex->getMessage()
443+
);
444+
}
445+
try {
446+
$filter([
447+
'test' => ['abc', 'def'],
448+
'second' => [
449+
'test' => (object)['test'],
450+
'123' => null,
451+
]
452+
]);
453+
$this->fail('Null accepted where it was not wanted');
454+
} catch (\TypeError $ex) {
455+
$this->assertSame(
456+
'Expected an array<string, stdClass>. At least one element of <int, null> was found (second[123] == null).',
457+
$ex->getMessage()
458+
);
459+
}
460+
461+
$cf = (new GeneralFilterContainer());
462+
$cf->addFilter('test', new StrictArrayFilter('string', 'callable'));
463+
$cf([
464+
'test' => [
465+
'a' => function() { return 'foo'; },
466+
'b' => '\\strlen',
467+
'c' => [StringFilter::class, 'nonEmpty']
468+
],
469+
]);
470+
471+
$fuzz = \bin2hex(\random_bytes(33));
472+
try {
473+
$cf([
474+
'test' => [
475+
'a' => function() { return 'foo'; },
476+
'b' => $fuzz
477+
],
478+
]);
479+
$this->fail('Invalid function name was declared');
480+
} catch (\TypeError $ex) {
481+
$this->assertSame(
482+
'Expected an array<string, callable>. At least one element of <string, string> was found (test["b"] == "' . $fuzz . '").',
483+
$ex->getMessage()
484+
);
485+
}
486+
}
351487

352488
/**
353489
* @covers StringArrayFilter

0 commit comments

Comments
 (0)