Laravelでタスク管理のプログラムを作る(実装)

# こんにちわ。
前回の投稿からの続き物です。
https://qiita.com/oya_hide/items/1a6fdf3d19312e701c52

今回は中身の実装を行なっていきます。
実装したい具体的な機能としては、以下の通りです。

- 1. タスクの登録・編集
- 2. タスクの検索・表示

構成としては認証機能でログイン後、ホーム画面で「タスクの検索・表示」ができるようにします。
タスクの登録・編集は別な画面で行うようにします。
あくまでサンプル程度のプログラムなので機能的に雑ですが、
もし参考にしていただける場合は、ページングとか諸々の機能を追加していくのが良いかと思います。

※前回でやっておいた方が良かったのですが、
langディレクトリ配下に日本語化用のファイルを配置しておいてください。
フォームでバリデーションエラーが発生した時に出力されるメッセージやログイン画面の日本語化対応になります。
やらなくても動くには動くのですが。

---
# 手順説明

- 1. Model準備
- 2. ルーティングファイルの編集
- 3. bladeにデザインを実装
- 4. 機能を実装
- 5. 説明

---
# Model準備

前回で作成したテーブルのModelを作成します。
以下の通りです。
tasksテーブル以外のModelでは特に何もやってはいません。

~~~php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class TaskType extends Model
{
    use HasFactory;
    use SoftDeletes;
}

~~~

~~~php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class TaskStatus extends Model
{
    use HasFactory;
    use SoftDeletes;

    const COMPLETE = 4; // 完了
}

~~~

~~~php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Priority extends Model
{
    use HasFactory;
    use SoftDeletes;
}

~~~

~~~php

<?php

namespace App\Models;

use App\Models\TaskStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;

class Task extends Model
{
    use HasFactory;
    use SoftDeletes;

    protected $dates = [
        'start',
        'end',
        'created_at',
        'updated_at',
        'deleted_at',
    ];

    protected $fillable = [
        'name','task_status_id','task_type_id','prioritie_id','start','end','memo'
    ];

    // 登録フォームのバリデーション
    static function taskValidator($request) {
        $rule = [
            'name' => 'required|max:255',
            'type' => 'numeric',
            'prioritie' => 'numeric',
            'status' => 'numeric',
            'memo' => 'max:1000',
        ];
        if (!empty($request->start)) {
            $rule['start'] = 'date';
        }
        if (!empty($request->end)) {
            $rule['end'] = 'date';
        }
        return Validator::make($request->all(), $rule);
    }

    // 検索フォームのクエリ
    static function getList($request) {
        $qb = self::select('tasks.id', 'tasks.name', DB::raw('task_types.name AS type'),
            DB::raw('priorities.name AS priority'), DB::raw('task_statuses.name AS status'),
            'tasks.start', 'tasks.end', 'tasks.memo'
        );
        $qb->leftjoin('task_types', 'tasks.task_type_id', '=', 'task_types.id')
            ->leftjoin('priorities', 'tasks.prioritie_id', '=', 'priorities.id')
            ->leftjoin('task_statuses', 'tasks.task_status_id', '=', 'task_statuses.id');
        if (!empty($request['name'])) {
            $qb->where('tasks.name', 'like', '%' . $request['name'] . '%');
        }
        if (!empty($request['type'])) {
            $qb->where('tasks.task_type_id', $request['type']);
        }
        if (!empty($request['priority'])) {
            $qb->where('tasks.prioritie_id', $request['priority']);
        }
        if (!empty($request['status'])) {
            if ($request['status'] == 99) {
                $qb->where('tasks.task_status_id', '<>', TaskStatus::COMPLETE);
            } else {
                $qb->where('tasks.task_status_id', $request['status']);
            }
        }
        if (!empty($request['start'])) {
            $qb->whereDate('tasks.start', '>=', $request['start']);
        }
        if (!empty($request['end'])) {
            $qb->whereDate('tasks.end', '<=', $request['end']);
        }
        if (!empty($request['memo'])) {
            $qb->where('tasks.memo', 'like', '%' . $request['memo'] . '%');
        }

        return $qb->get();
    }
}

~~~

php artisan make:model ファイル名
上記コマンドにてModelを作成します。

