验证(Validation)

验证是在Web应用程序中一种很常见的任务。在表单中输入数据时需要被验证。数据还需要在写入到数据库或传送到Web服务时进行验证。

Symfony 配备了一个Validator 组件,它让校验工作变得简单易懂。该组件是基于JSR303 Bean校验规范

验证的基础知识 

了解验证的最好方法就是看他的实际应用。开始,假设在你的应用程序中创建了一个原始的php对象:

1
2
3
4
5
6
7
// src/AppBundle/Entity/Author.php
namespace AppBundle\Entity;
 
class Author
{
    public $name;
}

到目前为止,这只是给你程序提供某些用途的普通类。验证的目的就是要告诉你,一个对象的数据是有效的。为了这个目的,你需要配置一个对象必须遵守规则列表(调用 约束条件constraints)来让自己的数据合法。这些规则可以通过许多不同的格式(YAML,XML、注释或PHP)来指定。

比如,我们保证属性$name不能为空,来添加下面的规则:

1
2
3
4
5
6
7
8
9
10
11
12
// src/AppBundle/Entity/Author.php
 
// ...
use Symfony\Component\Validator\Constraints as Assert;
 
class Author
{
    /**
     * @Assert\NotBlank()
     */
    public $name;
}
1
2
3
4
5
# src/AppBundle/Resources/config/validation.yml
AppBundle\Entity\Author:
    properties:
        name:
            - NotBlank: ~
1
2
3
4
5
6
7
8
9
10
11
12
<!-- src/AppBundle/Resources/config/validation.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
 
    <class name="AppBundle\Entity\Author">
        <property name="name">
            <constraint name="NotBlank" />
        </property>
    </class>
</constraint-mapping>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/AppBundle/Entity/Author.php
 
// ...
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Constraints\NotBlank;
 
class Author
{
    public $name;
 
    public static function loadValidatorMetadata(ClassMetadata $metadata)
    {
        $metadata->addPropertyConstraint('name', new NotBlank());
    }
}

Protected和private属性以及“getter”方法也都可以被校验。(请查看,约束的投放范围)

使用验证服务 

接下来,使用validator服务(Validator类)的validate方法来真正的校验Author对象。 validator的工作很简单:读取一个类的约束规则来校验一个对象的数据是否符合这些规则约束。如果校验失败,一个错误列表(ConstraintViolationList类)将被返回。现在我们在一个控制器中来执行它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ...
use Symfony\Component\HttpFoundation\Response;
use AppBundle\Entity\Author;
 
// ...
public function authorAction()
{
    $author = new Author();
 
    // ... do something to the $author object
 
    $validator = $this->get('validator');
    $errors = $validator->validate($author);
 
    if (count($errors) > 0) {
        /*
         * Uses a __toString method on the $errors variable which is a
         * ConstraintViolationList object. This gives us a nice string
         * for debugging.
         */
        $errorsString = (string) $errors;
 
        return new Response($errorsString);
    }
 
    return new Response('The author is valid! Yes!');
}

如果$name属性是空的,你会看到一个错误信息:

1
2
AppBundle\Author.name:
    This value should not be blank

如果你为name属性插入一个值,那么你会获得让你高兴的成功信息。

大多数时候,你不需要直接跟validator服务交流或者根本不需要担心打印出来的错误信息。大多数情况下,你将在处理提交表单数据时,间接使用校验。想了解更多请阅读(验证与表单).

你也可以传递一个错误信息集合到一个模版:

1
2
3
4
5
if (count($errors) > 0) {
    return $this->render('author/validation.html.twig', array(
        'errors' => $errors,
    ));
}

在模版中,你可以根据需要精确的输出错误列表:

