未経験からエンジニア転職をするための最強ロードマップロードマップ

【Laravel教材⑤】Laravelでブログシステム構築!〜記事投稿機能実装編〜

ファド

こんにちは!
PHPのLaravelやJavaScriptでWeb開発をしているフリラーンスエンジニアのファドと申します!

こちらの記事はLaravel教材の第5回目の記事になります。

その他のLaravel教材を学習したい方は下記リンクから直接教材へ飛ぶことができます。

目次

記事投稿

ここまで記事一覧画面と記事詳細画面を作ってきましたが、ここからは記事を投稿する機能の作成をしていきます。

記事投稿の流れは下記のようになっています。

  1. ビューのフォームからデータを飛ばす
  2. ルートで受け取りコントローラーへ渡す
  3. フォームリクエストを使用してバリデーションをする
  4. モデルでデータを登録する
  5. エラー処理を記述

ビューのフォームからデータを飛ばす

まずは記事投稿フォームに記事のタイトルと内容が入力され、登録ボタンがクリックされた時にデータを飛ばしていきます。

ルートで受け取りコントローラーへ渡す

ルートでリクエストを受け取り、データを登録するためのメソッドにデータを渡します。

フォームリクエストを使用してバリデーションをする

Laravelにはバリデーションを簡単に実装するためのフォームリクエストという機能が最初から組み込まれています。

フォームリクエストの作成方法を説明する前に、そもそもバリデーションとは何なのかということについても学習していきましょう。

バリデーションとは

入力されたデータが正しい値かどうかをチェックする機能をバリデーションと言います。

例えば、マイグレーションでarticlesテーブルを作成した時に、テーブル定義でタイトルと内容は必須で登録しなければいけないことにしています。
しかし、記事をフォームで登録する時にタイトルと内容の2つが入力されていなかった場合は、ない値をデータベースに登録することはできないので、エラーになってしまいます。

バリデーションではこのような事態を避けるために、必須チェックというものを実装することができます。
簡単に説明すると、フォームに値が入っているか入っていないかのチェックができるということです。
また、バリデーションは必須チェックだけではなく、データの型チェックや文字数チェックなど様々なチェックをすることができます。

つまり、開発者の意図しないデータがフォームに入力されることを防ぐことができるので、セキュリティ強化の面からバリデーションはほぼ必須となります。

フォームリクエストの作成方法

それではバリデーションがどのようなものか説明したので、フォームリクエストの作成方法について説明します。
フォームリクエストも下記コマンド1つで作成することができます。

$ ./vendor/bin/sail php artisan make:request クラス名

このコマンドを使用すると、1度目の実行時にfirst-app/app/Http/ディレクトリにRequestsというディレクトリが生成され、その中にクラス名で指定した名前のPHPファイルが作成されます。

2回目以降はfirst-app/app/Http/Requestsディレクトリがすでに存在しているので、その中に新しいフォームリクエストファイルが作成されます。

使用方法などは実装する時に説明していきます。

モデルでデータを登録

すでにモデルの機能であるallメソッドfindメソッドについて学習しましたが、モデルでデータを登録する時にはcreateメソッドというものを使用します。

使い方は下記の通りです。

Article::create([
    'title' => 'タイトル',
    'content' => 'コンテンツ',
]);

このようにArticle::create()と記述し、第一引数に配列で登録するデータを入れるだけです。

また、Laravelにはもう1つデータを登録するためのfillメソッド + saveメソッドが提供されています。
使い方は下記の通りです。

$article = new Article;

$article->fill([
    'title' => 'タイトル',
    'content' => 'コンテンツ',
]);

$article->save();

他にも登録方法はありますが、セキュリティの観点からそれらは非推奨となっているので、上記2つの方法のどちらかでデータ登録機能を実装しましょう。
おすすめは記述するコードが少ないcreateメソッドです。

また、これら2つの登録方法を使用した場合、変更できるカラムはfirst-app/app/Models/Article.phpprotected $fillableprotected $guardedで指定したものだけです。

そのため、first-app/app/Models/Article.phpprotected $fillabletitlecontentを指定しています。

エラー処理を記述

最後にデータベースへ登録できなかった場合のエラー処理をしておきます。

これをすることによって万が一エラーが発生した時に、ユーザーが視覚的に記事を登録できないことを把握することができます。

実装

それでは記事登録の流れを説明したので、さっそく実装していきましょう。

記事投稿用のルート設定

まずは記事を登録するフォームを表示するためのルートを設定していきましょう。
first-app/routes/web.phpを下記の通り編集してください。

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ArticleController;

Route::get('/', function () {
    return view('welcome');
});

// 投稿一覧を表示
Route::get('/articles', [ArticleController::class, 'showArticles'])->name('showArticles');

// 記事投稿フォームを表示
Route::get('/article/create', [ArticleController::class, 'createArticle'])->name('createArticle'); // ここを追加

// 投稿詳細を表示
Route::get('/article/{id}', [ArticleController::class, 'showArticle'])->name('showArticle');

いつも通りRoute::getでパスとコントローラー、メソッド名を指定しています。
今回は記事を作成するという意味のcreateArticleメソッドとしました。

