PHP
form
laravel
laravel5
formbuilder
LaravelDay 18

LaravelでFormを作るために「FormBuilder」を触ってみる(kristijanhusak/laravel-form-builderの紹介)

本記事は Laravel Advent Calender 2017 18日目の記事です。
初参加でちょっとドキドキしています……。

何がしたい?

  • Formを書くときに、もう<input type="radio">と書きたくない。
  • フロントサイドバリデーションも、サーバーサイドと一緒にやってくれませんか?
  • データ型(Model)を書いたら、バリデーションもフォーム作りも全部自動でやってほしい。

そんな人のための、というよりそんな人の1人である僕が、Form Builderと出会って明るい未来を思い描けた(到達はしていない)、というお話です。

長い前置きの長い前置き

いきなりですが、LaravelのFormって使いにくくないですか? というよりも、LaravelのFormデータのやり取り=「ControllerでPOSTデータを受け取ってまるごとバリデーションに通してまるごとFillする一連の流れ」があまりにエレガントなのに対し、Formはほとんど、HTMLをスクラッチで書いているようなイメージ……。もっとこう、エレガントな書き方が無いものか?と。

それを探るための試験的な記事です。
たまたま見かけたForm Builderと言うものを使うとうまくいくらしい、といった立ち位置から試しに使ってみた記事なので、もっとこうしたほうが良いよ! こうするのがアタリマエだぜそんなことも知らんのか! とかあればお気軽にコメントください。

標準のForm

Larvel5からFormはコアモジュールから削除されていて、LaravelCollectiveというサードパーティプロジェクトに移管されています。インストール手順などは下記に詳しく書いてあります。

初めてのLARAVEL 5.1 : (16) FORMの作成

これを利用するとFormを少し機能的に、シンプルに書けるようになります。

sample.blade.php
{!! Form::open(array('url' => 'foo/bar')) !!}
{!! Form::token() !!}
{!! Form::text('email', 'example@gmail.com') !!}
{!! Form::password('password') !!}
{!! Form::submit('Click Me!') !!}
{!! Form::close() !!}

Larvel5からFormはコアモジュールから削除されていて

これ、さらっと流しましたが、つまりLaravelには標準でFormづくりを助けてくれる機能が無いってことですよね! 切り離すのが惜しいくらい親和性があって便利なモジュールがないということですよね!?

個人的に理想のFormの書き方

僕はHTMLが書きたいんじゃない! データ入力フォームが書きたいんだ!!

ちょっと話が飛びますが、たとえば、いくつかの選択肢から1つを選ぶというフォームを書く場合、SELECTやRADIOを使うことになりますが、HTMLだと

<select class="" name="option1">
  <option value="0">---選択されていません--</option>
  <option value="1" selected>選択肢1</option>
  <option value="2">選択肢2</option>
  <option value="3">選択肢3</option>
</select>

だったり

<fieldset>
  <label><input type="radio" name="option1" value="1" checked="checked">選択肢1</label>
  <label><input type="radio" name="option1" value="2">選択肢2</label>
  <label><input type="radio" name="option1" value="3">選択肢3</label>
</fieldset>

と書きます。
select の中の選択肢タグはなんだっけ? value と name はドコに書くんだっけ? 選択済みは selected だっけ checked だっけ? radioタグは無くてinputタグのtype="radio"か? labelは外か隣かどっちに書くんだっけ?……といった、目的とは違うことをいろいろ考える必要があります。

LaravelCollectiveのFormも結構便利になっていて、SELECTは

echo Form::select('option1', array(
    '1' => '選択肢1', 
    '2' => '選択肢2',
    '3' => '選択肢3',
  ), '1');

とかなりスッキリしますが、RADIOは

<fieldset>
  <label><?php echo Form::radio('option1', '1', true); ?>選択肢1</label>
  <label><?php echo Form::radio('option1', '2'); ?>選択肢2</label>
  <label><?php echo Form::radio('option1', '3'); ?>選択肢3</label>
</fieldset>

といったように、まだHTML風です。なんていうか、あくまでLaravelCollectiveの機能はHTMLのマクロといったスタンスなんですよね。これだったらHTMLをそのまま書いたほうが早いような。

そもそも、このHTMLのRADIOの仕様がイマイチです。RADIOを単独で使うことはまず無いので、nameのoption1を3回書くのが不自然だし、できればLabelも自動的に書き出してほしい。それよりも、「選択肢から選ぶ」という行為はSELECTと同じなので、書き方も同じであってほしい。つまり、こう。

echo Form::radio('option1', array(
    '1' => '選択肢1', 
    '2' => '選択肢2',
    '3' => '選択肢3',
  ), '1');

もう少し言うと、入力するデータをチェックするバリデーションも必要です。例えば、こう。

echo Form::radio('option1', array(
    '1' => '選択肢1', 
    '2' => '選択肢2',
    '3' => '選択肢3',
  ), null, array( 'validation_rules'=>'required' ) );

kristijanhusak/laravel-form-builder

前置きが長くなりましたが、そんなフォーム作りを実現してくれるのが Form Builder というもので(他のフレームワークにもあったり、単独でライブラリとして配布されていたりする一般名詞)、Laravel用だと kristijanhusak/laravel-form-builder がGITHUBでのスターも多く、広く受け入れられているのかなぁという印象。早速使ってみます。

kristijanhusak/laravel-form-builder
https://github.com/kristijanhusak/laravel-form-builder

インストール

公式のREADMEに従っていきます。(が、以下はLaravel5.5のまっさらなプロジェクトからスタートしているので公式と若干異なります。)

composer require kris/laravel-form-builder

つぎに config/app.php を開いて、以下を追加。