1
2
3
4
5
6
7
{# app/Resources/views/author/validation.html.twig #}
<h3>The author has the following errors</h3>
<ul>
{% for error in errors %}
    <li>{{ error.message }}</li>
{% endfor %}
</ul>
1
2
3
4
5
6
7
<!-- app/Resources/views/author/validation.html.php -->
<h3>The author has the following errors</h3>
<ul>
<?php foreach ($errors as $error): ?>
    <li><?php echo $error->getMessage() ?></li>
<?php endforeach ?>
</ul>

每个验证错误(都称为“约束冲突”),由一个ConstraintViolation对象代表。

验证和表单 

validator服务可以在任何时候使用,来验证任何对象。 事实上,你将经常在处理表单时,间接使用validator。Symfony的表单类库间接使用validator服务来在数据被提交和绑定后验证底层对象。对象违反约束信息将被转化到FormError 对象,该对象可以很容易的被展示在你的表单中。在一个控制器中看看的传统表单提交流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ...
use AppBundle\Entity\Author;
use AppBundle\Form\AuthorType;
use Symfony\Component\HttpFoundation\Request;
 
// ...
public function updateAction(Request $request)
{
    $author = new Author();
    $form = $this->createForm(AuthorType::class, $author);
 
    $form->handleRequest($request);
 
    if ($form->isValid()) {
        // the validation passed, do something with the $author object
 
        return $this->redirectToRoute(...);
    }
 
    return $this->render('author/form.html.twig', array(
        'form' => $form->createView(),
    ));
}

该实例使用一个 AuthorType表单类,更多信息请查看表单章。

配置 

Symfony的验证默认情况下是启用的。但是如果你使用了注释方法来指定你的约束,那么你需要显式的开启注释功能:

1
2
3
# app/config/config.yml
framework:
    validation: { enable_annotations: true }
1
2
3
4
5
6
7
8
9
10
11
12
<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:framework="http://symfony.com/schema/dic/symfony"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
 
    <framework:config>
        <framework:validation enable-annotations="true" />
    </framework:config>
</container>
1
2
3
4
5
6
// app/config/config.php
$container->loadFromExtension('framework', array(
    'validation' => array(
        'enable_annotations' => true,
    ),
));

约束规则 

Validator的设计目的是用来按照约束规则验证对象。为了验证一个对象,只需要映射一个或者多个约束到它要验证的类,然后把它传递给validator服务即可。

本质上,一个约束就是一个简单的PHP对象,它可以生成一个决断语句。 在现实生活中,一个约束可以是”蛋糕不能烤焦了” 这样的规则约束。在Symfony中,约束都差不多:他们断言某个条件是否成立。给定一个值,约束会告诉你这个值是否遵守了你的约束规则。

下列可以满足多数需求的约束,在Symfony中原生可用。

基本约束 

这些是基本的约束:使用它们来断言属性值相关的非常基础的东西,或者断言你程序中的方法之返回值。

字符串约束 

数字约束 

比较约束 

日期约束 

Collection约束 

文件约束 

财务数字约束 

其他约束 

你也可以创建自己的自定义约束。请查看“如何创建一个自定义的验证约束”。

约束的配置 

一些约束,比如NotBlank,很简单,但是其它的比如Choice约束,有许多配置项需要设置。假设Author类有另外一个属性,gender可以被设置为”male”、”female”或者“other”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/AppBundle/Entity/Author.php
 
// ...
use Symfony\Component\Validator\Constraints as Assert;
 
class Author
{
    /**
     * @Assert\Choice(
     *     choices = { "male", "female", "other" },
     *     message = "Choose a valid gender."
     * )
     */
    public $gender;
 
    // ...
}
1
2
3
4
5
6
# src/AppBundle/Resources/config/validation.yml
AppBundle\Entity\Author:
    properties:
        gender:
            - Choice: { choices: [male, female, other], message: Choose a valid gender. }
        # ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- src/AppBundle/Resources/config/validation.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
 
    <class name="AppBundle\Entity\Author">
        <property name="gender">
            <constraint name="Choice">
                <option name="choices">
                    <value>male</value>
                    <value>female</value>
                    <value>other</value>
                </option>
                <option name="message">Choose a valid gender.</option>
            </constraint>
        </property>
 
        <!-- ... -->
    </class>
</constraint-mapping>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/AppBundle/Entity/Author.php
 
// ...
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Constraints as Assert;
 
class Author
{
    public $gender;
 
    // ...
 
    public static function loadValidatorMetadata(ClassMetadata $metadata)
    {
        // ...
 
        $metadata->addPropertyConstraint('gender', new Assert\Choice(array(
            'choices' => array('male', 'female', 'other'),
            'message' => 'Choose a valid gender.',
        )));
    }
}

一个约束的选项通常都是通过一个数组来传递的。有些约束也允许你通过指定一个一个值,”default”,用这个配置来代替数组。在Choice约束时,choices选项就可以通过这种方式指定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/AppBundle/Entity/Author.php
 
// ...
use Symfony\Component\Validator\Constraints as Assert;
 
class Author
{
    /**
     * @Assert\Choice({"male", "female", "other"})
     */
    protected $gender;
 
    // ...
}
1
2
3
4
5
6
# src/AppBundle/Resources/config/validation.yml
AppBundle\Entity\Author:
    properties:
        gender:
            - Choice: [male, female, other]
        # ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- src/AppBundle/Resources/config/validation.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
 
    <class name="AppBundle\Entity\Author">
        <property name="gender">
            <constraint name="Choice">
                <value>male</value>
                <value>female</value>
                <value>other</value>
            </constraint>
        </property>
 
        <!-- ... -->
    </class>
</constraint-mapping>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/AppBundle/Entity/Author.php
 
// ...
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Constraints as Assert;
 
class Author
{
    protected $gender;
 
    public static function loadValidatorMetadata(ClassMetadata $metadata)
    {
        // ...
 
        $metadata->addPropertyConstraint(
            'gender',
            new Assert\Choice(array('male', 'female', 'other'))
        );
    }
}

这纯粹是让最常见的配置选项用起来更加简单和快速。

如果你不确定如何指定一个配置,你可以检查api文档或者为了保险起见你还是通过数组配置吧(上面第一种方式)。

约束的投放范围 

约束可以被用于一个类的属性(如,name)或者一个公共的getter方法(如,getFullName)。属性约束最常用也最简单,而公共的getter方法约束则允许你指定一个复杂的约束规则。最后,如果你想验证一个整体的类,类约束适用这个场景。

属性约束 

属性验证一个最常规的验证技术。Symfony允许你校验private,protected或者public属性。下面代码显示如何配置Author对象的$firstName属性至少有3个字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/AppBundle/Entity/Author.php
 
// ...
use Symfony\Component\Validator\Constraints as Assert;
 
class Author
{
    /**
     * @Assert\NotBlank()
     * @Assert\Length(min=3)
     */
    private $firstName;
}
1
2
3
4
5
6
7
# src/AppBundle/Resources/config/validation.yml
AppBundle\Entity\Author:
    properties:
        firstName:
            - NotBlank: ~
            - Length:
                min: 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- src/AppBundle/Resources/config/validation.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
 
    <class name="AppBundle\Entity\Author">
        <property name="firstName">
            <constraint name="NotBlank" />
            <constraint name="Length">
                <option name="min">3</option>
            </constraint>
        </property>
    </class>
</constraint-mapping>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/AppBundle/Entity/Author.php
 
// ...
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Constraints as Assert;
 
class Author
{
    private $firstName;
 
    public static function loadValidatorMetadata(ClassMetadata $metadata)
    {
        $metadata->addPropertyConstraint('firstName', new Assert\NotBlank());
        $metadata->addPropertyConstraint(
            'firstName',
            new Assert\Length(array("min" => 3))
        );
    }
}

Getters约束 

约束也可以应用于一个方法的返回值。Symfony允许你添加一个约束到任何”get”、”is”或者”has”开头的public方法。这种类型的方法被称为“getters”。

该技术的好处是允许你动态的校验你的对象。比如,假设你想确认密码字段不匹配用户的firstname(一个属性)(因为安全原因)。你可以通过创建一个isPasswordLegal 方法,然后强制决断这个方法必须返回true:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/AppBundle/Entity/Author.php
 
// ...
use Symfony\Component\Validator\Constraints as Assert;
 
class Author
{
    /**
     * @Assert\IsTrue(message = "The password cannot match your first name")
     */
    public function isPasswordLegal()
    {
        // ... return true or false
    }
}
1
2
3
4
5
# src/AppBundle/Resources/config/validation.yml
AppBundle\Entity\Author:
    getters:
        passwordLegal:
            - 'IsTrue': { message: 'The password cannot match your first name' }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- src/AppBundle/Resources/config/validation.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
 
    <class name="AppBundle\Entity\Author">
        <getter property="passwordLegal">
            <constraint name="IsTrue">
                <option name="message">The password cannot match your first name</option>
            </constraint>
        </getter>
    </class>
</constraint-mapping>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/AppBundle/Entity/Author.php
 
// ...
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Constraints as Assert;
 
class Author
{
    public static function loadValidatorMetadata(ClassMetadata $metadata)
    {
        $metadata->addGetterConstraint('passwordLegal', new Assert\IsTrue(array(
            'message' => 'The password cannot match your first name',
        )));
    }
}

现在我们创建一个isPasswordLegal()方法,并且包含你需要逻辑:

1
2
3
4
public function isPasswordLegal()
{
    return $this->firstName !== $this->password;
}

眼尖的人可能会注意到getter的前缀(“get”、”is”或者“has”)在映射时被忽略了。这允许你在不改变验证规则的前提下,把一个约束移动到一个具有同名属性上,反之亦然。

类约束 

有一些约束可以应用到被验证的整个类。例如,回调(Callback)类型的约束,就是一个通用的,用于类自身的约束。当类被验证之后,约束所指定的方法将被直接执行,以便提供更多的自定义验证。

总结 

Symfony的validator(验证)是一个强大的工具,它可以被用来保证任何对象数据的合法性。它的强大来源于约束规则,你可以把它们应用于你对象的属性和getter方法。其实,你大多数情况下都是在使用表单时,间接的应用了验证框架,记住它可以被应用于任何地方验证任何对象。

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)