createArticleメソッドの作成

次に、Articleコントローラーにルートで指定したcreateArticleメソッドを記述していきましょう。

first-app/app/Http/Controllers/ArticleController.phpを下記の通り編集してください。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Article;

class ArticleController extends Controller
{
    /**
     * 記事一覧を表示する
     */
    public function showArticles()
    {
        // 全ての記事データを取得
        $articles = Article::all();

        // compact関数で渡す場合
        return view('articles.index', compact('articles'));
    }

    /**
     * 記事投稿フォームを表示
     */
    public function createArticle()
    {
        return view('articles.create');
    }

    /**
     * 記事詳細を表示する
     */
    public function showArticle($id)
    {
        // 渡されてきた記事IDのデータを取得
        $article = Article::find($id);

        return view('articles.detail', compact('article'));
    }
}

追加したコードは下記の通りです。

/**
 * 記事投稿フォームを表示
 */
public function createArticle()
{
    return view('articles.create');
}

これで記事投稿フォームを表示するためのcreateArticleメソッドを定義することができました。
投稿フォームに表示させるデータは何もないので、create.blade.phpへ渡す処理をviewメソッドを使用して記述するだけでOKです。

記事投稿用フォームの作成

次にfirst-app/resources/views/articles配下にcreate.blade.phpをコマンドかVSCodeで作成してください。

コマンドで作成する場合は、ターミナルにて下記コマンドをfirst-appディレクトリ上で実行してください。

$ touch resources/views/articles/create.blade.php

作成したfirst-app/resources/views/articles/create.blade.phpを下記の通り編集してください。

@extends('layout')

@section('title')
    記事作成
@endsection

@section('content')
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <h2>記事投稿フォーム</h2>
            <form method="POST" action="">
                <div class="form-group">
                    <label for="title">
                        タイトル
                    </label>
                    <input id="title" name="title" class="form-control" value="{{ old('title') }}" type="text">
                    @if ($errors->has('title'))
                        <div class="text-danger">
                            {{ $errors->first('title') }}
                        </div>
                    @endif
                </div>
                <div class="form-group mt-3">
                    <label for="content">
                        内容
                    </label>
                    <textarea id="content" name="content" class="form-control" rows="4">{{ old('content') }}</textarea>
                    @if ($errors->has('content'))
                        <div class="text-danger">
                            {{ $errors->first('content') }}
                        </div>
                    @endif
                </div>
                <div class="mt-3">
                    <a class="btn btn-secondary mr-2" href="{{ route('showArticles') }}">
                        キャンセル
                    </a>
                    <button type="submit" class="btn btn-primary">
                        投稿
                    </button>
                </div>
            </form>
        </div>
    </div>
@endsection

それではhttp://localhost/article/createへアクセスしてください。
下記画像のような画面が出てくればOKです。

フォーム部分のコードを説明していきます。

まず、form要素のmethod属性はPOSTにしています。
action属性は本来登録処理のルートを設定しなければなりませんが、今は何も指定していません。
というのも、存在しないルートをビューで指定してしまうと下記のようなエラーが出てしまうためです。

上記のエラーは、まだ存在していないルート名storeArticlerouteメソッドで指定した時のエラーです。

次にタイトル入力欄について確認してみましょう。

<input id="title" name="title" class="form-control" value="{{ old('title') }}" type="text">

まずタイトル入力欄にはinputタグを使用しています。
name属性にはtitleを設定しており、登録処理の際にname属性で入力された値を指定することでデータを受け取ることができます。

また、value属性old('title')と記述していますが、こちらはLaravelのヘルパーメソッドであるoldメソッドを使用しています。
()内にname属性と同じ値を指定することで、バリデーションのエラー時にフォームの値を保持することができます。
ここはバリデーション実装時に改めて確認しましょう。

また、下記のコードも追加しています。

@if ($errors->has('title'))
    <div class="text-danger">
        {{ $errors->first('title') }}
    </div>
@endif

Bladeテンプレートでは条件分岐を@if(条件式)~@endifと記述することができます。
もちろん複数の条件分岐をしたい場合は@if(条件式A)~@elseif(条件式B)~@else~@endifのように記述することもできます。

今回のコードでは条件式に$error->has('title')と記述されています。
このコードの意味は、タイトルをバリデーションで検証した結果、エラーがあればtrue、なければfalseを出力するといったものです。

そして、エラーがあった場合はtrueとなり条件分岐内の処理が実装され、$errors->first('title')と記述されているので、1つ目のエラーメッセージが出力されます。

こちらもバリデーションを実装した時に動きを確認してみましょう。

次に内容入力欄について確認してみましょう。
ほとんどタイトル入力欄と同じですが、inputタグではなくtextareaタグを使用しています。
textarea要素にはvalue属性がないので、old('content')textareaの開始タグtextareaの終了タグで囲みましょう。

バリデーションエラーを出すためのコードはタイトル部分と全く同じです。

最後にボタン部分について確認してみましょう。
まず、キャンセルボタンに関してはaタグを使用して、クリックされた時にroute('showArticles')に遷移する処理を実装しています。
こうすることで、キャンセルボタンをクリックされた後に処理一覧画面に戻すことができます。

