見出し画像

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

こちらはLaravelを利用したお知らせアプリの記事の二回目です。表紙はこちらのマガジンからどうぞご確認ください

前回管理画面からユーザーを追加・削除出来るようになりました。本稿ではユーザー側の投稿機能の準備を行います。

用語
・1:N
・リレーション
・外部キー

# Userに紐付いた投稿テーブルの作成

掲示板を作ろうでは投稿のテーブルは独立したものでした。

今回作成するアプリは投稿のテーブルをUserと紐付いた形にする必要があります。

まずはマイグレーションの作成から順に確認していきましょう。

make:migrationを使って「create_news_entry」マイグレーションを作成します。

php artisan make:migration create_news_entry

作成されたマイグレーションファイルを修正していきます。

作成されたマイグレーションファイルは実行タイミングで異なりますので、出力メッセージに合わせて置き換えてください。

今回は「Created Migration: 2020_06_20_035233_create_news_entry」と表示されましたので、次のファイルを編集します。

database/migrations/2020_06_20_035233_create_news_entry.php

<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateNewsEntry extends Migration
{
   /**
    * Run the migrations.
    *
    * @return void
    */
   public function up()
   {
       Schema::create('news_entry', function (Blueprint $table) {
			
			$table->id();
			$table->timestamps();
			
			//記事のデータ
			$table->string("title");
			$table->string("description");
			$table->string("body");
			$table->string("thumbnail_url");
			$table->string("image_url");
			
			//ユーザーテーブルと連携するためのカラムuser_id
			$table->bigInteger('user_id')->unsigned();

			//外部キー制約
			$table->foreign('user_id')
                   ->references('id')
                   ->on('users')
					->onDelete('cascade');
					
		});
   }
   /**
    * Reverse the migrations.
    *
    * @return void
    */
   public function down()
   {
       Schema::dropIfExists('news_entry');
   }
}

「news_entry」テーブルを作成するためのマイグレーションです。

ポイントはusersテーブルと連携するためのカラムuser_idを追加することです。

$table->bigInteger('user_id')->unsigned();

まずカラムを追加して、その後外部キーと呼ばれる設定を追加します。

//外部キー制約
$table->foreign('user_id')
   ->references('id')
   ->on('users')
    ->onDelete('cascade');

この外部キーがテーブルとテーブルを紐付ける設定です。

上記の記述では、usersが削除されたらこのnews_entryも一緒に削除される設定をしています。

少し難しいですが、usersテーブルと連携するテーブルはこのuser_idの追加と外部キー制約を必ず行う必要があるのでよく使うパターンとして記憶しておきましょう。

php artisan migrate

マイグレーションを実行してエラーが出なかったら成功です。

もしエラーが出る人はmigrate:refreshを試して最初から作り直してみてください。

php artisan migrate:refresh


# NewsEntryモデルの作成

作成したnews_entryと関連するNewsEntryモデルを作成します。

モデルの作成はmake:modelコマンドを使います。

php artisan make:model NewsEntry

作成された「app/NewsEntry.php」を開いて編集していきます。

NewsEntryクラスでやるべきことは2点です。

1. テーブル名をnews_entryに設定すること
2. Userとの連携を設定すること

app/NewsEntry.php

<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class NewsEntry extends Model
{
	protected $table = 'news_entry';
	
	public function user()
	{
		return $this->belongsTo('App\User');
	}
	
}

NewsEntryからUserを取得するための関係性をuser()関数を作成して定義しています。

public function user(){
    return $this->belongsTo('App\User');
}

これは【NewsEntry】は【User】に所属している、という意味を表しています。

では逆に【User】から見て【NewsEntry】はどういう関係性でしょうか?

答えは【User】には【NewsEntry】が複数個ある、という関係性です。このような関係性を「1:N」「1対多」と表現します。図にするとこんな感じです。

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

# UserクラスにもNewsEntryの紐付けを追加する

NewsEntryから見たUserの関係性(belongsTo)を設定しました。今度はUserにもNewsEntryとの関係を追加してお互いに連携出来るようにします。

app/User.phpを編集します(+が追加部分)

app/User.php

class User extends Authenticatable
{
	(...)
	
+	public function newsEntry() {
+		return $this->hasMany("App\NewsEntry", 'user_id', 'id');
+	}

}

UserとNewsEntryは1対多の関係性です。LaravelではhasMany()関数を使って表現します。第2引数、第3引数が外部キーの制約を行ったカラムの関係性です。

