Memo

メモ > 技術 > フレームワーク: Laravel6 > 複雑なCRUDの新規作成例(記事管理を作成)

■複雑なCRUDの新規作成例(記事管理を作成)
マイグレーションを作成
$ php artisan make:migration create_entries_table
作成されたマイグレーションを以下のように修正 bigInteger の引数は、「カラム名」「自動採番するか」「0以上の正の整数とするか」となっている 「$table->bigInteger('user_id')->unsigned();」と書けるかも。それならその方が直感的でいいかも 要検証
/** * Run the migrations. * * @return void */ public function up() { Schema::create('entries', function (Blueprint $table) { $table->engine = 'InnoDB'; $table->bigIncrements('id'); $table->dateTime('datetime')->comment('日時'); $table->string('title')->comment('タイトル'); $table->text('body')->comment('本文'); $table->bigInteger('user_id', false, true); $table->timestamps(); $table->softDeletes(); }); DB::statement('ALTER TABLE entries COMMENT \'記事\''); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('entries'); }
マイグレーションを実行
$ php artisan migrate
プログラムを作成 app/Models/Entry.php
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class Entry extends Model { use SoftDeletes; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'datetime', 'title', 'body', 'user_id', ]; /** * The attributes that should be cast to native types. * * @var array */ protected $casts = [ 'datetime' => 'datetime', ]; }
app/Contracts/Repositories/EntryRepository.php
<?php namespace App\Contracts\Repositories; interface EntryRepository { /** * 取得 * * @param int $id * @return mixed */ public function find($id); /** * 検索 * * @param array $conditions * @param array $orders * @param int|null $limit * @return mixed */ public function search(array $conditions, array $orders, $limit); /** * 件数 * * @return int */ public function count(); /** * 保存 * * @param array $data * @param int|null $id * @return mixed */ public function save(array $data, $id); /** * 削除 * * @param int $id * @return mixed */ public function delete($id); }
app/Repositories/EntryRepository.php
<?php namespace App\Repositories; use App\Contracts\Repositories\EntryRepository as EntryRepositoryContract; use App\Models\Entry; class EntryRepository implements EntryRepositoryContract { /** @var Entry */ protected $entry; /** * コンストラクタ * * @param Entry $entry * @return void */ public function __construct(Entry $entry) { $this->entry = $entry; } /** * 取得 * * @param int $id * @return mixed */ public function find($id) { return $this->entry->find($id); } /** * 検索 * * @param array $conditions * @param array $orders * @param int|null $limit * @return mixed */ public function search(array $conditions = array(), array $orders = array(), $limit = null) { $query = $this->entry->query(); $query = $this->setConditions($query, $conditions); foreach ($orders as $order) { $query->orderBy($order[0], $order[1]); } if ($limit == null) { return $query->get(); } else { return $query->paginate($limit); } } /** * 件数 * * @param array $conditions * @return int */ public function count(array $conditions = array()) { $query = $this->entry->query(); $query = $this->setConditions($query, $conditions); return $query->count(); } /** * 保存 * * @param array $data * @param int|null $id * @return Entry */ public function save(array $data, $id = null) { return $this->entry->updateOrCreate(['id' => $id], $data); } /** * 削除 * * @param int $id * @return mixed */ public function delete($id) { return $this->entry->findOrFail($id)->delete(); } /** * 検索条件を設定 * * @param int $query * @param array $conditions * @return \Illuminate\Database\Query\Builder */ private function setConditions($query, array $conditions = array()) { if (isset($conditions['id'])) { $query->where('id', $conditions['id']); } if (isset($conditions['title'])) { $query->where('title', $conditions['title']); } if (isset($conditions['title_like'])) { $query->where('title', 'like', $conditions['title_like']); } return $query; } }
app/Services/EntryService.php
<?php namespace App\Services; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use App\Contracts\Repositories\EntryRepository; use App\Http\Requests\StoreEntryRequest; use App\Http\Requests\UpdateEntryRequest; class EntryService { /** @var EntryRepository */ protected $entryRepository; /** * コンストラクタ * * @param EntryRepository $entryRepository * @return void */ public function __construct(EntryRepository $entryRepository) { $this->entryRepository = $entryRepository; } /** * 1件取得 * * @param int $id * @return mixed */ public function getEntry($id) { return $this->entryRepository->find($id); } /** * 検索して取得 * * @param array $conditions * @param array $orders * @param int $limit * @return mixed */ public function getEntries(array $conditions = array(), array $orders = array(), $limit = null) { return $this->entryRepository->search($conditions, $orders, $limit); } /** * 件数を取得 * * @param array $conditions * @return int */ public function countEntries(array $conditions = array()) { return $this->entryRepository->count($conditions); } /** * 登録 * * @param StoreEntryRequest $request * @return \App\Models\Entry */ public function storeEntry(StoreEntryRequest $request) { $input = $request->only([ 'datetime', 'title', 'body', 'user_id', ]); // 保存 try { return DB::transaction(function () use ($input) { $result = $this->entryRepository->save($input); return $result; }); } catch (\Exception $e) { Log::error('EntryService:storeEntry', ['message' => $e->getMessage(), 'input' => $input]); return null; } } /** * 編集 * * @param UpdateEntryRequest $request * @param int $id * @return \App\Models\Entry */ public function updateEntry(UpdateEntryRequest $request, $id) { $input = $request->only([ 'datetime', 'title', 'body', 'user_id', ]); // 保存 try { return DB::transaction(function () use ($input, $id) { $result = $this->entryRepository->save($input, $id); return $result; }); } catch (\Exception $e) { Log::error('EntryService:updateEntry', ['message' => $e->getMessage(), 'input' => $input, 'id' => $id]); return null; } } /** * 削除 * * @param int $id * @return \App\Models\Entry */ public function deleteEntry($id) { try { return DB::transaction(function () use ($id) { return $this->entryRepository->delete($id); }); } catch (\Exception $e) { Log::error('EntryService:deleteEntry', ['message' => $e->getMessage(), 'id' => $id]); return null; } } }
app/Providers/AppServiceProvider.php
$this->app->bind( \App\Contracts\Repositories\EntryRepository::class, \App\Repositories\EntryRepository::class );
app/Http/Requests/StoreEntryRequest.php
<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class StoreEntryRequest extends FormRequest { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ 'datetime' => ['required'], 'title' => ['required', 'string', 'max:255'], 'body' => ['required'], 'user_id' => ['required', 'integer'], ]; } /** * バリデーションエラーのカスタム属性の取得 * * @return array */ public function attributes() { return [ 'datetime' => '日時', 'title' => 'タイトル', 'body' => '本文', 'user_id' => 'ユーザ', ]; } }
main/app/Http/Requests/UpdateEntryRequest.php
<?php namespace App\Http\Requests; class UpdateEntryRequest extends StoreEntryRequest { }
app/Http/Controllers/Admin/EntryController.php
<?php namespace App\Http\Controllers\Admin; use Illuminate\Http\Request; use App\Http\Controllers\Controller; use App\Http\Requests; use App\Http\Requests\StoreEntryRequest; use App\Http\Requests\UpdateEntryRequest; use App\Models\Entry; use App\Services\EntryService; use App\Services\UserService; class EntryController extends Controller { /** * Create a new controller instance. * * @param EntryService $entryService * @param UserService $userService * @return void */ public function __construct( EntryService $entryService, UserService $userService ) { $this->middleware('auth'); $this->entryService = $entryService; $this->userService = $userService; } /** * Display a list of all entries. * * @param Request $request * @return Response */ public function index(Request $request) { return view('admin.entry.index', [ 'entries' => $this->entryService->getEntries([], [['id', 'desc']]), ]); } /** * Display a form of new entry. * * @param Request $request * @return Response */ public function create(Request $request) { return view('admin.entry.form', [ 'users' => $this->userService->getUsers(), ]); } /** * Create a new entry. * * @param Request $request * @return Response */ public function store(StoreEntryRequest $request) { if ($this->entryService->storeEntry($request)) { return redirect()->route('admin.entry.index')->with('message', '記事を登録しました。'); } else { return redirect()->route('admin.entry.index')->with('error', '記事を登録できませんでした。'); } } /** * Display a form of edit entry. * * @param Request $request * @return Response */ public function edit(Request $request, $id) { return view('admin.entry.form', [ 'entry' => $this->entryService->getEntry($id), 'users' => $this->userService->getUsers(), ]); } /** * Update a entry. * * @param Request $request * @return Response */ public function update(UpdateEntryRequest $request, $id) { if ($this->entryService->updateEntry($request, $id)) { return redirect()->route('admin.entry.index')->with('message', '記事を編集しました。'); } else { return redirect()->route('admin.entry.index')->with('error', '記事を編集できませんでした。'); } } /** * Destroy the given entry. * * @param Request $request * @param string $id * @return Response */ public function destroy(Request $request, $id) { if ($this->entryService->deleteEntry($id)) { return redirect()->route('admin.entry.index')->with('message', '記事を削除しました。'); } else { return redirect()->route('admin.entry.index')->with('error', '記事を削除できませんでした。'); } } }
resources/views/admin/home.blade.php
<li><a href="{{ route('admin.entry.index') }}">記事管理</a></li>
resources/views/admin/entry/index.blade.php
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">記事一覧</div> <div class="card-body"> @if (session('message')) <div class="box"> <div class="alert alert-success"> {{ session('message') }} </div> </div> @elseif (session('error')) <div class="box"> <div class="alert alert-danger"> {{ session('error') }} </div> </div> @endif <p><a href="{{ route('admin.entry.create') }}" class="btn btn-primary">記事登録</a></p> <table class="table table-striped"> <thead> <th>日時</th> <th>タイトル</th> <th>編集</th> <th>削除</th> </thead> <tbody> @foreach ($entries as $entry) <tr> <td class="table-text"><div>{{ $entry->datetime->format('Y/m/d H:i:s') }}</div></td> <td class="table-text"><div>{{ $entry->title }}</div></td> <td class="table-text"><a href="{{ route('admin.entry.edit', ['id' => $entry->id]) }}" class="btn btn-primary">Edit</a></td> <td> <form action="{{ route('admin.entry.delete', ['id' => $entry->id]) }}" method="POST"> {{ csrf_field() }} {{ method_field('DELETE') }} <button type="submit" class="btn btn-danger"> Delete </button> </form> </td> </tr> @endforeach </tbody> </table> </div> </div> </div> </div> @endsection
resources/views/admin/entry/form.blade.php
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">記事 @if (!Request::is('*/create')) {{ '編集' }} @else {{ '登録' }} @endif</div> <div class="card-body"> @if (count($errors) > 0) <div class="alert alert-danger"> @foreach ($errors->all() as $error) {{ $error }}<br> @endforeach </div> @endif <form action="{{ Request::is('*/create') ? route('admin.entry.create') : route('admin.entry.edit', ['id' => $entry->id]) }}" method="POST" class="form-horizontal"> @if (!Request::is('*/create')) {{ method_field('put') }} @endif {{ csrf_field() }} <div class="form-group"> <label for="datetime" class="col-sm-3 control-label">日時</label> <div class="col-sm-6"> <input type="text" name="datetime" id="datetime" class="form-control" value="{{ old('datetime', isset($entry) ? $entry->datetime : '') }}"> </div> </div> <div class="form-group"> <label for="title" class="col-sm-3 control-label">タイトル</label> <div class="col-sm-6"> <input type="text" name="title" id="title" class="form-control" value="{{ old('title', isset($entry) ? $entry->title : '') }}"> </div> </div> <div class="form-group"> <label for="body" class="col-sm-3 control-label">本文</label> <div class="col-sm-12"> <textarea name="body" id="body" rows="10" cols="10" class="form-control">{{{ old('body', isset($entry) ? $entry->body : '') }}}</textarea> </div> </div> <div class="form-group"> <label for="user_id" class="col-sm-3 control-label">ユーザ</label> <div class="col-sm-6"> <select name="user_id" class="form-control"> <option value=""></option> @foreach ($users as $user) <option value="{{ $user->id }}" @if (old('user_id', isset($entry) ? $entry->user_id : '') == $user->id) selected @endif>{{ $user->name }}</option> @endforeach </select> </div> </div> <div class="form-group"> <div class="col-sm-offset-3 col-sm-6"> <button type="submit" class="btn btn-primary"> <i class="fa fa-btn fa-plus"></i>@if (!Request::is('*/create')) {{ '編集' }} @else {{ '登録' }} @endif </button> </div> </div> </form> </div> </div> </div> </div> @endsection
routes/web.php
// 記事管理 Route::get('/entry', 'EntryController@index')->name('.entry.index'); Route::get('/entry/create', 'EntryController@create')->name('.entry.create'); Route::post('/entry/create', 'EntryController@store')->name('.entry.store'); Route::get('/entry/edit/{id}', 'EntryController@edit')->name('.entry.edit'); Route::put('/entry/edit/{id}', 'EntryController@update')->name('.entry.update'); Route::delete('/entry/delete/{id}', 'EntryController@destroy')->name('.entry.delete');
■1対1でリレーション(記事に関連するユーザを取得) app/Models/Entry.php に以下を追加
/** * 記事に関連するユーザを取得 */ public function user() { return $this->belongsTo('App\Models\User'); }
resources/views/admin/entry/index.blade.php のテーブルヘッダ部分とテーブルデータ部分に、それぞれ以下を追加
<th>ユーザ</th> <td class="table-text"><div>{{ $entry->user->name }}</div></td>
このとき、一覧表示のために発行されるSQLは以下のようになる 記事数に応じてselectの数が増える、いわゆる「N+1問題」によって処理速度が遅くなる
select * from `entries` where `entries`.`deleted_at` is null order by `id` desc select * from `users` where `users`.`id` = 14 and `users`.`deleted_at` is null limit 1 select * from `users` where `users`.`id` = 1 and `users`.`deleted_at` is null limit 1 select * from `users` where `users`.`id` = 47 and `users`.`deleted_at` is null limit 1 select * from `users` where `users`.`id` = 2 and `users`.`deleted_at` is null limit 1 select * from `users` where `users`.`id` = 1 and `users`.`deleted_at` is null limit 1
app/Repositories/EntryRepository.php の search() 内にある以下の部分を変更
$query = $this->entry->query(); ↓ $query = $this->entry->query()->with('user');
これなら最初に必要なデータを一括して取得するので、上記の問題は解消できる
select * from `entries` where `entries`.`deleted_at` is null order by `id` desc select * from `users` where `users`.`id` in (1, 2, 14, 47) and `users`.`deleted_at` is null
Eloquent:リレーション 6.x Laravel https://readouble.com/laravel/6.x/ja/eloquent-relationships.html ■多対多でリレーション(記事に関連するカテゴリを複数登録して取得) マイグレーションを作成
$ php artisan make:migration create_category_entry_table
作成されたマイグレーションを以下のように修正
/** * Run the migrations. * * @return void */ public function up() { Schema::create('category_entry', function (Blueprint $table) { $table->bigInteger('category_id', false, true)->index(); $table->bigInteger('entry_id', false, true)->index(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('category_entry'); }
マイグレーションを実行
$ php artisan migrate
引き続きプログラムを実装する app/Models/Entry.php に以下を追加
/** * 記事に関連するカテゴリを取得 */ public function categories() { return $this->belongsToMany('App\Models\Category'); }
app/Repositories/EntryRepository.php
/** * 取得 * * @param int $id * @return mixed */ public function find($id) { return $this->entry->with(['categories' => function($query) { $query->orderBy('sort'); }, 'user'])->find($id); } /** * 検索 * * @param array $conditions * @param array $orders * @param int|null $limit * @return mixed */ public function search(array $conditions = array(), array $orders = array(), $limit = null) { $query = $this->entry->query()->with(['categories' => function($query) { $query->orderBy('sort'); }, 'user']); 〜中略〜 /** * 保存 * * @param array $data * @param int|null $id * @return Entry */ public function save(array $data, $id = null) { $entry = $this->entry->updateOrCreate(['id' => $id], $data); $model = $this->entry->find($entry->id); $model->categories()->sync($data['categories']); return $entry; }
app/Services/EntryService.php
/** * 登録 * * @param StoreEntryRequest $request * @return \App\Models\Entry */ public function storeEntry(StoreEntryRequest $request) { $input = $request->only([ 'datetime', 'title', 'body', 'categories', 'user_id', ]); 〜中略〜 /** * 編集 * * @param UpdateEntryRequest $request * @param int $id * @return \App\Models\Entry */ public function updateEntry(UpdateEntryRequest $request, $id) { $input = $request->only([ 'datetime', 'title', 'body', 'categories', 'user_id', ]);
app/Http/Requests/StoreEntryRequest.php
/** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ 'datetime' => ['required'], 'title' => ['required', 'string', 'max:255'], 'body' => ['required'], 'categories' => ['required'], 'user_id' => ['required', 'integer'], ]; } /** * バリデーションエラーのカスタム属性の取得 * * @return array */ public function attributes() { return [ 'datetime' => '日時', 'title' => 'タイトル', 'body' => '本文', 'categories' => 'カテゴリ', 'user_id' => 'ユーザ', ]; }
app/Http/Controllers/Admin/EntryController.php
<?php namespace App\Http\Controllers\Admin; use Illuminate\Http\Request; use App\Http\Controllers\Controller; use App\Http\Requests; use App\Http\Requests\StoreEntryRequest; use App\Http\Requests\UpdateEntryRequest; use App\Models\Entry; use App\Services\EntryService; use App\Services\CategoryService; use App\Services\UserService; class EntryController extends Controller { /** * Create a new controller instance. * * @param EntryService $entryService * @param CategoryService $categoryService * @param UserService $userService * @return void */ public function __construct( EntryService $entryService, CategoryService $categoryService, UserService $userService ) { $this->middleware('auth'); $this->entryService = $entryService; $this->categoryService = $categoryService; $this->userService = $userService; } /** * Display a list of all entries. * * @param Request $request * @return Response */ public function index(Request $request) { return view('admin.entry.index', [ 'entries' => $this->entryService->getEntries([], [['id', 'desc']]), ]); } /** * Display a form of new entry. * * @param Request $request * @return Response */ public function create(Request $request) { return view('admin.entry.form', [ 'categories' => $this->categoryService->getCategories(), 'users' => $this->userService->getUsers(), ]); } /** * Create a new entry. * * @param Request $request * @return Response */ public function store(StoreEntryRequest $request) { if ($this->entryService->storeEntry($request)) { return redirect()->route('admin.entry.index')->with('message', '記事を登録しました。'); } else { return redirect()->route('admin.entry.index')->with('error', '記事を登録できませんでした。'); } } /** * Display a form of edit entry. * * @param Request $request * @return Response */ public function edit(Request $request, $id) { $entry = $this->entryService->getEntry($id); return view('admin.entry.form', [ 'entry' => $entry, 'entry_categories' => array_column($entry->categories->toArray(), 'id'), 'categories' => $this->categoryService->getCategories(), 'users' => $this->userService->getUsers(), ]); } /** * Update a entry. * * @param Request $request * @return Response */ public function update(UpdateEntryRequest $request, $id) { if ($this->entryService->updateEntry($request, $id)) { return redirect()->route('admin.entry.index')->with('message', '記事を編集しました。'); } else { return redirect()->route('admin.entry.index')->with('error', '記事を編集できませんでした。'); } } /** * Destroy the given entry. * * @param Request $request * @param string $id * @return Response */ public function destroy(Request $request, $id) { if ($this->entryService->deleteEntry($id)) { return redirect()->route('admin.entry.index')->with('message', '記事を削除しました。'); } else { return redirect()->route('admin.entry.index')->with('error', '記事を削除できませんでした。'); } } }
resources/views/admin/entry/index.blade.php
<th>カテゴリ</th> <td class="table-text"> @foreach ($entry->categories as $category) {{ $category->name }} @endforeach </td>
resources/views/admin/entry/form.blade.php
<div class="form-group"> <label class="col-sm-3 control-label">カテゴリ</label> <div class="col-sm-6"> @foreach ($categories as $category) <div> <label class="control-label conditions"><input type="checkbox" name="categories[]" value="{{ $category->id }}" @if (in_array($category->id, old('categories', isset($entry) ? $entry_categories : []))) checked @endif> {{ $category->name }}</label> </div> @endforeach </div> </div>
Eloquent:リレーション 6.x Laravel https://readouble.com/laravel/6.x/ja/eloquent-relationships.html

Advertisement