はじめに
-
【マイスター・ギルド】本物の Advent Calendar 2021の4日目の記事です。
- 他にもROCKな記事ばっかりなので要チェック!!!
- 本記事は筆者が以下の2つの記事を自身の学習用にまとめたものです。
大変参考にさせていただきました。ありがとうございます。
- モダンフロントとLaravelの組み合わせを想定しているので、bladeは使いません。
- フロント部分についてはコードは載せますが、詳細は触れません。
Laravel側へリクエスト投げていれば何でも大丈夫です。今回はReactとaxiosを用います。
- フロント部分についてはコードは載せますが、詳細は触れません。
執筆の経緯
- LaravelでCRUDをやってみたいが、bladeを使っている記事ばかり。
モダンフロントとLaravelの記事が少ない・・・。 - なかったら作れ!(ハック精神)
想定読者
- 前回の記事で環境構築された方。
記事の通りに環境構築された場合はバージョンは以下になっているはず。- PHP : 7.3.32
- Composer : 2.0.14
- Laravel : 6.20.41
- Laravel初心者の方。
- LaravelおよびMVCモデルの基礎について簡単に理解している前提で話を進めます。
今回のゴール
- 簡単なCRUD処理を実装する。
- タスク管理アプリを作る。
実行環境
- PC : MacBookAir(M1, 2020)
- OS : macOS Big Sur11.4
- チップ : Apple M1
- メモリ : 16GB
- DockerDesktop : 3.5.2
- Docker : 20.10.7
目次
- 何を作るか
- Laravelの前準備
- 処理のイメージ
- Controllerを作成する。
- web.phpを編集し、ルーティングを設定する。
- .envを編集する
- Modelおよびmigrationを作成し、テーブルを作成する
- Seederを作成して実行し、サンプルレコードを作成する
- フロントエンドの完成形
- 【要件1の実装】データベースからレコードを読み込む(Read)
- 【要件2の実装】データベースへレコードを挿入する(Create)
- 【要件3の実装】データベースの情報を更新する(Update)
- 【要件4の実装】データベースの情報を削除する(Delete)
- リポジトリ
何を作るか
- 簡単なタスク管理アプリを作成します。簡単な要件は以下の通り。
Laravelの前準備
Laravelの処理の簡単なイメージ
- とりあえずModel、View、Controller、Route、DBが分かっていればOK。小売店と卸問屋で例えてみましょう。
- View
ブラウザ株式会社という小売店のスタッフ。お客さんに画面という情報を提供している。
今回はここがbladeじゃなくてReact。 - Route
サーバー株式会社という卸問屋の電話番。 - Controller
サーバー株式会社の営業マン。 - Model
サーバー株式会社の在庫管理の部署。 - DB
サーバー株式会社の倉庫。
- View
- イメージ
- Viewから商品問い合わせの電話が入った。
- Routeが電話を受けて、担当営業マンのControllerに繋ぐ。
- ControllerがModelへ在庫を確認。
- ModelはDBへ在庫を確認、Viewへ発送する。
- ControllerがViewへ納期を伝える。
- イメージ違ってたらご指摘いただけると幸いです。
Controllerを作成する。
- Controllerを作成する
- 中身は後ほど実装するので、とりあえず作成だけしておく。
backendコンテナの中
php artisan make:controller TodoController
web.phpを編集し、ルーティングを設定する。
- 以下の通り設定。
- これでpublic/fetch_todosにgetメソッドでリクエストが来たら、TodoControllerのfetchTodosメソッドを実行するよう設定ができている。
- 前述の通りcontrollerの中身は後ほど実装します。
routes/web.php
Route::get('fetch_todos', 'TodoController@fetchTodos');
.envファイルを編集する
- Laravelプロジェクト直下にある.envファイルを以下の通り修正。
- dockerは各コンテナ間のネットワークも自動で作成をしてくれている。
そのためLaravel(backendコンテナ)からMySQL(dbコンテナ)へサービス名でアクセスできるようになっている。
つまり「DB_HOST」の値は「db」でOK。
- dockerは各コンテナ間のネットワークも自動で作成をしてくれている。
.env(一部抜粋)
DB_CONNECTION=mysql
// dockerで環境構築した場合は、docker-compose.ymlデータベースのコンテナのサービス名を入力する。
DB_HOST=db
DB_PORT=3306
// 任意のデータベース名。先にDBコンテナに入ってデータベースだけは作成しておくこと。
DB_DATABASE=tasuku
// rootユーザーは初期設定で存在しているので、このままでOK。
DB_USERNAME=root
// docker-compose.ymlで設定したパスワードを入力。
DB_PASSWORD=pass
Modelおよびmigrationを作成し、テーブルを作成する
- modelファイルとmigrationファイルを作成する
- migrationファイルとは簡易に書け、なおかつ複数実行可能なSQL文の集まりのようなものです。
- -mオプションをつけることで、modelと一緒にmigrationも作成されます。
- 今回はタスクを格納するtodosテーブルのみ作成します。
- model名は単数形にすること。(migrationファイルは複数形名で自動生成されている)
ってことはmodelはきっとテーブルの各レコードってことなんだろうか。
backendコンテナ内
php artisan make:model Models/Todo -m
- migrationファイルを編集
-
->nullable(false)
のような書き方をすることで、該当のカラムにオプションを付与します。
私はいつも頭の中でSQLを思い浮かべてからどう書くのか検索したりしてます。
-
database/migrations/(作成時の日時)_create_todos_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTodosTable extends Migration
{
public function up()
{
Schema::create('todos', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name', 255)->nullable(false);
$table->string('status', 255)->nullable(false);
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('todos');
}
}
- migrationを実行
- 事前にdbコンテナ内で.envで指定したデータベースを作成しておかないとエラーになるので注意。
- 実行したらdbコンテナ内でテーブルができているか確認。
- SQLポチポチ叩かなくて良いので便利!
backendコンテナ内
php artisan migrate
Seederを作成して実行し、サンプルレコードを作成する
- Seederファイルを作成する
- Seederファイルとはサンプルのデータを作成できるファイル。
- さっきのmigrationがcreate table文の簡易版とすると、seederはinsert文の簡易版。
backendコンテナ内
php artisan make:seeder TodosTableSeeder
- Seederファイルを編集
- 適当にファイルを作成する。
id,created_at,updated_atはSeederには入力不要。 - 初期だとDBがimportされていないので、追記すること。
- 適当にファイルを作成する。
database/seeds/TodosTableSeeder.php
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Seeder;
class TodosTableSeeder extends Seeder
{
public function run()
{
DB::table('todos')->insert(
[
[
'name' => '買い物',
'status' => '作業中'
],
[
'name' => '料理',
'status' => '作業中'
],
[
'name' => '洗濯',
'status' => '作業中'
]
]
);
}
}
- DatabaseSeederへの登録
- 後述のseedコマンドを実行すると、このファイルが実行される。
つまりこのファイルに先ほど作成したSeederを記載しないと意味なし。
- 後述のseedコマンドを実行すると、このファイルが実行される。
database/seeds/DatabaseSeeder.php
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* @return void
*/
public function run()
{
$this->call(TodosTableSeeder::class);
}
}
- Seederファイルの実行
- dbコンテナにデータが入っていることを確認。
backendコンテナ内
php artisan db:seed
フロントエンドの完成形
- 一部抜粋です。完成系は本記事の終わりのgithub参照。何やっているか雰囲気だけ掴んでもらえれば。
- Reactで書いてますが、画面表示時にLaravelへgetメソッド投げれたら何でも良いです。
- Vue.jsの場合はcreated()にfetchTodosメソッド、それ以外のメソッドはv-onで制御すればOKかと思います。
- バニラJSの場合はaddEventListener駆使してください。
Main.tsx
import { useState, useEffect, ChangeEvent } from 'react'
import { client, postMethod } from './lib/axios'
import { createURLSearchParams } from './utils'
interface TodoType {
id: string
name: string
status: string
}
const WORK_IN_PROGRESS = '作業中'
const DONE = '完了'
export const Main = () => {
const [todos, setTodos] = useState<TodoType[]>([])
const [todoName, setTodoName] = useState('')
const bindTodoNameValue = (event: ChangeEvent<HTMLInputElement>) => {
setTodoName(event.target.value)
}
// 要件1
const fetchTodos = async () => {
const { data } = await client.get<TodoType[]>('/fetch_todos')
setTodos(data)
}
useEffect(() => {
fetchTodos()
}, [])
// 要件2
const pushTodo = () => {
const params = createURLSearchParams<TodoType>([
['name', todoName],
['status', WORK_IN_PROGRESS],
])
postMethod('push_todo', params).then((_response) => fetchTodos())
}
// 要件3
const changeStatus = (id: string, status: string) => {
const statusParam = status === WORK_IN_PROGRESS ? DONE : WORK_IN_PROGRESS
const params = createURLSearchParams<TodoType>([
['id', id],
['status', statusParam],
])
postMethod('change_status', params).then((_response) => fetchTodos())
}
// 要件4
const deleteTodo = (id: string) => {
const params = createURLSearchParams<TodoType>([['id', id]])
postMethod('delete_todo', params).then((_response) => fetchTodos())
}
return (
<>
<p>タスクを追加する</p>
<input onChange={bindTodoNameValue} type="text" />
<button onClick={pushTodo}>追加する</button>
<ul>
{todos.length > 0 &&
todos.map(({ id, name, status }, index) => {
return (
<li key={id}>
<p>Index:{index + 1}</p>
<p>タスク名:{name}</p>
<p>状況:{status}</p>
<button onClick={() => changeStatus(id, status)}>
変更する
</button>
<button onClick={() => deleteTodo(id)}>削除する</button>
</li>
)
})}
</ul>
</>
)
}
【要件1の実装】データベースからレコードを読み込む(Read)
- 処理が簡単なので、まずはReadからやります。
- TodoControllerを以下の通り実装。
- header("Access-Control-Allow-Origin: *")を付けないとCSRFのエラーが出ます。
簡単に言うと「違うドメインから通信来ても受け入れるよ」という設定です。
本来はちゃんとした設定をしないといけないんですが、今回は省略します。 - migrationの時と似たような書き方でselect文を書いているイメージです。
- json形式に変換してレスポンスを返します。
-
Todo::select()
のようにModelを利用してDBへアクセスしていますが、DBから直接取ってくる実装方法もあります。
役割を考えると、Modelを利用するべきではと考えています。
- header("Access-Control-Allow-Origin: *")を付けないとCSRFのエラーが出ます。
TodoController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class TodoController extends Controller
{
public function fetchTodos() {
header("Access-Control-Allow-Origin: *");
$todos = Todo::select('id' ,'name', 'status')->get();
return json_decode($todos);
}
}
【要件2の実装】データベースへレコードを挿入する(Create)
- web.phpを編集してルーティングを追加
- クライアント側から情報を受け取る時は大事な情報が入っている場合もあるでしょうから、post通信にします。
- ちなみにそのままaxiosでLaravelにPOSTすると、エラーコード419が返ってきます。
これはCSRFトークンが埋め込まれていないためのエラーです。
app/Http/Kernel.php
の$middlewareGroups
の\App\Http\Middleware\VerifyCsrfToken::class
をコメントアウトすれば一旦解決。
根本解決するにはリクエスト内にCSRFトークンを埋め込む必要がある。(勉強中)
bladeを使用する場合はとても簡単に解決可能ですが、Laravelでフロント作る気になれなかったので今回は割愛。
routes/web.php
<?php
Route::get('fetch_todos', 'TodoController@fetchTodos');
// 追加
Route::post('push_todo', 'TodoController@pushTodos');
- TodoControllerを以下の通り実装
- フロント側から受け取った情報は$request内に格納されている。
-
$todo = new Todo()
でModelの新しい営業担当を呼び出したイメージ。
$requst->input('name')
はバニラPHPで言えば、$_POST['name']
というところ。
$todo->save()
でDBに格納できる。
TodoController.php
<?php
namespace App\Http\Controllers;
// 追加
use App\Models\Todo;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class TodoController extends Controller
{
public function fetchTodos() {
header("Access-Control-Allow-Origin: *");
$todos = Todo::select('id' ,'name', 'status')->get();
return json_decode($todos);
}
//追加
public function pushTodo(Request $request) {
header("Access-Control-Allow-Origin: *");
$todo = new Todo();
$todo->name = $request->input('name');
$todo->status = $request->input('status');
$todo->save();
}
}
- フロント側から適当にタスクを追加する。
- 200番が返ってきていることを確認。Laravel側で何も返していないので、dataは空でOK。
- リロードしても今追加したタスクが消えないことを確認。
【要件3の実装】データベースの情報を更新する(Update)
- web.phpを編集してルーティングを追加
routes/web.php
<?php
Route::get('fetch_todos', 'TodoController@fetchTodos');
Route::post('push_todo', 'TodoController@pushTodo');
// 追加
Route::post('change_status', 'TodoController@changeStatus');
- TodoControllerを以下の通り実装
-
$id = (int) $request->input('id')
でフロント側からきているidはstring型なので、int型へ型キャスト。 -
$todo = Todo::find($id)
でTodoModelへ「$idを満たすレコードを頂戴」と言っている。
今回はstatusだけを変更したいので、上書きして保存。
-
TodoController.php
<?php
namespace App\Http\Controllers;
use App\Models\Todo;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class TodoController extends Controller
{
public function fetchTodos() {
header("Access-Control-Allow-Origin: *");
$todos = Todo::select('id' ,'name', 'status')->get();
return json_decode($todos);
}
public function pushTodo(Request $request) {
header("Access-Control-Allow-Origin: *");
$todo = new Todo();
$todo->name = $request->input('name');
$todo->status = $request->input('status');
$todo->save();
}
// 追加
public function changeStatus(Request $request) {
header("Access-Control-Allow-Origin: *");
$id = (int) $request->input('id');
$todo = Todo::find($id);
$todo->status = $request->input('status');
$todo->save();
}
}
- フロント側で「変更ボタン」を押下して、「状況」が変わることを確認。
- 200番が出ていることも確認。
- リロードしても「状況」が「完了」のままだと確認。
【要件4の実装】データベースの情報を削除する(Delete)
- web.phpを編集してルーティングを追加
routes/web.php
<?php
Route::get('fetch_todos', 'TodoController@fetchTodos');
Route::post('push_todo', 'TodoController@pushTodo');
Route::post('change_status', 'TodoController@changeStatus');
// 追加
Route::post('delete_todo', 'TodoController@deleteTodo');
- TodoControllerを以下の通り実装
-
delete()
でレコード削除。
-
TodoController.php
<?php
namespace App\Http\Controllers;
use App\Models\Todo;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class TodoController extends Controller
{
public function fetchTodos() {
header("Access-Control-Allow-Origin: *");
$todos = Todo::select('id' ,'name', 'status')->get();
return json_decode($todos);
}
public function pushTodo(Request $request) {
header("Access-Control-Allow-Origin: *");
$todo = new Todo();
$todo->name = $request->input('name');
$todo->status = $request->input('status');
$todo->save();
}
public function changeStatus(Request $request) {
header("Access-Control-Allow-Origin: *");
$id = (int) $request->input('id');
$todo = Todo::find($id);
$todo->status = $request->input('status');
$todo->save();
}
// 追加
public function deleteTodo(Request $request) {
header("Access-Control-Allow-Origin: *");
$id = (int) $request->input('id');
$todo = Todo::find($id);
$todo->delete();
}
}
- フロント側から適当にタスクを削除。
- 今回は「買い物」を削除してみた。
- 200番が返ってくることを確認。
リポジトリ
- パイセンがガッツリレビューしてくれてプルリクまで出してくれました。ギザ優しす。
- そんなパイセンのアドカレ記事はこちらです。
終わりに
- 今までfirebaseしか触ったことがなかったので、バックエンド触るのは新鮮だった。
- API組むの楽しすぎてハマりそう。
- だがやっぱりbladeは好きになれなかった。
- Laravelはドキュメントが非常に充実していてありがたい。