LaravelでCRUDのあるToDoアプリを作ってみた

Laravelを理解するために、CRUDのあるToDoアプリを作ってみました。

そこで、後で思い出しやすいよう、慣れているDjangoでの作り方も併記する形で、メモを残しておきます。

 
目次

   

環境

 

プロジェクトの作成

Djangoでは、 django-adminmanage.py を使って、Djangoプロジェクトを作成します。

# Djangoプロジェクト
$ django-admin startproject todolist .

# Djangoアプリ
$ python manage.py startapp web

 
Laravelは Composer を使って、Laravelプロジェクトを作成します。

$ composer create-project laravel/laravel todolist --prefer-dist "5.5.*"

 

モデルの作成

テーブル定義

今回のテーブル定義は以下です。

項目名 定義 内容
id 自動インクリメント 主キー
title char(50) タイトル
content text 内容
priority int 優先度
created_at 日付 作成日
updated_at 日付 更新日

 

モデルとマイグレーションファイル

Djangoでは、テーブル定義をモデルファイルに実装後、 migrate を使ってマイグレーションファイルを作成します。

一方、Laravelでは artisan (アルチザン) でモデルとマイグレーションファイルのひな形を作成します。その後、テーブル定義をマイグレーションファイルに実装します。

まずは、artisanでひな形を作成します。今回は、モデルとともにマイグレーションファイルも作成します。
https://readouble.com/laravel/5.7/ja/eloquent.html#defining-models]

php artisan make:model ToDo --migration

Model created successfully.
Created Migration: 2019_06_08_131917_create_to_dos_table

 
ひな形ができたので、次はテーブル定義をマイグレーションファイルに実装します。

今回は todolist/database/migrations/2019_06_08_131917_create_to_dos_table.php にある up() メソッドに実装します。

なお、Laravelでは、作成日/更新日のタイムスタンプ系は timestamps() を使うことで、自動で定義されます。

<?php
public function up()
{
    Schema::create('to_dos', function (Blueprint $table) {
        $table->increments('id');
        $table->timestamps();

        $table->char('title', 50);
        $table->text('content');
        $table->integer('priority');
    });
}

 
ちなみに、モデルファイル todolist/app/ToDo.php も自動で生成されています。ただし、今回実装するものはありません。

 

マイグレーションの実行

artisan を使って、データベースにモデルの内容を反映するマイグレーションを行います。

$ php artisan migrate

 

コントローラーのひな形作成

DjangoのViewに該当する、Laravelのコントローラーを作成します。

コントローラーも artisan を使ってひな形を作成します。

php artisan make:controller ToDoController --model=App\\ToDo

Controller created successfully.

 

今回のアプリのURLとコントローラーの関係

今回

  • URL
  • Laravelのコントローラーのメソッド
  • Djangoのビュー

の各関係は以下の通りです。

URL 機能 Laravelのコントローラー Djangoのビュー
/todo/ 一覧 index() ToDoListView
/todo/(pk) 詳細 show() ToDoDetailView
/todo/create 作成 [GET]create(), [POST]store() ToDoCreateView
/todo/(pk)/update 更新 [GET]edit(), [POST]update() ToDoUpdateView
/todo/(pk)/delete 削除 [GET]confirm(), [POST]destroy() ToDoDeleteView

 

routesの作成

routesでは、

  • URL
  • Laravelのコントローラーのメソッド

の紐付けを行います。

Djangourls.py にあたるものを、Laravelでは todolist/routes/web.php に実装します。

<?php

