Laravel: コントローラーを介したデータの送受信

Laravelを使って研究室の材料・サンプル管理システムを開発していきます。

以下の前回の終了時点の状態を再現できます。

$ git clone https://kiku3.tsbio.info/git/study-laravel.git study_laravel
$ git checkout step2

一度ここで自分のブランチをつくっておくとよいです。

$ git checkout -b my2

今後は基本的にsrcディレクトリで作業します。VisualStudioCodeを使っている場合は、code srcとするとよいです。

コントローラー(controller)を導入する

ユーザーからのリクエスト(URLや送信データ)に応じて、ビューを返す前に、プログラム的な処理を行うために、コントローラーを間にいれます。 route – controller – view という流れでPHPのスクリプトが順次実行されていきます。

controllerは app/Http/Controllers/ 以下に置きます。 artisanコマンドが使えるときは ./artisan make:controller controller_name とすることでファイルができます。しかし、特定のクラスを継承する必要があるのではないので、0からつくっていってもかまいません。

コントローラーを経由するようにルートを編集する

/oligoへのアクセスを OligoControllerに回すようにします。 そのために routes/web.php を以下のように編集します。

routes/web.php
<?php
 
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\OligoController;
 
/*
 *	コントローラーを経由するルート
 */
Route::prefix('/oligo')->as('web.oligo.')->group(function (){
    Route::get('/', [OligoController::class, 'index'])->name('index');
    Route::get('/create', [OligoController::class, 'create'])->name('create');
    Route::post('/', [OligoController::class, 'store'])->name('store');
});

getの2番目の引数が action で、このルートが選ばれた場合に実行する関数を与えます。 [クラス名, メソッド名]はオブジェクトのメソッドを指定する形です。

次いで、 app/Http/Controllers/OligoController.php を以下のように作成します。

app/Http/Controllers/OligoController.php
<?php
 
namespace App\Http\Controllers;
 
class OligoController
{
    public function index(){
        return view('oligo.index');
    }
 
    public function create(){
        return view('oligo.create');
    }
}

/oligo にアクセスすると OligoController の index メソッドを経由して、 view()が返り、これまでと同じ画面がブラウザーに表示されます。

ルートをグループ化する

3つのルートをつくります。

  • /oligo への GET でのアクセスに対し、一覧を返すルート (index)。
  • /oligo/create への GET でのアクセスに対し、データ登録用のフォームを返すルート (create)。
  • /oligo へ新しいオリゴDNAの情報が POST されたとき、データを登録するルート (store)。

このようにしようとすると、繰り返し Route::prefix に /oligo を渡すことになります。重複した設定を省くために、グループを作成します。

同時に、ルートに名前をつけます。名前をつけることによってlaravelが提供するroute関数が使えるようになり、URLを自動で作成できるようになります。これにより、.envのAPP_URLが変わったとき、すなわち開発環境と本番環境でのURLの違いを、ソースコードの変更なく吸収できるようになります。

ルートの名前にも階層構造をつけるため、asメソッドを使います。

routes/web.php は以下のようになります。

routes/web.php
<?php
 
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\OligoController;
 
/*
 *	コントローラーを経由するルート
 */
Route::prefix('/oligo')->as('web.oligo.')->group(function (){
    Route::get('/', [OligoController::class, 'index'])->name('index');
    Route::get('/create', [OligoController::class, 'create'])->name('create');
    Route::post('/', [OligoController::class, 'store'])->name('store');
});

正しくrouteができているかは ./artisan route:list として確認します。

# php artisan route:list

  GET|HEAD  oligo .................. web.oligo.index › OligoController@index
  POST      oligo .................. web.oligo.store › OligoController@store
  GET|HEAD  oligo/create ......... web.oligo.create › OligoController@create

リンク先を直接書き込んでいた resources/views/oligo/module.blade.php を修正します。

resources/views/oligo/module.blade.php
@extends('layouts.app')
 
@section('module')
  <x-module id='oligo'>
    <!-- モジュールのメニュー -->
    <nav>
      <ul class='module-tabs' role='tablist'>
        @foreach([
          'index' => '一覧',
          'create' => '追加',
        ] as $page => $label)
          <li><a href='{{ route("web.oligo.{$page}") }}' id='oligo-{{ $page }}-tab' role='tab'>{{ $label }}</a></li>
        @endforeach
      </ul>
    </nav>
    
    <!-- featureが並ぶところ -->
    <div class='features'>
      @yield('feature')
    </div>
  </x-module>