【NewsEntryのuser_id】が【自分(User)のid】と紐付いているという意味になります。


このようにお互いに関係性を記述することでUserからNewsEntryを取り出すこと、NewsEntryからUserを取り出すことが可能になります。

取り出し方は次のように関係性を記述した関数を経由して行います。

//Userからnewsの取り出し
$user = User::find(1);
$news = $user->newsEntry()->first();

//Newsからuserの取り出し
$user = $news->user;

ここまで出来たらNewsEntryの作成機能を作っておきましょう。


# 交通整理

作成用のリンクが設定出来ていなかったので少し交通整理を行います。

管理画面TOPにユーザー作成のリンクを追加します(+が追加箇所)。

resources/views/admin/admin_top.blade.php


@extends('layouts.admin')
@section('content')
<div class="container">
	<div class="card">
		<div class="card-header">管理側TOP</div>
		<div class="card-body">
			<div class="mb-3">
				<a href="{{ url('admin/user_list') }}" class="btn btn-primary">ユーザー一覧</a>
+				<a href="{{ url('admin/user/create') }}" class="btn btn-primary">ユーザー作成</a>
			</div>
			
			<form method="post" action="{{ url('admin/logout') }}">
				@csrf
				<input type="submit" class="btn btn-danger" value="ログアウト" />
			</form>
		</div>
	</div>
</div>
@endsection


ユーザーのログインTOPに記事作成リンクを追加します(+が追加箇所)。

resources/views/home.blade.php

@extends('layouts.app')
@section('content')
<div class="container">
   <div class="row justify-content-center">
       <div class="col-md-8">
           <div class="card">
               <div class="card-header">Dashboard</div>
               <div class="card-body">
                   @if (session('status'))
                       <div class="alert alert-success" role="alert">
                           {{ session('status') }}
                       </div>
                   @endif
+                   <div class="mb-3">
+						<a href="{{ url('/news/create') }}" class="btn btn-primary">記事作成</a>
+					</div>
               </div>
           </div>
       </div>
   </div>
</div>
@endsection

記事作成のためのリンク「/news/create」の遷移先の記事作成機能を作っていきます。

# 記事作成機能の開発

いつもはController -> View -> ルーティングの順で作りましたが、今回は気分を変えて次の順に作っていきます。

1. ルーティングを設定
2. Viewを作成
3. Controllerを作成

routes/web.php

//ユーザーログインが必須のページ
Route::group(['middleware' => ['auth']], function(){

	//記事の作成
	Route::get('/news/create', 'user\ManageEntryController@showCreateForm');
	Route::post('/news/create', 'user\ManageEntryController@create');
	//記事の編集
	Route::get('/news/edit/{id}', 'user\ManageEntryController@showEditForm');
	Route::post('news/edit/{id}', 'user\ManageEntryController@update');
	//記事の削除
	Route::post('/news/delete/{id}', 'user\ManageEntryController@delete');

});

記事の管理は全て認証が必要なページとするため、管理側と同じようにgroupを使って認証をさせます。

標準の認証機能を有効にするには"auth"のmiddlewareを実行します。

ルーティングを先に作ると、このように全機能を確認することが出来るので機能が漏れているなどの問題が避けられます。

次にコントローラーを作成します。Viewを作るのでは?と疑問に思うかもしれませんが、次のようなコントローラーを作っていきます。

コントローラーの作成make:controllerコマンドを使ってコントローラーを作成します。

php artisan make:controller user/ManageEntryController

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

app/Http/Controllers/user/ManageEntryController.php

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

class ManageEntryController extends Controller
{
	/* 記事の作成 */
   function showCreateForm(){
		return view("news.create_form");
	}
	function create(Request $request){
		//@TODO 記事の作成
		return redirect("home")->withStatus("記事を作成しました");
	}
	/* 記事の編集 */
	function showEditForm(){
		return view("news.edit_form");
	}
	function update(Request $request, $id){
		//@TODO 記事の更新
		return redirect("home")->withStatus("記事を更新しました");
	}
	/* 記事の削除 */
	function delete($id){
		return redirect("home")->withStatus("記事を削除しました");
	}
}

全ての関数を中身を空っぽでリダイレクトするだけで設定しています。

withStatus()は戻った時にメッセージを表示するための機能です。

このように今から作るものをガワだけ用意して「@TODO やること」とメモしておくと先のタスクの見通しがよくなります。