config/app.php
    'providers' => [
        // ...
        Kris\LaravelFormBuilder\FormBuilderServiceProvider::class
    ]

これは、後ほどコントローラメソッドに public function create(FormBuilder $formBuilder) と書いたときに、Laravelから $formBuilder のインスタンスを受け取るための設定(DI:依存性注入)。続いて同じファイルに、

config/app.php
    'aliases' => [
        // ...
        'FormBuilder' => Kris\LaravelFormBuilder\Facades\FormBuilder::class
    ]

これは、おもむろにFormBuilder::xxxx()と静的呼び出しをしたときに実際に実行されるクラスの指定で、先程の、Laravelにインスタンスを作ってもらう時に必要な設定(たぶん……)。

フォームクラスの作成

インストールできたら、フォームを作ります。

php artisan make:form Forms/SongForm --fields="name:text, lyrics:textarea, publish:checkbox"

まずフォームをクラス化するというのが良い感じですし、それを artisan で実行できるというのがLaravelっぽくてますます良い感じ。このコマンドで、app/Formsというディレクトリに下記のファイルが生成されます。

app/Forms/SongForm.php
<?php

namespace App\Forms;

use Kris\LaravelFormBuilder\Form;

class SongForm extends Form
{
    public function buildForm()
    {
        $this
            ->add('name', 'text')
            ->add('lyrics', 'textarea')
            ->add('publish', 'checkbox');
    }
}

そのままだとフィールド値しかないので、試しにSUBMITボタンを追加してみます。

app/Forms/SongForm.php
            ->add('publish', 'checkbox') // ;を取って
            ->add('submit', 'submit', ['label' => '保存']); // 追加

コントローラを準備

公式には書いてないけどコントローラを作るならこう。

php artisan make:controller SongsController
app/Http/Controllers/SongsController.php
<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Kris\LaravelFormBuilder\FormBuilder; // 追記

class SongsController extends Controller
{
  // 以下を追記
  public function create(FormBuilder $formBuilder)
  {
      $form = $formBuilder->create(\App\Forms\SongForm::class, [
          'method' => 'POST',
          'url' => route('song.store')
      ]);

      return view('song.create', compact('form'));
  }

  public function store(FormBuilder $formBuilder)
  {
      $form = $formBuilder->create(\App\Forms\SongForm::class);

      if (!$form->isValid()) {
          return redirect()->back()->withErrors($form->getErrors())->withInput();
      }

      // 保存する処理はここ(今回は省略)
  }
}

ルーティング

routes/web.php
// 以下を追記
Route::get('songs/create', [
    'uses' => 'SongsController@create',
    'as' => 'song.create'
]);

Route::post('songs', [
    'uses' => 'SongsController@store',
    'as' => 'song.store'
]);

VIEW

公式は @extends とありますが、親bladeをまだ作ってないので割愛し、代わりにBootstrapのスタイルシートを読み込んでみます。

resources/views/song/create.blade.php
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
{!! form($form) !!}

テスト

artisan でローカルサーバーを立ち上げて……

php artisan serve

ブラウザで http://127.0.0.1:8000/songs/create にアクセスします(もしドメインが異なれば適宜読み替えてください)。下図のようなフォームが表示されました。

form1.jpg

生成されたHTMLはこのとおり

<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
<form method="POST" action="http://127.0.0.1:8000/songs" accept-charset="UTF-8">
  <input name="_token" type="hidden" value="8Jr8nQ6kE7abrDu0jtrN7tuM7jDGtq9wTRHkgFAc">
  <div class="form-group">
    <label for="name" class="control-label">Name</label>
    <input class="form-control" name="name" type="text" id="name">
  </div>
  <div class="form-group">
    <label for="lyrics" class="control-label">Lyrics</label>
    <textarea class="form-control" name="lyrics" cols="50" rows="10" id="lyrics"></textarea>
  </div>
  <div class="form-group">
    <input id="publish" name="publish" type="checkbox" value="1">
    <label for="publish" class="control-label">Publish</label>
  </div>
  <button class="form-control" type="submit">保存</button>
</form>

チェックボックスにLabelが付与されてdivで囲ってくれているのがまさに理想的。
CSRFトークンが自動で付与されていたりもします。

form-control というクラスがデフォルトで付与されているので、Bootstrapを当てるとちょっとスタイリッシュになります。本気でスタイリングする場合は、下記のように class を変更したり、設定を変更したりするようです。

app/Forms/SongForm.php
            ->add('publish', 'checkbox') // ;を取って
            ->add('submit', 'submit', ['label' => '保存','attr'=>['class'=>'btn btn-primary']])

ひとまずここまで……

次のステップはバリデーションで、ここがスッキリ書けるとLaraverlのフォームまわりはだいぶスッキリするんじゃないかと思いますが、実際に自分のプロジェクトで組み込んでみて使い慣れてから書き進めたいと思います。

本当はさらにもっと踏み込んで、Modelクラス内に、

MyModel.php
MyModel extends Model {

  protected $columns = [
    'id'    => [ 'label'=>'ID',   'type':'SERIAL', 'rule'=>'required' ],
    'name'  => [ 'label'=>'名前',   'type':'TEXT',   'rule'=>'required|max-length:50' ],
    'email' => [ 'label'=>'メール', 'type':'EMAIL',  'rule'=>'required|with-confirm' ],
    'job'   => [ 'label'=>'職業',   'type':'SELECT', 'rule'=>'required', 'option'=>['会社員','自営業',...] ],
  ];

}

と書いておいたら、フォームづくりも、フロントとサーバーのバリデーションも、CSVデータのエクスポートとインポートも、全部自動でできるようになったらいいなーと妄想しています。