数据验证

在您保存数据之前,您可能希望确保数据正确且一致。在 CakePHP 中,我们有两个阶段的验证

  1. 在请求数据被转换为实体之前,可以应用围绕数据类型和格式的验证规则。

  2. 在数据保存之前,可以应用域或应用程序规则。这些规则有助于确保您的应用程序的数据保持一致。

构建实体前验证数据

将数据编组到实体时,您可以验证数据。验证数据允许您检查数据的类型、形状和大小。默认情况下,请求数据将在被转换为实体之前进行验证。如果任何验证规则失败,返回的实体将包含错误。包含错误的字段将不会出现在返回的实体中

$article = $articles->newEntity($this->request->getData());
if ($article->getErrors()) {
    // Entity failed validation.
}

在使用启用了验证的实体时,会发生以下情况

  1. 创建验证器对象。

  2. 附加tabledefault 验证提供者。

  3. 调用命名的验证方法。例如,validationDefault

  4. 将触发Model.buildValidator 事件。

  5. 将验证请求数据。

  6. 将请求数据类型转换为与列类型匹配的类型。

  7. 将错误设置到实体中。

  8. 将有效数据设置到实体中,而验证失败的字段将被排除在外。

如果您希望在转换请求数据时禁用验证,请将validate 选项设置为 false

$article = $articles->newEntity(
    $this->request->getData(),
    ['validate' => false]
);

patchEntity() 方法也是如此

$article = $articles->patchEntity($article, $newData, [
    'validate' => false
]);

创建默认验证集

为了方便起见,验证规则在 Table 类中定义。这定义了与数据将要保存的位置相关的应该验证哪些数据。

要在您的表中创建默认验证对象,请创建validationDefault() 函数

use Cake\ORM\Table;
use Cake\Validation\Validator;

class ArticlesTable extends Table
{
    public function validationDefault(Validator $validator): Validator
    {
        $validator
            ->requirePresence('title', 'create')
            ->notEmptyString('title');

        $validator
            ->allowEmptyString('link')
            ->add('link', 'valid-url', ['rule' => 'url']);

        ...

        return $validator;
    }
}

可用的验证方法和规则来自Validator 类,并在创建验证器部分中进行了说明。

注意

验证对象主要用于验证用户输入,即表单和任何其他已发布的请求数据。

使用不同的验证集

除了禁用验证外,您还可以选择要应用的验证规则集

$article = $articles->newEntity(
    $this->request->getData(),
    ['validate' => 'update']
);

上述操作将在表实例上调用validationUpdate() 方法以构建所需的规则。默认情况下,将使用validationDefault() 方法。我们的文章表的示例验证器将是

class ArticlesTable extends Table
{
    public function validationUpdate($validator)
    {
        $validator
            ->notEmptyString('title', __('You need to provide a title'))
            ->notEmptyString('body', __('A body is required'));

        return $validator;
    }
}

您可以根据需要创建任意数量的验证集。有关构建验证规则集的更多信息,请参见验证章节

为关联使用不同的验证集

还可以为每个关联定义验证集。在使用newEntity()patchEntity() 方法时,您可以将额外的选项传递给要转换的每个关联

$data = [
     'title' => 'My title',
     'body' => 'The text',
     'user_id' => 1,
     'user' => [
         'username' => 'mark',
     ],
     'comments' => [
         ['body' => 'First comment'],
         ['body' => 'Second comment'],
     ],
 ];

 $article = $articles->patchEntity($article, $data, [
     'validate' => 'update',
     'associated' => [
         'Users' => ['validate' => 'signup'],
         'Comments' => ['validate' => 'custom'],
     ],
 ]);

组合验证器

由于验证器对象的构建方式,您可以将它们的构造过程分解成多个可重复使用的步骤

// UsersTable.php

public function validationDefault(Validator $validator): Validator
{
    $validator->notEmptyString('username');
    $validator->notEmptyString('password');
    $validator->add('email', 'valid-email', ['rule' => 'email']);
    ...

    return $validator;
}

public function validationHardened(Validator $validator): Validator
{
    $validator = $this->validationDefault($validator);

    $validator->add('password', 'length', ['rule' => ['lengthBetween', 8, 100]]);

    return $validator;
}

鉴于上述设置,在使用hardened 验证集时,它还将包含在default 集中声明的验证规则。

验证提供者

验证规则可以使用在任何已知提供者上定义的函数。默认情况下,CakePHP 设置了一些提供者

  1. 表类或其行为上的方法在table 提供者中可用。

  2. 核心Cake\Validation\Validation 类被设置为default 提供者。

创建验证规则时,可以命名该规则的提供者。例如,如果您的表具有isValidRole 方法,您可以将其用作验证规则

use Cake\ORM\Table;
use Cake\Validation\Validator;

