📚

Laravelでとってもフツーなコーポレートサイトを作る

2020/10/14に公開

とってもフツーなコーポレートサイトを作成するチュートリアルです。フツー過ぎて意外に情報を集めるのが実務でも難しいと感じているのでまとまった記事として作成してみました。 以前hatenaブログでも書こうと試みて断念したことがあるので、見覚えのある方がいたら嬉しい。

仕様

  • Laravel5.6.xを想定。
  • トップページにお知らせ3件を表示する。
  • お知らせ一覧 、詳細ページを持つ。
  • お知らせのカテゴリーは別テーブルではなく、簡易的に文字列で持つ。
    (加筆次第追加予定)

前提

  • Laravelのプロジェクトの作成ができている。
  • DBとの接続は成功している
    この前提部分に関しては今後に、別記事で作っておきたいと思っています。

この記事でやらないこと

  • デザインの調整
  • カテゴリーを別の関連テーブルから取得する。(今後加筆予定)

事前準備

MySQLのバージョンによりutf8mb4に関係してエラーが発生します。後述するマイグレーション実行時にエラーが発生しない場合は必要ない対応です。長さがセットされていないカラムがマイグレーションファイルに記載されている場合に発生します。デフォルトで存在しているusersテーブルなども指定がないのでマイグレート時にエラーとなってしまうのです。

https://laravel-news.com/laravel-5-4-key-too-long-error

そのためマイグレート時にエラーが発生する方は以下の記述をapp/Providers/AppServiceProvider#boot()に追加してください。
Schemaクラスを使用するのでuse文も忘れないようにしましょう。


use Illuminate\Support\Facades\Schema;


    public function boot()
    {
        // mysqlのバージョンによるutf8mb4のエラーを回避する対応
        Schema::defaultStringLength(191);
    }

公開側のお知らせを実装する

モデルの作成

お知らせを扱うModelをInformationとします。なぜDBのテーブル名より先にModel名を決めるのかというとLaravelの場合、DBのテーブル名は英単語の複数形となるのが基本のため、非英語話者にとって複数形を考える時間が必要になる場合があるからです。book->booksとかであればシンプルですが、information, series, pressの複数系とか悩みませんか?Laravelの場合Model作成時に-mをつけることでModelの作成と同時にマイグレーションファイルの作成を行うことができます。何が嬉しいかというと、自動作成されたマイグレーシンファイルに想定される複数形のテーブル名が既にセットされるのです。助かります。

以下のコマンドを実行します。

# make Information model with the migration file
php artisan make:model Information -m

これでモデルとマイグレーションファイルが自動作成されます。ちなみにinformationは単数複数同じです。単数複数同じ英単語の場合、後で変数名で悩みやすいパターンなのですが、newsも同様なので汎用的につかえるinformationでいきたいと思います。

マイグレーションファイルの修正

ではマイグレーションファイルにコーポレートサイトでよく必要とされる情報を追加していきましょう。マイグレーションファイルはapp/database/migrations/{今日の日付+数値+}create_information_table.phpのような名前で作成されているはずです。手動でマイグレーションファイルを作成した方は、そのファイルと読み替えてください。

まず基本項目としてタイトル、本文、画像用カラムを追記します。
最初からセットされているidとtimestampsの間に入れましょう。カラムの順番は好みです。

続けて以下の昨日も設けます。

  • 論理削除機能(ソフトデリート)
  • 予約投稿機能用日付
    published_atの日付によって公開するかどうかの判定に使用します。

/**
 * Run the migrations.
 *
 * @return void
 */
public function up()
{
    Schema::create('information', function (Blueprint $table) {
        $table->increments('id');
        // title
        $table->string('title');
        
        // content
        $table->text('content');
        
        // image
        $table->string('image')->nullable();

        $table->timestamp('published_at')->nullable();

        $table->softDeletes();
        $table->timestamps();
    });
}

いったんここで実行してみましょう。rollbackですぐに実行前に戻せるので問題ありません。

# do migration
php artisan migrate

# rollback migration
php artisan migrate:rollback

データベースにinformationテーブルが作成されていれば成功です。引き続きカラムの追加を行なっていくので確認後は忘れずにrollbackを実行してください。

Seederを使ってダミーデータを用意する

Seederと呼ばれる機能を使うことでダミーデータを大量に用意することができます。「ページネーションが表示されなかった」「実は全件表示になっていた」「公開/非公開が効いていなかった」というようなケースを早めに見つけるのにも便利です。