次に投稿ボタンに関してはbuttonタグを使用して、type属性submitを指定することで、ボタンがクリックされた時にフォームを送信するようにしています。

以上でフォーム部分の説明は終わりです。

記事投稿処理用のルート設定

次に投稿処理用のルートを設定していきましょう。
first-app/routes/web.phpを下記の通り編集してください。

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ArticleController;

Route::get('/', function () {
    return view('welcome');
});

// 投稿一覧を表示
Route::get('/articles', [ArticleController::class, 'showArticles'])->name('showArticles');

// 記事投稿フォームを表示
Route::get('/article/create', [ArticleController::class, 'createArticle'])->name('createArticle');

// 記事登録処理
Route::post('/article/store', [ArticleController::class,'storeArticle'])->name('storeArticle'); // ここを追加

// 投稿詳細を表示
Route::get('/article/{id}', [ArticleController::class, 'showArticle'])->name('showArticle');

今回はフォームからPOSTでデータが送られてくるので、Route::getではなくRoute::postとしています。
あとはいつも通りパスとコントローラー、メソッド名を指定しています。
メソッド名は保存するという意味のstoreArticleメソッドとしました。

フォームにaction属性を追加

ルートを設定したので、create.blade.phpaction属性を追加します。

first-app/resources/views/articles/create.blade.phpform開始タグを下記のように変更してください。

<form method="POST" action="{{ route('storeArticle') }}">

action属性に登録処理用のルート名であるstoreArticleを指定すると、投稿ボタンがクリックされるとArticleコントローラーstoreArticleメソッドが実行されます。

storeArticleメソッドの作成

では、コントローラーにstoreArticleメソッドを記述していきましょう。
first-app/app/Http/Controllers/ArticleController.phpを下記の通り編集してください。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Article;

class ArticleController extends Controller
{
    /**
     * 記事一覧を表示する
     */
    public function showArticles()
    {
        // 全ての記事データを取得
        $articles = Article::all();

        // compact関数で渡す場合
        return view('articles.index', compact('articles'));
    }

    /**
     * 記事投稿フォームを表示
     */
    public function createArticle()
    {
        return view('articles.create');
    }

    /**
     * 記事投稿処理
     */
    public function storeArticle(Request $request)
    {
        dd($request);
    }

    /**
     * 記事詳細を表示する
     */
    public function showArticle($id)
    {
        // 渡されてきた記事IDのデータを取得
        $article = Article::find($id);

        return view('articles.detail', compact('article'));
    }
}

追加したコードは下記の通りです。

/**
 * 記事投稿処理
 */
public function storeArticle(Request $request)
{
    dd($request);
}

フォームから送信された値はstoreArticle(Request $request)と記述することで、変数requestとして受け取ることができます。

これはArticleコントローラーの5行目あたりを見てもらうとuse Illuminate\Http\Request;と記述されています。
この機能を使用することで、Laravelではフォームから送信された値を変数requestとして受け取ることができるということです。

まずはフォームから何か値が送られてくるときはRequest $requestとなることを覚えておきましょう。

フォームから送られてきた値が本当に取得できているのかを確認するためにdd($request)と記述しておきます。

それでは、変更が完了したらhttp://localhost/article/createへアクセスし、タイトルと内容に適当なテキストを入力して投稿ボタンをクリックしてください。

すると419 PAGE EXPIREDエラー画面が表示されます。

CSRF保護

実は、Laravelでフォームを使用する場合はCSRF保護というものをしなければなりません。

そもそもCSRF(シーサーフ)とは、クロスサイトリクエストフォージェリの略で、Webアプリケーションに存在する脆弱性、もしくはその脆弱性を利用した攻撃方法のことです。

具体的には、アプリケーションに対して攻撃しようとしている攻撃者が作成した「不正なリンク」を攻撃対象者にクリックさせることで、あたかも攻撃対象者が操作したかのようにみせかけて攻撃者がアプリケーションに対して不正なリクエストを送信することです。

このような攻撃からアプリケーションを守るために、LaravelではCSRF保護というものが提供されています。
やり方は至って簡単で、layout.blade.php<meta name="csrf-token" content="{{ csrf_token() }}">を追加することとform開始タグ直後に@csrfというコードを追加するだけです。

こうすることでフォーム送信時にトークンが発行され、そのトークンを使用してリクエストがなりすましかどうかを判断することができます。

CSRFの概念は難しいので理解をする必要はありません。
どのようにCSRFの対策ができるのかを理解しておきましょう。

それではさっそくfirst-app/resources/views/layout.blade.phpを下記の通り編集してください。

<!DOCTYPE HTML>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <!-- titleは可変 -->
    <title>@yield('title', 'ブログ')</title> 
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/css/bootstrap.min.css" integrity="sha384-r4NyP46KrjDleawBgD5tp8Y7UzmLA05oM1iAEQ17CSuDqnUK2+k9luXQOfXJCJ4I" crossorigin="anonymous">
</head>