class UsersTable extends Table
{
    public function validationDefault(Validator $validator): Validator
    {
        $validator
            ->add('role', 'validRole', [
                'rule' => 'isValidRole',
                'message' => __('You need to provide a valid role'),
                'provider' => 'table',
            ]);

        return $validator;
    }

    public function isValidRole($value, array $context): bool
    {
        return in_array($value, ['admin', 'editor', 'author'], true);
    }

}

您也可以对验证规则使用闭包

$validator->add('name', 'myRule', [
    'rule' => function ($value, array $context) {
        if ($value > 1) {
            return true;
        }

        return 'Not a good value.';
    }
]);

验证方法可以在失败时返回错误消息。这是一种简单的方法,可以根据提供的值动态生成错误消息。

从表中获取验证器

在您的表类中创建了一些验证集之后,您可以按名称获取结果对象

$defaultValidator = $usersTable->getValidator('default');

$hardenedValidator = $usersTable->getValidator('hardened');

默认验证器类

如上所述,默认情况下,验证方法接收Cake\Validation\Validator 的实例。相反,如果您希望每次都使用自定义验证器的实例,可以使用表的$_validatorClass 属性

// In your table class
public function initialize(array $config): void
{
    $this->_validatorClass = \FullyNamespaced\Custom\Validator::class;
}

应用应用程序规则

虽然基本数据验证是在将请求数据转换为实体时完成的,但许多应用程序还有更复杂的验证,这些验证应该只在基本验证完成后应用。

验证确保您的数据的形式或语法正确,而规则侧重于将数据与应用程序或网络的现有状态进行比较。

这些类型的规则通常被称为“域规则”或“应用程序规则”。CakePHP 通过“规则检查器”公开此概念,这些检查器在实体持久化之前应用。一些示例应用程序规则是

  • 确保电子邮件唯一性

  • 状态转换或工作流步骤,例如,更新发票的状态。

  • 防止修改软删除项目。

  • 执行使用情况/速率限制上限。

在调用 Table 的save()delete() 方法时,会检查应用程序规则。

创建规则检查器

规则检查器类通常由您表类中的buildRules() 方法定义。行为和其他事件订阅者可以使用Model.buildRules 事件来增强给定 Table 类的规则检查器

use Cake\ORM\RulesChecker;

// In a table class
public function buildRules(RulesChecker $rules): RulesChecker
{
    // Add a rule that is applied for create, update and delete operations
    $rules->add(function ($entity, $options) {
        // Return a boolean to indicate pass/failure
    }, 'ruleName');

    // Add a rule for create.
    $rules->addCreate(function ($entity, $options) {
        // Return a boolean to indicate pass/failure
    }, 'ruleName');

    // Add a rule for update
    $rules->addUpdate(function ($entity, $options) {
        // Return a boolean to indicate pass/failure
    }, 'ruleName');

    // Add a rule for the deleting.
    $rules->addDelete(function ($entity, $options) {
        // Return a boolean to indicate pass/failure
    }, 'ruleName');

    return $rules;
}

您的规则函数可以预期获得正在检查的实体和选项数组。选项数组将包含errorFieldmessagerepositoryrepository 选项将包含附加规则的表类。由于规则接受任何callable,您也可以使用实例函数

$rules->addCreate([$this, 'uniqueEmail'], 'uniqueEmail');

或可调用类

$rules->addCreate(new IsUnique(['email']), 'uniqueEmail');

在添加规则时,您可以定义规则所属的字段以及错误消息作为选项

$rules->add([$this, 'isValidState'], 'validState', [
    'errorField' => 'status',
    'message' => 'This invoice cannot be moved to that status.'
]);

调用实体的 getErrors() 方法时,错误将可见

$entity->getErrors(); // Contains the domain rules error messages

创建唯一字段规则

由于唯一规则非常常见,CakePHP 包含一个简单的规则类,允许您定义唯一的字段集

use Cake\ORM\Rule\IsUnique;

// A single field.
$rules->add($rules->isUnique(['email']));

// A list of fields
$rules->add($rules->isUnique(
    ['username', 'account_id'],
    'This username & account_id combination has already been used.'
));

在设置外键字段上的规则时,请务必记住,规则中只使用列出的字段。唯一规则集将通过 find('all') 找到。这意味着设置 $user->account->id 不会触发上述规则。

许多数据库引擎允许 NULL 在 UNIQUE 索引中成为唯一值。要模拟此行为,请将 allowMultipleNulls 选项设置为 true

$rules->add($rules->isUnique(
    ['username', 'account_id'],
    ['allowMultipleNulls' => true]
));

外键规则

虽然您可以依靠数据库错误来强制约束,但使用规则代码可以帮助提供更好的用户体验。因此,CakePHP 包含一个 ExistsIn 规则类