Route::prefix('todo')->group(function(){
    // 一覧
    Route::get('/', 'ToDoController@index')->name('todo.index');

    // 詳細
    Route::get('/{id}', 'ToDoController@show')
        ->where('id', '[0-9]+')
        ->name('todo.show');

    // 新規作成
    Route::get('/create', 'ToDoController@create')
        ->name('todo.create');
    Route::post('/create', 'ToDoController@store')
        ->name('todo.store');

    // 編集
    Route::get('/{id}/edit', 'ToDoController@edit')
        ->where('id', '[0-9]+')
        ->name('todo.edit');
    Route::post('/{id}/update', 'ToDoController@update')
        ->where('id', '[0-9]+')
        ->name('todo.update');

    // 削除
    Route::get('/{id}/delete', 'ToDoController@confirm')
        ->where('id', '[0-9]+')
        ->name('todo.confirm');
    Route::post('/{id}/delete', 'ToDoController@destroy')
        ->where('id', '[0-9]+')
        ->name('todo.destroy');
});

 
Django同様、Laravelでも名前付きルートの定義が name() で可能です。
https://readouble.com/laravel/5.5/ja/routing.html#named-routes

名前付きルートをDjangonamespace:name のような形にしたい場合、Laravelでは <モデル名>.<メソッド名> と表記している例があったため、それを採用しました。もし、他に正しい表記方法があれば、ご指摘ください。
https://webdevetc.com/blog/laravel-naming-conventions

 
他に、Django<int:pk> のような定義を、 where() を使って実装しています。上記例では id を数字のみに制限しています。
https://readouble.com/laravel/5.5/ja/routing.html#parameters-regular-expression-constraints

なお、グローバル制約も可能なようですが、今回は使っていません。
https://readouble.com/laravel/5.5/ja/routing.html#parameters-global-constraints

 

コントローラーの作成

artisan で作成したひな形 (todolist/app/Http/Controllers/ToDoController.php) に実装します。

 

一覧

id の降順で、 ToDo モデルの全データを表示したい場合、DjangoではViewの ordering を使いますが、Laravelでは latest() を使うのが簡単なようです。
https://readouble.com/laravel/5.5/ja/queries.html#ordering-grouping-limit-and-offset

<?php

public function index()
{
    $todos = ToDo::latest('id')->get();
    return view('todo.list', ['todos' => $todos]);
}

 

詳細

詳細は

  • データがあれば、その内容を表示
  • データがなければ、HTTP 404 を表示

とします。

Djangoでは get_object_or_404() があります。
https://docs.djangoproject.com/ja/2.2/intro/tutorial03/#a-shortcut-get-object-or-404

一方、Laravelでは firstOrFail() を使います。
https://readouble.com/laravel/5.7/ja/eloquent.html#retrieving-single-models

<?php

public function show(int $id)
{
    return view(
        'todo.detail',
        ['todo' => ToDo::where('id', $id)->firstOrFail()]
    );
}

 

バリデーション

新規登録に行く前に、バリデーションを検討します。

新規登録や更新処理では、入力値に対する検証(バリデーション)を行う必要があります。

Djangoであれば、 ModelForm を使うことで、モデルで定義した制約(文字列長や型)に対して自動的にバリデーションが行われます。

一方、Laravelではフォームとモデルでバリデーションを共有する方法は見つけられませんでした。

ただ、Laravelにもバリデーション機能があり、いくつかの実装方法がありました。

その中から今回は

  • カスタムバリデーションルール
  • フォームリクエストバリデーション

の2つを使って実装してみます。

 

カスタムバリデーションルール

Laravelでは標準でたくさんのバリデーションルールがあります。
https://readouble.com/laravel/5.5/ja/validation.html#available-validation-rules

今回は、 タイトルの英字は大文字のみ という独自のバリデーションルールを作成してみます。
https://readouble.com/laravel/5.5/ja/validation.html#custom-validation-rules

 
artisan でルールオブジェクトを作成します。

$ php artisan make:rule Uppercase

Rule created successfully.

 
生成された todolist/app/Rules/Uppercase.php を編集します。

今回は

  • passes() で、バリデーションが成功するときの内容
  • message() で、バリデーションエラーになったときのメッセージ

を実装します。

<?php

class Uppercase implements Rule
{
    public function passes($attribute, $value)
    {
        return mb_strtoupper($value) === $value;
    }

    public function message()
    {
        return 'タイトルの英字は、すべて大文字にしてください';
    }
}

 

