{% raw %}

# Symfony 保留表单值

> 原文： [http://zetcode.com/symfony/keepformvalues/](http://zetcode.com/symfony/keepformvalues/)

Symfony 保留表单值教程展示了在表单提交失败后如何在表单提交后保持表单值。 在本教程中，我们进行传统的表单提交； 我们不使用表单构建器。

## Symfony

Symfony 是一组可重用的 PHP 组件和一个用于 Web 项目的 PHP 框架。 Symfony 于 2005 年发布为免费软件。Symfony 的原始作者是 Fabien Potencier。 Symfony 受到 Spring 框架的极大启发。

## 保留表单值

用户提交表单后，将由应用进行验证。 当验证失败时，应用将用户重定向回表单，显示验证错误。 最好将已输入的值保留在表格中。

## Symfony 保留表单值示例

在示例中，我们有一个简单的表单，其中包含两个字段：名称和电子邮件。 提交表单后，我们检查 CSRF 保护并使用 Symfony 的`Validator`验证输入值。 我们将输入的值存储到会话中，以在提交失败时取回它们。

### 建立应用

我们首先使用`composer`建立应用。

```php
$ composer create-project symfony\skeleton formkeepvals
$ cd formkeepvals

```

我们创建一个新的 Symfony 骨架项目，然后进入新创建的项目目录。

```php
$ composer require twig annot validator

```

我们安装了三个基本的 Symfony 包：`twig`，`annot`和`validator`。 包可能具有别名。 例如，`symfony/validator`具有两个别名：`validator`和`validation`。 有关更多详细信息，请检查 [Symfony 食谱服务器](https://flex.symfony.com/)。

```php
$ composer require symfony/security-csrf
$ composer require symfony/monolog-bundle

```

跨站点请求伪造需要`security-csrf`包，而日志记录则需要`monolog-bundle`包。

```php
$ composer require symfony/property-access

```

我们安装了`PropertyAccess`组件，该组件用于方便读取和写入对象和数组的属性/键。

```php
$ composer require maker server --dev

```

我们安装制造商组件和开发服务器。

```php
$ php bin/console make:controller HomeController

```

我们创建一个`HomeController`。 控制器将表单发送给客户端。

`src/Controller/HomeController.php`

```php
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class HomeController extends AbstractController
{
    /**
     * @Route("/home", name="home")
     */
    public function index()
    {
        return $this->render('home/index.html.twig');
    }
}

```

这是一个简单的控制器，可将包含 Web 表单的视图发送给用户。

```php
$ php bin/console make:controller MessageController

```

我们创建一个`MessageController`来响应表单提交。

`src/Controller/MessageController.php`

```php
<?php

namespace App\Controller;

use App\Service\ValidationService;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class MessageController extends AbstractController
{
    /**
     * @Route("/message", name="message")
     */
    public function index(Request $request, ValidationService $validator)
    {
        $token = $request->get("token");

        $valid = $validator->validateToken($token);

        if (!$valid) {

            return new Response("Operation not allowed", Response::HTTP_BAD_REQUEST,
                ['content-type' => 'text/plain']);
        }

        $name = $request->request->get("name");
        $email = $request->request->get("email");

        $input = ['name' => $name, 'email' => $email];

        $errorMessages = $validator->validateInput($input);

        if (count($errorMessages) > 0)
        {
            $session = $request->getSession();
            $session->set('name', $name);
            $session->set('email', $email);

            foreach ($errorMessages as $key => $val) {
                $this->addFlash($key, $val);
            }

            return $this->redirectToRoute('home');

        } else {

            return new Response("User saved", Response::HTTP_OK,
                ['content-type' => 'text/plain']);
        }
    }
}

```

在`MessageController`中，我们检查 CSRF 令牌，验证表单输入值，并将响应发送回客户端。

```php
public function index(Request $request, ValidationService $validator)
{

```

验证委派给`ValidationService`，后者被注入到方法中。

```php
$token = $request->get("token");

$valid = $validator->validateToken($token);

if (!$valid) {

    return new Response("Operation not allowed", Response::HTTP_BAD_REQUEST,
        ['content-type' => 'text/plain']);
}

```

我们获得 CSRF 令牌并对其进行验证。 如果验证失败，我们将带有错误消息的响应发送回客户端。

```php
$name = $request->request->get("name");
$email = $request->request->get("email");

$input = ['name' => $name, 'email' => $email];

$errorMessages = $validator->validateInput($input);

```

我们检索表单输入值，并使用验证服务对其进行验证。 如果验证服务失败，它将返回错误消息。

```php
if (count($errorMessages) > 0)
{
    $session = $request->getSession();
    $session->set('name', $name);
    $session->set('email', $email);
...

```

如果有一些错误消息，我们将输入值添加到会话中，以便我们可以在重定向后检索它们。

```php
foreach ($errorMessages as $key => $val) {
    $this->addFlash($key, $val);
}

```

我们将消息添加到 Flash 包中； 闪存袋用于存储临时消息，例如我们的验证消息。

```php
return $this->redirectToRoute('home');

```

我们使用`redirectToRoute()`重定向回表单。

`src/Service/ValidationService.php`

```php
<?php

namespace App\Service;

use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;

class ValidationService
{
    private $tokenManager;
    private $validator;
    private $accessor;
    private $logger;

    public function __construct(CsrfTokenManagerInterface $tokenManager,
        ValidatorInterface $validator, PropertyAccessorInterface $accessor,
        LoggerInterface $logger)
    {
        $this->tokenManager = $tokenManager;
        $this->validator = $validator;
        $this->accessor = $accessor;
        $this->logger = $logger;
    }

    public function validateToken($token): bool
    {
        $csrf_token = new CsrfToken('myform', $token);

        $isValid = $this->tokenManager->isTokenValid($csrf_token);

        if (!$isValid) {
            $this->logger->error("CSRF failure");
        }

        return $isValid;
    }

    public function validateInput(array $input): array
    {
        $constraints = new Assert\Collection([
            'name' => [new Assert\Length(['min' => 2]), new Assert\NotBlank],
            'email' => [new Assert\Email, new Assert\NotBlank],
        ]);

        $violations = $this->validator->validate($input, $constraints);

        if (count($violations) > 0) {

            $this->logger->info("Validation failed");

            $messages = [];

            foreach ($violations as $violation) {

                $this->accessor->setValue($messages,
                    $violation->getPropertyPath(),
                    $violation->getMessage());
            }

            return $messages;
        } else {

            return [];
        }
    }
}

```

`ValidationService`检查 CSRF 令牌并验证输入。

```php
public function __construct(CsrfTokenManagerInterface $tokenManager,
    ValidatorInterface $validator, PropertyAccessorInterface $accessor,
    LoggerInterface $logger)
{
    $this->tokenManager = $tokenManager;
    $this->validator = $validator;
    $this->accessor = $accessor;
    $this->logger = $logger;
}

```

我们在构造器中注入了四个对象：令牌管理器，验证器，属性访问器和记录器。

```php
public function validateToken($token): bool
{
    $csrf_token = new CsrfToken('myform', $token);

    $isValid = $this->tokenManager->isTokenValid($csrf_token);

    if (!$isValid) {
        $this->logger->error("CSRF failure");
    }

    return $isValid;
}

```

此代码使用令牌管理器验证 CSRF 令牌。

```php
$constraints = new Assert\Collection([
    'name' => [new Assert\Length(['min' => 2]), new Assert\NotBlank],
    'email' => [new Assert\Email, new Assert\NotBlank],
]);

```

这些是验证表单输入的约束。

```php
$violations = $this->validator->validate($input, $constraints);

```

使用验证器，我们可以验证表单输入值。

```php
if (count($violations) > 0) {

    $this->logger->info("Validation failed");

    $messages = [];

    foreach ($violations as $violation) {

        $this->accessor->setValue($messages,
            $violation->getPropertyPath(),
            $violation->getMessage());
    }

    return $messages;
} else {

    return [];
}

```

如果存在一些违规行为，我们将记录故障并生成验证错误消息。 为了构建消息，我们利用 Symfony 属性访问器。 如果没有违规，我们将返回一个空数组。

`templates/home/index.html.twig`

```php
{% extends 'base.html.twig' %}

{% block title %}Home page{% endblock %}

{% block stylesheets %}
<style>
    .topmargin {
        margin-top: 10px;
    }
</style>
{% endblock %}

{% block body %}

<section class="ui container topmargin">

    <form class="ui form" action="message" method="post">

        <input type="hidden" name="token" value="{{ csrf_token('myform') }}" />

        {% for msg in app.flashes('name') %}
        <div class="ui small red message">
            {{ msg }}
        </div>
        {% endfor %}

        <div class="field">
            <label>Name:</label>
            <input type="text" name="name" value="{{app.session.get('name')}}">
        </div>

        {% for msg in app.flashes('email') %}
        <div class="ui small red message">
            {{ msg }}
        </div>
        {% endfor %}

        <div class="field">
            <label>Email</label>
            <input type="text" name="email" , value="{{app.session.get('email')}}">
        </div>

        <button class="ui button" type="submit">Send</button>

    </form>

</section>

{% endblock %}

```

主页上有一个表格。 该表格包含两个字段：姓名和电子邮件。

```php
<input type="hidden" name="token" value="{{ csrf_token('myform') }}" />

```

它还包含一个隐藏字段，以防止跨站点请求伪造。

```php
{% for msg in app.flashes('name') %}
<div class="ui small red message">
    {{ msg }}
</div>
{% endfor %}

```

如果闪存包中有一些错误消息，我们将显示它们。

```php
<input type="text" name="name" value="{{app.session.get('name')}}">

```

输入标签从会话中检索其值（如果有）。 重定向到表单后，这很有用。

`templates/base.html.twig`

```php
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        <link href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css"
                 rel="stylesheet">
        {% block stylesheets %}{% endblock %}
    </head>
    <body>
        {% block body %}{% endblock %}

        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.3.1/semantic.min.js"></script>
        {% block javascripts %}{% endblock %}
    </body>
</html>

```

这是基本的 Twig 模板。 它包含语义 UI CSS 框架。

在本教程中，我们验证了 Symfony 应用中的简单表单。

您可能也对以下相关教程感兴趣： [Symfony 简介](/symfony/intro/)， [Symfony 服务教程](/symfony/service/)， [Symfony Flash 消息](/symfony/flash/)， [Symfony 表单教程](/symfony/form/) ， [PHP 教程](/lang/php/)或列出[所有 Symfony 教程](/all/#symfony)。

{% endraw %}