以前「Laravel5.4でシンプルなCMSを作るチュートリアル」というのを書いたのですが、バージョンが古くなったりしているので新しいバージョンで書き直した記事になります。

作る機能

  1. 投稿一覧&詳細ページ
  2. 管理画面へのログイン機能
  3. 投稿管理(CRUD)機能
  4. ユーザーと投稿の関連付け(多対一:HasMany)
  5. 投稿のタグ分け(多対多:ManyToMany)
  6. ユーザーロール(権限)の設定)

インストール

最初にLaravelをComposerでインストールします。
バージョン指定しない場合、現在(2020年10月)の最新バージョンである8がインストールされます。

$ composer create-project --prefer-dist laravel/laravel LaravelMiniCMS

バージョンを指定する場合はコマンドの最後にバージョンを追記します。

$ composer create-project --prefer-dist laravel/laravel LaravelMiniCMS "8.*"

インストールにしばらく時間が掛かりますが、終わったらインストールしたディレクトリをカレントにします。

$ cd LaravelMiniCMS

ビルドインサーバーを起動してとりあえず動くか確認してみましょう。

$ php artisan serve

ブラウザでhttp://localhost:8000/にアクセスしてLaravelって表示されれば成功です。

開発用ライブラリ

Laravelの開発を始めるにあたり便利なライブラリをいれておきましょう。

Laravel IDE Helper

IDEを使用していると自動解析でエラーになることがありますが、このライブラリを入れておくと正常に表示できるようになります。
その他にも補完ができるようにしてくれたりします。
IDEを使用している場合は基本入れておきましょう。

$ composer require barryvdh/laravel-ide-helper --dev

インストールしたらファイルを生成します。

$ php artisan ide-helper:generate

モデル作成後もモデルに対応したファイルを生成することができます。

$ php artisan ide-helper:models "App\Models\Post"

Laravel Debugbar

このライブラリはブラウザの開発ツールのようなパネルを表示してくれます。
セッションやどのようなSQLが発行されているか確認することができます。

$ composer require barryvdh/laravel-debugbar --dev

インストールしたらファイルを生成します。

$ php artisan vendor:publish --provider="Barryvdh\Debugbar\ServiceProvider"

Laravel Collective

Laravel Collectiveはフォーム処理を短く書けるヘルパー集です。
最近メンテナンスされてない感があり、ちょっと不安な感じはするのですが、Laravel8では使用することができるようです。

$ composer require laravelcollective/html

※インストール時メモリが不足しているエラーが表示される場合は「COMPOSER_MEMORY_LIMIT=-1」を付けると一時的にメモリ制限をなくせます。

$ COMPOSER_MEMORY_LIMIT=-1 composer require

初期設定

configのapp.phpを開き、言語設定をします。

config/app.php