<body>
    <!-- ヘッダー -->
    @include('header')

    <!-- コンテンツ -->
    <div class="container mt-4">
        @yield('content')
    </div>

    <!-- フッター -->
    @include('footer')

    <!-- JavaScript -->
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js" integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/" crossorigin="anonymous"></script>
</body>

</html>

head要素の中に<meta name="csrf-token" content="{{ csrf_token() }}">という1コードを追加しています。

次にfirst-app/resources/views/articles/create.blade.phpform開始タグの直後に下記コードを追加してください。

<form method="POST" action="{{ route('storeArticle') }}">
    @csrf

再度http://localhost/article/createへアクセスして、タイトルと内容に適当なテキストを入れて投稿ボタンをクリックしてください。
下記画像のようにddメソッドを使用した変数requestの結果が表示されていればOKです。

その中のrequestを展開してみましょう。
requestの中にはparametersがあり、それを展開すると_tokentitlecontentが入っています。
_tokenはCSRF保護で作成されたトークンのことであり、titlecontentは入力した値が入っていればフォームで入力された値が取得できていればOKです。

これで変数requestを使用すると、簡単にフォームから入力された値が取得できることが確認できました。
ddメソッドはとても便利ですね!

登録処理を実装

それでは、Articleコントローラーにフォームから受け取った値を登録する処理を記述していきましょう。
first-app/app/Http/Controllers/ArticleController.phpを下記の通り編集してください。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Article;

class ArticleController extends Controller
{
    /**
     * 記事一覧を表示する
     */
    public function showArticles()
    {
        // 全ての記事データを取得
        $articles = Article::all();

        // compact関数で渡す場合
        return view('articles.index', compact('articles'));
    }

    /**
     * 記事投稿フォームを表示
     */
    public function createArticle()
    {
        return view('articles.create');
    }

    /**
     * 記事投稿処理
     */
    public function storeArticle(Request $request)
    {
        // 記事登録処理
        Article::create([
            'title' => $request->title,
            'content' => $request->content,
        ]);

        return redirect()->route('showArticles');
    }

    /**
     * 記事詳細を表示する
     */
    public function showArticle($id)
    {
        // 渡されてきた記事IDのデータを取得
        $article = Article::find($id);

        return view('articles.detail', compact('article'));
    }
}

変更したコードは下記の通りです。

/**
 * 記事投稿処理
 */
public function storeArticle(Request $request)
{
    // 記事登録処理
    Article::create([
        'title' => $request->title,
        'content' => $request->content,
    ]);

    return redirect()->route('showArticles');
}

先ほど説明した通りcreateメソッドを使用しています。

また、return redirect()->route('showArticles');と記述しています。
以前作成した他のメソッドとは少し違いviewメソッドではなくredirectメソッドを使用して画面の遷移を実装しています。

viewメソッドredirectメソッドも画面を遷移させる処理という点は同じですが、ルートを記述した時にRoute::getとした場合はviewメソッドを使用して、反対にRoute::postとした場合はredirectメソッドを使用して画面遷移を実装してください。

わかりやすいように表にまとめておきます。

ルートメソッド
Route::getview()
Route::postredirect()

それではhttp://localhost/article/createへアクセスして、タイトルと内容に適当なテキストを入れて投稿ボタンをクリックしてください。

登録ボタンをクリックすると記事一覧画面へリダイレクトされ、入力した内容が4つ目の記事として表示されていれば記事投稿処理は無事に実装できています。

フォームリクエストを使用してバリデーションをする

投稿処理を実装することはできましたが、記事投稿画面でタイトルと内容が空の状態で投稿ボタンをクリックされると下記画像のようなエラーが発生してしまいます。

これはテーブルの設定でtitlecontentがNULL(値がないこと)を許可していないからです。

なので、titlecontentには何かしら文字列型の値が入っている必要があります。
その制御であるバリデーションをLaravelではフォームリクエストという機能を使用して実装します。

それでは、先ほど説明した下記コマンドをターミナルにてfirst-app配下で実行してください。

$ ./vendor/bin/sail php artisan make:request ArticleStoreRequest

今回は記事登録のバリデーションを作成したいので、クラス名はArticleStoreRequestとします。
指定したクラス名+phpの拡張子のファイルであるArticleStoreRequest.phpfirst-app/app/Http/Requestsに作成されていればOKです。
first-app/app/Http/Requests/ArticleStoreRequest.phpを下記の通り編集してください。

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ArticleStoreRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true; // falseからtrueへ変更
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, mixed>
     */
    public function rules()
    {
        return [
            'title' => 'required|string|max:100',
            'content' => 'required|string',
        ];
    }
}

まず、ArticleStoreRequestクラスではFormRequestクラスを継承していることから、フォームリクエストというLaravelの機能が使用できます。

次にauthorizeメソッドreturn falsereturn trueに変更してください。
こちらのauthorizeメソッドは、フォームリクエストの利用が許可されているかどうかを示すメソッドで、falseの場合はArticleStoreRequestクラスが使用できなくなるので、必ずtrueにしてください。

次にrulesメソッドreturn [];にバリデーションをしたいカラム名を入力して=> ''と記述し、''内に、どのようなバリデーションをするかを記述します。