まずinformationテーブル用のseederファイルを以下のコマンドで作成します。

php artisan make:seeder InformationTableSeeder

app/database/seeds/にseederファイルが作成されます。この時点では名前にInformationと付いているだけで、informationテーブルとは繋がっていません。まず、基本の使い方としてModelを直接呼び出して何個かデータを入れてみます。run()を以下のように修正します。
(imageカラムにセットしているimages/sample1.jpgなどはあくまで画像のパスを示す文字列であり、実ファイルが登録される訳ではありません)

<?php

public function run()
{
    $articles = [
        [
            'title' => 'title 1',
            'content' => 'Lorem ipsum dolor sit amet ...',
            'image'   => 'images/sample1.jpg'
        ],
        [
            'title' => 'title 2',
            'content' => 'Lorem ipsum dolor sit amet ...',
            'image'   => 'images/sample2.jpg'
        ],
        [
            'title' => 'title 3',
            'content' => 'Lorem ipsum dolor sit amet ...',
            'image'   => 'images/sample3.jpg'
        ],
    ];

    foreach ($articles as $article) {
        $information = new \App\Information();
        $information->title = $article['title'];
        $information->published_at = date('Y-m-d'); // today
        $information->content = $article['content'];
        $information->image = $article['image'];
        $information->save();
    }
}

次にこれを簡単なコマンドで実行できるようにするため、同じフォルダにあるDatabaseSeeder.phpのrun()を修正します。


public function run()
{
    // $this->call(UsersTableSeeder::class);
    $this->call(InformationTableSeeder::class);
}

設定自体はこれで完了ですが、作成したものをオートローダに読み込ませるために以下のコマンドをコマンドラインで実行する必要があります。実行しなくても動くときもありますが、やっておきましょう。

composer dump-autoload

これで準備ができました。以下を実行するとテーブルにデータが追加されます。実行するたびにデータは増えていきます。

php artisan db:seed

お知らせを公開側トップに表示する

いよいよお知らせをサイトに表示してブラウザで確認できるようにします。初期状態だとトップの画面は「Laravel」と大きく表示されています。これをHomeControllerからの出力に変更します。

php artisan make:controller HomeController

app/Http/Controllers/に作成されたHomeControllerにindex()メソッドを追加し、viewとしてhomeを指定します。


public function index()
{
    return view('home');
}

resources/views/home.blade.phpを空で作成し、testなど確認用の文言を入れます。

testです

ルーティング(routes/web.php)でトップページの表示用にhomeを設定します。既存のwelcomeを呼び出すルーティングは消すかコメントアウトします。


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

Route::get('/', 'HomeController@index')->name('home');

これでトップにアクセスすると画面に「testです」と表示されれば成功です。

ルーティングができたので、実際にお知らせデータ全件をDBから取得してみます。


use App\Information;

class HomeController extends Controller
{
    public function index()
    {
        $information_list = Information::get();

        echo("取得件数: ".count($information_list));

        return view('home', compact('information_list'));
    }
}

少し表示は崩れますが画面上に件数の情報が出れば成功です。

view()関数に渡すことでview上でもその変数を使用できるようにしています。home.blade.phpにお知らせを表示します。

@if (filled($information_list))
  @foreach($information_list as $information)
      日付:   {{ $information->published_at }} <br>
      タイトル:   {{ $information->title }} <br>
  @endforeach
@endif

これでコントローラーを使用したルーティングを行なってDBのデータをviewに表示する一連の流れの完成です。日付は秒数まで出てしまっていますが後で対応するのでそのままとします。

お知らせにカテゴリーを付ける (Optional)

お知らせがカテゴリーで別れているケースもあるかと思います。追加していきます。今回はシンプルに文字でカテゴリーを表示するだけを想定しているので文字列型のカラムとし、インデックスを張る程度の実装とします。アイコンの色を変えたいなどの要望があれば別テーブルに出した方がよいでしょう。まずマイグレーションファイルを作成し、カラム追加をします。

php artisan make:migration add_category_to_information --table=information

今回はテーブルの新規作成ではなく、カラム追加になります。作成されたマイグレーションファイルに以下を追記します。忘れずにdown()も記入しておきます。


public function up()
{
    Schema::table('information', function (Blueprint $table) {
        $table->string('category')->nullable()->index();
    });
}
public function down()
{
    Schema::table('information', function (Blueprint $table) {
        $table->dropColumn('category');
    });
}