return [
	// ...
	'timezone' => 'Asia/Tokyo',
	'locale' => 'ja',
	'faker_locale' => 'ja_JP',

次はデータベースの設定です。
デフォルトではMySQLですが、今回はSQLiteを使用します。
.envを開いて、DB_CONNECTION=sqliteを追記して既存のMySQLの設定部分は削除します。

.env

DB_CONNECTION=sqlite

#DB_CONNECTION=mysql
#DB_HOST=127.0.0.1
#DB_PORT=3306
#DB_DATABASE=homestead
#DB_USERNAME=homestead
#DB_PASSWORD=secret

touchコマンドでsqliteファイルを作成。

$ touch database/database.sqlite

設定を変更したら、ビルドインサーバーを再起動しましょう。

ファイルの雛形を作成

一つ一つファイルを作成してもいいのですが、LaravelにはMVCに必要なファイルの雛形を一括で生成してくれるコマンドがあります。
次のコマンドを実行してみましょう。

$ php artisan make:model Post -a

Postテーブルの作成

どのようなデータを扱うかテーブルの設定をします。
先ほどのコマンドを実行していると、database/migrationsディレクトリに日付+create_posts_table.phpのファイル名で生成されています。
次のように編集しましょう。

database/migrations/XXXX_XX_XX_XXXXXX_create_posts_table.php

public function up()
{
	Schema::create('posts', function (Blueprint $table) {
		$table->id();
		$table->string('title');
		$table->text('body')->nullable();
		$table->boolean('is_public')->default(true)->comment('公開・非公開');
		$table->dateTime('published_at')->default(DB::raw('CURRENT_TIMESTAMP'))->comment('公開日');
		$table->timestamps();
	});
}

次のコマンドを実行することでデータベースにテーブルが作成されます。

$ php artisan migrate

ダミーデータの作成

作成したテーブルに登録する仮のデータを作成します。
最初はある程度の量のデータを入れておいた方がいいと思いますので、Fakerというランダムデータを生成する機能を使ってみましょう。

PostFactory.phpを次のように編集します。

database/factories/PostFactory.php

<?php

namespace Database\Factories;

use App\Models\Post;
use Illuminate\Database\Eloquent\Factories\Factory;

class PostFactory extends Factory
{
	/**
	 * The name of the factory's corresponding model.
	 *
	 * @var string
	 */
	protected $model = Post::class;

	/**
	 * Define the model's default state.
	 *
	 * @return array
	 */
	public function definition()
	{
		$random_date = $this->faker->dateTimeBetween('-1year', '-1day');

		return [
			'title' => $this->faker->realText(rand(20,50)),
			'body' => $this->faker->realText(rand(100,200)),
			'is_public' => $this->faker->boolean(90),
			'published_at' => $random_date,
			'created_at' => $random_date,
			'updated_at' => $random_date
		];
	}
}

シーダーファイルで作成したファクトリーを実行します。
countの部分に作成したいデータの数を指定しましょう。
50件登録するには次のようにします。

database/seeds/PostSeeder.php

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\Post;

class PostSeeder extends Seeder
{
	/**
	 * Run the database seeds.
	 *
	 * @return void
	 */
	public function run()
	{
		Post::factory()->count(50)->create();
	}
}

次にこのシーダーが実行されるようにDatabaseSeeder.phpに登録します。

database/seeders/DatabaseSeeder.php

public function run()
{
	$this->call(PostSeeder::class);
}

次のコマンドを実行するとデータが登録されます。

$ php artisan db:seed

これでpostsテーブルにダミーデータが登録されました。

Postモデルの作成

fillableに更新できるプロパティを設定します。
id,created_at,updated_atはDBとフレームワークの機能で更新するのでここには書きません。

castsにはどのような型を扱うかを設定します。
is_publicはboolean、published_atはdatetimeで扱いたいので設定しておきましょう。

app/Post.php

<?php

namespace App\Models;

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

class Post extends Model
{
    use HasFactory;

    protected $fillable = [
        'title', 'body', 'is_public', 'published_at'
    ];

    protected $casts = [
        'is_public' => 'bool',
        'published_at' => 'datetime'
    ];
}

モデルを作ったらIDEヘルパーのコマンドを実行して補完できるようにしておきましょう。

$ php artisan ide-helper:model

ルーティング設定

今回は大きく一般ユーザーが閲覧できる画面とデータを管理する管理画面の2つのルートで分けます。
Laravelのルーティングroutes/web.phpにそのまま記述してもいいですが、今回は一般ユーザー用のfront.phpと、管理画面用のback.phpの2つのファイルに分けてみます。
ルーターファイルをroutes/web.phpから変更する場合はRouteServiceProvider.phpを編集します。

app/Providers/RouteServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;

class RouteServiceProvider extends ServiceProvider
{
	// ...
	protected $namespace = 'App\\Http\\Controllers';

	public function boot()
	{
		$this->configureRateLimiting();

		$this->routes(function () {
			Route::prefix('api')
				->middleware('api')
				->namespace($this->namespace)
				->group(base_path('routes/api.php'));

			// フロント画面
			Route::middleware('web')
				->namespace($this->namespace . '\Front')
				->as('front.')
				->group(base_path('routes/front.php'));

			// 管理画面
			Route::prefix('admin')
				->middleware('web')
				->namespace($this->namespace . '\Back')
				->as('back.')
				->group(base_path('routes/back.php'));
		});
	}

	// ...
}

middlewareにコントローラーのネームペースを設定して、asはリンクを設定するときのルート名を設定しています。

routesディレクトリにfront.phpback.phpファイルを作成し、ひとまず下記のようにします。

routes/front.php

<?php
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
	echo 'front';
});