フォームリクエストバリデーション

フォームリクエストバリデーションとは

フォームリクエストは、バリデーションロジックを含んだカスタムリクエストクラスです

https://readouble.com/laravel/5.5/ja/validation.html#form-request-validation

とのことです。

 
artisan を使って、フォームリクエストクラスを生成します。

$ php artisan make:request SaveToDo

Request created successfully.

 
生成された todolist/app/Http/Requests/SaveToDo.php を編集します。実装すべきものは

  • authorize()
    • 認証 (今回は認証なしなので、常に true を返す)
  • rules()
    • 対象の項目とバリデーション内容
  • messages()
    • バリデーションエラーとなった場合のメッセージ

です。

<?php

public function authorize()
{
    return true;
}

public function rules()
{
    return [
        'title' => ['max:50', new Uppercase],
        'priority' => ['integer'],
    ];
}

public function messages()
{
    return [
        'priority.integer' => '優先度は数字で入力してください',
    ];
}

 
ここまでで、新規登録/更新のバリデーション定義が終わりました。

 

新規登録

GET時の create() と、POST時の store() を実装します。

<?php

public function create()
{
    return view('todo.create');
}

public function store(SaveToDo $request)
{
    $target = new ToDo;
    $target->title = $request->input('title');
    $target->content = $request->input('content');
    $target->priority = $request->input('priority');
    $target->save();

    // save()後、$todoには保存したときのidがセットされる
    return redirect()->route('todo.show', ['todo' => $target]);
}

create() では、view() ヘルパ関数でビューを返すだけです。
https://readouble.com/laravel/5.5/ja/helpers.html#method-view

 
store() では色々と実装したため、ポイントをまとめておきます。

  • store()の引数の型として、バリデーションオブジェクト SaveToDo を指定
    • バリデーションエラーの場合は、 store() を処理することなく、フォームなどへ戻る
  • 入力データは、引数 $request へ格納
  • モデルの save() メソッドで、データベースへ保存

 
また、 created_atupdated_at

saveが呼び出された時にcreated_atとupdated_atタイムスタンプは自動的に設定されますので、わざわざ設定する必要はありません。

https://readouble.com/laravel/5.5/ja/eloquent.html#inserting-and-updating-models

とのことです。

 
一方、悩んだところです。

GETとPOSTでコントローラーのメソッドを同一/別々、どちらにするのがLaravelっぽいのか分からなかったことです。

$request にはHTTPリクエストメソッドがあるため、Djangoの関数ベースビューのように一つのメソッドで実装できそうでした。 https://readouble.com/laravel/5.5/ja/requests.html#request-path-and-method

ただ、WebではGETとPOSTは別メソッドなことが多かったため、今回はメソッドを分けてみました。

 

更新

GET時の edit() と、POST時の update() を実装します。

今まで出てきた内容ですので、詳細は省略します。

<?php

public function edit(int $id)
{
    return view(
        'todo.edit',
        ['todo' => ToDo::where('id', $id)->firstOrFail()]
    );
}

public function update(SaveToDo $request, int $id)
{
    $target = ToDo::where('id', $id)->firstOrFail();
    $target->title = $request->input('title');
    $target->content = $request->input('content');
    $target->priority = $request->input('priority');
    $target->save();

    return redirect()->route('todo.show', ['todo' => $id]);
}

 

削除

DjangoDeleteView では確認画面がありました。

一方、Laravelの artisan のひな形には、確認画面用のメソッドは生成されませんでした。

そのため、コントローラーに confirm() メソッドを追加して、GETで表示する確認画面を追加してみました。

また、モデルの削除では、今回使った delete() の他に destory() もあるようです。
https://readouble.com/laravel/5.5/ja/eloquent.html#deleting-models

<?php

public function confirm(int $id)
{
    return view(
        'todo.confirm_delete',
        ['todo' => ToDo::where('id', $id)->firstOrFail()]
    );
}