マイグレーションを実行するとカラムがテーブルの末尾に追加されます。リリース前にマイグレーションファイルを整理すると思うのでこれで良いのですが、気になる方はafter()メソッドを使って指定位置にカラムを追加しても良いです。後はseederにもカテゴリーの反映を行なっておきましょう。

お知らせ一覧ページを作成する

お知らせ一覧と詳細で使用するコントローラーを作成します。HomeControllerと同じようにガリガリと実装するのも良いのですが、Laravelにはリソースという仕組みが用意されているのでそちらを使用します。

php artisan make:controller InformationController --resource

作成されるInformationControllerにはあらかじめ複数のメソッドが記述されているので、index()とshow()以外は全て削除します。確認用にecho文をつけて起きます。


class InformationController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        echo "index";
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show($id)
    {
        echo "show";
    }
}

次にルーティングです。routes/web.phpに以下を追加します。


Route::resource('information', 'InformationController')->only(['index', 'show']);

トップページのviewにお知らせ一覧へのリンクを作成します。

<a href="{{ route('information.index') }}">お知らせ</a>

リンクをクリックして画面にindexと表示されればOKです。ひとまず疎通が確認できました。

次にお知らせ一覧のviewを作っていきます。お知らせ一覧のviewはresources/views/information/index.blade.phpという名称にします。

これはLaravelのルールではありませんが、リソースの仕組みの命名ルールに寄せています。informationは単数形複数形が同じなので分かりにくいですが、このフォルダ名”information”は他の単語であれば複数形にします。

bookならbooksです。後はInformationController@index()でこのviewを呼び出すようにして疎通確認までしてしまいましょう。


public function index()
{
    echo "index";

    return view('information.index');
}

疎通確認ができれば後はDBのデータを表示するだけです。echo部分を書き換え、ひとまずトップと同じく全件を画面上に表示してみましょう。


public function index()
{
    $information_list = Information::get();

    return view('information.index', compact('information_list'));
}

viewにhomeのviewと同様にループ処理を書いて確認してみてください。

お知らせ一覧をカテゴリーで絞り込む(Optional)

カテゴリーで絞り込んだお知らせ一覧を表示してみます。簡易的にカテゴリーの渡し方はGETパラメータでカテゴリー名を渡す方法とします。以下の例はcategoryカラムにaaa, bbbが入っていることを想定しています。

<a href="{{ route('information.index') }}">お知らせ</a>
<a href="{{ route('information.index') }}?category=aaa">aaa</a>
<a href="{{ route('information.index') }}?category=bbb">bbb</a>

以下はInformationController@index()


use Illuminate\Http\Request;
public function index(Request $request)
{
    $category = $request->input('category');
    // whenを使うことでcategoryが空の場合はカテゴリー条件をスキップできる
    $information_list = Information::when(
        filled($category), 
        function($q) use ($category) {
            $q->where('category', $category);
        })->get();

    return view('information.index', compact('information_list'));
}

お知らせの詳細ページへリンクする。

Laravelのresourceのroutingの仕組みを使用した場合、詳細ページへのリンクの記述方法はあまり直感的ではありません。お知らせの記事へのリンクは以下のようになります。

<a href="{{ route('information.show', ['information' => $information->id]) }}">{{ $information->title }}</a>

※$informationはinformationテーブルのインスタンスとする

これもまたinformationが単数形複数形が同じなので分かりにくいので、bookの場合も示しておきます。どこが複数系でどこが単数形にあたるのかを把握しておきましょう。

<a href="{{ route('books.show', ['book' => $book->id]) }}">{{ $book->title }}</a>

※$bookはbooksテーブルのインスタンスとする

配列のキー名がinformationやbookであるのに対して、値はidをセットするのが少し気持ち悪い感じがしますが慣れます。リンクができたので、リンク先のコントローラーを作成していきます。

以下はInformationController@show()


public function show($id)
{
    $information = Information::findOrFail($id);
    return view('information.show', compact('information'));
}

findOrFail()を使用して、存在しないidが渡された場合は即座に落とすパターンです。ログの取得や分岐などを行う必要がある場合は、find()を使用して独自の処理を追加してください。お知らせ詳細ページのviewはresources/views/information/show.blade.phpという名称にします。これもLaravelのルールに寄せているだけですので、独自のview名にすることは可能です。informationのモデルインスタンスを渡しているので、$informationとしてview上で使用できます。