routes/back.php

<?php
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
	echo 'back';
});

http://localhost:8000にアクセスして、「front」が、 http://localhost:8000/adminにアクセスして、「back」が表示されることを確認してください。

Postコントローラーの作成

フロントのコントローラーから作っていきます。
makeコマンドでapp/Http/Controllersディレクトリ直下にPostController.phpが作られていると思いますが、先ほどの設定でFrontディレクトリに配置するように変更したので移動しましょう。
そして次のように編集します。

app/Http/Controllers/Front/PostController.php

<?php

namespace App\Http\Controllers\Front;

use App\Http\Controllers\Controller;
use App\Models\Post;

class PostController extends Controller
{
	/**
     * 一覧画面
     *
     * @return \Illuminate\Contracts\View\View
     */
	public function index()
	{
		// 公開・新しい順に表示
		$posts = Post::where('is_public', true)
            ->orderBy('published_at', 'desc')
			->paginate(10);
			
		return view('front.posts.index', compact('posts'));
	}

	/**
     * 詳細画面
     *
     * @param int $id
     * @return \Illuminate\Contracts\View\View
     */
	 public function show(int $id)
	 {
		 $post = Post::where('is_public', true)->findOrFail($id);
 
		 return view('front.posts.show', compact('post'));
	 }
}

showアクションは初期状態では引数にモデルバインディングがしてある状態ですが、is_publicで検索する必要があった為、idに変更しています。

ビューレイアウトの作成

次にビューファイルを作成しますが、ナビゲーションなど共通で使用するファイルをレイアウトファイルとして作ります。
LaravelではデフォルトではBladeというテンプレートライブラリを使用します。
このライブラリにはテンプレートを継承する機能がありこれを使用すればレイアウトを実現できます。

resources/views/front/layouts/base.blade.php

<!doctype html>
<html lang="ja">
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<title>{{ isset($title) ? $title . ' | ' : '' }}Laravel CMS</title>
	<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" crossorigin="anonymous">
</head>
<body>
<div id="app">
	<nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
		<div class="container">
			<a class="navbar-brand" href="{{ route('front.home') }}">
				Laravel CMS
			</a>
			<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false">
				<span class="navbar-toggler-icon"></span>
			</button>

			<div class="collapse navbar-collapse" id="navbarSupportedContent">
				<ul class="navbar-nav ml-auto">
					<li class="nav-item{{ Request::is('/') ? ' active' : '' }}">
						<a class="nav-link" href="{{ route('front.home') }}">ホーム</a>
					</li>
					<li class="nav-item{{ Request::is('posts', 'posts/*') ? ' active' : '' }}">
						<a class="nav-link" href="{{ route('front.posts.index') }}">お知らせ</a>
					</li>
				</ul>
			</div>
		</div>
	</nav>
	<main class="py-4">
		<div class="container">
			<div class="row justify-content-center">
				<div class="col-md-12">
					<div class="card">
						@yield('content')
					</div>
				</div>
			</div>
		</div>
	</main>
</div>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" crossorigin="anonymous"></script>
</body>
</html>

@yield('content')の部分にこれから作る各ビューファイルの内容が入ります。

Post一覧(index)ビューの作成

コントローラーのindexに対応したビューファイルです。
ビューファイルはmakeコマンドで作成されていないので、普通に作ります。

link_to_routeは最初にインストールした、Laravel Collectiveの機能です。

resources/views/front/posts/index.blade.php

<?php
/**
 * @var Illuminate\Pagination\LengthAwarePaginator|\App\Models\Post[] $posts
 */
$title = '投稿一覧';
?>
@extends('front.layouts.base')

@section('content')
<div class="card-header">{{ $title }}</div>
<div class="card-body">
	@if($posts->count() <= 0)
		<p>表示する投稿はありません。</p>
	@else
		<table class="table">
			@foreach($posts as $post)
				<tr>
					<td>{{ $post->published_at->format('Y年m月d日') }}</td>
					<td>{!! link_to_route('front.posts.show', $post->title, $post) !!}</td>
				</tr>
			@endforeach
		</table>
		<div class="d-flex justify-content-center">
			{{ $posts->links() }}
		</div>
	@endif