public function destroy(int $id)
{
    $target = ToDo::where('id', $id)->firstOrFail();
    $target->delete();

    return redirect()->route('todo.index');
}

 
以上でコントローラーの実装が終わりました。

 

ビューの作成

Laravelでは、テンプレートエンジンとして Blade を使います。 https://readouble.com/laravel/5.5/ja/blade.html

Djangoと同じように、Bladeテンプレートではテンプレート継承が使えるので、今回試してみます。
https://readouble.com/laravel/5.5/ja/blade.html#template-inheritance

 

継承元テンプレート

Django{% block %} と同じような仕組みとして、Laravelでは @yield があります。

また、CSSなどを参照する場合、Djangoでは {% static %} を使いましたが、Laravelでは asset() ヘルパ関数を使います。
https://readouble.com/laravel/5.5/ja/helpers.html#method-asset

あとは、DjangoLANGUAGE_CODE の代わりに、Config::get() を使いました。

全体像は以下の通りです。

<!DOCTYPE html>

<html lang="{{ Config::get('app.locale') }}">
<head>
    <meta charset="UTF-8">
    <title>
        @yield('title')
    </title>
    <link rel="stylesheet" href="{{ asset('/css/todo.css') }}">
</head>

<body>
<main class="container">
    @yield('content')
</main>
</body>
</html>

 

一覧ビュー

todolist/resources/views/todo/list.blade.php を作成します。

@extends('layouts.todo')

@section('title')
    ToDo一覧
@endsection

@section('content')

<h3>ToDo一覧</h3>
<a href="{{ route('todo.create') }}">作成</a>
<table>
    <thead>
    <tr>
        <th>ID</th>
        <th>タイトル</th>
        <th>優先度</th>
        <th>操作</th>
    </tr>
    </thead>
    <tbody>

    @foreach($todos as $todo)
    <tr>
        <td><a href="{{ route('todo.show', ['id' => $todo->id]) }}">{{ $todo->id }}</a></td>
        <td><a href="{{ route('todo.show', ['id' => $todo->id]) }}">{{ $todo->title }}</a></td>
        <td><a href="{{ route('todo.show', ['id' => $todo->id]) }}">{{ $todo->priority }}</a></td>
        <td>
            <a href="{{ route('todo.edit', ['id' => $todo->id]) }}">変更</a>
            <a href="{{ route('todo.confirm', ['id' => $todo->id]) }}">削除</a>
        </td>
    </tr>
    @endforeach

    </tbody>
</table>

@endsection

 

詳細ビュー

todolist/resources/views/todo/detail.blade.php を作成します。

@extends('layouts.todo')

@section('title')
    ToDo詳細
@endsection

@section('content')

    <h3>ToDo詳細</h3>

    <table>
        <thead>
        <tr>
            <th>ID</th>
            <th>タイトル</th>
            <th>優先度</th>
            <th>内容</th>
        </tr>
        </thead>
        <tbody>
        <tr>
            <td>{{ $todo->id }}</td>
            <td>{{ $todo->title }}</td>
            <td>{{ $todo->priority }}</td>
            <td>{{ $todo->content }}</td>
        </tr>
        </tbody>
    </table>

<a href="{{ route('todo.index') }} ">一覧へ</a>

@endsection

 

新規登録ビュー

todolist/resources/views/todo/create.blade.php を作成します。

 

LaravelCollective/htmlのインストール

新規登録ではフォームを使います。

Djangoで簡単にフォームを作る場合、 Form.as_table などを使います。

一方、Laravelでは、コアにフォーム機能が含まれていないようです。
https://stackoverflow.com/questions/35695949/why-are-form-and-html-helpers-deprecated-in-laravel-5-x

 
そのため、フォームは自分で作成するか、別途ライブラリを入れるかします。

今回は、Laravel5になった時にコミュニティ管理となった LaravelCollective/html を使います。
https://github.com/LaravelCollective/html

 
まずは Composer でインストールします。

なお、今回 Laravel5.5系を使っているため、バージョンを指定しないとインストールエラーになります。