@endsection

合わせて app/Http/Controllers/OligoController.php も編集し、createとstoreのメソッドを追加します。

受信したデータ(リクエスト)の処理

データ送信用フォームの作成

新しいデータ(オリゴDNA)を登録するためのフォームをつくります。

resources/views/oligo/create.blade.php
@extends('oligo.module')
 
@section('feature')
<x-feature id='oligo-create'>
  <form action="{{ route('web.oligo.store') }}" method="POST">
    @csrf
    <dl>
      <dt>名前</dt>
      <dd><input type='text' name='name'></dd>
      <dt>配列</dt>
      <dd><input type='text' name='sequence'></dd>
      <dt>作成日</dt>
      <dd><input type='text' name='updated_at'></dd>
      <dt>所有者</dt>
      <dd><input type='text' name='owner'></dd>
      <dt><button type='submit'>登録</button></dt>
    </dl>
  </form>
</x-feature>
@endsection

3つ新しいbladeの使い方が入っています。

  1. フォームによる送信先(action属性)に route関数 を使う。ここで指定している web.oligo.store は routes/web.php で設定したルートに対する名前で、asとnameによって、web.oligo.store という文字列に組み立てられています。
  2. POSTメソッドの場合は をform要素のどこかに書く。これは Cross-Site Request Forgeries というリクエストの偽造による攻撃に対する対応です。laravelはCSRF攻撃に対する防御を自動的に行います。
  3. __ (アンダーバーを2個並べたもの)は翻訳を行う関数です。’登録’に対する訳語を設定すると、ブラウザーの言語に合わせて翻訳してくれます(どこで設定するのかは今後掲載予定)。

/oligo/createにアクセスすると次のような画面が表示されます。

My Lab's Stock

名前
配列
作成日
所有者

まだ OligoController.php にstoreメソッドを実装していないので、送信しても何も表示されません。

入力データの検証

コントローラーに送られてくるデータは必ず検証(validation)が必要です。 検証は app/Http/Requestsディレクトリ以下に作成する Request クラスで行います。

Laravelでの値の検証方法は https://readouble.com/laravel/13.x/ja/validation.html を参照してください。

app/Http/Requests/OligoRequest.php
<?php
 
namespace App\Http\Requests;
 
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
 
class OligoRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:100'],
                // 配列ではなく、| で区切って文字列にしてもよい。
                // 例: 'required|string|max:100' 
            'sequence' => ['required', 'string', 'regex:/^[A-Z]{6,255}$/'],
            'updated_at' => ['nullable', 'string', 'regex:/^[0-9]{6}$/'],
            'owner' => ['nullable', 'string', 'max:100'],
        ];
    }
 
    /*
     *  エラーメッセージはなくてもよい。デフォルトのメッセージが使われる。
     */
    public function messages(): array
    {
        return [
            'name.max' => 'サンプル名は255文字以内で入力してください。',
            'sequence.regex' => '配列はアルファベット大文字で6-255の長さにしてください。',
            'owner.max' => '所有者は100文字以内で入力してください。',
        ];
    }
}
  • フォームから送信されてくるデータは FormRequestクラスを継承したクラスで行います。
  • rules メソッドで、フィールド名(inputタグのname属性の値)をキーに、それに適用するルールの配列を値とする配列を返します。
  • messagesメソッドは フィールド名.ルール名 をキーに、対応するエラーメッセージを値とする配列を返します。

コントローラー側ではこのリクエストクラスのオブジェクトを受け取るようにします。

app/Http/Controllers/OligoController.php
<?php
 
namespace App\Http\Controllers;
 
use App\Http\Requests\OligoRequest;
 
class OligoController
{
    public function index(){
        return view('oligo.index');
    }
 
    public function create(){
        return view('oligo.create');
    }
 
    public function store(OligoRequest $request){
        var_dump($request->validated());
    }
}

$request->validated()とすることで、検証済みのデータを取得できます。

検証失敗時の処理