</div>
@endsection

$posts->links()はページャーナビゲーションの部分なのですが、このままだと崩れていると思います。
Laravel8ではTailwindcssというCSSのフレームワークがベースになっている為です。
今回はBootstrapを読み込んでいるので変更しましょう。

app/Providers/AppServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Pagination\Paginator;

class AppServiceProvider extends ServiceProvider
{
	// ...

	/**
	* Bootstrap any application services.
	*
	* @return void
	*/
	public function boot()
	{
		Paginator::useBootstrap();
	}
}

これでページャーナビゲーションの部分が正常に表示されるようにりました。

Post詳細(show)ビューの作成

コントローラーのshowに対応したビューファイルです。

resources/views/front/posts/show.blade.php

<?php
/**
 * @var \App\Models\Post $post
 */
$title = '投稿詳細';
?>
@extends('front.layouts.base')

@section('content')
<div class="card-header">{{ $title }}</div>
<div class="card-body">
	<h2>{{ $post->title }}</h2>
	<time>{{ $post->published_at->format('Y年m月d日') }}</time>
	<div>{!! nl2br(e($post->body)) !!}</div>
	{!! link_to_route(
		'front.posts.index', '一覧へ戻る', null,
		['class' => 'btn btn-secondary'])
	!!}
</div>
@endsection

ルーターの設定

最後にコントローラーにアクセスできるようにルーターを編集します。
トップにアクセスするとPostControllerindexが表示されるようにして、 postsPostControllerがアクセスできるようにします、フロントではindexshowだけにアクセスしたいのでonlyで指定します。

routes/front.php

Route::get('/', 'PostController@index')->name('home');
Route::resource('posts', 'PostController')->only(['index','show']);

モデルスコープでクエリをコントローラーから移動する

このままでも問題はないのですが、Laravelの機能を使用してもう少し良いコードにしてみましょう。
現在のコントローラーをみてみるとこんな感じでDBのクエリが入っています。

app/Http/Controllers/Front/PostController.php

$posts = Post::where('is_public', true)
		->orderBy('published_at', 'desc')
		->paginate(10);

このままですと、他のコントローラーで同じクエリを実行したいとき、同じ処理を書かなければいけません。
スコープという機能を使用してモデルに処理を移すことでこの問題を解決してみましょう。

app/Models/Post.php

<?php

namespace App\Models;

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

class Post extends Model
{
	// ...

	// 公開のみ表示
	public function scopePublic(Builder $query)
	{
		return $query->where('is_public', true);
	}

	// 公開記事一覧取得
	public function scopePublicList(Builder $query)
	{
		return $query
			->public()
			->latest('published_at')
			->paginate(10);
	}

	// 公開記事をIDで取得
	public function scopePublicFindById(Builder $query, int $id)
	{
		return $query->public()->findOrFail($id);
	}
}

これで公開Postの一覧はPost::publicList()で、詳細画面ではPost::publicFindById($id)でデータを取得できるようになります。

public function index()
{
	$posts = Post::publicList();
	return view('front.posts.index', compact('posts'));
}

public function show(int $id)
{
	$post = Post::publicFindById($id);
	return view('front.posts.show', compact('post'));
}

コントローラーの記述がすっきりしましたね。

ゲッターを使用してフォーマットを共通で使う

ビューファイルの公開日(created_at)は年月日で表示しているのですが、この書式は一覧と詳細画面にありますね。

$post->created_at->format('Y年m月d日')

日付フォーマットは同じものを使うことが多いので、この部分の処理をまとめましょう。
この場合はモデルのゲッターという機能を使用します。
ゲッターはモデルにget●●●Attributeという名前でメソッドを作ります。

app/Models/Post.php

<?php
// ...

class Post extends Model
{
	// ...

	// 公開日を年月日で表示
	public function getPublishedFormatAttribute()
	{
		return $this->published_at->format('Y年m月d日');
	}

}

ビューで次のようにしていすることで年月日で表示できるようになります。

{{ $post->published_format }}

今回は一般ユーザー向けのフロント表示部分の作成をしました。
次回は管理画面に入る為のログイン機能を作っていきます。

ソースコードはGitHubに置いてます。

LaravelMiniCMS2020