''内に記述できるバリデーションは多岐に渡り、ドキュメント使用可能なバリデーションルールに全て記載されています。

これらのバリデーションルールを暗記する必要は全くありません!
使用したいバリデーションルールを実装する時にドキュメントを見ながら設定できればOKです!

今回、タイトルと内容に適応させたバリデーションルールは下記の通りです。

  • title(タイトル):required(必須チェック)、string(文字列型チェック)、max:100(文字数制限100文字まで)
  • content(内容):required(必須チェック)、string(文字列型チェック)

タイトルのmax:100についてですが、テーブル設定の際に最大文字数を100文字に指定しているので、PHP側でも100文字までしかタイトルを入れられないようにしています。

これでArticleStoreRequest.phpの編集は終わりなので、Articleコントローラーへ反映させましょう。

独自に作成したフォームリクエストをコントローラーに反映させるためにはuse文を使用して、作成したフォームリクエストクラスを読み込まなければなりません。

なので、今回作成したArticleStoreRequestを読み込むためには、下記のコードがArticleコントローラーに必要となります。
use App\Http\Requests\ArticleStoreRequest;

次に記事投稿処理であるstoreArticleメソッドの引数にRequest $requestと記述していますが、Request部分をArticleStoreRequestとします。

こうすることでフォームから送信された値がArticleStoreRequest.phpで指定したバリデーションを通過した後に、Articleコントローラーの変数requestで受け取れるようになります。

それではfirst-app/app/Http/Controllers/ArticleController.phpを下記の通り編集してください。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Article;
use App\Http\Requests\ArticleStoreRequest;

class ArticleController extends Controller
{
    /**
     * 記事一覧を表示する
     */
    public function showArticles()
    {
        // 全ての記事データを取得
        $articles = Article::all();

        // compact関数で渡す場合
        return view('articles.index', compact('articles'));
    }

    /**
     * 記事投稿フォームを表示
     */
    public function createArticle()
    {
        return view('articles.create');
    }

    /**
     * 記事投稿処理
     */
    public function storeArticle(ArticleStoreRequest $request)
    {
        // 記事登録処理
        Article::create([
            'title' => $request->title,
            'content' => $request->content,
        ]);

        return redirect()->route('showArticles');
    }

    /**
     * 記事詳細を表示する
     */
    public function showArticle($id)
    {
        // 渡されてきた記事IDのデータを取得
        $article = Article::find($id);

        return view('articles.detail', compact('article'));
    }
}

これでバリデーションの実装は完了したので、さっそく動作確認をしてみましょう。

http://localhost/article/createへアクセスして、タイトルと内容に何も入力しないで投稿ボタンをクリックしてください。
下記画像の通り、エラーメッセージが表示されればOKです。

エラーメッセージが英語でわかりにくいですが、titlecontentは入力必須です。というエラーが起こっています。

日本語化

初期設定で日本語の設定はしたはずですが、なぜかバリデーションエラーが英語のままです。
それは、初期設定の設定だけだと設定を変更しただけで日本語のファイルがLaravelにないために起こります。

Laravelでは日本語のファイルをダウンロードする方法がドキュメントに記載されているので、そちらのコマンドを実行していきましょう。

ドキュメントには./vendor/bin/sailが付いていませんが、今回はDocker環境なので付けて実行しましょう。
それでは、ターミナルで下記3つのコマンドをfirst-appディレクトリ上で実行してください。

$ ./vendor/bin/sail php -r "copy('https://readouble.com/laravel/8.x/ja/install-ja-lang-files.php', 'install-ja-lang.php');"
$ ./vendor/bin/sail php -f install-ja-lang.php
$ ./vendor/bin/sail php -r "unlink('install-ja-lang.php');"

この3つのコマンドを実行しても、特にコマンド実行成功などのテキストはターミナルには表示されませんがfirst-app/resources/lang/jaフォルダが追加されており、さらにその中には4つのファイルが追加されます。
これら4つのファイルが日本語用のファイルになります。

改めてhttp://localhost/article/createへアクセスして、タイトルと内容に何も入力しないで投稿ボタンをクリックしてください。
下記画像の通り、エラーメッセージが表示されればOKです。

まだtitlecontentが英語なので、これも修正していきましょう。

これを修正するためにはArticleStoreRequest.phpを編集する必要があります。
first-app/app/Http/Requests/ArticleStoreRequest.phpを下記の通り編集してください。

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ArticleStoreRequest 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<string, mixed>
     */
    public function rules()
    {
        return [
            'title' => 'required|string|max:100',
            'content' => 'required|string',
        ];
    }

    public function attributes()
    {
        return [
            'title' => 'タイトル',
            'content' => '内容',
        ];
    }
}

このようにattributesメソッドを追加して、name属性値 => '翻訳内容'と記述して翻訳していきましょう。

それではhttp://localhost/article/createへアクセスして、タイトルと内容に何も入力しないで投稿ボタンをクリックしてください。
下記画像の通り、エラーメッセージが表示されればOKです。

これでバリデーションのエラーメッセージの日本語化は完了です。