// A single field.
$rules->add($rules->existsIn('article_id', 'Articles'));

// Multiple keys, useful for composite primary keys.
$rules->add($rules->existsIn(['site_id', 'article_id'], 'Articles'));

在相关表中检查是否存在字段必须是主键的一部分。

当您的复合外键的可空部分为 null 时,您可以强制 existsIn 传递

// Example: A composite primary key within NodesTable is (parent_id, site_id).
// A Node may reference a parent Node but does not need to. In latter case, parent_id is null.
// Allow this rule to pass, even if fields that are nullable, like parent_id, are null:
$rules->add($rules->existsIn(
    ['parent_id', 'site_id'], // Schema: parent_id NULL, site_id NOT NULL
    'ParentNodes',
    ['allowNullableNulls' => true]
));

// A Node however should in addition also always reference a Site.
$rules->add($rules->existsIn(['site_id'], 'Sites'));

在大多数 SQL 数据库中,多列 UNIQUE 索引允许多个 null 值存在,因为 NULL 不等于自身。虽然允许多个 null 值是 CakePHP 的默认行为,但您可以使用 allowMultipleNulls 在唯一检查中包含 null 值

// Only one null value can exist in `parent_id` and `site_id`
$rules->add($rules->existsIn(
    ['parent_id', 'site_id'],
    'ParentNodes',
    ['allowMultipleNulls' => false]
));

关联计数规则

如果您需要验证属性或关联是否包含正确数量的值,可以使用 validCount() 规则

// In the ArticlesTable.php file
// No more than 5 tags on an article.
$rules->add($rules->validCount('tags', 5, '<=', 'You can only have 5 tags'));

在定义基于计数的规则时,第三个参数允许您定义要使用的比较运算符。 ==, >=, <=, >, <, 和 != 是可接受的运算符。要确保属性的计数在一定范围内,请使用两个规则

// In the ArticlesTable.php file
// Between 3 and 5 tags
$rules->add($rules->validCount('tags', 3, '>=', 'You must have at least 3 tags'));
$rules->add($rules->validCount('tags', 5, '<=', 'You must have at most 5 tags'));

请注意,如果属性不可数或不存在,validCount 将返回 false

// The save operation will fail if tags is null.
$rules->add($rules->validCount('tags', 0, '<=', 'You must not have any tags'));

将实体方法用作规则

您可能希望将实体方法用作域规则

$rules->add(function ($entity, $options) {
    return $entity->isOkLooking();
}, 'ruleName');

使用条件规则

您可能希望根据实体数据有条件地应用规则

$rules->add(function ($entity, $options) use($rules) {
    if ($entity->role == 'admin') {
        $rule = $rules->existsIn('user_id', 'Admins');

        return $rule($entity, $options);
    }
    if ($entity->role == 'user') {
        $rule = $rules->existsIn('user_id', 'Users');

        return $rule($entity, $options);
    }

    return false;
}, 'userExists');

条件/动态错误消息

规则,无论是 自定义可调用对象,还是 规则对象,都可以返回一个布尔值,指示它们是否通过,或者它们可以返回一个字符串,这意味着规则未通过,并且应将返回的字符串用作错误消息。

通过 message 选项定义的可能存在的错误消息将被规则返回的消息覆盖

$rules->add(
    function ($entity, $options) {
        if (!$entity->length) {
            return false;
        }

        if ($entity->length < 10) {
            return 'Error message when value is less than 10';
        }

        if ($entity->length > 20) {
            return 'Error message when value is greater than 20';
        }

        return true;
    },
    'ruleName',
    [
        'errorField' => 'length',
        'message' => 'Generic error message used when `false` is returned',
    ]
 );

注意

请注意,为了使返回的消息实际使用,您必须还提供 errorField 选项,否则规则将默默地失败,即没有在实体上设置错误消息!

创建自定义可重用规则

您可能希望重用自定义域规则。您可以通过创建自己的可调用规则来实现这一点

// Using a custom rule of the application
use App\ORM\Rule\IsUniqueWithNulls;
// ...
public function buildRules(RulesChecker $rules): RulesChecker
{
    $rules->add(new IsUniqueWithNulls(['parent_id', 'instance_id', 'name']), 'uniqueNamePerParent', [
        'errorField' => 'name',
        'message' => 'Name must be unique per parent.',
    ]);

    return $rules;
}

有关如何创建此类规则的示例,请参阅核心规则。

创建自定义规则对象

如果您的应用程序包含经常重复使用的规则,将这些规则打包到可重用类中将非常有用

// in src/Model/Rule/CustomRule.php
namespace App\Model\Rule;

use Cake\Datasource\EntityInterface;

