初心者でもlaravel 7.x を使い、45分でCRUD簡易掲示板を作る【Restful対応】
リハビリがてらlaravelでCRUDに対応した簡易掲示板を作る。この記事の手順に沿ってやれば、45分もあれば作れる。
本記事ではlaravel 7.X系を使用している。
流れ
- プロジェクトを作る
- DBはSQliteを読み込むように設定する
- Restful対応コントローラーを作る
- ルーティングの設定
- モデル定義とマイグレーション実行
- リクエストを作る
- ビューを作る
- コントローラーの修正
- タイムゾーンの修正
プロジェクトを作る
composer create-project --prefer-dist laravel/laravel laravel_crud_restful
コンポーザーコマンドを実行してプロジェクトを作る。
DBはSQliteを読み込むように設定する
デフォルトではMySQLが設定されているので、SQLiteを使うようにする。まずは下記コマンドでdatabase.sqlite
ファイルを作る。このファイル名(database.sqlite)の通りに作らないと動かないので注意。
touch ./database/database.sqlite
続いて、.env
ファイルの9行目付近にあるMySQL設定を全てコメントアウト、SQLiteを読み出すよう設定する。
# DB_CONNECTION=mysql
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
DB_CONNECTION=sqlite
これでDBの準備は完了。
Restful対応コントローラーを作る
make:controller
コマンドを使用してコントローラーを作る。今回はRestfulに対応したコントローラーを作るため、--resource
オプションを指定することを忘れなく。
php artisan make:controller TopicsController --resource
ルーティングの設定
--resource
オプションで作ったコントローラーをルーティング設定する際には、下記一行を追加すれば良いだけ。
Route::resource('/topics', 'TopicsController');
resource()
メソッドを使用するだけでアクションとURIの対応付けをまとめてやってくれる。そのためresource()
メソッドの第二引数に@index
等のアクション名を指定する必要はない。
ちなみに、この–resourceを実行することで、まとめて書かれたルーティング情報、分解して記述したい場合は、下記記事に倣って、このように書く。
Laravelで–resourceで作ったコントローラのルーティングを解体する
#Route::resource('/topics', 'TopicsController');
# ↑と↓は等価
Route::get('/topics', 'TopicsController@index')->name('topics.index');
Route::get('/topics/create', 'TopicsController@create')->name('topics.create');
Route::post('/topics', 'TopicsController@store')->name('topics.store');
Route::get('/topics/{id}', 'TopicsController@show')->name('topics.show');
Route::get('/topics/{id}/edit', 'TopicsController@edit')->name('topics.edit');
Route::put('/topics/{id}', 'TopicsController@update')->name('topics.update');
Route::delete('/topics/{id}', 'TopicsController@destroy')->name('topics.destroy');
これは、VScodeのスニペットにも登録しておくと即変換ができて便利で良いだろう。VScodeのスニペット登録の方法は下記記事を参照。
VisualStudioCode(VScode)を使う前にやっておきたい設定と覚えておくと良い操作方法
モデル定義とマイグレーション実行
下記コマンドを実行して、Topic
モデルを作る。--migration
コマンドでマイグレーションファイルも同時に作る。
php artisan make:model Topic --migration
まず、モデルの定義から。app/Topic.php
を下記のように編集する。
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Topic extends Model
{
protected $fillable = [
"name","content"
];
}
モデルの役割は、コントローラーがクライアントからデータを受け取った後、受け取ったデータをDBに入れてよいかどうかを決めること。上記のコードはname
とcontent
のみクライアントからの編集を許可する。それ以外のデータは受け取った時に全て破棄する。
続いて、マイグレーションファイルを編集する。database/migrations/[タイムスタンプ]_create_topics_table.php
にテーブルのカラムと挿入するデータを記述する。[タイムスタンプ]
はマイグレーションファイルを作った日時なので、適宜解釈するように。
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTopicsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('topics', function (Blueprint $table) {
$table->id();
$table->string("name",10);
$table->string("content",200);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('topics');
}
}
上記コードにより、topicsテーブルにオートインクリメント数値型のid
、10文字までのname
、200文字までのcontent
。timestamps()
で投稿日を意味するcreated_at
、編集日を意味するupdated_at
の5つのカラムが作られる。
コード下の方に書かれてあるdown()
メソッドはマイグレーションを元に戻すときのテーブル削除処理。そのままで良い。
定義したマイグレーションファイルに基づき、マイグレーションを実行する。
php artisan migrate
リクエストを作る
ユーザーから受け取ったリクエストをバリデーション(検証)して、DBに格納させるか否かを判定する。それがLaravelのリクエスト。下記コマンドを実行してリクエストを作る。
php artisan make:request CreateTopicRequest
続いて、app/Http/Requests/CreateTopicRequest.php
を下記のように編集
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CreateTopicRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
#↓trueに書き換える
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required|max:15',
'content' => 'required|max:2000',
];
}
public function messages() {
return [
'name.required' => '名前を入力してください',
'name.max' => '名前は15文字でお願いします。',
'content.required' => 'コメントを入力してください',
'content.max' => 'コメントは2000文字でお願いします。',
];
}
}
バリデーション対象とバリデーションルール、バリデーション失敗したときのメッセージが表示される。
ビューを作る
Restful化に対応させるため、作る必要のあるビューが5つある。ただ、今回はテンプレートの継承機能を使うので、1ファイル当たりのコード行数は30行程度。
まず、resources/views/base.blade.php
を作る。このbase
を継承して、残り4つのファイルを作る。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>簡易掲示板</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
@yield("extra_head")
</head>
<body>
<a href="{{ route('topics.index') }}"><h1 style="background:orange;color:white;text-align:center;">簡易掲示板</h1></a>
<main class="container">@yield("main")</main>
</body>
</html>
ディレクティブの@yield([任意の文字列])
を記述することで、その部分は継承したビューが自由に内容を追加することができる。例えば、上記base
を継承したindex
が@yield("main")
の部分にトップページである旨を表示させることが可能になる。
resources/views/index.blade.php
は下記のように記述する。
@extends("base")
@section("main")
<a class="btn btn-outline-success" href="{{ route('topics.create') }}">+ トピックを作る</a>
@forelse( $topics as $topic )
<div class="border my-2 p-2">
<div class="text-secondary">{{ $topic->name }} さん</div>
<div class="p-2">{!! nl2br(e($topic->content)) !!}</div>
<div class="text-secondary">投稿日:{{ $topic->created_at }}</div>
<div class="text-secondary">編集日:{{ $topic->updated_at }}</div>
<a class="btn btn-outline-primary" href="{{ route('topics.show',$topic->id) }}">詳細</a>
<a class="btn btn-outline-success" href="{{ route('topics.edit',$topic->id) }}">編集</a>
<form action="{{ route('topics.destroy',$topic->id) }}/" method="POST" style="display:inline-block;">
{{ csrf_field() }}
{{ method_field("delete") }}
<button class="btn btn-outline-danger" type="submit">削除</button>
</form>
</div>
@empty
<p>投稿はありません</p>
@endforelse
@endsection
冒頭に、@extends()
で継承元になるbase
を指定する。続いて、@section()
にmain
を指定する。base.blade.php
には@yield("main")
と指定してある部分に当たる。つまりmain
タグの中に@section("main")
から@endsection
までの内容が入るのだ。
ちなみに、@forelse()
以下はコントローラーが受け取った内容を全て記述している。この処理はまだ書いていないので次項にて解説する。
削除ボタンのmethod_field("DELETE")
はフォーム送信時HTTPリクエストのDELETEメソッドを使用してリクエストを送る指定をしている。
続いて、resources/views/create.blade.php
を作る。こちらもbase
を継承して作っている。
@extends("base")
@section("main")
@if( count($errors) )
<ul>
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
@endif
<form class="" action="{{ route('topics.store') }}" method="POST">
{{ csrf_field() }}
<input class="form-control" type="text" name="name" placeholder="名前">
<textarea id="" class="form-control" name="content" rows="4" placeholder="コメント"></textarea>
<input class="form-control" type="submit" value="送信">
</form>
@endsection
create
は新しいトピックを作るフォーム。input
タグ等のname
属性はマイグレーションフィールドで定義したカラムに基づいている。
ここでバリデーションに失敗したときのメッセージを表示させている。
続いて、resources/views/show.blade.php
を作る。こちらもbase
を継承して作っている。
@extends("base")
@section("main")
@forelse($topics as $topic )
<div class="border my-2 p-2">
<div class="text-secondary">{{ $topic->name }} さん</div>
<div class="p-2">{!! nl2br(e($topic->content)) !!}</div>
<div class="text-secondary">投稿日:{{ $topic->created_at }}</div>
</div>
@empty
<p>ありません。</p>
@endforelse
@endsection
show
は個別ページを表示する。
続いて、resources/views/edit.blade.php
を作る。こちらもbase
を継承して作っている。
@extends("base")
@section("main")
@if( count($errors) )
<ul>
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
@endif
@forelse($topics as $topic )
<form action="{{ route('topics.update',$topic->id) }}" method="POST">
{{ csrf_field() }}
{{ method_field("put") }}
<input class="form-control" type="text" name="name" placeholder="名前" value="{{ $topic->name }}">
<textarea id="" class="form-control" name="content" rows="4" placeholder="コメント">{{ $topic->content }}</textarea>
<input class="form-control" type="submit" value="送信">
</form>
@empty
<p>ありません。</p>
@endforelse
@endsection
その名の通り、編集用のフォーム。method_field("PUT")
はフォーム送信時HTTPリクエストのPUTメソッドを使用してリクエストを送る指定をしている。
作成時と同様に入力値にエラーがあった時、エラー文を表示させている。
これで一通りのビューは完成した。後はこのビューに値を入れるコントローラーを修正していく。
コントローラーの修正
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Topic;
use App\Http\Requests\CreateTopicRequest;
class TopicsController extends Controller
{
public function index()
{
$topics = Topic::latest()->get();
$context = [ "topics" => $topics ];
return view("index",$context);
}
public function create()
{
return view("create");
}
public function store(CreateTopicRequest $request)
{
Topic::create($request->all());
return redirect(route("topics.index"));
}
public function show($id)
{
$context = [];
$context["topics"] = Topic::where("id",$id)->get();
#HACK:↑と↓は等価。
#contextに仕込むモデルオブジェクトが増えるたび、context定義時の行数が増えるので、混乱を防ぐために↑のやり方のほうが良いかもしれない。
/*
$topics = Topic::where("id",$id)->get();
$context = [ "topics" => $topics ];
*/
return view("show",$context);
}
public function edit($id)
{
$topics = Topic::where("id",$id)->get();
$context = [ "topics" => $topics ];
return view("edit",$context);
}
public function update(CreateTopicRequest $request, $id)
{
#編集処理のベストプラクティス
Topic::find($id)->update($request->all());
#HACK:このやり方ではモデルフィールドが増えると対処しきれない。
/*
$topic = Topic::find($id);
$topic->name = $request->name;
$topic->content = $request->content;
$topic->save();
*/
return redirect(route("topics.index"));
}
public function destroy($id)
{
#削除のベストプラクティス
Topic::destroy($id);
#HACK:もっと短く書ける
#Topic::find($id)->delete();
#HACK:deleteメソッドはつなげ書いても良い。
/*
$topic = Topic::find($id);
$topic->delete();
*/
return redirect(route("topics.index"));
}
}
長いのでそれぞれの処理内容は省略し、共通するものだけ解説する。
Topic::latest()->get();
はDBからデータを全て抜き取り、最新が上になるように並び替える。間にwhere()
メソッドが入ることで、絞り込みができる。find()
メソッドに$id
を指定することで一意に特定もできる。
create
及びupdate
はリクエストのバリデーションを経由しているので、不適切な値であればフォーム入力欄にそのままエラー文が表示される仕組みになっている。
モデルのメソッドには、save()
メソッド、delete()
メソッドが用意されている。それぞれ、指定した値でデータを保存、削除することができる。
route("topics.index")
は名前解決によって、topics
を意味する。route/web.php
に定義した内容に基づいている。Route::resource("/topics", "TopicsController");
のみだが、resource()
メソッドにより、下記の名前とURIが対応付けられているのだ。
+--------+-----------+---------------------+----------------+-----------------------------------------------+------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+-----------+---------------------+----------------+-----------------------------------------------+------------+
| | GET|HEAD | / | | Closure | web |
| | GET|HEAD | api/user | | Closure | api |
| | | | | | auth:api |
| | GET|HEAD | topics | topics.index | App\Http\Controllers\TopicsController@index | web |
| | POST | topics | topics.store | App\Http\Controllers\TopicsController@store | web |
| | GET|HEAD | topics/create | topics.create | App\Http\Controllers\TopicsController@create | web |
| | GET|HEAD | topics/{topic} | topics.show | App\Http\Controllers\TopicsController@show | web |
| | PUT|PATCH | topics/{topic} | topics.update | App\Http\Controllers\TopicsController@update | web |
| | DELETE | topics/{topic} | topics.destroy | App\Http\Controllers\TopicsController@destroy | web |
| | GET|HEAD | topics/{topic}/edit | topics.edit | App\Http\Controllers\TopicsController@edit | web |
+--------+-----------+---------------------+----------------+-----------------------------------------------+------------+
つまり、Route("topics.index")
はtopics
を返し、Route("topics.create")
はtopics/create
を返すのだ。{topic}
は引数になっており、ここには数値が入る。この引数指定により、ビューにて、{{ route('topics.show',$topic->id) }}
と指定して、idに応じたパスを返すことができる。
タイムゾーンの修正
最後に、日付の問題を解決するため、config/app.php
のタイムゾーンを修正する。
'timezone' => 'Asia/Tokyo',
これで日本時間に設定できた。
実際に動かしてみる
開発用サーバーを起動する。
php artisan serve
編集したら編集日時が記録されるようになっている。
結論
laravelには『Web職人のためのフレームワーク』というキャッチコピーがあるが、調べれば調べるほど開発メソッドの選択肢が多く存在する。初学者からしてみれば選択肢の多さは、学習進捗に関わるので、ベストプラクティスのようなものがあると良いのだが。
とは言え、Djangoよりも自由度が高く、編集と作成の日時がまとめて指定できるtimestamps()
は良いと思った。
だが、timestamp型はデプロイするときにMySQLを使うと2038年問題を引き起こしてしまう。Laravelのデプロイ時にはDBはMySQLではなく、PostgreSQLを使いましょう。