Skip to content
This repository was archived by the owner on Dec 19, 2022. It is now read-only.

Commit 823dec4

Browse files
authored
Merge pull request #1 from safe-k/feature/implicit-struct
Support implicit struct array validation
2 parents e10fc0d + bb60a28 commit 823dec4

10 files changed

+117
-43
lines changed

README.md

+36-7
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ $directory = [
6161
try {
6262
validate($directory, struct('Directory', [
6363
'path' => 'is_dir',
64-
'file' => optional('is_file', __DIR__ . '/directory-validation.php'),
64+
'file' => optional('is_file', __DIR__ . '/README.md'),
6565
'content' => arrayOf(struct('Paragraph', [
6666
'header' => 'is_string',
6767
'line' => not('is_null'),
@@ -75,11 +75,40 @@ try {
7575
echo "Path: {$directory['path']}" . PHP_EOL;
7676
echo "File: {$directory['file']}" . PHP_EOL;
7777
// Prints:
78-
// Path: /Users/seifkamal/src/struct-array/examples
79-
// File: /Users/seifkamal/src/struct-array/examples/directory-validation.php
78+
// Path: /Users/seifkamal/src/struct-array
79+
// File: /Users/seifkamal/src/struct-array/README.md
8080
```
8181

82-
Here's the same one using static class methods:
82+
You can also just use an array directly, without creating a `Struct`:
83+
84+
```php
85+
<?php
86+
87+
use function SK\StructArray\{
88+
arrayOf, optional, not, validate
89+
};
90+
91+
$directory = [...];
92+
93+
validate($directory, [
94+
'path' => 'is_dir',
95+
'file' => optional('is_file', __DIR__ . '/README.md'),
96+
'content' => arrayOf([
97+
'header' => 'is_string',
98+
'line' => not('is_null'),
99+
]),
100+
]);
101+
```
102+
103+
This is tailored for quick usage, and therefore assumes the defined interface is non-exhaustive
104+
(ie. the array submitted for validation is allowed to have keys that aren't defined here). It also
105+
means error messages will be more generic, ie you'll see:
106+
> Struct failed validation. ...
107+
108+
instead of:
109+
> Directory failed validation. ...
110+
111+
Here's another example directly using the static class methods:
83112

84113
```php
85114
<?php
@@ -105,7 +134,7 @@ $paragraphStruct = Struct::of('Paragraph', [
105134
]);
106135
$directoryStruct = Struct::of('Directory', [
107136
'path' => 'is_dir',
108-
'file' => Type::optional('is_file', __DIR__ . '/directory-validation.php'),
137+
'file' => Type::optional('is_file', __DIR__ . '/README.md'),
109138
'content' => Type::arrayOf($paragraphStruct),
110139
]);
111140

@@ -119,8 +148,8 @@ try {
119148
echo "Path: {$directory['path']}" . PHP_EOL;
120149
echo "File: {$directory['file']}" . PHP_EOL;
121150
// Prints:
122-
// Path: /Users/seifkamal/src/struct-array/examples
123-
// File: /Users/seifkamal/src/struct-array/examples/directory-validation.php
151+
// Path: /Users/seifkamal/src/struct-array
152+
// File: /Users/seifkamal/src/struct-array/README.md
124153
```
125154

126155
For more, see the [examples directory](examples).

docs/use-case.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ class StatsController {
214214
- Easy to read
215215
- Easy to scale
216216
- Automatic, customisable error messaging; Here's an example:
217-
> Struct 'Event' failed validation: Invalid value for property 'date'
217+
> Event failed validation. Invalid value for property: 'date'
218218
- All possible parameters and their validation rules are documented in code, in the method itself
219219
- Extensible - `Struct`s are essentially arrays of `callable`s (and other `Struct`s), so they can
220220
easily be worked into systems and be extended accordingly

src/Exception/InvalidValueException.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ class InvalidValueException extends \Exception
66
{
77
public function __construct(string $property)
88
{
9-
parent::__construct("Invalid value for property '{$property}'");
9+
parent::__construct("Invalid value for property: '{$property}'");
1010
}
1111
}

src/Exception/MissingPropertyException.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ class MissingPropertyException extends \Exception
66
{
77
public function __construct(string $property)
88
{
9-
parent::__construct("missing value for '{$property}'");
9+
parent::__construct("Missing value for property: '{$property}'");
1010
}
1111
}

src/Exception/StructValidationException.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class StructValidationException extends \Exception
99
public function __construct(Struct $struct, \Throwable $reason)
1010
{
1111
parent::__construct(
12-
"Struct '{$struct->name()}' failed validation: {$reason->getMessage()}",
12+
"{$struct->name()} failed validation. {$reason->getMessage()}",
1313
0,
1414
$reason
1515
);

src/Exception/UnexpectedPropertyException.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class UnexpectedPropertyException extends \Exception
77
public function __construct(string ...$properties)
88
{
99
parent::__construct(sprintf(
10-
"Unexpected %s '%s'",
10+
"Unexpected %s: '%s'",
1111
count($properties) === 1 ? 'property' : 'properties',
1212
implode(', ', $properties)
1313
));

src/Struct.php

+15-4
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ class Struct
66
{
77
/** @var string */
88
private $name;
9-
/** @var callable|Struct[] */
9+
/** @var callable[]|Struct[] */
1010
private $interface;
1111
/** @var bool */
1212
private $exhaustive;
1313

1414
/**
1515
* Struct constructor.
1616
*
17-
* @param string $name
18-
* @param callable|Struct[] $interface
17+
* @param string $name Used in error messaging, useful when nesting or validating multiple Structs.
18+
* @param callable[]|Struct[] $interface A list of expected keys and their associated validator.
1919
* @param bool $exhaustive Used to specify whether the declared Struct properties are exhaustive,
2020
* meaning data arrays submitted for validation must not contain unknown keys. This defaults
2121
* to `true`; Set to `false` if you only want to validate some of the array elements.
@@ -33,13 +33,24 @@ public static function of(
3333
return $struct;
3434
}
3535

36+
/**
37+
* @internal
38+
*
39+
* @param array $interface
40+
* @return static
41+
*/
42+
public static function default(array $interface): self
43+
{
44+
return static::of('Struct', $interface, false);
45+
}
46+
3647
public function name(): string
3748
{
3849
return $this->name;
3950
}
4051

4152
/**
42-
* @return callable|Struct[]
53+
* @return callable[]|Struct[]
4354
*/
4455
public function interface(): array
4556
{

src/Validator.php

+22-10
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@ class Validator
88
{
99
/**
1010
* @param array $data
11-
* @param Struct $struct
11+
* @param array|Struct $struct If an array is supplied, a generic, non-exhaustive Struct is used.
1212
* @return bool
1313
* @throws Exception\StructValidationException
1414
*/
15-
public static function validate(array &$data, Struct $struct): bool
15+
public static function validate(array &$data, $struct): bool
1616
{
1717
try {
18+
if (is_array($struct)) {
19+
$struct = Struct::default($struct);
20+
}
21+
1822
$unexpectedProperties = array_diff_key($data, $struct->interface());
1923
if ($struct->isExhaustive() && !empty($unexpectedProperties)) {
2024
throw new Exception\UnexpectedPropertyException(...array_keys($unexpectedProperties));
@@ -24,26 +28,34 @@ public static function validate(array &$data, Struct $struct): bool
2428
$value = array_key_exists($field, $data)
2529
? $data[$field]
2630
: Missing::property($field);
27-
$valueIsMissing = is_a($value, Missing::class);
31+
$propertyIsMissing = is_a($value, Missing::class);
2832

2933
if (is_callable($validator)) {
3034
if (!$validator($value)) {
3135
throw new Exception\InvalidValueException($field);
3236
}
3337
// If value has changed, set corresponding data field
34-
if ($valueIsMissing && !is_a($value, Missing::class)) {
38+
if ($propertyIsMissing && !is_a($value, Missing::class)) {
3539
$data[$field] = $value;
3640
}
37-
} elseif (is_a($validator, Struct::class)) {
38-
if ($valueIsMissing) {
39-
throw new Exception\MissingPropertyException($field);
40-
}
41+
return true;
42+
}
43+
44+
if ($propertyIsMissing) {
45+
throw new Exception\MissingPropertyException($field);
46+
}
47+
48+
if (is_array($validator)) {
49+
$validator = Struct::default($validator);
50+
}
51+
if (is_a($validator, Struct::class)) {
4152
if (!validate($value, $validator)) {
4253
throw new Exception\InvalidValueException($field);
4354
}
44-
} else {
45-
throw new Exception\InvalidValidatorException($validator);
55+
return true;
4656
}
57+
58+
throw new Exception\InvalidValidatorException($validator);
4759
}
4860
} catch (\Throwable $t) {
4961
/**

src/functions.php

+7-5
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,11 @@ function optional(callable $validator, $default = Missing::class): callable
7575
/**
7676
* @see Struct::of()
7777
*
78-
* @param string $name
79-
* @param callable|Struct[] $interface
80-
* @param bool $exhaustive
78+
* @param string $name Used in error messaging, useful when nesting or validating multiple Structs.
79+
* @param callable[]|Struct[] $interface A list of expected keys and their associated validator.
80+
* @param bool $exhaustive Used to specify whether the declared Struct properties are exhaustive,
81+
* meaning data arrays submitted for validation must not contain unknown keys. This defaults
82+
* to `true`; Set to `false` if you only want to validate some of the array elements.
8183
* @return Struct
8284
*/
8385
function struct(string $name, array $interface, bool $exhaustive = true): Struct
@@ -89,11 +91,11 @@ function struct(string $name, array $interface, bool $exhaustive = true): Struct
8991
* @see Validator::validate()
9092
*
9193
* @param array $data
92-
* @param Struct $struct
94+
* @param array|Struct $struct If an array is supplied, a generic, non-exhaustive Struct is used.
9395
* @return bool
9496
* @throws Exception\StructValidationException
9597
*/
96-
function validate(array &$data, Struct $struct): bool
98+
function validate(array &$data, $struct): bool
9799
{
98100
return Validator::validate($data, $struct);
99101
}

tests/Unit/StructTest.php

+32-12
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace SK\StructArray\Test\Unit;
44

55
use PHPUnit\Framework\TestCase;
6-
use SK\StructArray\Exception;
76
use SK\StructArray\Exception\StructValidationException;
87
use SK\StructArray\Property\Type;
98
use SK\StructArray\Struct;
@@ -15,42 +14,64 @@ class StructTest extends TestCase
1514
/**
1615
* @dataProvider dataProviderForTest
1716
*/
18-
public function test($data, $interface, $exhaustive, $expectedException)
17+
public function test($data, $struct, $expectedException)
1918
{
2019
if ($expectedException) {
2120
$this->expectException(StructValidationException::class);
2221
}
2322

24-
$this->assertTrue(validate($data, Struct::of('Test', $interface, $exhaustive)));
23+
$this->assertTrue(validate($data, $struct));
2524
}
2625

2726
public function dataProviderForTest(): array
2827
{
28+
$name = 'Test';
2929
return [
3030
[
3131
['name' => 'toasty',],
32-
['name' => 'invalid validator'],
33-
true,
32+
Struct::of($name, ['name' => 'invalid validator'], true),
3433
true,
3534
],
3635
[
3736
['name' => 10],
38-
['name' => 'is_string'],
39-
true,
37+
Struct::of($name, ['name' => 'is_string'], true),
4038
true,
4139
],
4240
[
4341
[],
44-
['name' => 'is_string'],
45-
true,
42+
Struct::of($name, ['name' => 'is_string'], true),
4643
true,
4744
],
4845
[
4946
['name' => 'toasty', 'age' => 10],
50-
['name' => 'is_string'],
51-
true,
47+
Struct::of($name, ['name' => 'is_string'], true),
5248
true,
5349
],
50+
[
51+
[
52+
'id' => '123',
53+
'type' => 'theatre',
54+
'date' => new \DateTime(),
55+
'price' => [
56+
'value' => 20.5,
57+
'currency' => 'GBP',
58+
],
59+
'tickets' => ['General', 10],
60+
'onSale' => true,
61+
'artist' => 'some guy',
62+
],
63+
Struct::of($name, [
64+
'id' => Type::allOf('is_string', 'is_numeric'),
65+
'type' => 'is_string',
66+
'date' => Type::anyOf(Type::classOf(\DateTime::class), 'is_null'),
67+
'price' => Struct::of('Price', [
68+
'value' => 'is_float',
69+
'currency' => 'is_string'
70+
]),
71+
'tickets' => Type::arrayOf(Type::not('is_null')),
72+
], false),
73+
null
74+
],
5475
[
5576
[
5677
'id' => '123',
@@ -74,7 +95,6 @@ public function dataProviderForTest(): array
7495
]),
7596
'tickets' => Type::arrayOf(Type::not('is_null')),
7697
],
77-
false,
7898
null
7999
],
80100
];

0 commit comments

Comments
 (0)