imarc/checkpoint

简单、明确且强大的验证范式

2.0-beta3 2023-08-16 17:46 UTC

README

Checkpoint 是围绕 Respect/Validation 的验证包装器,旨在允许自定义规则生成和验证错误消息记录。其一些主要目标包括

  • 不要抽象验证逻辑
  • 启用对多种类型数据的验证
  • 将验证隔离到显式封装的对象中

创建检查器

class CustomInspector extends Checkpoint\Inspector
{
	protected function validate($data)
	{
		//
		// Your validation logic here
		//
	}
}

设置验证器

$custom_inspector = new CustomInspector();

$custom_inspector->setValidator(new Respect\Validation\Validator());

通常,验证器的实例化和设置将在您的依赖注入器中设置。使用像 Auryn 这样的依赖注入器允许您为 Checkpoint\Validation 接口定义一个准备器,这样任何实例化的检查器都将通过其设置方法自动注入验证器。

$auryn->prepare('Checkpoint\Validation', function($inspector) {
	$inspector->setValidator(new Respect\Validation\Validator());
});

额外的验证依赖

由于验证器封装在自己的类中,因此可以轻松注入其他可能需要的依赖以验证数据。如果您的依赖注入器执行递归构建,则可以在自定义检查器上设置这些依赖。例如,一个常见的要求是确定电子邮件地址是否在数据库表或存储库中是唯一的,因此您可能在检查器类中这样做

public function __construct(PeopleRepository $people)
{
	$this->people = $people;
}

然后在进行验证时

public function validate($data)
{
	if ($this->people->findOneByEmail($data['email'])) {
		$this->log('email', 'The e-mail address must be unique in our system.');
	}
}

执行验证

所有验证逻辑都应该封装在 validate() 方法中。您收到的要验证的数据可以是任何类型的数据,并且您负责传递有效的数据格式。您可以使用数组或对象,如何编写验证由您决定。

public function validate($data)
{
	$this->check('firstName', $data['firstName'], ['notBlank']);
}

或者可能是来自您的 ORM 的模型/实体

public function validate($data)
{
	$this->check('firstName', $data->getFirstName(), ['notBlank']);
}

自定义规则

check() 方法支持 Respect 提供的一组默认规则,包括

  • alpha
  • email
  • phone
  • lowercase
  • notBlank

默认规则将始终包括不需要额外参数的规则。要定义自定义规则,可以使用定义方法,该方法接受规则名称、要记录的错误消息,并返回一个 Respect\Validation\Validator 以链式规则。

public function validate($data)
{
	$this->define('descLength', 'Please enter a description of at least 100 characters.')
		 -> length(100);

	$this->check('description', $data['description'], ['descLength']);
}

运行验证

一旦设置 validate() 方法,您可以通过传递所需数据来简单地运行验证

$custom_inspector->run($data);

由于要检查的数据可以是您想要的数据,因此这可以采用多种格式

$person = new Person();
$person->setFirstName('Matthew');
$person->setLastName('Sahagian');
...
$person_inspector->run($person)

或者一个更明确的数组

$person_inspector->run([
	'firstName' => 'Matthew',
	'lastName'  => 'Sahagian'
]);

这种灵活性允许您验证从表单输入、模型到单个值的所有内容

$email_inspector->run('user@example.com');

为适合您的任何内容编写验证器,包括直接请求输入

$registration_inspector->run($this->request->getParsedBody());

检查消息

check() 方法的第一个参数定义了消息将记录的名称。消息始终添加到以提供名称为键的数组中,以便可以添加多个验证消息。您可以使用 countMessages() 方法获取记录的消息总数。

if ($custom_inspector->countMessages()) {
	throw new Checkpoint\ValidationException('Please correct the errors below.');
}

要获取特定消息,您可以使用 getMessages() 方法并提供消息的路径。顶级检查器的路径只是调用 check 时传递的键名。

if ($messages = $custom_inspector->getMessages('description')) {
	foreach ($messages as $message) {
		echo '<li>' . $message . '</li>';
	}
}

在大多数情况下,您将想要为此定义一个模板部分。以下是一个 twig 示例

{% if errors %}
	{% if errors|length == 1 %}
		<div class="messaging error">
			<p>{{ errors[0]|raw }}</p>
		</div>
	{% else %}
		<ul class="messaging error">
			{% for error in errors %}
				<li>{{ error|raw }}</li>
			{% endfor %}
		</ul>
	{% endif %}
{% endif %}

您可以看到如何将此部分内联包含以生成每字段消息

<label>First Name</label>
{% include '@messaging/errors.html' with {'errors': inspector.messages('firstName')} %}
<input type="text" name="firstName" value="{{ params.firstName }}" />