Requestの検証に失敗すると、コントローラーに処理が渡ることなく、HTTP response 302がブラウザーに返されます。そのとき、送信フォームを取得したURLがリダイレクト先に指定されます。

このとき$errorsという変数がつくられます。

old関数を使ってユーザーが送信した値を取得できます。 @error というbladeのディレクティブでエラーがあったときの処理を分岐できます。

resources/views/oligo/create.blade.php
@extends('oligo.module')
 
@section('feature')
<x-feature id='oligo-create'>
  <form action="{{ route('web.oligo.store') }}" method="POST">
    @csrf
    <dl>
      @foreach([
        'name' => '名前',
        'sequence' => '配列',
        'updated_at' => '作成日',
        'owner' => '所有者',
      ] as $field => $label)
        <dt>{{ __($label) }}</dt>
        <dd>
          <input type='text' name='{{ $field }}'
            value="{{ old($field) }}"
            style="@error($field) border: 2px solid red @enderror"
          >
          @error($field)
            <p style='color: red'>{{ $message }}</p>
          @enderror
        </dd>
      @endforeach
      <dt><button type='submit'>登録</button></dt>
    </dl>
  </form>
</x-feature>
@endsection

フィールド名が異なるだけなので、foreachを使った繰り返しにしました。

ファイルの入出力

ファイルへ書き出し

/oligo へ POST されたデータをファイルに保存します。後にデータベースを導入し、それを活用することになりますが、ファイルの入出力

storageディレクトリにファイルを保存するのが標準です。外部から直接アクセスされないものはapp/privateもしくはapp/publicに置きます。これらへのパスを storage_path()でつくります。

OligoControllerのstoreメソッドで、ファイルへデータを書き込みます。

app/Http/Controllers/OligoController.php
<?php
 
namespace App\Http\Controllers;
 
use App\Http\Requests\OligoRequest;
 
class OligoController
{
    public function index(){
        return view('oligo.index');
    }
 
    public function create(){
        return view('oligo.create');
    }
 
    public function store(OligoRequest $request){
        $validated = $request->validated();
        $data = [];
        foreach(['name', 'sequence', 'created_at', 'owner'] as $key){
            $data[] = sprintf('"%s"', $validated[$key] ?? '');
        }
        $csv = storage_path('app/private/oligo.csv');
        file_put_contents($csv, implode(',', $data).PHP_EOL, FILE_APPEND);
 
        return redirect()->route('web.oligo.index')->with('status', '保存しました');;
    }
}

ファイルの読み込み

indexメソッドでcsvファイルを読み込むようにしてみます。

app/Http/Controllers/OligoController.php
<?php
 
namespace App\Http\Controllers;
 
use App\Http\Requests\OligoRequest;
 
class OligoController
{
    public function index(){
        $csv = storage_path('app/private/oligo.csv');
        $fp = fopen($csv, 'r');
        $items = [];
        while($row = fgetcsv($fp)){
            $items[] = array_combine(['name', 'sequence', 'created_at', 'owner'], $row);
        }
        fclose($fp);
        return view('oligo.index', compact('items'));
    }
 
    public function create(){
        return view('oligo.create');
    }
 
    public function store(OligoRequest $request){
        $validated = $request->validated();
        $data = [];
        foreach(['name', 'sequence', 'created_at', 'owner'] as $key){
            $data[] = sprintf('"%s"', $validated[$key] ?? '');
        }
        $csv = storage_path('app/private/oligo.csv');
        file_put_contents($csv, implode(',', $data).PHP_EOL, FILE_APPEND);
 
        return redirect()->route('web.oligo.index')->with('status', '保存しました');;
    }
}

viewのほうで配列を表に組み上げます。

resources/views/oligo/index.blade.php
@extends('oligo.module')
 
@section('feature')
<x-feature id='oligo-index'>
  <table>
    <thead>
      <tr><th>id</th><th>名前</th><th>配列</th><th>作成日</th><th>所有者</th></tr>
    </thead>
    <tbody>
      @foreach($items as $item)
        <tr>
          <td>{{ $item['name'] }}</td>
          <td>{{ $item['sequence'] }}</td>
          <td>{{ $item['created_at'] }}</td>
          <td>{{ $item['owner'] }}</td>
        </tr>
      @endforeach
    </tbody>
  </table>
</x-feature>
@endsection