forensic / handler
位于控制器和模型之间,执行请求数据验证、序列化和完整性检查
Requires
- php: >=7.1
Requires (Dev)
- php-coveralls/php-coveralls: ^2.1
- phpunit/phpunit: ^7.3
- squizlabs/php_codesniffer: ^3.3
This package is auto-updated.
Last update: 2024-09-23 08:02:33 UTC
README
Forensic Handler 是一个位于控制器和模型之间的独立 PHP 模块,执行请求数据验证、序列化和完整性检查。它易于设置,并且独立于任何 PHP 框架和 ORM。
它简化了验证过程,只需要定义数据验证规则,这些规则只是 PHP 数组。
最有趣的部分是验证字段数据和文件数组有多容易,以及它提供的广泛验证规则类型。它也可以扩展,以便在需要时定义更多验证规则。有关说明,请参阅 如何编写自定义验证类型
关于数据库完整性检查,它足够灵活,允许你通过定义一个抽象的 DBCheckerAbstract
类来将数据库检查的实现留给你。这使得它与任何框架或 ORM 无关。有关说明,请参阅 如何实现 DBCheckerAbstract 接口
入门
通过 composer 安装:
composer require forensic/handler
简单用法示例
处理器:
/** * file AuthHandler.php */ //make sure composer autoloader is loaded namespace app\Handler; use Forensic\Handler\Handler as BaseHandler; use app\Model\UserModel; //our model class AuthHandler extends BaseHandler { public function getDB() { //return db orm } /** * executes signup *@param array|string [$source = 'post'] - the source of the data. can also be an array */ public function executeSignup($source = 'post') { $rules = [ //email field rule. 'email' => [ 'type' => 'email', 'err' => '{this} is not a valid email address' ], 'password1' => [ 'type' => 'password', ], 'password2' => [ 'type' => 'password', 'matchWith' => '{password1}' ], ]; $this->setSource($source)->setRules($rules); if (!$this->execute()) return $this->succeeds(); //return immediately if there are errors /* * check if user exists, so that we dont create same user again * we can check using the model, or execute a prepared separate query. we could also * define this integrity check right in the rules above, only that we must implement * the DBCheckerAbstract class which will be shown later */ $query = 'SELECT id FROM users WHERE email = ? AND password = ?'; $result = $this->getDB()->select($query, array($this->email, $this->password1)); if (count($result) > 0) { $this->setError('email', 'User already exists, please login instead'); return $this->succeeds(); //return immediately if there is error } //create user $user = new UserModel(); /** * do not copy password2 and rename password1 to just password when mapping processed * data to our model */ $this->modelSkipField('password2')->modelRenameField('password1', 'password'); $this->mapDataToModel($user)->save(); // it returns the model //set the new user id so that it can be accessed outside the class $this->setData('id', $user->id); return $this->succeeds(); } }
控制器:
/** * file auth controller */ namespace app\Controller; use SomeNamespace\Request; use SomeNamespace\JsonResponse; use SomeNamespace\Controller as BaseController; use app\Handler\AuthHandler; class AuthController extends BaseController { public function signup(Request $request, Response $response, AuthHandler $handler) { if ($handler->executeSignup()) return $response([ 'status' => 'success', 'data' => [ 'userId' => $handler->id ], ]); else return $response([ 'status' => 'failed', 'data' => [ 'errors' => $handler->getErrors() ], ]); } }
请注意,由于 PHP 的魔法
__get()
方法,可以直接在实例上访问处理后的数据。然而,如果您尝试访问未定义键的数据,则会抛出KeyNotFoundException
,以帮助进行故障排除。.
验证规则格式
验证规则定义为以字段名称为键的数组。每个字段数组可以包含以下规则属性
-
type:这表示要对字段执行的验证类型。
-
required:表示字段是否为必需的布尔值。默认为 true。
-
default:此属性适用于非必需字段。默认值为 null。
-
filters:定义应用于字段值之前的过滤器数组。
-
check:包含要运行在字段值上的数据库完整性检查的数组。
-
checks:包含要在字段值上运行的多项数据库完整性检查的数组。
-
options:包含其他验证规则的数组。
-
requiredIf:一个条件语句,如果满足条件,则使字段成为必需。
要引用验证原则,使用的约定是在字符串中将原则字段名称用大括号括起来。'{field-name}'。模块将找到并解决此类,用字段值替换它。
其他约定包括 {this}
,它引用正在验证的当前字段值;{_this}
引用正在验证的当前字段名;而 {_index}
引用正在验证的当前字段值索引位置(在验证字段数组的情况下)。
最后,还有 {CURRENT_DATE}
、{CURRENT_YEAR}
和 {CURRENT_TIME}
,分别引用当前日期、当前年和当前时间戳值。
$rules = [ 'first-name' => [ 'type' => 'text', 'options' => [ 'min' => 3, 'minErr' => '{_this} should be at least 3 charaters length', //first-name should be at least 3 characters length ], ], //we are expecting an array of favorite colors 'favorite-colors' => [ 'type' => 'choice', 'filters' => [ 'toLower' => true, //convert the colors to lowercase ], 'options' => [ //choices to choose from 'choices' => array('green', 'white', 'blue', 'red', 'violet', 'purple'), 'err' => 'color {_index} is not a valid color', //color 1 is not a valid color' ], ], 'subscribe-newsletter' => [ 'type' => 'boolean', ], //email is required if user checks the subscribe checkbox, else, do not require it 'email' => [ 'type' => 'email', 'requireIf' => [ 'condition' => 'checked', 'field' => 'subscribe-newsletter' ] 'err' => '{this} is not a valid email address' ], ]
验证过滤器
在验证之前应用于字段值。您可以使用过滤器在验证之前修改字段值。可用的过滤器包括
-
decode:在字段值上调用 PHP 的
urldecode()
函数。默认为 true -
trim:在字段值上调用 PHP 的
trim()
函数。默认为 true -
stripTags:在字段值上调用 PHP 的
strip_tags()
函数。默认为 true -
stripTagsIgnore:定义当
stripTags
过滤器设置为true时不应删除的HTML标签字符串。默认为空字符串 -
numeric:在字段值上调用php
floatval()
函数。默认为false -
toUpper:在字段值上调用php
strtoupper()
函数。默认为false -
toLower:在字段值上调用php
strtolower()
函数。默认为false
$rules = [ 'country' => [ 'filters' => [ 'toLower' => true //convert to lowercase ], ], 'comment' => [ 'filter' => [ 'stripTagsIgnore' => '<p><br>' ], ], ];
验证规则类型
该模块定义了许多验证规则类型,覆盖了广泛的验证场景。以下是一些类型:
限制规则验证
限制规则验证选项影响每个验证。在这里,我们可以定义字符串、日期或数值值的最大长度。这些选项包括min(最小值)、max(最大值)、gt(大于)和lt(小于)。
$rules = [ 'first-name' => [ 'type' => 'text', 'options' => [ 'min' => 3, 'minErr' => 'first name should be at least 3 characters length', 'max' => 15, ] ], 'favorite-integer' => [ 'type' => 'positiveInteger', 'options' => [ 'lt' => 101, //should be less than 101, or max of 100. ] ], 'date-of-birth' => [ 'type' => 'date', 'options' => [ 'min' => '01-01-1990', //only interested in people born on or after 01-01-1990 'max' => '{CURRENT_DATE}' ] ], ];
正则表达式规则验证
对字段值执行不同类型的正则表达式规则测试非常简单。有四种正则表达式规则。包括单个regex测试、regexAny、regexAll和regexNone测试。
regex类型必须匹配测试,否则将被标记为错误。对于regexAny,至少有一个测试必须匹配。对于regexAll,所有正则表达式测试都必须匹配。对于regexNone,不应匹配任何正则表达式测试。
$rules = [ 'first-name' => [ 'type' => 'text', 'regexAll' => [ //name must start with letter [ 'test' => '/^[a-z]/i', 'err' => 'name must start with an alphabet' ], //only aphabets, dash and apostrophe is allowed in name [ 'test' => '/^[-a-z\']+$/', 'err' => 'only aphabets, dash, and apostrophe is allowed in name' ] ] ], 'country' => [ 'type' => 'text', 'options' => [ 'regex' => [ 'test' => '/^[a-z]{2}$/', 'err' => '{this} is not a 2-letter country iso-code name' ] ], ], 'phone-number' => [ 'type' => 'text', 'options' => [ 'regexAny' => [ 'tests' => [ //phone number can match nigeria mobile number format '/^0[0-9]{3}[-\s]?[0-9]{3}[-\s]?[0-9]{4}$/', //phone number can match uk mobile number format '/^07[0-9]{3}[-\s]?[0-9]{6}$/' ], 'err' => 'only nigeria and uk number formats are accepted' ] ] ], 'favorite-colors' => [ 'options' => [ 'regexNone' => [ //we dont accept white as a color [ 'test' => '/^white$/i', 'err' => '{this} is not an acceptable color' ], //we dont accept black either [ 'test' => '/^black$/i', 'err' => '{this} is not an acceptable color' ], ], ], ], ]
匹配规则验证
当您想确保字段值与另一个字段值匹配时,例如在密码确认字段以及电子邮件和电话确认场景中,此规则非常方便。
$rules = [ 'password1' => [ 'type' => 'password' ], 'password2' => [ 'type' => 'password', 'options' => [ 'matchWith' => '{password1}', //reference password1 value 'err' => 'Passwords do not match' ], ], ];
日期验证
要验证日期,将类型属性设置为'date'。您可以指定验证日期是否在给定范围内的限制规则。
$rules = [ 'date-of-birth' => [ 'type' => 'date', 'options' => [ 'min' => '01-01-1990', //only interested in people born on or after 01-01-1990 'max' => '{CURRENT_DATE}' ] ], ];
范围验证
要将字段作为值范围进行验证,将类型属性设置为range。范围类型接受三个更多选项键,分别是from(起始值)、to(结束值)以及可选的step键,默认值为1。
$rules = [ 'day' => [ 'type' => 'range', 'options' => [ 'from' => 1, 'to' => 31, ], ], 'month' => [ 'type' => 'range', 'options' => [ 'from' => 1, 'to' => 12, ], ], 'year' => [ 'type' => 'range', 'options' => [ 'from' => 1950, 'to' => '{CURRENT_YEAR}', ], ], 'even-number' => [ 'type' => 'range', 'options' => [ 'from' => 0, 'to' => 100, 'step' => 2, 'err' => '{this} is not a valid even number between 0-100' ], ] ];
选择验证
要将字段作为选项集进行验证,将类型属性设置为choice。可以使用choices属性指定可接受选项,该属性作为数组使用。该类型验证器内部使用此类型。
$rules = [ 'country' => [ 'type' => 'choice', 'options' => [ 'choices' => array('ng', 'gb', 'us', 'ca', ...),// array of country codes, 'err' => '{this} is not a valid country code' ], ], ];
电子邮件验证
要验证电子邮件地址,将类型属性设置为email
。
$rules = [ 'email' => [ 'type' => 'email' ], ];
URL验证
要验证URL,将类型属性设置为url
。
$rules = [ 'website' => [ 'type' => 'url' ], ];
数值验证
要验证数值,无论是浮点数还是整数,都定义了相应的验证类型。以下是一些类型:float(货币或数字)、positiveFloat或pFloat、negativeFloat或nFloat、integer或int、positiveInteger(正整数)、negativeInteger(负整数)。
$rules = [ 'favorite-number' => [ 'type' => 'number' ], 'user-id' => [ 'type' => 'positiveInt', ] ];
密码验证
密码类型验证类似于文本验证,除了添加了一些限制规则和正则表达式规则。默认验证实现是密码长度至少为8个字符,最大长度为28个字符。必须包含至少两个字母和至少两个非字母字符。如果您愿意,可以覆盖此默认设置。
[ 'min' => 8, 'max' => 28, 'regexAll' => [ //password should contain at least two alphabets [ 'test' => '/[a-z].*[a-z]/i', 'err' => 'Password must contain at least two letter alphabets' ], //password should contain at least two non letter alphabets [ 'test' => '/[^a-z].*[^a-z]/i', 'err' => 'Password must contain at least two non letter alphabets' ], ], ];
文件验证
该模块可以验证文件,包括文件MIME类型的完整性。它提供广泛的文件验证类型,如图像、视频、音频、文档和存档。
准确识别文件大小单位,包括bytes(字节)、kb(千字节)、mb(兆字节)、gb(吉字节)和tb(太字节)。
$rules => [ 'picture' => [ 'type' => 'file', 'options' => [ 'min' => '50kb' //it will be converted accurately ] ], ];
您可以使用 moveTo 选项定义绝对路径来移动文件。当文件正在处理时,会为其计算一个哈希名称,并存储在字段中,以便可以直接使用实例上的 getData()
实例方法访问。
use Forensic\Handler\Handler; $move_to = getcwd() . '/storage/media/pictures'; $rules => [ 'picture' => [ 'type' => 'file', 'options' => [ 'moveTo' => $move_to ], ], ]; $handler = new Handler('post', $rules); $handler->execute(); if ($handler->succeeds()) { $file_name = $handler->picture; //the computed hash name is stored in the field $file_abs_path = $move_to . '/' . $file_name; }
处理多值字段和文件
处理器可以处理多值字段和文件字段。字段值在处理后被存储在数组中。
示例:
$move_to = getcwd() . '/storage/media/pictures'; $rules => [ 'pictures' => [ 'type' => 'file', 'options' => [ 'max' => '400kb', 'moveTo' => $move_to ], ], ]; $handler = new Handler('post', $rules); $handler->execute(); if ($handler->succeeds()) { array_walk(function($file_name) { /** * we walk through all the files, and do whatever we want. */ $abs_path = $move_to . '/' . $file_name; // abs path of current file. }, $handler->pictures); }
指定接受的文件Mimes扩展
您可以在验证过程中指定接受的Mime文件扩展。请注意,处理器有一个 FileExtensionDetector
模块,可以根据其第一个魔法字节检测文件扩展名,从而限制文件扩展名欺骗错误。请注意,当前文件魔法字节的列表仍在更新中,您可以向我们报告更多缺失的魔法字节代码来帮助我们。
要指定接受的Mimes,请使用 mimes
选项。
示例:
$move_to = getcwd() . '/storage/media/pictures'; $rules => [ 'pictures' => [ 'type' => 'file', 'options' => [ 'max' => '400kb', 'moveTo' => $move_to, 'mimes' => array('jpeg', 'png') //we only accept jpeg and png files. no gif, 'mimeErr' => 'we only accept jpeg and png images' ], ], ];
图像文件验证
验证图像文件的最简单方法是使用 image
类型选项。接受的图像Mimes包括 JPEG、PNG 和 GIF。
$move_to = getcwd() . '/storage/media/pictures'; $rules => [ 'pictures' => [ 'type' => 'image', 'options' => [ 'max' => '400kb', 'moveTo' => $move_to, ], ], ];
音频文件验证
验证音频文件的最简单方法是使用 audio
类型选项。接受的音频Mimes包括 MP3 等。
$move_to = getcwd() . '/storage/media/audios'; $rules => [ 'pictures' => [ 'type' => 'audio', 'options' => [ 'max' => '400mb', 'moveTo' => $move_to, ], ], ];
视频文件验证
验证视频文件的最简单方法是使用 video
类型选项。接受的视频Mimes包括 MP4、OGG、MOVI 等。
$move_to = getcwd() . '/storage/media/videos'; $rules => [ 'pictures' => [ 'type' => 'video', 'options' => [ 'max' => '400mb', 'moveTo' => $move_to, ], ], ];
媒体文件验证
验证媒体文件(视频、图像和音频)的最简单方法是使用 media
类型选项。接受的Mimes是 video、image 和 audio Mimes 的组合。
$move_to = getcwd() . '/storage/media'; $rules => [ 'pictures' => [ 'type' => 'media', 'options' => [ 'max' => '400mb', 'moveTo' => $move_to, ], ], ];
文档文件验证
验证文档文件的最简单方法是使用 document
类型选项。接受的文档Mimes包括 DOCX、PDF 和 DOC 等。
$move_to = getcwd() . '/storage/documents'; $rules => [ 'pictures' => [ 'type' => 'document', 'options' => [ 'max' => '4mb', 'moveTo' => $move_to, ], ], ];
存档文件验证
验证归档文件的最简单方法是使用 archive
类型选项。接受的归档Mimes包括 ZIP、TAR.GZ 和 TAR 等。
$move_to = getcwd() . '/storage/archives'; $rules => [ 'pictures' => [ 'type' => 'archive', 'options' => [ 'max' => '50mb', 'moveTo' => $move_to, ], ], ];
如何实现DBCheckerAbstract接口
要启用数据库完整性检查,您必须在 DBCheckerAbstract
类上实现两个方法,即 buildQuery()
和 execute()
方法。然后您必须将您具体类的实例作为 Handler
的第四个参数提供。
以下是如何在 Laravel 中实现此操作的示例
<?php namespace app\Handler; use Forensic\Handler\Abstracts\DBCheckerAbstract; use Illuminate\Support\Facades\DB; class DBChecker extends DBCheckerAbstract { /** * construct query from the given options * *@param array $options - array of options * the options array contains the following fields. * 'entity': is the database table * 'params': which is array of parameters. defaults to empty array * 'query': which is the query to run. defaults to empty string * 'field': if the query parameter is empty string, then there is the field parameter * that refers to the database table column to check */ protected function buildQuery(array $options): string { $query = $options['query']; //if the query is empty string, we build it according to our orm if ($query === '') { //build the query $query = 'SELECT * FROM ' . $options['entity'] . ' WHERE ' . $options['field'] . ' = ?'; } return $query; } /** * executes the query. the execute method should return array of result or empty * array if there is no result */ protected function execute(string $query, array $params, array $options): array { return DB::select($query, $params); } }
然后我们可以定义我们自己的 BaseHandler
并将我们具体类的实例作为第四个参数提供给父构造函数,如下所示
//file BaseHandler namespace app\Handler; use Forensic\Handler\Handler as ParentHandler; class BaseHandler extends ParentHandler { public function construct($source = null, array $rules = null) { parent::__construct($source, $rules, null, new DBChecker()); } }
因此,我们现在可以使用如下的 'check' 和 'checks' 规则选项
// file AuthHandler.php namespace app\Handler; use app\Model\UserModel; //our model class AuthHandler extends BaseHandler { /** * executes signup *@param array|string [$source = 'post'] - the source of the data. can also be an array */ public function executeSignup($source = 'post') { $rules = [ //email field rule. 'email' => [ 'type' => 'email', 'err' => '{this} is not a valid email address', //db check rule goes here 'check' => [ 'if' => 'exists', // note that it is error if it exists 'entity' => 'users', 'field' => 'email', 'err' => 'User with email {this} already exists, login instead', ] ], 'password1' => [ 'type' => 'password', ], 'password2' => [ 'type' => 'password', 'matchWith' => '{password1}' ], ]; $this->setSource($source)->setRules($rules); if (!$this->execute()) return $this->succeeds(); //return immediately if there are errors //create user $user = new UserModel(); //do not copy password2 and rename password1 to just password $this->modelSkipField('password2')->modelRenameField('password1', 'password'); $this->mapDataToModel($user)->save(); // it returns the model //set the new user id $this->setData('id', $user->id); return $this->succeeds(); } }
定义检查和检查完整性规则
check
选项定义单个数据库完整性检查,而 checks
选项定义数据库完整性检查的数组。
在定义规则时,可以选择编写要执行的select查询。在这种情况下,应该有一个 query
属性,如果需要,还有一个 params
数组属性(如果没有提供,则默认为空数组)。
第二种选择是在单个实体字段上执行检查。在这种情况下,应该有一个引用要从中选择的数据库表的 entity
属性,以及定义在where子句中使用的表列的 field
属性。如果省略了 field
属性,如果字段值是整数,则默认为 id
,否则默认为解析的字段名(根据 modelCamelizeFields($status) 方法的状态,可以是驼峰式或蛇形命名)。
$rules = [ 'userid' => [ 'type' => 'positiveInt', 'check' => [ 'if' => 'notExist', 'entity' => 'users' //since no query is defined, the field option will default to id rather than // userid because the field value is an integer, //params options will default to array(current_field_value) ] ], 'email' => [ 'type' => 'email', 'checks' => [ //first check [ 'if' => 'exists', 'entity' => 'users' //since no field is defined, it will defualt to email ], //more checks goes here ] ], 'country' => [ 'type' => 'text', 'checks' => [ //first check [ 'if' => 'notExist', 'query' => 'SELECT * from countries WHERE value = ?', 'params' => array('{this}'), 'err' => '{this} is not recognised as a country in our database' ], ], ], ];
如何编写您的自定义验证类型
该模块旨在可扩展,以便您定义更多验证方法和使用自定义的规则类型。您需要了解一些关于模块工作方式的基本知识。检查 ValidatorInterface
和 Validator
类文件是一个不错的起点。下面展示了如何轻松实现这一功能。
定义继承自主验证器的自定义验证器:
<?php //file CustomValidator.php namespace app\Handler; use Forensic\Handler\Validator; class CustomValidator extends Validator { protected function validateName(bool $required, string $field, $value, array $options, int $index = 0): bool { $options['min'] = 3; $options['max'] = 15; $options['regexAll'] = [ //only alphabets dash and apostrophe is allowed in names [ 'test' => '/^[-a-z\']$/i', 'err' => 'only alphabets, hyphen and apostrophe allowed in names' ] //name must start with at least two alphabets [ 'test' => '/^[a-z]{2,}/i', 'err' => 'name must start with at least two alphabets' ], ]; return $this->validateText($required, $field, $value, $options, $index); } }
然后我们可以定义自己的 BaseHandler
,并集成新添加的 name 类型验证方法,如下所示
//file BaseHandler namespace app\Handler; use Forensic\Handler\Handler as ParentHandler; class BaseHandler extends ParentHandler { public function construct($source = null, array $rules = null) { parent::__construct($source, $rules, new CustomValidator(), new DBChecker()); } /** *@override the parent method. */ public function getRuleTypesMethodMap(): array { return array_merge(parent::getRuleTypesMethodMap(), [ 'name' => 'validateName' ]); } }
从此以后,我们就可以使用 name 类型来验证名称,如下所示
// file ProfileHandler.php namespace app\Handler; use app\Model\UserModel; //our model class ProfileHandler extends BaseHandler { /** * updates user profile *@param array|string [$source = 'post'] - the source of the data. can also be an array */ public function updateProfile($source = 'post') { $rules = [ //email field rule. 'id' => [ 'type' => 'positiveInteger', //db check rule goes here 'check' => [ 'if' => 'doesNotExist', 'entity' => 'users', 'err' => 'No user found with id {this}', ] ], 'first-name' => [ 'type' => 'name', ], 'last-name' => [ 'type' => 'name', ], 'middle-name' => [ 'type' => 'name', 'required' => false, 'default' => '' ] ]; //more codes below } }
RequiredIf 或 RequireIf 选项
使用此选项,我们可以使一个字段在满足给定条件时成为必填项。这些条件包括
-
如果另一个字段被选中或未被选中
$rules = [ 'is-current-work' => [ 'type' => 'boolean', ], 'work-end-month' => [ 'type' => 'range', 'options' => [ 'from' => 1, 'to' => 12 ], 'requiredIf' => [ 'condition' => 'notChecked', 'field' => 'is-current-work' ], ], 'subscribe-newsletter' => [ 'type' => 'boolean' ], 'email' => [ 'requiredIf' => [ 'condition' => 'checked', 'field' => 'subscribe-newsletter' ], ], ];
-
如果另一个字段等于给定值或与其不相等
$rules = [ 'country' => [ 'type' => 'choice', 'options' => [ 'choices' => array('ng', 'us', 'gb', 'ca', 'gh') ], ], //if your country is not nigeria, tell us your country calling code 'calling-code' => [ 'requiredIf' => [ 'condition' => 'notEquals', 'value' => 'ng', 'field' => 'country' ], ], //if you are in nigeria, you must tell us your salary demand 'salary-demand' => [ 'requiredIf' => [ 'condition' => 'equals', 'value' => 'ng', 'field' => 'country' ], ], ];