tasksテーブルのモデルには検索用のクエリと登録フォーム用のバリデーションを定義しています。
こういったものはなるべく、Modelに記載していった方がコードが見やすくなります。

---
# ルーティングファイルの編集

~~~php

<?php

use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Auth::routes();
Route::get('/', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
Route::match(['get', 'post'], '/task_create', [App\Http\Controllers\HomeController::class, 'task_create'])->name('task_create');

~~~

---
# bladeにデザインを実装

~~~php

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            {{-- 検索フォーム --}}
            <form method="GET" action="{{ route('home') }}">
                @csrf
                <div class="mb-3 row">
                    <label class="col-sm-2 col-form-label">名称</label>
                    <div class="col-sm-4">
                        <input type="text" class="form-control" name="name" value="{{ old('name', $fome_data['name']) }}">
                        <span class="text-danger">{{ $errors->first('name') }}</span>
                    </div>
                    <label class="col-sm-2 col-form-label">種別</label>
                    <div class="col-sm-4">
                        <select class="form-control col-sm-2" name="type">
                            <option value="0">選択してください</option>
                            @foreach ($types as $type)
                                <option value="{{ $type->id }}" @if (old('type', $fome_data['type']) == $type->id) selected @endif>{{ $type->name }}</option>
                            @endforeach
                        </select>
                        <span class="text-danger">{{ $errors->first('type') }}</span>
                    </div>
                </div>
                <div class="mb-3 row">
                    <label class="col-sm-2 col-form-label">優先度</label>
                    <div class="col-sm-4">
                        <select class="form-control col-sm-2" name="priority">
                            <option value="0">選択してください</option>
                            @foreach ($prioritie as $priority)
                                <option value="{{ $priority->id }}" @if (old('type', $fome_data['priority']) == $priority->id) selected @endif>{{ $priority->name }}</option>
                            @endforeach
                        </select>
                        <span class="text-danger">{{ $errors->first('priority') }}</span>
                    </div>
                    <label class="col-sm-2 col-form-label">状態</label>
                    <div class="col-sm-4">
                        <select class="form-control col-sm-2" name="status">
                            <option value="0">選択してください</option>
                            @foreach ($statuses as $status)
                                <option value="{{ $status->id }}" @if (old('status', $fome_data['status']) == $status->id) selected @endif>{{ $status->name }}</option>
                            @endforeach
                            <option value="99" @if (old('status', $fome_data['status']) == 99) selected @endif>完了以外</option>
                        </select>
                        <span class="text-danger">{{ $errors->first('status') }}</span>
                    </div>
                </div>
                <div class="mb-3 row">
                    <label class="col-sm-2 col-form-label">開始</label>
                    <div class="col-sm-4">
                        <input type="date" class="form-control" name="start" value="{{ old('start', $fome_data['start']) }}" autocomplete="off">
                        <span class="text-danger">{{ $errors->first('start') }}</span>
                    </div>
                    <label class="col-sm-2 col-form-label">終了</label>
                    <div class="col-sm-4">
                        <input type="date" class="form-control" name="end" value="{{ old('end', $fome_data['end']) }}" autocomplete="off">
                        <span class="text-danger">{{ $errors->first('end') }}</span>
                    </div>
                </div>
                <div class="mb-3 row">
                    <label class="col-sm-2 col-form-label">メモ</label>
                    <div class="col-sm-8">
                        <input type="text" class="form-control" name="memo" value="{{ old('memo', $fome_data['memo']) }}">
                        <span class="text-danger">{{ $errors->first('memo') }}</span>
                    </div>
                </div>
                <div class="d-grid gap-2 d-md-flex pb-3 justify-content-md-end">
                    <button type="submit" class="btn btn-secondary">検索</button>
                </div>
            </form>
        </div>
        <div class="col-md-10">
            {{-- 課題リスト --}}
            <table class="table table-bordered">
                <thead>
                    <tr>
                        <th>ID</th>
                        <th>名称</th>
                        <th>種別</th>
                        <th>優先度</th>
                        <th>状態</th>
                        <th>開始</th>
                        <th>終了</th>
                        <th>メモ</th>
                    </tr>
                </thead>
                <tbody>
                    @foreach ($tasks as $task)
                    <tr>
                        <td><a href="{{ route('task_create', ['id' => $task['id']]) }}">{{ $task['id'] }}</a></td>
                        <td>{{ $task['name'] }}</td>
                        <td>{{ $task['type'] }}</td>
                        <td>{{ $task['priority'] }}</td>
                        <td>{{ $task['status'] }}</td>
                        <td>{{ $task['start'] }}</td>
                        <td>{{ $task['end'] }}</td>
                        <td>{{ $task['memo'] }}</td>
                    </tr>
                    @endforeach
                </tbody>
            </table>
        </div>
    </div>
</div>
@endsection

~~~

~~~php

@extends('layouts.app')

@section('content')

<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            @if (session('error_message'))
                <div class="alert alert-danger">{{ session('error_message') }}</div>
                @php session()->forget('error_message') @endphp
            @endif
            @if (session('message'))
                <div class="alert alert-success">{{ session('message') }}</div>
                @php session()->forget('message') @endphp
            @endif

            {{-- 登録フォーム --}}
            <form method="POST" action="{{ route('task_create') }}">
                @csrf
                <input type="hidden" name="id" value="{{ old('id', $fome_data['id']) }}">
                <div class="mb-3 row">
                    <label class="col-sm-2 col-form-label">名称</label>
                    <div class="col-sm-4">
                        <input type="text" class="form-control" name="name" value="{{ old('name', $fome_data['name']) }}">
                        <span class="text-danger">{{ $errors->first('name') }}</span>
                    </div>
                    <label class="col-sm-2 col-form-label">種別</label>
                    <div class="col-sm-4">
                        <select class="form-control col-sm-2" name="type">
                            <option value="0">選択してください</option>
                            @foreach ($types as $type)
                                <option value="{{ $type->id }}" @if (old('type', $fome_data['type']) == $type->id) selected @endif>{{ $type->name }}</option>
                            @endforeach
                        </select>
                        <span class="text-danger">{{ $errors->first('type') }}</span>
                    </div>
                </div>
                <div class="mb-3 row">
                    <label class="col-sm-2 col-form-label">優先度</label>
                    <div class="col-sm-4">
                        <select class="form-control col-sm-2" name="priority">
                            <option value="0">選択してください</option>
                            @foreach ($prioritie as $priority)
                                <option value="{{ $priority->id }}" @if (old('type', $fome_data['priority']) == $priority->id) selected @endif>{{ $priority->name }}</option>
                            @endforeach
                        </select>
                        <span class="text-danger">{{ $errors->first('priority') }}</span>
                    </div>
                    <label class="col-sm-2 col-form-label">状態</label>
                    <div class="col-sm-4">
                        <select class="form-control col-sm-2" name="status">
                            <option value="0">選択してください</option>
                            @foreach ($statuses as $status)
                                <option value="{{ $status->id }}" @if (old('status', $fome_data['status']) == $status->id) selected @endif>{{ $status->name }}</option>
                            @endforeach
                        </select>
                        <span class="text-danger">{{ $errors->first('status') }}</span>
                    </div>
                </div>
                <div class="mb-3 row">
                    <label class="col-sm-2 col-form-label">開始</label>
                    <div class="col-sm-4">
                        <input type="date" class="form-control" name="start" value="{{ old('start', $fome_data['start']) }}" autocomplete="off">
                        <span class="text-danger">{{ $errors->first('start') }}</span>
                    </div>
                    <label class="col-sm-2 col-form-label">終了</label>
                    <div class="col-sm-4">
                        <input type="date" class="form-control" name="end" value="{{ old('end', $fome_data['end']) }}" autocomplete="off">
                        <span class="text-danger">{{ $errors->first('end') }}</span>
                    </div>
                </div>
                <div class="mb-3 row">
                    <label class="col-sm-2 col-form-label">メモ</label>
                    <div class="col-sm-8">
                        <input type="text" class="form-control" name="memo" value="{{ old('memo', $fome_data['memo']) }}">
                        <span class="text-danger">{{ $errors->first('memo') }}</span>
                    </div>
                </div>
                <hr>
                <div class="d-grid gap-2 d-md-flex justify-content-md-end">
                    <button type="submit" class="btn btn-secondary">登録</button>
                </div>
            </form>
        </div>
    </div>
</div>
@endsection

~~~
# 機能を実装

~~~php

<?php

namespace App\Http\Controllers;

use App\Models\Priority;
use App\Models\TaskType;
use App\Models\TaskStatus;
use App\Models\Task;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class HomeController extends Controller {
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct() {
        $this->middleware('auth');
    }

    /**
     * Show the application dashboard.
     *
     * @return \Illuminate\Contracts\Support\Renderable
     */
    public function index(Request $request) {
        $prioritie = Priority::get();
        $types = TaskType::get();
        $statuses = TaskStatus::get();
        $tasks = Task::getList($request->all());
        $fome_data = [
            'name' => $request->name,
            'type' => $request->type,
            'priority' => $request->priority,
            'status' => $request->status,
            'start' => $request->start,
            'end' => $request->end,
            'memo' => $request->memo
        ];
        return view('home', compact('prioritie','types','statuses','fome_data','tasks'));
    }

    public function task_create(Request $request) {
        $prioritie = Priority::get();
        $types = TaskType::get();
        $statuses = TaskStatus::get();

        if ($request->method() === 'POST') {
            $fome_data = [
                'id' => $request->id,
                'name' => $request->name,
                'type' => $request->type,
                'priority' => $request->priority,
                'status' => $request->status,
                'start' => $request->start,
                'end' => $request->end,
                'memo' => $request->memo
            ];

            $validator = Task::taskValidator($request);
            if ($validator->fails()) {
                return redirect()->back()->withInput()->withErrors($validator);
            }

            try {
                DB::beginTransaction();
                if (empty($request->id)) {
                    $task = Task::create([
                        'name' => $request->name,
                        'task_status_id' => $request->status,
                        'task_type_id' => $request->type,
                        'prioritie_id' => $request->priority,
                        'start' => $request->start,
                        'end' => $request->end,
                        'memo' => $request->memo,
                    ]);
                } else {
                    $task = Task::where('id', $request->id)->first();
                    $task->name = $request->name;
                    $task->task_status_id = $request->status;
                    $task->task_type_id = $request->type;
                    $task->prioritie_id = $request->priority;
                    $task->start = $request->start;
                    $task->end = $request->end;
                    $task->memo = $request->memo;
                    $task->save();
                }

                $fome_data = [
                    'id' => $task->id,
                    'name' => $task->name,
                    'type' => $task->task_type_id,
                    'priority' => $task->prioritie_id,
                    'status' => $task->task_status_id,
                    'start' => (empty($task->start))? null : $task->start->format('Y-m-d'),
                    'end' => (empty($task->end))? null : $task->end->format('Y-m-d'),
                    'memo' => $task->memo
                ];

                session(['message' => '課題を登録しました。']);
                DB::commit();
            } catch (Exception $ex) {
                session(['error_message' => '課題の登録に失敗しました。']);
                Log::debug($ex);
            } finally {
                DB::rollBack();
            }
        } else {
            if (empty($request->id)) {
                $fome_data = [
                    'id' => null,
                    'name' => null,
                    'type' => null,
                    'priority' => null,
                    'status' => null,
                    'start' => null,
                    'end' => null,
                    'memo' => null
                ];
            } else {
                $task = Task::where('id', $request->id)->first();
                if (empty($task)) {
                    $fome_data = [
                        'id' => null,
                        'name' => null,
                        'type' => null,
                        'priority' => null,
                        'status' => null,
                        'start' => null,
                        'end' => null,
                        'memo' => null
                    ];
                } else {
                    $fome_data = [
                        'id' => $task->id,
                        'name' => $task->name,
                        'type' => $task->task_type_id,
                        'priority' => $task->prioritie_id,
                        'status' => $task->task_status_id,
                        'start' => (empty($task->start))? null : $task->start->format('Y-m-d'),
                        'end' => (empty($task->end))? null : $task->end->format('Y-m-d'),
                        'memo' => $task->memo
                    ];
                }
            }
        }

        return view('task_create', compact('prioritie','types','statuses','fome_data'));
    }

}

~~~

---

---
# 説明
特に難しいものではないです。
基本的には公式のドキュメントに書いてある事だけで実装可能です。
わからない点はメモを取り一つ一つ、公式のドキュメントと照らしていけば上手く生きていけます。

雑なプログラムで恐縮ですが、万が一にもご質問、ご指摘あればご連絡いただけますと幸いです。
以上になります。ありがとうございました。

お先に失礼します。

---