子检查器

有时您需要验证非常复杂的数据结构或相关对象。为此,Checkpoint 支持添加子检查器以进行额外的验证。要添加子检查器,您必须执行以下步骤:

注册子检查器

由于子检查器是父检查器的依赖项,我们可以将其注入构造函数并注册。

public function __construct(PersonInspector $person_inspector)
{
	$this->add('person', $person_inspector);
}

检索和运行子检查器

现在您已添加子检查器,可以在验证期间使用数据子集检索并运行它。

public function validate($data)
{
	$this->fetch('person')->run($data->getPerson());
}

检查子消息

子消息将可通过顶层验证器通过对象符号递归引用子检查器,并最终获取特定检查的消息。

$messages = $registration_inspector->getMessages('person.firstName');

一个复杂示例

以下是一个包含基于我们之前已讨论的一些概念的子检查器的中等复杂示例。

class ProfileInspector extends Checkpoint\Inspector
{
	public function __construct(PeopleRepository $people, PersonInspector $pinspector, CompanyInspector $cinspector)
	{
		$this->people = $people;
		$this->add('person', $pinspector);
		$this->add('company', $cinspector);
	}


	protected function validate($data)
	{
		if ($this->people->findOneByEmail($data['person']['email'])) {
			$this->log('duplicate', TRUE);
			return;
		}

		$this->fetch('person')->run($data['person']);
		$this->fetch('company')->run($data['company']);
	}
}

在这个例子中,ProfileInspector 是一个顶层检查器,用于检查重复项,如果找到,则立即返回。否则,它将继续验证其他详细信息。它的依赖项是 PersonInspectorCompanyInspector,将检查数据子集,例如。

class CompanyInspector extends Checkpoint\Inspector
{
	protected function validate($data)
	{
		$this->define('zipCode', 'Please enter a valid zipcode for the US')
			 ->postalCode('US');

		$this
			->check('name', $data['name'], ['notBlank'])
			->check('address', $data['address'], ['notBlank'])
			->check('city', $data['city'], ['notBlank'])
			->check('state', $data['state'], ['notBlank'])
			->check('zipCode', $data['zipCode'], ['zipCode'])
		;
	}
}

类似地,PersonInspector 将只关注检查/记录与人员相关数据的错误。一旦您拥有所有三个对象,就可以执行以下操作:

$profile_inspector = new ProfileInspector(new PeopleRepository, new PersonInspector, new CompanyInspector);
$profile_inspector->run([
	'person' => [
		'firstName' => 'Matthew',
		'lastName'  => 'Sahagian'
	],
	'company' => [
		'name'    => 'Imarc LLC',
		'address' => '111 1/2 Cooper Street',
		'city'    => 'Santa Cruz',
		'state'   => 'CA',
		'zipCode' => '95060'
	]
]);

if ($profile_inspector->countMessages()) {
	throw new Checkpoint\ValidationException('Please correct the errors below.');
}

请记住,您提交的数据可能是以 POST 数据的形式直接通过表单或对象提交的,因此实际使用不会如此冗长。此外,递归依赖注入器将有助于正确填充依赖项。检查公司的邮编消息可能如下所示:

<label>Company Name</label>
{% include '@messaging/errors.html' with {'errors': inspector.messages('company.name')} %}
<input type="text" name="company[name]" value="{{ params.company.name }}" />

您也可以在顶部检查重复项,以提供更明显的警告。

{% if inspector.messages('duplicate') %}
	<div class="error">
		<p>
			The e-mail you are trying to use is already taken by another person in our system.  If you are the
			owner of that account, you may want to try <a href="/forgot-password">recovering your account</a>.
		</p>
	</div>
{% endif %}

结论

Checkpoint 的编写是为了解决其他验证系统中发现的大部分不灵活性。添加子检查器的功能意味着您可以为简单对象聚合多个检查器,并将它们组合在一起以进行更复杂的验证,例如注册表单可能实际上代表多个域模型数据结构。检查器可以从底部(单个字段)编写和组合到顶部(复杂的表单)。

通过封装验证逻辑和使用一些干净的辅助方法进行实际逻辑,可以执行更复杂的验证,同时将特定域模型的规则保留在一个地方,而不是将简单的检查写入配置,并将更复杂的检查写入单独注册的辅助程序或作为单独的规则对象。

Repsect/Validation 提供了许多默认规则,因此简单的验证仍然很简单。有关 Respect/Validation 的更多信息,请参阅 他们的 GitHub 仓库

代码检查和测试

运行分析

php vendor/bin/phpstan -l7 analyse src/

运行测试

php vendor/bin/phpunit --bootstrap vendor/autoload.php test/cases