$ composer require laravelcollective/html
...
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Installation request for laravelcollective/html ^5.8 -> satisfiable by laravelcollective/html[v5.8.0].
    - Conclusion: remove laravel/framework v5.5.45
    - Conclusion: don't install laravel/framework v5.5.45
...
    - Installation request for laravel/framework (locked at v5.5.45, required as 5.5.*) -> satisfiable by laravel/framework[v5.5.45].

 
そのため、バージョンを指定してインストールします。

$ composer require "laravelcollective/html":"^5.5.0"

 

LaravelCollective/htmlを使ったビューの作成

LaravelCollective/html の公式サイトがメンテナナス中っぽいので、GitHubにあるドキュメントを参考に実装します。

全体像です。

@extends('layouts.todo')

@section('title')
    ToDo
@endsection


@section('content')
<h3>ToDo</h3>

{!! Form::open(['route' => 'todo.store']) !!}
    {!! Form::label('title', 'タイトル') !!}
    {!! Form::text('title') !!}
    <p>{{ $errors->first('title') }}</p>

    {!! Form::label('content', '内容') !!}
    {!! Form::textarea('content') !!}
    <p>{{ $errors->first('content') }}</p>

    {!! Form::label('priority', '優先度') !!}
    {!! Form::text('priority') !!}
    <p>{{ $errors->first('priority') }}</p>

    {!! Form::submit('保存') !!}

{!! Form::close() !!}

<a href="{{ route('todo.index') }}">一覧へ</a>

@endsection

 

更新ビュー

todolist/resources/views/todo/edit.blade.php を作成します。

更新ビューも LaravelCollective/html を使って実装します。

また、データベースの値を画面に表示するため、 LaravelCollective/html のフォームモデルバインディングを使います。
https://github.com/LaravelCollective/docs/blob/5.6/html.md#form-model-binding

フォームモデルバインディングは、

{!! Form::model($todo, ['route' => ['todo.update', $todo->id]]) !!}

と、 Form::open() の代わりに Form::model() を使います。

残りの部分は新規登録と同じなため、コードは省略します。

 

削除ビュー

todolist/resources/views/todo/confirm_delete.blade.php を作成します。

今回は、詳細ビューに

<form method="post">
    {{ csrf_field() }}

    <button type="submit">削除</button>
</form>

と、 CSRF対策の csrf_field() を持つフォームを追加するだけにしました。
https://readouble.com/laravel/5.5/ja/csrf.html

 

静的ファイル

ビューの layouts/todo.blade.php にて、

<link rel="stylesheet" href="{{ asset('/css/todo.css') }}">

CSSファイルを参照していました。

Laravelでは、CSSなどの静的ファイルは

  • public
  • resources/assets

のどちらかに入れます。
https://readouble.com/laravel/5.5/ja/structure.html#the-public-directory

使い分けは、公式ドキュメントに

publicディレクトリ publicディレクトリには、アプリケーションへの全リクエストの入り口となり、オートローディングを設定するindex.phpファイルがあります。また、このディレクトリにはアセット(画像、JavaScriptCSSなど)を配置します。

resourcesディレクトリ resourcesディレクトリはビューやアセットの元ファイル(LESS、SASS、JavaScript)で構成されています。また、すべての言語ファイルも配置します。

https://readouble.com/laravel/5.5/ja/structure.html#the-public-directory

とありました。

そのため、今回は todolist/public/css/todo.css としてCSSファイルを作成します。

th {
    font-weight: bold;
}

table {
    border: solid 1px;
    border-collapse: collapse;
}

td, th {
    border: solid 1px;
}

 

以上で、ToDoアプリの実装が終わりました。

 

起動

artisan で起動します。

以下は localhost:8001 でアクセスできるようにしています。

$ php artisan serve --host=localhost --port=8001

 

ソースコード

GitHubに上げました。

なお、実際に動かす場合は、 Docker Compose で MySQL を起動する必要があります。