日付:   {{ $information->published_at }} <br>
タイトル: {{ $information->title }} <br>
本文:   {!! nl2br(e($information->content)) !!} <br>

上記のような感じです。改行コードを
に変換する必要がある本文(contentカラム)の場合はnl2br(), e()などの反映が必要なので少し長くなってしまいますね。記述の仕方に注意してください。

これでresourceのroutingの仕組みを利用した一覧表示から詳細ページへの流れが完成したことになります。ただこのままではお知らせ一覧は全件表示のままですのでページネーションの仕組みを使用してページ送りを付けていきます。

トップのお知らせを3件に絞り込む。

ではページ送りの実装に入る前にトップに表示していたお知らせも全件表示のままなので、3件に絞り込む実装を入れていきましょう。


class HomeController extends Controller
{
    public function index()
    {
        $information_list = Information::orderBy('published_at', 'DESC')->take(3)->get();

        echo("取得件数: ".count($information_list));

        return view('home', compact('information_list'));
    }
}

take()を使用することで指定した件数だけ取得することができます。ただし、take()だけを使用するとDBのデフォルトのソート順となるので、SQLのORDER BY句を指定するためにorderBy()を同時に使用します。

お知らせ一覧にページ送りを追加する

Laravelの場合、ページ送りの実装は非常に簡単です。簡単なのですがブラックボックスになっている箇所をイメージできるようにしておきましょう。

以下はInformationController@index()


public function index()
{
    $information_list = Information::orderBy('published_at', 'DESC')->paginate(10);

    return view('information.index', compact('information_list'));
}

基本的にはこれだけです。get()ではなくpaginate()で終わる点に注意してください。前へ、次へのリンクが押された場合のメソッドが必要になる気がしますが、不要です。ページ送りのどんなリンクも現在呼び出されたメソッド(ここではindex())が呼び出されます。ページ数を意味するパラメータや、requestオブジェクトの処理すら必要ありません。内部的にはGETのパラメータとしてリクエストにpageが存在する前提で実装されており、それをpaginate()の中で暗黙で使用しているのだろうと考えられます。すみませんソースまでは追えていないです。

view側はループ部分は何も変更ありません。ページ送りのリンクを表示する部分を追加します。

デザインによって場所は変わると思いますが、基本としてループ処理の@endforeachの下あたりに{ $information_list->links() }}を記述してみましょう。

~~省略
@endforeach

{{ $information_list->links() }}

一覧のview上の記述は以上です。ただしこのまま実行するとリンク部分のviewが無い旨のエラーとなります。Laravelではページ送りのリンク部分を別のviewとして用意するようになっています。雛形となるbootstrapベースのリンク用viewはartisanの以下のコマンドを実行すると、views自動的に追加されます。

php artisan vendor:publish --tag=laravel-pagination

実行するとresources/views/vendor/paginationフォルダが作成され、その中に複数のviewが配置されます。その中のbootstrap-4.blade.phpがデフォルトでは自動的に呼び出されるようになっています。この状態で画面を更新すると正しくページ送りのリンクが表示されることが確認できます。

ページネーションにカテゴリーの絞り込みを反映する (Optional)

一覧画面の表示でGETパラメータを使用してカテゴリーの絞り込みを行なっている場合、ページネーションのリンクにもGETパラメータを渡してやらなければ2ページ目以降カテゴリーの絞り込みが反映されません。(ページ送りに関してセッションでパラメータを保存するのはダサいのでやめましょう。)
Laravelではappends()を使用することで、ページネーションのリンクに自動的にGETパラメータが付与されます。
GETパラメータを取得する方法は複数用意されていますが、ここではメソッドの第一引数に自動的にセットされるリクエストオブジェクトを使用します。
index()メソッドの引数にIlluminate\Http\Requestのオブジェクトを指定するだけで、自動的にリクエストオブジェクトが取得できます。
Requestは同名クラスが多いのでuseで正しいクラスを追加してください。


use Illuminate\Http\Request;
~~省略

public function index(Request $request)
{
    $category = $request->input('category');
    
    // cateogryというGETパラメータを取得しているとする。
    $params = compact('category');

 // 簡易版なのでカテゴリー無しの全件表示は考慮していない。
    $information_list = Information::where(‘category’, $category)->orderBy('published_at', 'DESC')->paginate(10)->appends($params);
}

appends()の追加位置に注意です。paginate()の後です。paginate()で取得できるオブジェクトが持つメソッドなのです。

(続きます。修正し、追記予定です。)

Discussion