記事作成フォームのテンプレートを作成します。showCreateForm()で設定した「news.create_form」にそろえて「resources/views/news/create_form.blade.php」を作成します。

resources/views/news/create_form.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('news/create') }}">
			@csrf 
			<div class="form-group">
				<label>タイトル: </label><br />
				<input class="form-control" type="text" name="title" value="{{old('title')}}" />
			</div>
			<div class="form-group">
				<label>概要: </label><br />
				<input class="form-control" type="text" name="description" value="{{old('description')}}" />
			</div>
			<div class="form-group">
				<label>本文: </label><br />
				<textarea class="form-control" name="body">{{old('body')}}</textarea>
			</div>
			
			<div class="mt-3">
				<input class="btn btn-primary" type="submit" value="作成" />
			</div>
			</form>
		</div>
	</div>
</div>
@endsection

画像周りは後で設定するので、タイトル、概要、本文のフォームを作成しています。

ではここでブラウザで確認してみましょう。


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

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

記事のフォームが表示されること、作成ボタンを押すと作成していませんがメッセージが出てくることを確認したら、実装に移ります。

今回やったのは、「ルーティングを設定して関数の漏れが無いようにすること」、「空っぽのコントローラーを作ってViewを確認すること」を先に行う、プロトタイプ開発です。

機能を作ることなく画面上の項目の漏れやデザインを確認出来るので、このようなやり方が向いている場合もあります。

特にCRUDと呼ばれる作成、表示、更新、削除などの機能を一気に作る場合は先にルーティングを設定して置いたほうが見通しが良いです。


# 記事作成機能の開発 create()関数の実装

フォームの遷移先のcreate()関数を作っていきます。

create()関数は管理者側機能のユーザー作成機能を再利用して少し変更するだけで作れます。

app/Http/Controllers/user/ManageEntryController.php

<?php
namespace App\Http\Controllers\user;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\NewsEntry;
use Auth;
use Validator;
class ManageEntryController extends Controller
{
    ...

	function create(Request $request){
	
		$input = $request->only('title', 'description', 'body');
		
		$validator = Validator::make($input, [
			'title' => 'required|string|max:200',
			'description' => 'string|max:200',
			'body' => 'required|string'
		]);
	
		//バリデーション失敗
		if($validator->fails()){
			return redirect('news/create')
				->withErrors($validator)
				->withInput();
		}
		
		//バリデーション成功
		$news = new NewsEntry();
		$news->title = $input["title"];
		$news->description = $input["description"];
		$news->body = $input["body"];
		$news->user_id = Auth::id();
		$news->thumbnail_url = "";
		$news->image_url = "";
		$news->save();
	
		return redirect("home")->withStatus("記事を作成しました");
	}
	
	...	
}
use App\NewsEntry;
use Auth;
use Validator;

利用するクラスの宣言です。今回はこの3個が追加されました。

$input = $request->only('title', 'description', 'body');

title, description, bodyの3項目を受け取ります。

$validator = Validator::make($input, [
  'title' => 'required|string|max:200',
  'description' => 'string|max:200',
  'body' => 'required|string'
]);

バリデーションはシンプルに最大200文字とするだけで、特に制限はかけていません。

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

バリデーション失敗時は記事作成画面にリダイレクトします。

//バリデーション成功
$news = new NewsEntry();
$news->title = $input["title"];
$news->description = $input["description"];
$news->body = $input["body"];
$news->user_id = Auth::id();
$news->thumbnail_url = "";
$news->image_url = "";
$news->save();

NewsEntryを作って保存しています。ここで重要なのがuser_idを設定しているところです。

$user->user_id = Auth::id();

現在ログイン中のユーザーのIDはAuth::id()で取得できます。

ここまで出来たらブラウザ記事作成フォームを使って記事を投稿してください。

phpMyAdminなどで投稿した内容が保存されているかを確認できます。

スクリーンショット 2020-06-21 1.32.22


# まとめ

記事の分量が多いため、今回は作成機能まで進みました。テーブルの作成、モデルの作成に新しい知識が必要となりますが、フォームの作成は今までの知識だけで作ることが出来ます。

今はレベルが上がって新しい攻撃呪文を覚えた、くらいの状態なのでどんどんレベルを上げてより強力な呪文を覚えていきましょう。

次の記事で記事一覧ページやユーザーページを作成していきます。

おつかれさまでした!

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