ちなみに、タイトルに100文字以上入力し、内容にこれは内容です。というテキストを入力してから投稿ボタンをクリックすると、バリデーションエラーが以下画像のようなメッセージに変わります。

まず、タイトルに入力した内容がバリデーションエラーが出ても消えていないことに注目してください。

例えば、バリデーションエラーが起こった際に入力した内容が全て消えてしまっていたら、ユーザーはどのように入力してエラーになったのかがわかりません。
それだけではなく、入力した内容の一部分だけ修正したいと思っても入力内容が全てクリアされていたら、また1から書き直しということになってしまいます。

このような状態を改善するために、create.blade.phpではoldメソッドを使用しています。
oldメソッドold()()内にname属性値を指定することで、バリデーションエラー時にフォームの値を保持することができます。

今回タイトルに100文字以上の値を入力しているので、文字数オーバーのバリデーションエラーが出ていますが、oldメソッドをvalue属性で使用しているため、フォームの内容が保持されており、入力内容がクリアされることなくバリデーションエラー後も出力されています。

このようにLaravelでフォームを作成する際は基本oldメソッドを使用しましょう。

次に、内容部分に注目してください。
こちらはバリデーションエラーがないため、エラーメッセージは表示されません。
それは以前create.blade.phpで実装した下記の条件分岐の記述があったためです。

@if ($errors->has('content'))
    <div class="text-danger">
        {{ $errors->first('content') }}
    </div>
@endif

$errors->has('content')falseとなるので、エラーが表示されないということです。

一方でタイトル部分は100文字以上入力しているので、$errors->has('title')trueとなり、エラーメッセージが表示されています。

エラー処理を記述

無事に記事登録処理を実装することができましたが、データベースにデータを登録する際や編集・削除する際にはエラー処理を記述することが一般的です。

というのも、もし処理がうまく行かずデータが登録できなかった場合やデータベースに繋げなかった場合などにエラーが発生してしまいます。

そのような予期せぬエラーが発生してしまった場合に備えて、エラー時にデータを登録できないようにする処理とログにエラー内容を出力させる処理、エラー画面へ遷移させるための処理の3つを実装しておきます。

まずはfirst-app/app/Http/Controllers/ArticleController.phpを下記の通り編集してください。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Article;
use App\Http\Requests\ArticleStoreRequest;
use Illuminate\Support\Facades\DB; // ここを追加
use Illuminate\Support\Facades\Log; // ここを追加

class ArticleController extends Controller
{
    /**
     * 記事一覧を表示する
     */
    public function showArticles()
    {
        // 全ての記事データを取得
        $articles = Article::all();

        // compact関数で渡す場合
        return view('articles.index', compact('articles'));
    }

    /**
     * 記事投稿フォームを表示
     */
    public function createArticle()
    {
        return view('articles.create');
    }

    /**
     * 記事投稿処理
     */
    public function storeArticle(ArticleStoreRequest $request)
    {
        // トランザクション開始
        DB::beginTransaction();
        try {
            // 記事登録処理
            Article::create([
                'title' => $request->title,
                'content' => $request->content,
            ]);

            // トランザクションコミット
            DB::commit();
        } catch(\Exception $e) {
            // トランザクションロールバック
            DB::rollBack();

            // ログ出力
            Log::critical($e);

            // エラー画面遷移
            abort(500);
        }

        return redirect()->route('showArticles');
    }

    /**
     * 記事詳細を表示する
     */
    public function showArticle($id)
    {
        // 渡されてきた記事IDのデータを取得
        $article = Article::find($id);

        return view('articles.detail', compact('article'));
    }
}

それでは追加したコードについて説明します。

まず、トランザクションという処理を実装するためにuse Illuminate\Support\Facades\DB;を追加し、ログを出力させるためにuse Illuminate\Support\Facades\Log;を追加しています。

これらのuse文を暗記する必要はありません。
例えば、Laravel トランザクションと検索するだけでトランザクションを実装するための記事がたくさん出てくるからです。
もちろん、その記事の中でuse文を追加することが書いてあります。

ログ出力の場合もそうです。
Laravel ログ出力などで検索すると、トランザクションの時と同じように実装方法を説明してくれている記事がたくさん出てきます。

それらの記事を見ながら実装できれば完璧です。

次に、新たに記事投稿処理のstoreArticleメソッドtry-catch文トランザクション処理ログ出力処理エラー画面への遷移処理の4つを追加しています。

追加した4つについて詳しく見ていきましょう。

try-catch文

try-catch文を使用するとtry{}で囲まれた処理の中でエラーが発生した場合に処理を止めて、catch(){}内の処理を実行することができます。

例えば、今回でいうと登録処理に失敗した場合は、catch文の中にデータベースにデータを登録させないようにトランザクション処理を記述していたり、エラー内容を受け取ってログを出力させる処理、エラー画面へ遷移させる処理の3つを記述しています。

このようにエラー処理をすることで、エラーが発生しているにも関わらず不正にデータが登録されることを防ぎ、開発者にどこでどのようなエラーが起こっているのかがわかるようにログを出力し、ユーザーには何かしらのエラーが起こったことを画面を通して伝えることができます。

