mirror of
https://codeberg.org/reflect/reflect-rules-plugin.git
synced 2025-09-14 17:03:42 +02:00
Compare commits
No commits in common. "master" and "1.0.4" have entirely different histories.
5 changed files with 146 additions and 337 deletions
127
README.md
127
README.md
|
@ -1,15 +1,13 @@
|
|||
> [!IMPORTANT]
|
||||
> This plugin has since [Reflect version 3.8.5](https://codeberg.org/reflect/reflect/releases/tag/2.8.5) been deprecated. Reflect now has built-in request validation which is enabled by default. The built-in validator is based on this plugin.
|
||||
# Request validation plugin for the [Reflect API Framework](https://github.com/victorwesterlund/reflect)
|
||||
This request pre-processor adds request validation to an API written for the Reflect API Framework. Enforce request constraints against set rules and optionally return errors back with a `Reflect\Response` before your endpoint's code even starts running.
|
||||
|
||||
# Request validation plugin for the [Reflect API Framework](https://codeberg.org/reflect/reflect)
|
||||
This request pre-processor adds request validation for an API written in the Reflect API Framework.
|
||||
Write Reflect endpoints safer by assuming data is what you expect it to be before it reaches your endpoint's logic. This plugin will validate GET and POST parameters against user-defined constraints before letting a request through to a `Reflect\Endpoint`.
|
||||
A `Reflect\Response` will be generated and handled by this plugin if request data doesn't meet the defiend constraints.
|
||||
|
||||
Write safer Reflect endpoints by enforcing request data structure validation before the request reaches your endpoint's logic. This plugin validates GET and POST data (even JSON) and returns an array with scoped `Error`s that can be further acted upon if desired.
|
||||
|
||||
## Example
|
||||
*Example:*
|
||||
```
|
||||
POST Request: /my-endpoint?key1=lorem-ipsum&key2=dolor
|
||||
POST Body: {"key3":15, "key4":["hello", "world"]}
|
||||
Request: /my-endpoint?key1=lorem-ipsum&key2=dolor
|
||||
Response: (HTTP 422) {"key2": ["Value must be of type 'STRING']}
|
||||
```
|
||||
```php
|
||||
use \Reflect\Endpoint;
|
||||
|
@ -19,7 +17,7 @@ use \ReflectRules\Type;
|
|||
use \ReflectRules\Rules;
|
||||
use \ReflectRules\Ruleset;
|
||||
|
||||
class POST_MyEndpoint implements Endpoint {
|
||||
class GET_MyEndpoint implements Endpoint {
|
||||
private Ruleset $rules;
|
||||
|
||||
public function __construct() {
|
||||
|
@ -32,20 +30,9 @@ class POST_MyEndpoint implements Endpoint {
|
|||
->min(5)
|
||||
->max(50),
|
||||
(new Rules("key2")
|
||||
->required()
|
||||
->type(Type::NUMBER)
|
||||
->max(255)
|
||||
]);
|
||||
|
||||
$this->rules->POST([
|
||||
(new Rules("key3")
|
||||
->type(Type::ARRAY)
|
||||
->min(3),
|
||||
(new Rules("key4")
|
||||
->required()
|
||||
->type(Type::STRING)
|
||||
->max(255)
|
||||
]);
|
||||
}
|
||||
|
||||
public function main(): Response {
|
||||
|
@ -53,28 +40,6 @@ class POST_MyEndpoint implements Endpoint {
|
|||
}
|
||||
}
|
||||
```
|
||||
```php
|
||||
Ruleset->get_errors();
|
||||
[
|
||||
"GET" => [
|
||||
"key2" => [
|
||||
"INVALID_PROPERTY_TYPE" => ["STRING"]
|
||||
]
|
||||
],
|
||||
"POST" => [
|
||||
"key3" => [
|
||||
"VALUE_MIN_ERROR" => 3
|
||||
],
|
||||
"key4" => [
|
||||
"MISSING_REQUIRED_PROPERTY" => "key4"
|
||||
]
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
Use `Ruleset->is_valid(): bool` to quickly check if any errors are set.
|
||||
|
||||
You can also use `Ruleset->validate_or_exit(): true|Response` to automatically return a `Reflect\Response` with all errors to current STDOUT if validation fails.
|
||||
|
||||
# Installation
|
||||
|
||||
|
@ -105,26 +70,6 @@ public function __construct() {
|
|||
}
|
||||
```
|
||||
|
||||
# Errors
|
||||
|
||||
Error
|
||||
--|
|
||||
[`Error::VALUE_MIN_ERROR`](#min)
|
||||
[`Error::VALUE_MAX_ERROR`](#max)
|
||||
[`Error::UNKNOWN_PROPERTY_NAME`](#strict-mode)
|
||||
[`Error::INVALID_PROPERTY_TYPE`](#type)
|
||||
[`Error::INVALID_PROPERTY_VALUE`](#typeenum)
|
||||
[`Error::MISSING_REQUIRED_PROPERTY`](#required)
|
||||
|
||||
# Strict mode
|
||||
Enable strict mode by initializing a Ruleset with the "strict" argument set to `true`.
|
||||
|
||||
```php
|
||||
new Ruleset(strict: true);
|
||||
```
|
||||
|
||||
Strict mode will not allow undefined properties to be set in all configured scopes. If a property exists in `Scope` that hasn't been defined with a `Rules()` instance, a `Errors::UNKNOWN_PROPERTY_NAME` error will be set.
|
||||
|
||||
# Available rules
|
||||
The following methods can be chained onto a `Rules` instance to enforce certain constraints on a particular property
|
||||
|
||||
|
@ -133,9 +78,7 @@ The following methods can be chained onto a `Rules` instance to enforce certain
|
|||
Rules->required(bool = true);
|
||||
```
|
||||
|
||||
Make a property mandatory by chaining the `required()` method. Omitting this rule will only validate other rules on the property IF the key has been provided in the current scope.
|
||||
|
||||
Will set a `Error::MISSING_REQUIRED_PROPERTY` error on the current scope and property if failed.
|
||||
Make a property mandatory by chaining the `required()` method. Omitting this rule will only validate other rules on the property IF the key has been provided in the current scope
|
||||
|
||||
## `type()`
|
||||
```php
|
||||
|
@ -144,38 +87,15 @@ Rules->type(Type);
|
|||
|
||||
Enforce a data type on the request by chaining the `type()` method and passing it one of the available enum [`Type`](#types)s as its argument.
|
||||
|
||||
> [!TIP]
|
||||
> Allow multiple types (union) by chaining multiple `type()` methods
|
||||
> ```php
|
||||
> // Example
|
||||
> Rules->type(Type::NUMBER)->type(Type::NULL);
|
||||
> ```
|
||||
|
||||
### Types
|
||||
Type|Description
|
||||
--|--
|
||||
`Type::NUMERIC`|Value must be a number or a numeric string
|
||||
`Type::STRING`|Value must be a string
|
||||
`Type::BOOLEAN`|Value must be a boolean ([**considered bool for GET rules**](#boolean-coercion-from-string-for-search-parameters))
|
||||
`Type::ARRAY`|Value must be a JSON array or ([**CSV for GET rules**](#csv-for-search-parameters))
|
||||
`Type::BOOLEAN`|Value must be a boolean or ([**considered bool for GET rules**](#boolean-coercion-from-string-for-search-parameters))
|
||||
`Type::ARRAY`|Value must be a JSON array
|
||||
`Type::OBJECT`|Value must be a JSON object
|
||||
`Type::ENUM`|Value must be exactly one of pre-defined values ([**more information**](#typeenum))
|
||||
`Type::NULL`|Value must be null ([**considered null for GET rules**](#null-coercion-from-string-for-search-parameters))
|
||||
|
||||
Will set a `Error::INVALID_PROPERTY_TYPE` error on the current scope and property if failed, except Type::ENUM that will set a `Error::INVALID_PROPERTY_VALUE` with an array of the valid vaules.
|
||||
|
||||
#### `Type::ENUM`
|
||||
|
||||
Provided value for property must be an exact match of any value provided as an `array` to the second argument of `type(Type::ENUM, <whitelist>)`
|
||||
```php
|
||||
Rules->type(Type::ENUM, [
|
||||
"FOO",
|
||||
"BAR"
|
||||
]);
|
||||
```
|
||||
Any value that isn't `"FOO"` or `"BAR"` will be rejected.
|
||||
|
||||
Will set a `Error::INVALID_PROPERTY_VALUE` error on the current scope and property if failed.
|
||||
`Type::NULL`|Value must be null or ([**considered null for GET rules**](#null-coercion-from-string-for-search-parameters))
|
||||
|
||||
#### Boolean coercion from string for search parameters
|
||||
Search parameters are read as strings, a boolean is therefor coerced from the following rules.
|
||||
|
@ -197,19 +117,6 @@ Any other value will cause the `type()` rule to fail.
|
|||
> [!IMPORTANT]
|
||||
> This coercion is only applies for `Ruleset->GET()`. `Ruleset->POST()` will enforce real `true` and `type` values since it's JSON
|
||||
|
||||
#### CSV array for search parameters
|
||||
A CSV string is expected when `Type::ARRAY` is set for a GET rule.
|
||||
|
||||
*Example:*
|
||||
```
|
||||
https://example.com?typeArray=key1,key2,key3
|
||||
```
|
||||
|
||||
Any other value will cause the `type()` rule to fail.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This coercion is only applies for `Ruleset->GET()`. `Ruleset->POST()` will enforce a JSON array
|
||||
|
||||
#### Null coercion from string for search parameters
|
||||
Search parameters are read as strings, a null value is therefor coerced from an empty string `""`.
|
||||
|
||||
|
@ -218,12 +125,6 @@ Any value that isn't an empty string will cause the `type()` rule to fail.
|
|||
> [!IMPORTANT]
|
||||
> This coercion is only applies for `Ruleset->GET()`. `Ruleset->POST()` will enforce the real `null` value since it's JSON
|
||||
|
||||
## `default()`
|
||||
```php
|
||||
Rules->default(mixed);
|
||||
```
|
||||
Set superglobal property to a defined default value if the property was not provided in superglobal scope
|
||||
|
||||
## `min()`
|
||||
```php
|
||||
Rules->min(?int = null);
|
||||
|
@ -238,8 +139,6 @@ Type|Expects
|
|||
|
||||
**`min()` will not have an effect on [`Type`](#types)s not provided in this list.**
|
||||
|
||||
Will set a `Error::VALUE_MIN_ERROR` error on the current scope and property if failed
|
||||
|
||||
## `max()`
|
||||
```php
|
||||
Rules->max(?int = null);
|
||||
|
@ -253,5 +152,3 @@ Type|Expects
|
|||
`Type::ARRAY`, `Type::OBJECT`|Array size or object key count to be smaller or equal to provided value
|
||||
|
||||
**`max()` will not have an effect on [`Type`](#types)s not provided in this list.**
|
||||
|
||||
Will set a `Error::VALUE_MAX_ERROR` error on the current scope and property if failed
|
||||
|
|
|
@ -14,5 +14,8 @@
|
|||
"psr-4": {
|
||||
"ReflectRules\\": "src/"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"victorwesterlund/xenum": "^1.1"
|
||||
}
|
||||
}
|
||||
|
|
42
composer.lock
generated
42
composer.lock
generated
|
@ -4,8 +4,46 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "7c02351f2b860153c02a8f683d9c540f",
|
||||
"packages": [],
|
||||
"content-hash": "98953f6b9df8b6761e2d57fc66815033",
|
||||
"packages": [
|
||||
{
|
||||
"name": "victorwesterlund/xenum",
|
||||
"version": "1.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/VictorWesterlund/php-xenum.git",
|
||||
"reference": "8972f06f42abd1f382807a67e937d5564bb89699"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/VictorWesterlund/php-xenum/zipball/8972f06f42abd1f382807a67e937d5564bb89699",
|
||||
"reference": "8972f06f42abd1f382807a67e937d5564bb89699",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"victorwesterlund\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"GPL-3.0-only"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Victor Westerlund",
|
||||
"email": "victor.vesterlund@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "PHP eXtended Enums. The missing quality-of-life features from PHP 8+ Enums",
|
||||
"support": {
|
||||
"issues": "https://github.com/VictorWesterlund/php-xenum/issues",
|
||||
"source": "https://github.com/VictorWesterlund/php-xenum/tree/1.1.1"
|
||||
},
|
||||
"time": "2023-11-20T10:10:39+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "dev",
|
||||
|
|
136
src/Rules.php
136
src/Rules.php
|
@ -2,40 +2,27 @@
|
|||
|
||||
namespace ReflectRules;
|
||||
|
||||
use \victorwesterlund\xEnum;
|
||||
|
||||
use \ReflectRules\Scope;
|
||||
|
||||
// Supported types for is_type()
|
||||
enum Type {
|
||||
case NULL;
|
||||
case ENUM;
|
||||
case ARRAY;
|
||||
use xEnum;
|
||||
|
||||
case NUMBER;
|
||||
case STRING;
|
||||
case OBJECT;
|
||||
case BOOLEAN;
|
||||
case ARRAY;
|
||||
case OBJECT;
|
||||
case NULL;
|
||||
}
|
||||
|
||||
class Rules {
|
||||
private const CSV_DELIMITER = ",";
|
||||
|
||||
private string $property;
|
||||
|
||||
/*
|
||||
# Rule properties
|
||||
These properties store rules for an instance of a property
|
||||
*/
|
||||
|
||||
public bool $required = false;
|
||||
|
||||
// Matched Type against $types array
|
||||
private ?Type $type = null;
|
||||
// Typed array of type ReflectRules\Type
|
||||
public ?array $types = null;
|
||||
|
||||
public ?array $enum = null;
|
||||
|
||||
private bool $default_enabled = false;
|
||||
public mixed $default;
|
||||
public ?Type $type = null;
|
||||
|
||||
public ?int $min = null;
|
||||
public ?int $max = null;
|
||||
|
@ -61,13 +48,13 @@
|
|||
}
|
||||
|
||||
// Set the minimum lenth/size for property
|
||||
public function min(?int $value = null): self {
|
||||
public function min(?int $value = null) {
|
||||
$this->min = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Set the maximum length/size for property
|
||||
public function max(?int $value = null): self {
|
||||
public function max(?int $value = null) {
|
||||
$this->max = $value;
|
||||
return $this;
|
||||
}
|
||||
|
@ -78,25 +65,14 @@
|
|||
return $this;
|
||||
}
|
||||
|
||||
// Add Type constraint with optional argument
|
||||
public function type(Type $type, mixed $arg = null): self {
|
||||
if ($type === Type::ENUM) {
|
||||
if (!is_array($arg)) {
|
||||
throw new \Exception("Expected type 'array' of ENUM values as second argument");
|
||||
// Set property Type
|
||||
public function type(Type|string $type): self {
|
||||
// Coerce string to Type enum
|
||||
if (!($type instanceof Type)) {
|
||||
$type = Type::fromName($string);
|
||||
}
|
||||
|
||||
$this->enum = $arg;
|
||||
}
|
||||
|
||||
$this->types[] = $type;
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Set a default value if property is not provided
|
||||
public function default(mixed $value): self {
|
||||
$this->default_enabled = true;
|
||||
$this->default = $value;
|
||||
|
||||
$this->type = $type;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -135,45 +111,6 @@
|
|||
return is_bool($value);
|
||||
}
|
||||
|
||||
private function eval_type_enum(mixed $value): bool {
|
||||
// Return true if value isn't boolean and exists in enum array
|
||||
return !is_bool($value) && in_array($value, $this->enum);
|
||||
}
|
||||
|
||||
private function eval_object(mixed $value, Scope $scope): bool {
|
||||
// Arrays in POST parameters should already be decoded
|
||||
if ($scope === Scope::POST) {
|
||||
return is_array($value);
|
||||
}
|
||||
|
||||
// Decode stringified JSON
|
||||
$json = json_decode($value);
|
||||
|
||||
// Failed to decode JSON
|
||||
if ($json === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mutate property on superglobal with decoded JSON
|
||||
$GLOBALS[Scope::GET->value][$this->property] = $json;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function eval_array(string|array $value, Scope $scope): bool {
|
||||
// Arrays in POST parameters should already be decoded
|
||||
if ($scope === Scope::POST) {
|
||||
return is_array($value);
|
||||
}
|
||||
|
||||
// Mutate property on superglobal with decoded CSV if not already an array
|
||||
if (!is_array($_GET[$this->property])) {
|
||||
$GLOBALS[Scope::GET->value][$this->property] = explode(self::CSV_DELIMITER, $_GET[$this->property]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
## Public eval methods
|
||||
These are the entry-point eval methods that in turn can call other
|
||||
|
@ -187,38 +124,21 @@
|
|||
return true;
|
||||
}
|
||||
|
||||
// Property does not exist in superglobal, create one with default value if enabled
|
||||
if ($this->default_enabled) {
|
||||
$scope_data[$this->property] = $this->default;
|
||||
}
|
||||
|
||||
// Property does not exist in scope, create nulled superglobal for it
|
||||
$scope_data[$this->property] = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public function eval_type(mixed $value, Scope $scope): bool {
|
||||
$match = false;
|
||||
|
||||
foreach ($this->types as $type) {
|
||||
match($type) {
|
||||
Type::NUMBER => $match = is_numeric($value),
|
||||
Type::STRING => $match = is_string($value),
|
||||
Type::BOOLEAN => $match = $this->eval_type_boolean($value, $scope),
|
||||
Type::ARRAY => $match = $this->eval_array($value, $scope),
|
||||
Type::OBJECT => $match = $this->eval_object($value, $scope),
|
||||
Type::ENUM => $match = $this->eval_type_enum($value),
|
||||
Type::NULL => $match = is_null($value)
|
||||
return match($this->type) {
|
||||
Type::NUMBER => is_numeric($value),
|
||||
Type::STRING => is_string($value),
|
||||
Type::BOOLEAN => $this->eval_type_boolean($value, $scope),
|
||||
Type::ARRAY,
|
||||
Type::OBJECT => is_array($value),
|
||||
Type::NULL => is_null($value),
|
||||
default => true
|
||||
};
|
||||
|
||||
// Found a matching type
|
||||
if ($match) {
|
||||
// Set the matched Type for use in other rules
|
||||
$this->type = $type;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// No matching types were found
|
||||
return false;
|
||||
}
|
||||
|
||||
public function eval_min(mixed $value, Scope $scope): bool {
|
||||
|
@ -226,7 +146,7 @@
|
|||
Type::NUMBER => $this->eval_type($value, $scope) && $value >= $this->min,
|
||||
Type::STRING => $this->eval_type($value, $scope) && strlen($value) >= $this->min,
|
||||
Type::ARRAY,
|
||||
Type::OBJECT => $this->eval_type($value, $scope) && count($GLOBALS[$scope->value][$this->property]) >= $this->min,
|
||||
Type::OBJECT => $this->eval_type($value, $scope) && count($value) >= $this->min,
|
||||
default => true
|
||||
};
|
||||
}
|
||||
|
@ -236,7 +156,7 @@
|
|||
Type::NUMBER => $this->eval_type($value, $scope) && $value <= $this->max,
|
||||
Type::STRING => $this->eval_type($value, $scope) && strlen($value) <= $this->max,
|
||||
Type::ARRAY,
|
||||
Type::OBJECT => $this->eval_type($value, $scope) && count($GLOBALS[$scope->value][$this->property]) <= $this->max,
|
||||
Type::OBJECT => $this->eval_type($value, $scope) && count($value) <= $this->max,
|
||||
default => true
|
||||
};
|
||||
}
|
||||
|
|
173
src/Ruleset.php
173
src/Ruleset.php
|
@ -2,9 +2,12 @@
|
|||
|
||||
namespace ReflectRules;
|
||||
|
||||
// Use the Response class from Reflect to override endpoint processing if requested
|
||||
use \Reflect\Response;
|
||||
|
||||
use \ReflectRules\Rules;
|
||||
|
||||
use \Reflect\Response;
|
||||
require_once "../vendor/autoload.php";
|
||||
|
||||
require_once "Rules.php";
|
||||
|
||||
|
@ -12,66 +15,46 @@
|
|||
enum Scope: string {
|
||||
case GET = "_GET";
|
||||
case POST = "_POST";
|
||||
|
||||
static function get_array(): array {
|
||||
return [
|
||||
Scope::GET->name => [],
|
||||
Scope::POST->name => []
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
enum Error {
|
||||
case VALUE_MIN_ERROR;
|
||||
case VALUE_MAX_ERROR;
|
||||
case UNKNOWN_PROPERTY_NAME;
|
||||
case INVALID_PROPERTY_TYPE;
|
||||
case INVALID_PROPERTY_VALUE;
|
||||
case MISSING_REQUIRED_PROPERTY;
|
||||
}
|
||||
|
||||
class Ruleset {
|
||||
private bool $is_valid = true;
|
||||
private ?bool $strict;
|
||||
private array $errors;
|
||||
// This plugin will return exit with a Reflect\Response if errors are found
|
||||
private bool $exit_on_errors;
|
||||
|
||||
private array $rules_names;
|
||||
// Array of RuleError instances
|
||||
private array $errors = [];
|
||||
|
||||
public function __construct(bool $strict = false) {
|
||||
/*
|
||||
Strict mode can only be enabled or disabled as a bool argument.
|
||||
'null' is used internally on this property as a re-evaluate flag.
|
||||
*/
|
||||
$this->strict = $strict;
|
||||
|
||||
$this->errors = Scope::get_array();
|
||||
$this->rules_names = Scope::get_array();
|
||||
public function __construct(bool $exit_on_errors = true) {
|
||||
// Set exit on errors override flag
|
||||
$this->exit_on_errors = $exit_on_errors;
|
||||
}
|
||||
|
||||
// Return property names for all Rules in array
|
||||
private static function get_property_names(array $rules): array {
|
||||
return array_map(fn(Rules $rule): string => $rule->get_property_name(), $rules);
|
||||
}
|
||||
|
||||
// ----
|
||||
|
||||
// Append an error to the array of errors
|
||||
private function add_error(Error $error, Scope $scope, string $property, mixed $expected): void {
|
||||
// Create sub array if this is the first error for this property
|
||||
if (!array_key_exists($property, $this->errors[$scope->name])) {
|
||||
$this->errors[$scope->name][$property] = [];
|
||||
private function add_error(string $name, string $message): self {
|
||||
// Create sub array for name if it doesn't exist
|
||||
if (!array_key_exists($name, $this->errors)) {
|
||||
$this->errors[$name] = [];
|
||||
}
|
||||
|
||||
// Set expected value value for property in scope
|
||||
$this->errors[$scope->name][$property][$error->name] = $expected;
|
||||
// Unset valid flag
|
||||
$this->is_valid = false;
|
||||
$this->errors[$name][] = $message;
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Evaluate an array of Rules property names against scope keys
|
||||
private function eval_strict(Scope $scope): void {
|
||||
$name_diffs = array_diff(array_keys($GLOBALS[$scope->value]), $this->rules_names[$scope->name]);
|
||||
private function eval_property_name_diff($rules, $scope_keys): void {
|
||||
// Get property names that aren't defiend in the ruleset
|
||||
$invalid_properties = array_diff($scope_keys, self::get_property_names($rules));
|
||||
|
||||
// Set errors for each undefined property
|
||||
foreach ($name_diffs as $name_diff) {
|
||||
$this->add_error(Error::UNKNOWN_PROPERTY_NAME, $scope, $name_diff, null);
|
||||
// Add error for each invalid property name
|
||||
foreach ($invalid_properties as $invalid_property) {
|
||||
$this->add_error($invalid_property, "Unknown property name '{$invalid_property}'");
|
||||
}
|
||||
|
||||
// Unset strict mode property now that we have evaled it up to this point
|
||||
$this->strict = null;
|
||||
}
|
||||
|
||||
// Evaluate Rules against a given value
|
||||
|
@ -86,8 +69,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
$this->add_error(Error::MISSING_REQUIRED_PROPERTY, $scope, $name, $name);
|
||||
return;
|
||||
$this->add_error($name, "Value can not be empty");
|
||||
}
|
||||
|
||||
// Get value from scope for the current property
|
||||
|
@ -98,86 +80,55 @@
|
|||
The error messages will be returned
|
||||
*/
|
||||
|
||||
// Value is not of the correct type or enum value
|
||||
if ($rules->types && !$rules->eval_type($value, $scope)) {
|
||||
if (!$rules->enum) {
|
||||
// Get type names from enum
|
||||
$types = array_map(fn(Type $type): string => $type->name, $rules->types);
|
||||
|
||||
$this->add_error(Error::INVALID_PROPERTY_TYPE, $scope, $name, $types);
|
||||
} else {
|
||||
$this->add_error(Error::INVALID_PROPERTY_VALUE, $scope, $name, $rules->enum);
|
||||
}
|
||||
if ($rules->type && !$rules->eval_type($value, $scope)) {
|
||||
$this->add_error($name, "Value must be of type '{$rules->type->name}'");
|
||||
}
|
||||
|
||||
if ($rules->min && !$rules->eval_min($value, $scope)) {
|
||||
$this->add_error(Error::VALUE_MIN_ERROR, $scope, $name, $rules->min);
|
||||
$this->add_error($name, "Value must be larger or equal to {$rules->min}");
|
||||
}
|
||||
|
||||
if ($rules->max && !$rules->eval_max($value, $scope)) {
|
||||
$this->add_error(Error::VALUE_MAX_ERROR, $scope, $name, $rules->max);
|
||||
$this->add_error($name, "Value must be smaller or equal to {$rules->max}");
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate all Rules in this Ruleset against values in scope and return true if no errors were found
|
||||
private function eval_all_rules(array $rules, Scope $scope): bool {
|
||||
foreach ($rules as $rule) {
|
||||
$this->eval_rules($rule, $scope);
|
||||
}
|
||||
|
||||
return empty($this->errors);
|
||||
}
|
||||
|
||||
// ----
|
||||
|
||||
// Perform request processing on GET properties (search parameters)
|
||||
public function GET(array $rules): self {
|
||||
// (Re)enable strict mode if property is null
|
||||
if ($this->strict === null) {
|
||||
$this->strict = true;
|
||||
public function GET(array $rules): true|array|Response {
|
||||
$this->eval_property_name_diff($rules, array_keys($_GET));
|
||||
|
||||
$is_valid = $this->eval_all_rules($rules, Scope::GET);
|
||||
|
||||
// Return errors as a Reflect\Response
|
||||
if (!$is_valid && $this->exit_on_errors) {
|
||||
return new Response($this->errors, 422);
|
||||
}
|
||||
|
||||
foreach ($rules as $rule) {
|
||||
$this->rules_names[Scope::GET->name][] = $rule->get_property_name();
|
||||
|
||||
$this->eval_rules($rule, Scope::GET);
|
||||
}
|
||||
|
||||
return $this;
|
||||
return $is_valid ? true : $this->errors;
|
||||
}
|
||||
|
||||
// Perform request processing on POST properties (request body)
|
||||
public function POST(array $rules): self {
|
||||
// (Re)enable strict mode if property is null
|
||||
if ($this->strict === null) {
|
||||
$this->strict = true;
|
||||
public function POST(array $rules): true|array|Response {
|
||||
$this->eval_property_name_diff($rules, array_keys($_POST));
|
||||
|
||||
$is_valid = $this->eval_all_rules($rules, Scope::POST);
|
||||
|
||||
// Return errors as a Reflect\Response
|
||||
if (!$is_valid && $this->exit_on_errors) {
|
||||
return new Response($this->errors, 422);
|
||||
}
|
||||
|
||||
foreach ($rules as $rule) {
|
||||
$this->rules_names[Scope::POST->name][] = $rule->get_property_name();
|
||||
|
||||
$this->eval_rules($rule, Scope::POST);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// ----
|
||||
|
||||
// Return array of all set Errors
|
||||
public function get_errors(): array {
|
||||
// Strict mode is enabled
|
||||
if ($this->strict === true) {
|
||||
$this->eval_strict(Scope::GET);
|
||||
$this->eval_strict(Scope::POST);
|
||||
}
|
||||
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
public function is_valid(): bool {
|
||||
// Strict mode is enabled
|
||||
if ($this->strict === true) {
|
||||
$this->eval_strict(Scope::GET);
|
||||
$this->eval_strict(Scope::POST);
|
||||
}
|
||||
|
||||
return $this->is_valid;
|
||||
}
|
||||
|
||||
// Return Reflect\Response with errors and code 422 Unprocessable Content if validation failed
|
||||
public function validate_or_exit(): true|Response {
|
||||
return $this->is_valid() ? true : new Response($this->errors, 422);
|
||||
return $is_valid ? true : $this->errors;
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue