【初心者向け】Laravelへの理解を深める(慣れる)ためのブログアプリ開発

Webアプリ開発

Laravel初心者がWebアプリを開発するにあたって、とりあえず手を動かして感触を掴みたい方に向けて下記を体験できる記事を作成しました。

  • Sailでの開発
    • Laravel Framework 10.15.0での開発
  • データベースの準備
  • データの作成・読み込み・更新・削除の実装
    • モデルの実装
    • コントローラの実装
    • ルーティング
    • ビューの実装

Let’s 習うより慣れろ!

今回は作成するブログアプリの概要

今回作成するブログアプリは、タイトルと本文、カテゴリから構成される記事を作成・編集・表示・削除します。画面とデータベースの構成を説明します。

画面遷移の概要

ブログアプリは、4つの画面を持ち、それぞれのページをリンクまたはボタンをクリックすることで遷移します。
下図の各枠の上半分が画面名で下半分が画面遷移の方法です。
各画面をつなぐ、矢印が遷移する時に使用するHTTPメソッドおよびURLを表します。

view transition diagram in blog app
ブログアプリの画面遷移図

テーブル・モデルの概要

ブログアプリは、記事を管理するテーブルとカテゴリを管理するテーブルの操作を行います。
下図のようにそれぞれidを主キーとして、Articleはcategory_idを使って、Categoryのnameを参照できるように多対1の関係になります。

er diagram in blog app
ブログアプリのER図

Laravelプロジェクトの作成

開発環境が構築できていない場合は過去記事をご参照ください。

開発環境が構築できている場合は、下記コマンドでblogプロジェクトを作成しましょう。

curl -s https://laravel.build/blog | bash 

データベースの準備

データベース上のデータを利用しやすくするものがアプリです。
まだアプリができあっていない時点では、直接データベースを扱えると検証がしやすいです。
そのため、アプリなしでもデータを扱えるよう準備を行いましょう。

Sailの立ち上げ

Sail環境で開発する場合は、プロジェクトのディレクトリに移動して、下記コマンドでMySQL等のコンテナを起動します。

#Laravelプロジェクトに移動
cd blog
#バックグラウンドでコンテナを起動
sail up -d

マイグレーションファイルの作成

マイグレーションファイルは、データベースのバージョン管理を行うようなファイルのことです。
マイグレーションファイルは下記コマンドで作成できます。

sail artisan make:migration マイグレーションファイル名

しかし、今回は後工程にモデルやコントローラの実装も控えているため、別のコマンドを使用します。
下記コマンドのようにモデル作成のコマンドに引数を追加することでマイグレーションファイルとコントローラのファイルを作成しています。
モデルを引数の意味はそれぞれ下記のようになります。

  • -m:マイグレーションファイルを作る
  • -c:コントローラファイルを作る
  • -r:コントローラの中でもリソースコントローラ(標準的なCRUD操作と対応したindex、create、store、show、edit、update、destroy といったメソッドを含むコントローラ)を作る

また、Categoryにデータを追加するユースケースは今回のアプリでは想定していないため、モデルとマイグレーションファイルのみ作成しています。

#Categoryのマイグレーションファイルとモデルファイルの作成
sail artisan make:model Category -m
#Articleのマイグレーションファイルのついでにモデル、コントローラの作成
sail artisan make:model Article -m -c -r

ここで注意が必要な点は、コマンドの実行順序です。
コマンド実行時のタイムスタンプがマイグレーションファイルの名前に含まれます。そのタイムスタンプ順にマイグレーションが実行されるため、テーブル間で参照関係にある場合は、参照先が先に実行されなければなりません。
間違えた場合は、手作業でファイル名を変えると良いかもしれません。

マイグレーションファイルの編集

コマンドの実行でdatabase/migrationsに「2023_07_17_053500_create_categories_table.php」および「2023_07_17_053600_create_articles_table.php」のような名前のファイルができています。
これらを下記のように編集して、テーブルのカラムを定義します。

~略~
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->timestamps();
        });
~略~

今回のブログアプリはarticlesテーブルのcategory_idにcategoriesテーブルのidにある値だけが入る用外部キー制約を付けたいため、integer(‘category_id’)ではなく、foreignId(‘category_id’)としています。

~略~
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('articles', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->string('content');
            $table->foreignId('category_id')->constrained();
            $table->timestamps();
        });
~略~

マイグレーションの実行

作成したマイグレーションファイルを適用することでデータベースにテーブルが作成されます。
下記コマンドでマイグレーションが実行できます。

sail artisan migrate

phpMyAdminでデータの追加

ここまでのマイグレーション手順で作成したテーブルは空っぽのため、phpMyAdminを使ってデータを追加します。
phpMyAdminにログインしたら、下図のように順にクリックと入力を行い、categoriesテーブルに「グルメ」カテゴリを追加します。

add category by phpMyAdmin
phpMyAdminによるカテゴリの追加

同様の手順でcategoriesテーブルに「旅行」カテゴリの追加、articlesテーブルにtitle:test_title, content: test_content, category_id:2 のようなデータを追加します。

補足:phpMyAdminの導入方法

別途記事を作成します。

記事一覧画面の実装

記事一覧表示機能を有した画面の実装を行います。

ルーティングの実装

画面遷移図で示した矢印(HTTPメソッドとURL)と各矢印がコントローラ内のどの処理を実行するかを定義するルーティングを実装します。
記事一覧表示のためには記事を一覧表示する画面を取得するルートがあれば十分です。
また、名前付きルートにすると、Laravelのrouteヘルパ関数の引数にその名前を渡してリダイレクトを生成できるようになり便利です。

~略~
use App\Http\Controllers\ArticleController;
Route::controller(ArticleController::class)->group(function (){
    Route::get('/articles', 'index')->name('articles.index');
});
Route::redirect('/','/articles');
~略~

モデルの実装

Laravelでは、Eloquentと呼ばれるデータベース操作を簡単にするORM(Object Relational Mapper)が用意されています。Eloquentを利用する際は、各データベーステーブルに対応する「モデル」を実装します。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;//追加 リレーションをつけるため

class Article extends Model
{
    use HasFactory;

    protected $guarded = ['id','created_at'];//追加 $fillable か$guardedで更新できる値を明示する必要がある

    //追加 多対1の関係でのリレーション
    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }
}

sail artisan model:showを実行すると、下記のようにモデルの属性やリレーションを簡単に確認できます。

blog$ sail artisan model:show Article

  App\Models\Article ................................  
  Database .................................... mysql  
  Table .................................... articles  

  Attributes ............................ type / cast  
  id increments, unique ....... bigint unsigned / int  
  title fillable ........................ string(255)  
  content fillable ...................... string(255)  
  category_id fillable .............. bigint unsigned  
  created_at nullable ........... datetime / datetime  
  updated_at nullable, fillable . datetime / datetime  

  Relations .........................................  
  category BelongsTo ............ App\Models\Category  

  Observers .........................................  

Article.phpでarticlesテーブルの参照を定義していないことがわかるでしょうか?Eloquentでは、モデル名(アッパーキャメルケース)がスネークケースかつ複数形になった名前でテーブルがあると規約で決まってるため、articlesテーブルを参照する定義がありません。

コントローラの実装

ルーティングで割り当てられたコントローラメソッドの実装を行います。
ここでは、記事一覧を取得し、ビューへ変数として渡しています。

<?php

namespace App\Http\Controllers;

use App\Models\Article;
use App\Models\Category;//追加
use Illuminate\Http\Request;

class ArticleController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        $articles = Article::all();
        return view('index', ['articles' => $articles]);
    }
~略~

ビューの実装

Bootstrapのcssを利用して見た目を整える練習もしてみます。
また共通部を再利用できるようcommon_layout.blade.phpに切り出してみました。

<!DOCTYPE html>
<html>
    <head>
        <meta charset='utf-8'>
        <link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css' >
        <title>Blog</title>
        <style>body {padding: 100px;}</style>
    </head>
    <body>
        @yield('content')
    </body>
</html>

一覧画面の本体は下記になります。

@extends('common_layout')
@section('content')
    <h1>記事一覧</h1>
    <table class="table-striped">
        <tr>
            <th>タイトル</th><th>本文</th><th>カテゴリ</th>
        </tr>
        @foreach ($articles as $article)
            <tr>
                <td><a href={{ route('articles.show', ['id' => $article->id]) }}>{{ $article->title }}</a></td>
                <td>{{ $article->content }}</td>
                <td>{{ $article->category->name }}</td>
            </tr>
        @endforeach
    </table>
    <div>
        <a href={{ route('articles.create') }} class='btn btn-outline-primary'>新規記事</a>
    </div>
@endsection

上記コードは本記事で作成するアプリの最終状態のため、aタグでまだ作成していないURLの操作が含まれています。
この時点で画面確認したい場合は、aタグで囲まれた値部分だけ残すとよいです。

出来上がった画面イメージ

Sailが立ち上がっている環境のIPアドレスをブラウザに入力してあげると、IPアドレス/articlesにリダイレクトされて下記のような画面が表示されます。

index view in blog app
ブログアプリの記事一覧画面

記事作成画面の実装

記事作成機能を有した画面の実装を行います。

ルーティングの実装

画面の遷移図で記事作成画面に繋がる矢印のうち1つは、既に実装済みのため、残り2つを実装します。
1つは記事作成画面へ遷移するルート、もう1つは記事作成画面で作成した記事をデータベースに登録し、登録した記事の詳細画面に遷移するルートです。

~略~
    Route::get('/articles/new', 'create')->name('articles.create');
    Route::post('/articles', 'store')->name('articles.store');
~略~

コントローラの実装

記事作成画面が持つ機能を実装するため、記事作成画面を表示するcreate関数と記事のデータベースへの登録を行うstore関数を用意します。

create関数ではカテゴリの選択肢を取得してビューに渡しています。

store関数では、引数の$requestではなく、requestヘルパ関数を使ってリクエストから入力フィールドの値を取得しています。

~略~
    /**
     * Show the form for creating a new resource.
     */
    public function create()
    {
        $categories = Category::all();
        return view('new', ['categories' => $categories]);
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request)
    {
        $article = new Article;
        $article->title = request('title');
        $article->content = request('content');
        $article->category_id = request('category_id');
        $article->save();
        return redirect('articles/'.$article->id);
    }
~略~

ビューの実装

コントローラから受け取った$categoryを使って、カテゴリの選択ボックスを表示しています。

@extends('common_layout')
@section('content')
    <h1>新しい記事</h1>
    <form method="POST" action="/articles">
        @csrf
        <div class='form-group'>
            <label for="title">タイトル:</label>
            <input type="text" name="title" id="title" value="">
        </div>
        <div class='form-group'>
            <label for="content">本文:</label>
            <input type="text" name="content" id="content" value="">
        </div>
        <div class='form-group'>
            <label for="category_id">カテゴリ:</label>
            <select name="category_id" id="category_id">
                @foreach($categories as $category)
                    <option value="{{ $category->id }}">{{ $category->name }}</option>
                @endforeach
            </select>
        </div>
        <div class="form-group">
            <button type="submit" class="btn btn-otline-primary">作成する</button>
        </div>
    </form>
    <div>
        <a href={{ route('articles.index') }}>一覧に戻る</a>
    </div>
@endsection

出来上がった画面イメージ

出来上がった画面のイメージは下の画像のようになります。
ここで、タイトル:add_test_title, 本文:add_test_content, カテゴリ:グルメ、として作成ボタンをクリックすると、一覧画面のイメージにあった記事が確認できます。

記事詳細画面の実装

指定したidの記事の表示機能およびその記事の削除機能を有した記事詳細画面を実装していきます。

ルーティングの実装

記事詳細画面の表示と記事の削除を行うためのURL、メソッドを追加します。

~略~
    Route::get('/articles/{id}', 'show')->name('articles.show');
    Route::delete('/articles/{id}', 'destroy')->name('articles.destroy');
~略~

コントローラの実装

idで指定された記事をビューに表示する関数とidで指定された記事をデータベースから削除する関数を実装します。

~略~
    /**
     * Display the specified resource.
     * Article $articleを$idに変更
     */
    public function show($id)
    {
        $article = Article::find($id);
        return view('show',['article' => $article]);
    }
~略~
    /**
     * Remove the specified resource from storage.
     */
    public function destroy($id)
    {
        $article = Article::find($id);
        $article->delete();
        return redirect('/articles');
    }
}

ビューの実装

下記のようにコントローラから受け取った変数を表示するビューを実装します。
また、HTMLではGET、POSTメソッドしか使えませんが、Laravelのフレームワーク上ではDELETEメソッドであることを示すためformタグ内で@method(‘DELETE’)と書きます。