class CustomRule
{
    public function __invoke(EntityInterface $entity, array $options)
    {
        // Do work
        return false;
    }
}

// Add the custom rule
use App\Model\Rule\CustomRule;

$rules->add(new CustomRule(/* ... */), 'ruleName');

通过创建自定义规则类,您可以保持代码 DRY 并独立测试您的域规则。

禁用规则

在保存实体时,您可以根据需要禁用规则

$articles->save($article, ['checkRules' => false]);

验证与应用程序规则

CakePHP ORM 的独特之处在于它使用两层方法进行验证。

第一层是验证。验证规则旨在以无状态的方式运行。它们最适合确保数据的形状、数据类型和格式正确。

第二层是应用程序规则。应用程序规则最适合检查实体的带状态属性。例如,验证规则可以确保电子邮件地址有效,而应用程序规则可以确保电子邮件地址唯一。

正如您已经发现的那样,第一层是在调用 newEntity()patchEntity() 时通过 Validator 对象完成的

$validatedEntity = $articlesTable->newEntity(
    $unsafeData,
    ['validate' => 'customName']
);
$validatedEntity = $articlesTable->patchEntity(
    $entity,
    $unsafeData,
    ['validate' => 'customName']
);

在上面的示例中,我们将使用一个“自定义”验证器,它使用 validationCustomName() 方法定义

public function validationCustomName($validator)
{
    $validator->add(
        // ...
    );

    return $validator;
}

验证假定传递了字符串或数组,因为这是从任何请求中接收到的内容

// In src/Model/Table/UsersTable.php
public function validatePasswords($validator)
{
    $validator->add('confirm_password', 'no-misspelling', [
        'rule' => ['compareWith', 'password'],
        'message' => 'Passwords are not equal',
    ]);

    // ...

    return $validator;
}

直接在实体上设置属性时不会触发验证

$userEntity->email = 'not an email!!';
$usersTable->save($userEntity);

在上面的示例中,实体将被保存,因为验证只针对 newEntity()patchEntity() 方法触发。第二级验证旨在解决这种情况。

如上所述,应用程序规则将在每次调用 save()delete() 时进行检查

// In src/Model/Table/UsersTable.php
public function buildRules(RulesChecker $rules): RulesChecker
{
    $rules->add($rules->isUnique(['email']));

    return $rules;
}

// Elsewhere in your application code
$userEntity->email = '[email protected]';
$usersTable->save($userEntity); // Returns false

虽然验证适用于直接的用户输入,但应用程序规则专用于应用程序内部生成的数据转换

// In src/Model/Table/OrdersTable.php
public function buildRules(RulesChecker $rules): RulesChecker
{
    $check = function($order) {
        if ($order->shipping_mode !== 'free') {
            return true;
        }

        return $order->price >= 100;
    };
    $rules->add($check, [
        'errorField' => 'shipping_mode',
        'message' => 'No free shipping for orders under 100!',
    ]);

    return $rules;
}

// Elsewhere in application code
$order->price = 50;
$order->shipping_mode = 'free';
$ordersTable->save($order); // Returns false

将验证用作应用程序规则

在某些情况下,您可能希望对用户生成的数据和应用程序内部生成的数据运行相同的數據验证例程。这可能出现在运行直接在实体上设置属性的 CLI 脚本时

// In src/Model/Table/UsersTable.php
public function validationDefault(Validator $validator): Validator
{
    $validator->add('email', 'valid_email', [
        'rule' => 'email',
        'message' => 'Invalid email',
    ]);

    // ...

    return $validator;
}

public function buildRules(RulesChecker $rules): RulesChecker
{
    // Add validation rules
    $rules->add(function($entity) {
        $data = $entity->extract($this->getSchema()->columns(), true);
        if (!$entity->isNew() && !empty($data)) {
            $data += $entity->extract((array)$this->getPrimaryKey());
        }
        $validator = $this->getValidator('default');
        $errors = $validator->validate($data, $entity->isNew());
        $entity->setErrors($errors);

        return empty($errors);
    });

    // ...

    return $rules;
}

执行后,由于添加了新的应用程序规则,保存将失败

$userEntity->email = 'not an email!!!';
$usersTable->save($userEntity);
$userEntity->getError('email'); // Invalid email

使用 newEntity()patchEntity() 时可以预期相同的结果

$userEntity = $usersTable->newEntity(['email' => 'not an email!!']);
$userEntity->getError('email'); // Invalid email

删除规则

如果您需要从 RulesChecker 中删除规则,请使用 remove 方法

// Remove a general rule by name
$rules->remove('ruleName');

// Remove a create rule
$rules->removeCreate('ruleName');

// Remove an update rule
$rules->removeUpdate('ruleName');

// Remove a delete rule
$rules->removeDelete('ruleName');

在版本 5.1.0 中添加。