これらの処理を実装するための記述がtry-catch文です。

基本の使い方は下記の通りです。

try {
    // 一連の処理
} catch(\Exception $e) {
    // エラー時に行う処理
}

上記のように記述するだけでtry-catch文を使用することができます。
また、catch(\Exception $e){}と記述していますが、引数eにはエラー内容が渡されているので、エラー内容をログに残す際などに使用することができます。

トランザクション処理

トランザクションという言葉か何回か出てきましたが、そもそもトランザクションとは一連の作業を1つの処理として管理するために用いられるものです。
このトランザクションを使用することで、一連の処理が全て成功したどこかで失敗したの2パターンで処理を分けることができます。

今回でいうと、登録処理が無事に成功すればデータベースに値を登録し、登録処理が失敗したらデータを登録しないという処理を実装することができます。

Laravelでのトランザクションの使い方は下記の通りです。

use Illuminate\Support\Facades\DB; // use文

DB::beginTransaction();
try {
    // データベースへの保存や編集、削除処理など
    DB::commit();
} catch() {
    DB::rollback();
    // エラー時の処理
}

トランザクションはtry-catch文とセットで使うことを覚えておきましょう。
成功したらcommitとして、失敗した場合にはrollbackとすることで、処理が成功した時のみデータベースへデータが登録されることになります。

ログ出力

JavaScriptではconsole.log()を使用して、コンソールにログを出力していましたが、Laravelでも簡単にログを出力することができます。

Laravelではddメソッドが使用できるので、基本的なデバッグ時にログを使用することは少ないですが、ログを出力することはエラー時にエラー内容を確認したり、後々ddメソッドが使えない場合などのデバッグをする際にもとても役立につので、ぜひ覚えておきましょう。

Laravelでのログ出力方法は下記の通りです。

use Illuminate\Support\Facades\Log;

Log::emergency('エラーメッセージ');
Log::alert('エラーメッセージ');
Log::critical('エラーメッセージ');
Log::error('エラーメッセージ');
Log::warning('エラーメッセージ');
Log::notice('エラーメッセージ');
Log::info('エラーメッセージ');
Log::debug('エラーメッセージ');

()内にはもちろん変数も指定できるので、今回のコードのようにLog::critical($e)とすることで、エラー内容をログに出力することができます。
※変数eはtry-catct文を使用した際にエラーが入っている変数のこと

また、ログにはログレベルというものがあり、どのログを使用してもOKですが、エラー内容や緊急度によってログレベルを設定することができます。
emergencyが緊急度MAXで、下に行くほど緊急度が下がりdebugまでになると、開発者がデバッグするために出力するためのログになります。

基本使用するのは今回使用したエラー用のcriticalとデバッグしたい時に使用するdebug、何かの情報をログでお知らせをしたい場合(〇〇メソッドが実行された)などに使用するinfoなどが有名です。

基本はその3つをおさえておけばOKです!

またログはデフォルトでfirst-app/storage/logs/laravel.logに出力されるので、後ほど確認してみましょう。

エラー画面への遷移

エラー画面はLaravelにもともと備わっており、エラーが発生すると表示されます。

PHPの処理が失敗した時に出力されるエラーはステータスコードが500500エラーなので、Laravelのヘルパーメソッドであるabortメソッドを使用して、abort(500)と記述してあげることで500エラーページへ遷移させるようにしています。

今回説明したtry-catch文やトランザクション、ログ出力、エラー画面への遷移の実装は必須ではありませんが、プログラマーとして仕事をするのであれば実装しておきましょう。

最後に登録時にわざとエラーを起こして、ログが出力されているかを確認してみましょう。
first-app/app/Http/Controllers/ArticleController.phpstoreArticleメソッドを下記の通り変更してください。

/**
 * 記事投稿処理
 */
public function storeArticle(ArticleStoreRequest $request)
{
    // トランザクション開始
    DB::beginTransaction();
    try {
        // 記事登録処理
        Article::create([
            'title' => $request->title,
            // 'content' => $request->content,
        ]);

        // トランザクションコミット
        DB::commit();
    } catch(\Exception $e) {
        // トランザクションロールバック
        DB::rollBack();

        // ログ出力
        Log::critical($e);

        // エラー画面遷移
        abort(500);
    }

    return redirect()->route('showArticles');
}

登録処理のcontent部分をコメントアウトしたことにより、必須項目がデータベースへ登録されないのでエラーが発生します。

実際にhttp://localhost/article/createへアクセスして、タイトルと内容に適当なテキストを入れて投稿ボタンをクリックしてください。
下記画像のように500 | SERVER ERRORの画面が表示されればOKです。

もちろんこれがabortメソッド500を指定していたので、サーバーエラーの画面に遷移しました。
また、first-app/storage/logs/laravel.logを開いてください。
Laravelはデフォルトでここにログが残るようになっています。
今回Log::critical($e)としているので、下記のようなログが出力されます。

[2022-06-17 20:26:28] local.CRITICAL: PDOException: SQLSTATE[HY000]: General error: 1364 Field 'content' doesn't have a default value in /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connection.php:527