@extends('common_layout')
@section('content')
    <h1>{{ $article->title }}</h1>
    <div>
        <p>{{ $article->content}}</p>
        <p>{{ $article->category->name}}</p>
    </div>
    <div>
        <form method="GET" action="/articles/{{$article->id}}/edit">
            @csrf
            <div class="form-group">
                <button type="submit" class="btn btn-otline-primary">編集する</button>
            </div>
        </form>
        
        <form method="POST" action="/articles/{{$article->id}}">
            @method('DELETE')
            @csrf
            <div class="form-group">
                <button type="submit" class="btn btn-otline-primary">削除する</button>
            </div>
        </form>
    </div>
    <div>
        <a href={{ route('articles.index') }}>一覧に戻る</a>
    </div>
@endsection

出来上がった画面イメージ

最後まで実装した後の完成イメージのため、「編集する」ボタンについても表示されています。
ここまでの実装で表示したい場合は、「編集する」ボタンをビューの実装から一度削除すると良いでしょう。

記事編集画面の実装

記事の更新機能を有した画面の実装を行います。

ルーティングの実装

記事の更新には、更新画面の取得と更新画面で編集した記事をデータベースに対して再登録するルートが必要になります。

~略~
    Route::get('/articles/{id}/edit', 'edit')->name('articles.edit');
    Route::put('/articles/{id}', 'update')->name('articles.update');
~略~

コントローラの実装

記事更新画面を開いた時には現在の値が入っていると便利なため、edit関数では現在値を取得してビューに渡しています。

編集後の値を登録する際は、Eloquentのupdate関数を使います。update関数では自動でupdate_atを更新してくれるため、update_at更新の実装は不要です。

~略~
    /**
     * Show the form for editing the specified resource.
     */
    public function edit($id)
    {
        $article = Article::find($id);
        $categories = Category::all();
        return view('edit',['article' => $article, 'categories' => $categories]);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, $id)
    {
        Article::find($id)->update([
            'title' => request('title'),
            'content' => request('content'),
            'category_id' => request('category_id')
        ]);
        return redirect('articles/'.$id);
    }
~略~

ビューの実装

基本的に記事作成画面と同じ構成で、現在値を表示するための実装が追加されています。

@extends('common_layout')
@section('content')
    <h1>{{$article->title}}を編集する</h1>
    <form method="POST" action="/articles/{{$article->id}}">
        @method('PUT')
        @csrf
        <div class='form-group'>
            <label for="title">タイトル:</label>
            <input type="text" name="title" id="title" value="{{$article->title}}">
        </div>
        <div class='form-group'>
            <label for="content">本文:</label>
            <input type="text" name="content" id="content" value="{{$article->content}}">
        </div>
        <div class='form-group'>
            <label for="category_id">カテゴリ:</label>
            <select class="form-select" name="category_id" id="category_id" >
                <option value="{{ $article->category_id}}">{{ $article->category->name }}</option>
                @foreach($categories as $category)
                    <option value="{{ $category->id }}">{{ $category->name }}</option>
                @endforeach
            </select>
        </div>
        <div class="form-group">
            <button type="submit" class="btn btn-otline-primary">変更を保存する</button>
        </div>
    </form>
    <div>
        <a href={{ route('articles.list') }}>一覧に戻る</a>
    </div>
@endsection

出来上がった画面イメージ

記事作成画面とほとんど同じで現在値が入った画面が出来上がりました。

感想

2つのテーブルを操作するだけのアプリですが、Laravelでの基本的な実装を抑えることができたかと思います。

ちなみに、僕がこの記事を書いた理由は、2つの困ったことがあり、同じく悩んでいる人がいるかもって思ったからです。
1つ目は、Laravelのバージョンによって、ルーティングの書き方やモデルのnamespaceが変わるため、先人たちのブログだけではわからない(混乱する)部分が多くあったことです。
2つ目は、1記事でまとまっている、かつ、2つ以上のテーブルを扱ったチュートリアルがすぐには見つからなかったことです。
上記の困りごとに対して、僕自身はLaravel公式のリファレンスを参照してコードを書くことが一番合っていました。
しかし、参照しながら書くのでスピードが上がりませんでした。
フレームワークに慣れないうちは同じく悩みそうなので、ブログ記事としてアウトプットしてみました。

次は認証や認可周りや見た目をよくする方法を学んで、アウトプットしていきます。

後日談

記事を書き上げた後に見つけてやってみたのですが、Laravel Bootcampという私が最初に欲しかったものを満たすチュートリアルがありました。
チュートリアルの中で認証やメール通知まで抑えられていて勉強になりました。僕的には、本記事よりさらに「習うより慣れろ」感を強いので、僕の記事で1回Laravelを味わうのもありかもしれません。

コメント

タイトルとURLをコピーしました