mirror of
https://codeberg.org/reflect/reflect-rules-plugin.git
synced 2025-09-13 16:33:42 +02:00
initial commit
This commit is contained in:
commit
efc908b31b
6 changed files with 380 additions and 0 deletions
48
.gitignore
vendored
Normal file
48
.gitignore
vendored
Normal file
|
@ -0,0 +1,48 @@
|
|||
# Bootstrapping #
|
||||
#################
|
||||
/node_modules
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/vendor
|
||||
.env
|
||||
.env.backup
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# OS generated files #
|
||||
######################
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
Icon?
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
.directory
|
||||
|
||||
# Tool specific files #
|
||||
#######################
|
||||
# vim
|
||||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
# sublime text & textmate
|
||||
*.sublime-*
|
||||
*.stTheme.cache
|
||||
*.tmlanguage.cache
|
||||
*.tmPreferences.cache
|
||||
# Eclipse
|
||||
.settings/*
|
||||
# JetBrains, aka PHPStorm, IntelliJ IDEA
|
||||
.idea/*
|
||||
# NetBeans
|
||||
nbproject/*
|
||||
# Visual Studio Code
|
||||
.vscode
|
||||
# Sass preprocessor
|
||||
.sass-cache/
|
0
README.md
Normal file
0
README.md
Normal file
21
composer.json
Normal file
21
composer.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "reflect/plugin-rules",
|
||||
"description": "Add request search paramter and request body constraints to an API built with Reflect",
|
||||
"type": "library",
|
||||
"license": "GPL-3.0-only",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Victor Westerlund",
|
||||
"email": "victor.vesterlund@gmail.com"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "dev",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"ReflectRules\\": "src/"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"victorwesterlund/xenum": "dev-master"
|
||||
}
|
||||
}
|
59
composer.lock
generated
Normal file
59
composer.lock
generated
Normal file
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "035091a14ba6701d664e69d17b3f630b",
|
||||
"packages": [
|
||||
{
|
||||
"name": "victorwesterlund/xenum",
|
||||
"version": "dev-master",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/VictorWesterlund/php-xenum.git",
|
||||
"reference": "99b784841ee5b69fdfcc4c466ef54f3af4ea4a85"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/VictorWesterlund/php-xenum/zipball/99b784841ee5b69fdfcc4c466ef54f3af4ea4a85",
|
||||
"reference": "99b784841ee5b69fdfcc4c466ef54f3af4ea4a85",
|
||||
"shasum": ""
|
||||
},
|
||||
"default-branch": true,
|
||||
"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.0"
|
||||
},
|
||||
"time": "2023-10-09T11:32:07+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "dev",
|
||||
"stability-flags": {
|
||||
"victorwesterlund/xenum": 20
|
||||
},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.0.0"
|
||||
}
|
123
src/Rules.php
Normal file
123
src/Rules.php
Normal file
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
namespace ReflectRules;
|
||||
|
||||
use \victorwesterlund\xEnum;
|
||||
|
||||
// Supported types for is_type()
|
||||
enum Type {
|
||||
use xEnum;
|
||||
|
||||
case NUMBER;
|
||||
case STRING;
|
||||
case BOOLEAN;
|
||||
case ARRAY;
|
||||
case OBJECT;
|
||||
case NULL;
|
||||
}
|
||||
|
||||
class Rules {
|
||||
private string $property;
|
||||
|
||||
public bool $required = false;
|
||||
public ?Type $type = null;
|
||||
|
||||
public ?int $min = null;
|
||||
public ?int $max = null;
|
||||
|
||||
public function __construct(string $property) {
|
||||
$this->property = $property;
|
||||
}
|
||||
|
||||
public function get_property_name(): string {
|
||||
return $this->property;
|
||||
}
|
||||
|
||||
/*
|
||||
# Constraints
|
||||
Chain these methods to create rules for a particular property.
|
||||
When all rules are defiend, the eval_* methods will be called
|
||||
*/
|
||||
|
||||
// A sequential array of additional Rule instances for a
|
||||
private function object_rules(array $rules): self {
|
||||
$this->object_rules = $rules;
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Set the minimum lenth/size for property
|
||||
public function min(?int $value = null) {
|
||||
$this->min = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Set the maximum length/size for property
|
||||
public function max(?int $value = null) {
|
||||
$this->max = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
// This property has to exist in scope
|
||||
public function required(bool $flag = true): self {
|
||||
$this->required = $flag;
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Set property Type
|
||||
public function type(Type|string $type): self {
|
||||
// Coerce string to Type enum
|
||||
if (!($type instanceof Type)) {
|
||||
$type = Type::fromName($string);
|
||||
}
|
||||
|
||||
$this->type = $type;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/*
|
||||
# Eval methods
|
||||
These methods are used to check conformity against set rules.
|
||||
Methods are not called until all rules have been defined.
|
||||
*/
|
||||
|
||||
public function eval_required(array $scope): bool {
|
||||
return array_key_exists($this->property, $scope);
|
||||
}
|
||||
|
||||
public function eval_type(mixed $value): bool {
|
||||
return match($this->type) {
|
||||
Type::NUMBER => is_numeric($value),
|
||||
Type::STRING => is_string($value),
|
||||
Type::BOOLEAN => is_bool($value),
|
||||
Type::ARRAY => is_array($value),
|
||||
Type::OBJECT => $this->eval_object($value),
|
||||
Type::NULL => is_null($value),
|
||||
default => true
|
||||
};
|
||||
}
|
||||
|
||||
public function eval_min(mixed $value): bool {
|
||||
return match($this->type) {
|
||||
Type::NUMBER => $this->eval_type($value) && $value >= $this->min,
|
||||
Type::STRING => $this->eval_type($value) && strlen($value) >= $this->min,
|
||||
Type::ARRAY,
|
||||
Type::OBJECT => $this->eval_type($value) && count($value) >= $this->min,
|
||||
default => true
|
||||
};
|
||||
}
|
||||
|
||||
public function eval_max(mixed $value): bool {
|
||||
return match($this->type) {
|
||||
Type::NUMBER => $this->eval_type($value) && $value <= $this->max,
|
||||
Type::STRING => $this->eval_type($value) && strlen($value) <= $this->max,
|
||||
Type::ARRAY,
|
||||
Type::OBJECT => $this->eval_type($value) && count($value) <= $this->max,
|
||||
default => true
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Recursive Rules eval of multidimensional object
|
||||
public function eval_object(mixed $object): bool {
|
||||
return is_array($object);
|
||||
}
|
||||
}
|
129
src/Ruleset.php
Normal file
129
src/Ruleset.php
Normal file
|
@ -0,0 +1,129 @@
|
|||
<?php
|
||||
|
||||
namespace ReflectRules;
|
||||
|
||||
// Use the Response class from Reflect to override endpoint processing if requested
|
||||
use \Reflect\Response;
|
||||
|
||||
use \ReflectRules\Rules;
|
||||
|
||||
require_once "../vendor/autoload.php";
|
||||
|
||||
require_once "Rules.php";
|
||||
|
||||
class Ruleset {
|
||||
// This plugin will return exit with a Reflect\Response if errors are found
|
||||
private bool $exit_on_errors;
|
||||
|
||||
// Array of RuleError instances
|
||||
private array $errors = [];
|
||||
|
||||
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(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] = [];
|
||||
}
|
||||
|
||||
$this->errors[$name][] = $message;
|
||||
return $this;
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
// Add error for each invalid property name
|
||||
foreach ($invalid_properties as $invalid_property) {
|
||||
$this->add_error($invalid_property, "Unknown property name '{$invalid_property}'");
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate Rules against a given value
|
||||
private function eval_rules(Rules $rules, array &$scope): void {
|
||||
$name = $rules->get_property_name();
|
||||
|
||||
// Check if property name exists in scope
|
||||
if (!$rules->eval_required($scope)) {
|
||||
// Set property name value on superglobal to null if not defined
|
||||
$scope[$name] = null;
|
||||
|
||||
// Don't perform further processing if the property is optional and not provided
|
||||
if (!$rules->required) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->add_error($name, "Value can not be empty");
|
||||
}
|
||||
|
||||
$value = $scope[$name];
|
||||
|
||||
/*
|
||||
Eval each rule that has been set.
|
||||
The error messages will be returned
|
||||
*/
|
||||
|
||||
if ($rules->type && !$rules->eval_type($value)) {
|
||||
$this->add_error($name, "Value must be of type '{$rules->type->name}'");
|
||||
}
|
||||
|
||||
if ($rules->min && !$rules->eval_min($value)) {
|
||||
$this->add_error($name, "Value must be larger or equal to {$rules->min}");
|
||||
}
|
||||
|
||||
if ($rules->max && !$rules->eval_max($value)) {
|
||||
$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, array &$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): true|array|Response {
|
||||
$this->eval_property_name_diff($rules, array_keys($_GET));
|
||||
|
||||
$is_valid = $this->eval_all_rules($rules, $_GET);
|
||||
|
||||
// Return errors as a Reflect\Response
|
||||
if (!$is_valid && $this->exit_on_errors) {
|
||||
return new Response($this->errors, 422);
|
||||
}
|
||||
|
||||
return $is_valid ? true : $this->errors;
|
||||
}
|
||||
|
||||
// Perform request processing on POST properties (request body)
|
||||
public function POST(array $rules): true|array|Response {
|
||||
$this->eval_property_name_diff($rules, array_keys($_POST));
|
||||
|
||||
$is_valid = $this->eval_all_rules($rules, $_POST);
|
||||
|
||||
// Return errors as a Reflect\Response
|
||||
if (!$is_valid && $this->exit_on_errors) {
|
||||
return new Response($this->errors, 422);
|
||||
}
|
||||
|
||||
return $is_valid ? true : $this->errors;
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue