見出し画像

お知らせアプリを作ろう(#1) #Laravel基礎 #Laravelの教科書

# はじめに

Laraveで作るWebアプリケーションとして、今度はお知らせを管理するアプリケーションを作成していきます。

作成するアプリの仕様は次のようなものです。

■ ユーザー機能
・ユーザーはログインして自分のお知らせを管理することが出来る(作成、編集、削除)
・ユーザーは各ユーザー毎のお知らせページ(ユーザーページ)がある。URLはTwitterのような英数字でつけたURLを利用する。
・各お知らせは詳細画面がある
・お知らせは「タイトル」「概要」「本文」「画像大」「サムネイル」がある
・標準機能で会員機能を作成するが、会員登録機能は無し

■ 管理側機能
・新しいユーザーの追加、ユーザーの情報管理が管理画面で出来る

掲示板と違ってユーザーに紐付いたデータがある点が違うポイントです。

また、ユーザーページのような仕組みを作るにはどうすれば良いのかを学ぶことが出来ます。

本稿を通して次のことを学びましょう。

・標準機能のUserを改造したアプリケーション作成方法
・ID以外を利用した詳細画面の作成方法
・重複チェックなどの複雑なバリデーション

今回利用する用語、関数

・unique制約
・old()関数
・abort()関数


# 準備

本稿で作成するアプリは少し大きめになるため、準備については簡単にするため下記の記事で作成したものをベースにします。

管理側の機能についてはこちらを利用するのでここまで作成しておいてください。(Seederはつくらなくて大丈夫です)

本稿で作成するアプリは「news_app」とします。

記事の中でも説明していますが、ユーザー認証機能までの初期化の流れは下記のコマンドを実行するだけなので一気にやってしまってください。

composer create-project --prefer-dist laravel/laravel news_app "7.*"
cd news_app
composer require laravel/ui
php artisan ui vue --auth

※ 執筆時の状況に合わせてLaravel7でプロジェクトを作成しています。


この後、管理側機能の作成の記事を参考にユーザー一覧、ユーザー詳細の画面を作成してください。

下記のコントローラーの作成、画面の作成は完了している状態とします。

app/Http/Controllers/admin/AdminLoginController.php
app/Http/Controllers/admin/AdminLogoutController.php
app/Http/Controllers/admin/AdminTopController.php
app/Http/Controllers/admin/ManageUserController.php


# ユーザー登録機能の削除

標準の認証機能から新規の会員登録をオフにします。

設定方法は公式ドキュメントに記述があります。

次のように「routes/web.php」の記述を変更することで、会員登録機能をオフにすることができます。

routes/web.php

Auth::routes();

Auth::routes(["register" => false]);

ログインしていない状態で会員登録のリンクが消えていることを確認してください。

スクリーンショット 2020-06-19 22.52.37


# ユーザーテーブルの改造

ユーザーページの表示用にid以外にユニークなdisplay_nameという項目を追加していきます。

まずは変更のためのマイグレーションを作成します。マイグレーションの作成は「make:migration」です。

php artisan make:migration modify_user_table

マイグレーションファイルは「database/migrations/XXXX_XX_XX_XXXXXX_modify_user_table.php」のようなファイル名になります。実行タイミングでファイル名が異なりますので、出力されたメッセージを確認してください。

Created Migration: 2020_06_19_135350_modify_user_table

今回は作成でこちらのように表示されたので「2020_06_19_135350_modify_user_table.php」というファイルが作成されています。

database/migrations/2020_06_19_135350_modify_user_table.php

<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ModifyUserTable extends Migration
{
   /**
    * Run the migrations.
    *
    * @return void
    */
   public function up()
   {
       Schema::table('users', function (Blueprint $table) {
			$table->string('display_name')->unique();
		});
   }

   /**
    * Reverse the migrations.
    *
    * @return void
    */
   public function down()
   {
       Schema::table('users', function (Blueprint $table) {
			$table->dropColumn(['display_name']);
		});
   }
}
$table->string('display_name')->unique();

文字列のdisplay_nameというカラムをuniqueで登録するという設定を行っています。

down()にはマイグレーションをキャンセルする時の手順を書いていきます。今回はdisplay_nameを追加したのでdropColumnで削除しています。

このマイグレーションを実行するにはすでに登録されたデータが邪魔になります。なぜかというとunique(重複なし)と指定しているにすでに登録されたデータは値が無いため必ず重複するからです。

SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '' for key 'users_display_name_unique' (SQL: alter table `users` add unique `users_display_name_unique`(`display_name`))

実行すると上のようなエラーが表示されます。

解決方法はいくつかありますが、今回はすでに登録されているデータを空にすることで対応します。

php artisan migrate:refresh

「migrate」ではなく「migrate:refresh」を使うと登録したデータをリフレッシュしながら空にすることが出来ます。

これで重複なしのdisplay_nameカラムが追加されました。phpmyadminなどでカラムが追加されているのか確認しましょう。

スクリーンショット 2020-06-19 23.12.35


# ユーザー登録機能の作成

管理側の機能としてユーザー登録機能を作成していきます。

登録機能のURLは「/admin/user/create」として、こちらにアクセスするとユーザー登録フォームが表示します。

ユーザー登録機能の作成は次の手順で行います。

1. ManageUserControllerクラスにフォーム表示用のshowUserCreateForm()を追加
2. テンプレートを作成(resources/view/admin/user_create.blade.php)
3. routes/web.phpを修正してルーティングを設定
4. ManageUserControllerクラスに作成用の:create()関数を追加


# ユーザー登録フォームの作成

まずはフォーム表示用のshowUserCreateForm()関数を追加していきます。

app/Http/Controllers/admin/ManageUserController.php

class ManageUserController extends Controller
{

	function showUserCreateForm(){ 
		return view("admin.user_create");
	}

}

showUserCreateForm()はシンプルにViewを返すだけです。ここではuser_createというテンプレートを指定しています。

次にテンプレートを作成します。名前、表示名、メールアドレス、パスワード、パスワード確認の4項目を設定出来るようにします。

フォームの遷移先は登録フォーム表示画面と同様に「admin/user/create」を指定します。

resources/views/admin/user_create.blade.php

@extends('layouts.admin')
@section('content')
<div class="container">
	<div class="card">
		<div class="card-header">ユーザー新規登録</div>
		<div class="card-body">
			@if ($errors->any())
			<div style="color:red;">
			<ul>
				@foreach ($errors->all() as $error)
				<li>{{ $error }}</li>
				@endforeach
			</ul>
			</div>
			@endif
			<form method="post" action="{{ url('admin/user/create') }}">
			@csrf 
			<div class="form-group">
				名前: <input class="form-control" type="text" 
                       name="name" value="" />
				<p class="text-muted">表示名は自由に設定できます</p>
			</div>
			<div class="form-group">
				Email: <input class="form-control" type="email" 
                        name="email" value="" />
			</div>
			<div class="form-group">
				表示名: <input class="form-control" type="text" 
                    name="display_name" value="" />
				<p class="text-muted">半角英数字(30文字以内)</p>
			</div>
			<div class="form-group">
				ログインパスワード: <input class="form-control" 
                    type="password" name="password" value="" />
			</div>
			<div class="form-group">
				ログインパスワード(確認): <input class="form-control" 
                    type="password" name="password_confirmation" value="" />
			</div>
			<div class="mt-3">
				<input class="btn btn-primary" type="submit" value="作成" />
			</div>
			</form>
		</div>
	</div>
</div>
@endsection

bootstrapは「<div class="form-group">〜</div>」で各フォームを囲むといい感じに余白を設定してくれます。

その他、細かいポイントとしては
・メールアドレスはtype="email"にする
・<p class="text-muted">〜</p>を使って入力内容のヒントを入れる
などがあります。

次にルーティングを作成します。

管理側のmiddlewareの中にルーティングを設定しましょう。(+で書いた所を追記)

routes/web.php

//管理側
Route::group(['middleware' => ['auth.admin']], function () {
	
	//管理側トップ
	Route::get('/admin', 'admin\AdminTopController@show');
	//ログアウト実行
	Route::post('/admin/logout', 'admin\AdminLogoutController@logout');
	//ユーザー一覧
	Route::get('/admin/user_list', 'admin\ManageUserController@showUserList');
	
	//ユーザー登録
+	Route::get('/admin/user/create', 'admin\ManageUserController@showUserCreateForm');
+	Route::post('/admin/user/create', 'admin\ManageUserController@create');

	//ユーザー詳細
	Route::get('/admin/user/{id}', 'admin\ManageUserController@showUserDetail');
});

ユーザー詳細の「Route::get('/admin/user/{id}'」より前に書くことを注意してください。

順番を逆にした場合、/admin/user/createにアクセスしたとしても「/admin/user/{id}」にマッチしてこちらが先に実行されます。

ブラウザで確認テンプレートが問題なければブラウザで「/admin/user/create」にアクセスして、フォームの表示を確認しましょう。

スクリーンショット 2020-06-19 23.43.26


# ユーザー登録機能の作成

次にフォームの遷移先のcreate()関数を追加していきます。

app/Http/Controllers/admin/ManageUserController.php

use Validator;

class ManageUserController extends Controller
{
	function create(Request $request){ 
		$input = $request->only('name', 'email', 'display_name',
             'password', 'password_confirmation');
		
		$validator = Validator::make($input, [
			'name' => 'required|string|max:30',
			'email' => 'required|string|max:30|email|unique:users,email',
			'password' => 'required|confirmed|string|max:100',
			'display_name' => 'required|string|max:30|regex:/^[a-z0-9]+/|unique:users,display_name',
		]);

		//バリデーション失敗
		if($validator->fails()){
			return redirect('admin/user/create')
				->withErrors($validator)
				->withInput();
		}
		
		//バリデーション成功
		$user = new User();
		$user->name = $input["name"];
		$user->email = $input["email"];
		$user->display_name = $input["display_name"];
		$user->password = bcrypt($input["password"]);
		$user->save();

		return redirect('admin/user/' . $user->id);
	}
	
}

create()関数の中身を細かく確認していきます。


use句を使ってバリデーションを使うための指定を行います。

use Validator;

入力値を受け取るために$request->only()を使います。

$input = $request->only('name', 'email', 'display_name', 'password', 'password_confirmation');

バリデーションは少し複雑です。

'email' => 'required|string|max:30|email|unique:users,email',

メールアドレスは書き方がメールアドレス形式である必要があるためemailを指定しています。

また、すでに登録されているかを確認するためのuniqueを指定しています。「unique:users,email」とすることで「usersテーブルのemailカラムを確認する」バリデーションが行えます。

'password' => 'required|confirmed|string|max:100',

confirmedを設定すると「名前_confirmation」という項目と一致しているか確認してくれます。この場合「name="password_confirmation"」の要素と値が一致してくれるか確認する設定です。

'display_name' => 'required|string|max:30|regex:/^[a-z0-9]+/|unique:users,display_name',

メールアドレスと同じユニークチェックを行っています。

regex:/^[a-z0-9]+/」の指定は正規表現を使ったバリデーションで、入力可能な文字を小文字+数字に制限しています。正規表現は様々なルールを設定可能なのでWebアプリケーション開発ではよく使います。

「PHP 正規表現」で検索すると色々な情報が見つかるので是非調べてみてください。

//バリデーション失敗
if($validator->fails()){
    return redirect('admin/user/create')
        ->withErrors($validator)
        ->withInput();
}

バリデーション失敗の時、withErrors()だけでなくwithInput()を追加しています。これはエラー時などに以前入力した値を表示するために追加しています。

ユーザー登録フォームは重複している場合が考えられるため、登録フォームを何度も表示する必要が出てきます。そのため、入力値はエラーになった時にフォームにも表示したほうがユーザーの利便性があがります。

この段階で、ブラウザで名前だけ入力してフォームを入力し、エラーメッセージが表示されるか確認してみましょう。

スクリーンショット 2020-06-20 0.12.54



# ユーザー登録フォームの修正

バリデーションに失敗した時にフォームの内容が表示されるように各フォームを修正します。

withInput()でバリデーション失敗時に渡された内容は、次のようにvalueの中を{{old('name')}}に書き換えることで表示できます。

<input class="form-control" type="text" name="name" value="" />

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

この時フォームのname="xxxx"で指定している値とold("xxxx")の引数が一致するように注意してください。

修正したuser_create.blade.phpは次のようになります。

resources/views/admin/user_create.blade.php

@extends('layouts.admin')
@section('content')
<div class="container">
	<div class="card">
		<div class="card-header">ユーザー新規登録</div>
		<div class="card-body">
			@if ($errors->any())
			<div style="color:red;">
			<ul>
				@foreach ($errors->all() as $error)
				<li>{{ $error }}</li>
				@endforeach
			</ul>
			</div>
			@endif
			<form method="post" action="{{ url('admin/user/create') }}">
			@csrf 
			<div class="form-group">
				名前: <input class="form-control" type="text" 
                    name="name" value="{{old('name')}}" />
				<p class="text-muted">表示名は自由に設定できます</p>
			</div>
			<div class="form-group">
				Email: <input class="form-control" type="email" 
                    name="email" value="{{old('email')}}" />
			</div>
			<div class="form-group">
				表示名: <input class="form-control" type="text" 
                    name="display_name" value="{{old('display_name')}}" />
				<p class="text-muted">半角英数字(30文字以内)</p>
			</div>
			<div class="form-group">
				ログインパスワード: <input class="form-control" 
                    type="password" name="password" value="{{old('password')}}" />
			</div>
			<div class="form-group">
				ログインパスワード(確認): <input class="form-control" 
                    type="password" name="password_confirmation" value="" />
			</div>
			<div class="mt-3">
				<input class="btn btn-primary" type="submit" value="作成" />
			</div>
			</form>
		</div>
	</div>
</div>
@endsection

各要素のvalue=""にold()を追加しています。ただ、ログインパスワード(確認)だけ意図的に補完していません。

ブラウザで動作確認し、エラーをわざと発生させて(パスワード確認を空にするなど)入力内容が補完されることを確認してください。

スクリーンショット 2020-06-20 0.16.22


バリデーションが動作していることを確認したら、全ての項目を正しく入力してユーザー詳細に遷移することを確認してください。

スクリーンショット 2020-06-20 0.18.24


# ユーザー詳細画面の修正

ユーザー詳細画面を修正し、名前、表示名の変更が出来るようにします。

次の手順で行います。

1. ルーティングの設定
2. テンプレートを変更する
3. ManageUserControllerに変更用のupdate()関数を追加します

まずはルーティングの設定を行います。

routes/web.php

//ユーザー詳細
Route::get('/admin/user/{id}', 'admin\ManageUserController@showUserDetail');
↓
Route::get('/admin/user/{id}', 'admin\ManageUserController@showUserDetail');
Route::post('/admin/user/{id}', 'admin\ManageUserController@update');

このようにgetとpostは並べて書いた方が設定し忘れが防げます。


ユーザー詳細画面のテンプレートを編集していきます。

詳細画面と変更画面を兼ねる場合、old()関数を次のように書くことでエラーが出た時の入力値と保存されている値を出し分けることができます。

<input class="form-control" type="text" name="name" value="{{old('name', $user->name)}}" />

入力値を表示するold()の2個目の引数に初期値を指定します。この書き方で「入力値があればその値を、なければ$user->name」の値を表示することができます。

テンプレート全体は以下です。

resources/views/admin/user_detail.blade.php

@extends('layouts.admin')
@section('content')
<div class="container">
	<div class="card">
		<div class="card-header">
			<a href="{{ url('admin/user_list') }}">ユーザー一覧</a> &gt; ユーザー詳細
		</div>
		<div class="card-body">
		
			@if ($errors->any())
			<div style="color:red;">
			<ul>
				@foreach ($errors->all() as $error)
				<li>{{ $error }}</li>
				@endforeach
			</ul>
			</div>
			@endif
			<form method="post" action="{{ url('admin/user/' . $user->id) }}">
				@csrf 
				
				<ul class="list-group">
					<li class="list-group-item">
						<label>名前: </label>
						<input class="form-control" type="text" name="name" value="{{old('name', $user->name)}}" />
					</li>
					<li class="list-group-item">
						<label>メール: </label>
						<input class="form-control" type="email" name="email" value="{{old('email',$user->email)}}" />
					</li>
					<li class="list-group-item">
						<label>表示名: </label>
						<input class="form-control" type="text" name="display_name" value="{{old('display_name',$user->display_name)}}" />
					</li>
					<li class="list-group-item">作成日: {{ $user->created_at->format('Y/m/d H:i:s') }}</li>
					<li class="list-group-item">更新日: {{ $user->updated_at->format('Y/m/d H:i:s') }}</li>
				</ul>
				<div class="mt-3 text-center">
					<input class="btn btn-primary" type="submit" value="変更" />
				</div>
			</form>
		</div>
	</div>
</div>
@endsection

ブラウザを開いてユーザー詳細画面の表示を確認してください。



次にフォームの遷移先のupdate()関数を作成していきます。

基本的な内容はcreate()と同じですが、更新する項目が異なる点と、バリデーションで重複チェックを行う時自分自身は除外する必要がある点が異なります。

自分自身を除外するにはunique制約の設定を変更します。

uniqueの制約は、次のように3個目の値に除外するオブジェクトのIDを指定可能です。

unique:<テーブル名>,カラム名,除外するID


app/Http/Controllers/admin/ManageUserController.php

class ManageUserController extends Controller
{
	function update(Request $request, $id){ 
	
		$user = User::find($id);
		
		$input = $request->only('name', 'email', 'display_name');
		
		$validator = Validator::make($input, [
			'name' => 'required|string|max:30',
			'email' => 'required|string|max:30|email|unique:users,email,'.$user->id,
			'display_name' => 'required|string|max:30|regex:/^[a-z0-9]+/|unique:users,display_name,'.$user->id,
		]);
	
		//バリデーション失敗
		if($validator->fails()){
			return redirect('admin/user/' . $user->id)
				->withErrors($validator)
				->withInput();
		}
		
		//バリデーション成功
		$user->name = $input["name"];
		$user->email = $input["email"];
		$user->display_name = $input["display_name"];
		$user->save();
	
		return redirect('admin/user/' . $user->id);
	
	}
}
'email' => 'required|string|max:30|email|unique:users,email,'.$user->id,
'display_name' => 'required|string|max:30|regex:/^[a-z0-9]+/|unique:users,display_name,'.$user->id,

emailとdisplay_nameのバリデーションは$user->idを追加して自分自身を取り除いています。

//バリデーション成功
$user->name = $input["name"];
$user->email = $input["email"];
$user->display_name = $input["display_name"];
$user->save();

create()と異なり、パスワードは更新していません。

フォームを開いてユーザー情報の変更が出来ることを確認しましょう。


# ユーザーページの作成

ユーザーページを「/u/<display_name>」で表示できるように作成していきます。

まずは表示のためのコントローラー(UserPageController)を作成します。display_nameを引数として受け取るshow()関数を作成していきます。

php artisan make:controller UserPageController

作成されたUserPageController.phpを編集します。

app/Http/Controllers/UserPageController.php

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;

use App\User;

class UserPageController extends Controller
{
	function show($name){
		
		$user = User::where("display_name", $name)->first();
		if(!$user){
			return abort(404);
		}
		return view("user_page", [
			"user" => $user
		]);
	}
}
use App\User;

use句を使ってUserクラスを利用できるようにします。

$user = User::where("display_name", $name)->first();
if(!$user){
    return abort(404);
}

where()関数を使ってdisplay_nameが$nameと一致しているデータを取得します。

存在しない場合があるため、取得に失敗した場合はabort(404)を使ってエラーの表示を行います。


次にユーザーページ用のテンプレートを作成します。

resources/views/user_page.blade.php

@extends('layouts.app')
@section('content')
<div class="container">
	<div class="card">
		<div class="card-header">
			{{ $user->name }}からのお知らせ
		</div>
		<div class="card-body">
			
		</div>
	</div>
</div>
@endsection
@extends('layouts.app')

appレイアウトを使う宣言です。

<div class="card-header">
{{ $user->name }}からのお知らせ
</div>

どのユーザーかどうかわかるように「{{ $user->name }}からのお知らせ」を追加しています。お知らせ管理機能や表示部分は次回以降に行います。


ユーザーページ用のルーティングを設定するためにroutes/web.phpを編集します。

routes/web.php

Route::get('/home', 'HomeController@index')->name('home');
↓
Route::get('/home', 'HomeController@index')->name('home');
Route::get('/u/{name}', 'UserPageController@show');


管理画面からユーザーページにアクセス出来るようにリンクを設定します。(+を書いている部分を追加部分)

resources/views/admin/user_detail.blade.php

@section('content')
<div class="container">
	<div class="card">
		<div class="card-header">
			<a href="{{ url('admin/user_list') }}">ユーザー一覧</a> &gt; ユーザー詳細
		</div>
		<div class="card-body">
+			<div class="mb-3">
+			<a class="btn btn-secondary" href="{{ url('u/' . $user->display_name) }}">ユーザーページを確認</a>
+			</div>
			@if ($errors->any())
			<div style="color:red;">
			<ul>
				@foreach ($errors->all() as $error)
				<li>{{ $error }}</li>
				@endforeach
			</ul>
			</div>
			@endif


ブラウザで管理画面を開いてユーザーページにアクセス出来るか確認してみてください。


スクリーンショット 2020-06-20 1.08.07

スクリーンショット 2020-06-20 1.40.41



# まとめ

本稿で管理画面上からユーザーを作成する方法やユーザーページといったID以外を使った詳細画面を作るなど、Webアプリケーションによくある機能を実装しました。

少しずつWebアプリケーションらしい作りになってきました。

次回はお知らせの管理機能をユーザー側に追加していきます。


おつかれさまでした!


完成まで突っ走る意気込みです。サポートしていただけると非常に嬉しいです。応援よろしくお願いします。