3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

SymfonyのCollection Form Typeをいい感じにするライブラリの紹介

Last updated at Posted at 2020-09-20

はじめに

SymfonyでOneToManyのリレーションがあるとき、CollectionFormTypeは頻繁に出てきます。しかし、サーバーサイドはSymfonyがよしなにやってくれるものの、実際にフォームを表示するフロント側の、アイテムを増やしたり減らしたりする実装は意外とめんどくさかったりします。

今回は、そんなときに役に立つライブラリの紹介です。

準備

まず、事前準備として、OneToManyのリレーションを持つEntityとそのFormTypeを作成します。今回は、ProcessStepというエンティティを作り、Proccessが複数のStepを持つことにします。

それぞれのEntityとFormTypeの定義は以下になります。

src/Entity/Step.php

<?php

namespace App\Entity;

use App\Repository\StepRepository;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass=StepRepository::class)
 */
class Step
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity=Process::class, inversedBy="steps")
     */
    private $process;

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

    /**
     * @ORM\Column(type="text")
     */
    private $detail;

...
// setter / getter

src/Entity/Process.php

<?php

namespace App\Entity;

use App\Repository\ProcessRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass=ProcessRepository::class)
 */
class Process
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\OneToMany(targetEntity=Step::class, mappedBy="process", cascade={"persist"})
     */
    private $steps;

    public function __construct()
    {
        $this->steps = new ArrayCollection();
    }

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

    /**
     * @return Collection|Step[]
     */
    public function getSteps(): Collection
    {
        return $this->steps;
    }

    public function addStep(Step $step): self
    {
        if (!$this->steps->contains($step)) {
            $this->steps[] = $step;
            $step->setProcess($this);
        }

        return $this;
    }

    public function removeStep(Step $step): self
    {
        if ($this->steps->contains($step)) {
            $this->steps->removeElement($step);
            // set the owning side to null (unless already changed)
            if ($step->getProcess() === $this) {
                $step->setProcess(null);
            }
        }

        return $this;
    }
}


src/Form/StepType.php

<?php

namespace App\Form;

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

class StepType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name')
            ->add('detail')
        ;
    }

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

src/Form/ProcessType.php

<?php

namespace App\Form;

use App\Entity\Process;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ProcessType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('steps', CollectionType::class, [
            'entry_type' => StepType::class,
            'entry_options' => [
                'label' => false,
            ],
            'allow_add' => true,
            'allow_delete' => true,
        ])
        ;
    }

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


適当なControllerを作成し、ProcessTypeを表示するとこんな感じになります。Stepが一つも登録されていないので、ラベル以外はまだなにも表示されていません。

スクリーンショット 2020-09-20 19.38.04.png

表示の改善

今回使用するライブラリはこちら( https://github.com/ninsuo/symfony-collection )になります。このライブラリを使って、CollectionTypeをいい感じにしていきます。

インストール方法はnpm、composer、Bowerが用意されています。webpackなどを用いている場合はnpmでインストールするのが良いと思いますが、今回はcomposerを用いてインストールします。

composerでインストールした後、vendor/ninsuo/symfony-collection/jquery.collection.jsvendor/ninsuo/symfony-collection/jquery.collection.html.twigをそれぞれpublicディレクトリとtemplatesディレクトリに移動させます。

$ composer req ninsuo/symfony-collection
$ cp vendor/ninsuo/symfony-collection/jquery.collection.js public 
$ cp vendor/ninsuo/symfony-collection/jquery.collection.html.twig templates 

テンプレートファイルを以下のように書き換え、bootstrapのCSSとJQueryをCDNから読み込みます。そして、先ほどインストールしたライブラリのjavascriptと、フォームテーマを使用し、Collectionになっているフォームに対してcollection()メソッドを叩くと、以下のような表示になります。

templates/default/index.twig

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

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

{% block body %}
    {% form_theme form
        'bootstrap_4_layout.html.twig'
        'jquery.collection.html.twig'
    %}

    <div class="container">
        <div class="row">
            <div class="col-12 mt-5">
                {{ form_start(form) }}
                {{ form_row(form.steps, {attr: {class: 'collection-form'}}) }}
                {{ form_rest(form) }}
                {{ form_end(form) }}
            </div>
        </div>
    </div>
{% endblock %}

{% block stylesheets %}
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
{% endblock %}

{% block javascripts %}
    <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
    <script src="jquery.collection.js"></script>
    <script>
        $('.collection-form').collection()
    </script>
{% endblock %}

demo.gif

もう少しカスタマイズ

今回使用したライブラリは、collection()メソッドを呼び出すときにオプションを渡すことで表示をカスタマイズできます。
また、FormTypeをオーバーライドすることでボタンの位置などを変えることもできます。

例えば、fontawsomeのフォントを用いて、「追加ボタン」はコレクションの一番最後にのみ表示するオプションを有効にし、FormTypeを次のようにオーバーライドすると、こんな表示になります。

templates/jquery.collection.html.twig

...
{% block step_row %}
    <div class="card mb-3">
        <div class="card-header text-right">
            <a href="#" class="collection-up btn btn-secondary mr-1"><i class="fas fa-angle-up"></i></a>
            <a href="#" class="collection-down btn btn-secondary mr-1"><i class="fas fa-angle-down"></i></a>
            <a href="#" class="collection-remove btn btn-danger mr-1"><i class="fas fa-trash"></i></a>
        </div>
        <div class="card-body">
            {{ form_row(form) }}
        </div>
    </div>
{% endblock %}
templates/default/index.twig

...
{% block stylesheets %}
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
    <link rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css" integrity="sha384-AYmEC3Yw5cVb3ZcuHtOA93w35dYTsvhLPVnYs9eStHfGJvOvKxVfELGroGkvsg+p" crossorigin="anonymous"/>
{% endblock %}

{% block javascripts %}
    <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
    <script src="jquery.collection.js"></script>
    <script>
        $('.collection-form').collection({
            add_at_the_end: true,
            add: '<a href="#" class="btn btn-primary btn-block"><i class="fas fa-plus"></i></a>',
        })
    </script>
{% endblock %}

demo2.gif

また、表示だけでなく、アイテムが追加される前後のイベントをフックしたりもできるので、とても柔軟に使用することができます。
SymfonyのCollectionTypeのフロント実装で疲弊している方は是非採用を考えてみてください!

今回のソース

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?