エラーが起こった日時local.CRITICALとなっています。
また、エラー内容も出力されており、Field 'content' doesn't have a default valueとなっています。
つまり、「contentカラムはデフォルトの値がテーブル設定でされていないので何かしらの値が必要です」というエラーです。
ちなみに、Log::debug($e)と記述していた場合はlocal.DEBUGとなります。

このようにログを出力することで、ブラウザでは500 | SERVER ERRORとなっていてエラー内容が確認できませんが、ログを確認することでどのようなエラーが起こっているのかを確認できます。

ブラウザでエラーが確認できない場合は積極的にログを確認しましょう。

それでは、first-app/app/Http/Controllers/ArticleController.phpstoreArticleメソッドを下記の通り元に戻しておきましょう。

/**
 * 記事投稿処理
 */
public function storeArticle(ArticleStoreRequest $request)
{
    // トランザクション開始
    DB::beginTransaction();
    try {
        // 記事登録処理
        Article::create([
            'title' => $request->title,
            'content' => $request->content,
        ]);

        // トランザクションコミット
        DB::commit();
    } catch(\Exception $e) {
        // トランザクションロールバック
        DB::rollBack();

        // ログ出力
        Log::debug($e);

        // エラー画面遷移
        abort(500);
    }

    return redirect()->route('showArticles');
}

これで登録処理が全て完了しました。

ヘッダーの遷移先を編集

最後にヘッダーの記事投稿ボタンから記事投稿画面へ遷移できるようにheader.blade.phpを変更しましょう。
first-app/resources/views/header.blade.phpを下記の通り編集してください。

<header>
    <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
        <div class="container-fluid">
            <a class="navbar-brand" href="{{ route('showArticles') }}">ブログ</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse justify-content-end" id="navbarNavAltMarkup">
                <div class="navbar-nav">
                    <a class="nav-item nav-link active" href="{{ route('showArticles') }}">記事一覧 <span class="sr-only"></span></a>
                    <a class="nav-item nav-link" href="{{ route('createArticle') }}">記事投稿</a>
                </div>
            </div>
        </div>
    </nav>
</header>

変更した内容は下記の通りです。

まず、記事投稿ボタンhref属性に記事投稿画面を表示させるルート名routeメソッドを使用して指定しています。
これで記事投稿ボタンがクリックされると、投稿フォームを表示することができるようになりました。

また、ブログ記事一覧ボタンにも記事一覧画面を表示させるルート名routeメソッドを使用して指定しています。
これでブログと記事一覧ボタンがクリックされると、記事一覧画面を表示することができるようになりました。

いい感じにブログシステムが構築できてきましたね!
これで記事投稿機能の実装は完了です。

★検索ワード
・Laravel 登録機能実装
・Laravel create fill save
・Laravel バリデーション実装
・Laravel CSRF 実装
・Laravel リダイレクト
・Laravel フォームリクエスト
・Laravel 日本語化
・PHP try-catch 使い方
・Laravel トランザクション
・Laravel ログ出力
・Laravel abortメソッド

次の教材

次の教材は下記から簡単に飛ぶことができます!
引き続きプログラミングを楽しんでいきましょう!

あわせて読みたい
【Laravel教材⑥】Laravelでブログシステム構築!〜記事編集機能実装編〜 こちらの記事はLaravel教材の第6回目の記事になります。 その他のLaravel教材を学習したい方は下記リンクから直接教材へ飛ぶことができます。 【記事編集】 記事詳細画...

プログラミング学習サポートについて

「独学で挫折した。。。」

「一人でのプログラミング学習がしんどい。。。」

「未経験からエンジニア転職をしたいけど何をしたら良いかわからない。。。」

このような悩みをお持ちの方向けに、本教材作成者のファドがMENTAという学習サイトにてあなたのプログラミング学習とエンジニア転職を徹底サポートいたします!

サポート価格はなんと1日あたりたったの約300円!

教材で分からない箇所のサポートはもちろんのこと、本サイトで公開しているすべての課題の解答も公開しております。
また、MENTAで学習を終わらせていただいた方限定で懇意にしていただいている企業さんを紹介することもあります!

なお、サポート内容の詳細は下記の通りです。

  • 目標設定
  • マインドセット
  • オリジナル教材見放題
  • オリジナル課題見放題
  • オリジナル課題の解答見放題
  • 課題コードレビュー
  • 教材への無制限質問
  • 課題への無制限質問
  • ポートフォリオ作成アドバイス
  • 褒めのコーチング

いくつかのプランを用意させていただいておりますので、下記より一度ご覧ください!

あわせて読みたい
【プログラミング学習】1日あたりたったの「300円」のみでプログラミングを学び、最短で転職or稼ぐ!|【ME... プログラミングを学びたいすべての方へこれからの時代は格安でプログラミングを学び、最短で稼げ自己紹介はじめまして!この度はプランをご覧いただき、誠にありがとうござ...

コメント

    この記事が気に入ったら
    フォローしてね!

    よかったらシェアしてね!
    • URLをコピーしました!

    コメント

    コメントする

    目次