Laravelを理解するために、CRUDのあるToDoアプリを作ってみました。
そこで、後で思い出しやすいよう、慣れているDjangoでの作り方も併記する形で、メモを残しておきます。
目次
環境
プロジェクトの作成
Djangoでは、 django-admin
と manage.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のコントローラーのメソッド
の紐付けを行います。
Djangoで urls.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
名前付きルートをDjangoの namespace: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
へ格納input()
メソッドを使ってデータを取り出す- https://readouble.com/laravel/5.5/ja/requests.html#retrieving-input
- モデルの
save()
メソッドで、データベースへ保存
また、 created_at
と updated_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]); }
削除
Djangoの DeleteView
では確認画面がありました。
一方、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
あとは、Djangoの LANGUAGE_CODE
の代わりに、Config::get()
を使いました。
- https://docs.djangoproject.com/ja/2.2/ref/templates/api/#django.template.context_processors.i18n
- https://stackoverflow.com/questions/12706463/how-can-i-find-the-current-language-in-a-laravel-view
全体像は以下の通りです。
<!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ファイルがあります。また、このディレクトリにはアセット(画像、JavaScript、CSSなど)を配置します。
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 を起動する必要があります。