メモ > 技術 > フレームワーク: 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