Symfony4.4のフォームで自然数のバリデーションを行う

# はじめに

Symfony4.4のフォームを作成していて、0以上の整数値の入力をするフォームのバリデーションで四苦八苦をしたのでメモです。

VueやReactを使ったViewが主流なので、昔ながらフォームは需要が少ないこともあり、
また、Symfony4での作成例も少ないようですので、フォームの作成部分を詳しく記述してみました。

これ以外でいい方法があればご教示ください。

# TL;DR

## EntityにAnotationでバリデーションをつける

この場合は、Entityで指定されてしまうので、かなりキツめのバリデーションになります。
DBの自由度は残して、フォームだけバリデーションをしたい場合は、次を御覧ください。

```php
namespace App\Entity;

use Symfony\Component\Validator\Constraints as Assert;

class Student
{
    /**
     * @Assert\PositiveOrZero
     */
    protected $student_no;
}
```
※参考文献：[PositiveOrZero](https://symfony.com/doc/4.4/reference/constraints/PositiveOrZero.html)

## フォームでバリデーションをつける

フォームごとに細かくバリデーションを調整したい場合は、こちらがよさそう

```php
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\PositiveOrZero;
use Symfony\Component\Validator\Constraints\NotBlank;

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('student_no', NumberType::class, [
            'constraints' => [
                new PositiveOrZero(),
            ],
        ])
    ;
}

```
※参考文献：[How to Use a Form without a Data Class　の Adding Validation](https://symfony.com/doc/current/form/without_class.html#adding-validation)


# 試行錯誤過程

自然数を入力する番号があればいいので、Taskよりは、生徒番号がよさそうなので、Studentクラスで試行錯誤を行ってみます。

## Student Entityを作成

生徒番号(StudentNo)と名前(Name)のStudentクラスを作成します。

```console
# php bin/console make:entity

 Class name of the entity to create or update (e.g. VictoriousPizza):
 > Student

 created: src/Entity/Student.php
 created: src/Repository/StudentRepository.php

 Entity generated! Now let's add some fields!
 You can always add more fields later manually or by re-running this command.

 New property name (press <return> to stop adding fields):
 > studentNo

 Field type (enter ? to see all types) [string]:
 > integer

 Can this field be null in the database (nullable) (yes/no) [no]:
 >

 updated: src/Entity/Student.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > Name

 Field type (enter ? to see all types) [string]:
 >

 Field length [255]:
 >

 Can this field be null in the database (nullable) (yes/no) [no]:
 >

 updated: src/Entity/Student.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 >

  Success! 

 Next: When you're ready, create a migration with make:migration

```
```php:src/src/Entity/Student.php
<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\StudentRepository")
 */
class Student
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="integer")
     */
    private $studentNo;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $Name;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getStudentNo(): ?int
    {
        return $this->studentNo;
    }

    public function setStudentNo(int $studentNo): self
    {
        $this->studentNo = $studentNo;

        return $this;
    }

    public function getName(): ?string
    {
        return $this->Name;
    }

    public function setName(string $Name): self
    {
        $this->Name = $Name;

        return $this;
    }
}

```

## FormTypeを作成

Formに関連するFormTypeを作成します。

```console
bash-5.0# php bin/console make:form

 The name of the form class (e.g. AgreeablePizzaType):
 > StudentType

 The name of Entity or fully qualified model class name that the new form will be bound to (empty for none):
 > Student

 created: src/Form/StudentType.php

  Success! 
           
 Next: Add fields to your form and start using it.
 Find the documentation at https://symfony.com/doc/current/forms.html
```

```php:src/src/Form/StudentType.php
<?php

namespace App\Form;

use App\Entity\Student;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class StudentType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('studentNo')
            ->add('Name')
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Student::class,
        ]);
    }
}

```

## コントローラを作成する

Formを利用するコントローラを作成する

```console
bash-5.0# php bin/console make:controller

 Choose a name for your controller class (e.g. FierceKangarooController):
 > StudentController

 created: src/Controller/StudentController.php
 created: templates/student/index.html.twig

           
  Success! 
           

 Next: Open your new controller class and add some pages!

```
```php:src/src/Controller/StudentController.php
<?php

namespace App\Controller;

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

class StudentController extends AbstractController
{
    /**
     * @Route("/student", name="student")
     */
    public function index()
    {
        return $this->render('student/index.html.twig', [
            'controller_name' => 'StudentController',
        ]);
    }
}
```
```twig:src/templates/student/index.html.twig
{% extends 'base.html.twig' %}

{% block title %}Hello StudentController!{% endblock %}

{% block body %}
<style>
    .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
    .example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>

<div class="example-wrapper">
    <h1>Hello {{ controller_name }}! ✅</h1>

    This friendly message is coming from:
    <ul>
        <li>Your controller at <code><a href="{{ '/var/www/html/src/Controller/StudentController.php'|file_link(0) }}">src/Controller/StudentController.php</a></code></li>
        <li>Your template at <code><a href="{{ '/var/www/html/templates/student/index.html.twig'|file_link(0) }}">templates/student/index.html.twig</a></code></li>
    </ul>
</div>
{% endblock %}
```

http://127.0.0.1:8000/student を開くと以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/b5db68bf-f7ef-d728-6fd9-e166f2557226.png)

## Formを組み込む

SymfonyドキュメントのFormの章の[Rendering Forms](https://symfony.com/doc/4.4/forms.html#rendering-forms)を参考にしてControllerにFormを組み込みます。


```php:src/src/Controller/StudentController.php
<?php

namespace App\Controller;

use App\Entity\Student;
use App\Form\StudentType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class StudentController extends AbstractController
{
    /**
     * @Route("/student", name="student")
     */
    public function index()
    {
        $student = new Student();
        $form = $this->createForm(StudentType::class, $student);

        return $this->render('student/index.html.twig', [
            'controller_name' => 'StudentController',
            'form' => $form->createView(),
        ]);
    }
}
```

```twig:src/templates/student/index.html.twig
~~ 省略 ~~~
<div class="example-wrapper">
    <h1>Hello {{ controller_name }}! ✅</h1>

    This friendly message is coming from:
    <ul>
        <li>Your controller at <code><a href="{{ '/var/www/html/src/Controller/StudentController.php'|file_link(0) }}">src/Controller/StudentController.php</a></code></li>
        <li>Your template at <code><a href="{{ '/var/www/html/templates/student/index.html.twig'|file_link(0) }}">templates/student/index.html.twig</a></code></li>
    </ul>

    {{ form(form) }}
</div>
```

リロードすると、以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/903c4cc5-247c-723b-4f07-25ed4a7bcc41.png)


Submitボタンがないので、[Creating Form Classes](https://symfony.com/doc/4.4/forms.html#creating-form-classes)を参考にしてStudentTypeに追記します。

```php:src/src/Form/StudentType.php
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('studentNo')
            ->add('Name')
            ->add('save', SubmitType::class)
        ;
    }
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/b34a8117-f6c6-2276-806f-2f21f2a0102c.png)


Symfonyのベストプラクティスの[Add Form Buttons in Templates](https://symfony.com/doc/current/best_practices.html#add-form-buttons-in-templates)を見ると、Submitボタンは、フォームタイプに組み込むよりは、テンプレートに組み込む方ことで、フォームタイプの汎用性が上がると記載があります。

テンプレートに移していきましょう。

まずは、さきほどStudentTypeに追記したSubmitボタンをコメントアウトします。

```php:src/src/Form/StudentType.php
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('studentNo')
            ->add('Name')
//            ->add('save', SubmitType::class)
        ;
    }
```

テンプレートにsubmitボタンをつけます。
[How to Customize Form Rendering](https://symfony.com/doc/current/form/form_customization.html)を参考にしてフォームのレンダリングを調整します。

識別するようにボタン名を**save2**にします。

```twig:src/templates/student/index.html.twig
<div class="example-wrapper">
    <h1>Hello {{ controller_name }}! ✅</h1>

    This friendly message is coming from:
    <ul>
        <li>Your controller at <code><a href="{{ '/var/www/html/src/Controller/StudentController.php'|file_link(0) }}">src/Controller/StudentController.php</a></code></li>
        <li>Your template at <code><a href="{{ '/var/www/html/templates/student/index.html.twig'|file_link(0) }}">templates/student/index.html.twig</a></code></li>
    </ul>

    {{ form_start(form) }}
    {{ form_widget(form) }}
    <button class="btn btn-lg btn-primary" name="save" type="submit">save2</button>
    {{ form_end(form) }}
</div>
```

このようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/c1c4ce79-6b44-72f7-450e-7d2a2f09bbe9.png)


## Student Noにバリデーションを追加する

<code>student no</code>は、EntityでIntegerを指定していますので、フォームの<code>input type</code>に**number**が指定されています。

ただ、このままですと、**-1**、**-2**とか指定できてしまいますので、生徒番号として自然数のみにしたいので、フォームのバリデーションを指定していく必要があります。

```php:src/src/Entity/Student.php
    /**
     * @ORM\Column(type="integer")
     */
    private $studentNo;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/db000ef4-fb7d-c4aa-20f2-3b402bbbaf7f.png)

バリデーションなので、[ValidationのNumber Constraints](https://symfony.com/doc/current/validation.html#number-constraints)を見つけます。
そこに、**PositiveOrZero**というのがあります。
※自然数ならPositiveなのですが、画像取得していたときに間違えていました。。。汗

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/2c371f0d-ffd6-ed6f-fc03-c6e2af901dd2.png)

こんな感じでEntityの該当の変数にアノテーションでバリデーションを追記できます。
ただ、今回は、フォームで制限したいなぁ～と考えていますので、調査していきます。

```php:src/src/Entity/Student.php
namespace App\Entity;

use Symfony\Component\Validator\Constraints as Assert;

class Student
{
    /**
     * @ORM\Column(type="integer")
     * @Assert\PositiveOrZero
     */
    private $studentNo;
}
```

Entityではなくバリデーションを追加する方法も、[How to Use a Form without a Data Class](https://symfony.com/doc/current/form/without_class.html)に記載がありました。

このドキュメントを参考にして、<code>StudentType.php</code>を以下のように修正します。

```php:src/src/Form/StudentType.php
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('studentNo', NumberType::class, [
                'html5' => true,
                'constraints' => [
                    new NotBlank(),
                    new PositiveOrZero(),
                ],
            ])
            ->add('Name')
//            ->add('save', SubmitType::class)
        ;
    }
```

こんな感じで見た目は変更がありませんが、バリデーションを設定できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/fab3d82b-0ea5-07b6-4c96-191fbac0aa5b.png)

バリデーションを試すには、Controllerにも手を入れる必要がありました。
[Processing Forms](https://symfony.com/doc/4.4/forms.html#processing-forms)を参考にして、フォームの入力値のチェックを追加します。

```php:src/src/Controller/StudentController.php
class StudentController extends AbstractController
{
    /**
     * @Route("/student", name="student")
     */
    public function index(Request $request)
    {
        $student = new Student();
        $form = $this->createForm(StudentType::class, $student);

        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {

        }

        return $this->render('student/index.html.twig', [
            'controller_name' => 'StudentController',
            'form' => $form->createView(),
        ]);
    }

}
```

こちらでバリデーションを確認してみましょう。
きちんと、バリデーションができましたね。

![2019-12-23_00h13_15.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/6b5930a8-717d-ef67-331d-40bdab7b15b8.gif)

## Bootstrap4を適用する

バリデーションがでたのですが、装飾がないので味気ないですね。
Bootstrap4を適用してみましょう。

[Bootstrap 4 Form Theme](https://symfony.com/doc/4.4/form/bootstrap4.html)を参考にします。

ドキュメントとは違いますが、<code>base.html.twig</code>を以下のように修正しました。

```twig:src/templates/base.html.twig
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        {% block stylesheets %}{% endblock %}
        {# beware that the blocks in your template may be named different #}
        {% block head_css %}
            <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
        {% endblock %}
        {% block head_js %}
            <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
            <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
            <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
        {% endblock %}
    </head>
    <body>
        {% block body %}{% endblock %}
        {% block javascripts %}{% endblock %}
    </body>
</html>
```

そして<code>twig.yaml</code>を修正してテーマを指定します。

```yaml:src/config/packages/twig.yaml

twig:
    default_path: '%kernel.project_dir%/templates'
    debug: '%kernel.debug%'
    strict_variables: '%kernel.debug%'
    exception_controller: null
    form_themes: ['bootstrap_4_layout.html.twig']
```

一気にBootstrapになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/877a886c-01a5-55b9-11d0-b6c77bb98326.png)

この状態でバリデーションを確認してみます。
いい感じで、バリデーションのエラーが表示されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133291/0f36a93c-e0b4-0a2f-9f76-645714c66410.png)

# 最後に

今回作成したサンプルプログラムを以下で公開しています。
参考にしていただければ幸いです。

https://github.com/idani/symfony4.4-form-validate-integer