見出し画像

プログラミング初心者のための「0から始めるWebアプリケーション実践開発」第2章

みなさん、こんにちは、ゆうきです!(@RubyPHP2

今回は第2章を進めていきます。

第2章は第1章の続きから始めます。

第1章をまだ確認されていない方は、ぜひチェックしてみてください。

第1章ではLaravel環境構築をし、デプロイまでを行いました。

この第2章では実際にソースコードを書いて実装していきます!

⚠️ソースコードをコピペすると、インデントがずれる場合がございます。

ご自身でインデントはそろえるようにしてください。

今回は掲示板アプリを作成していきます。

最終的には以下の機能を実装していきます。

・ログイン/ログアウト
・ユーザ登録
・ユーザ編集
・ユーザ一覧表示
・ユーザ削除
・コンテンツ投稿
・投稿一覧表示

第2章では以下のようなことを行なっていきます。

・usersテーブル再作成
・micropostsテーブル作成
・テストデータの作成
・ログイン機能
・ログアウト機能

実はLaravelバージョン5台では、認証機能を一括実装できるコマンドが存在しますが、基礎を学ぶ上では学習にならないので、今回は一から実装します。

今回実装する掲示板アプリのDB設計は以下の通りです。

スクリーンショット 2020-03-16 6.19.17

まずusersテーブルについてです。

文字通りユーザの情報を格納します。

idは主キーと呼ばれるもので、テーブルを作る上では必須になります。

主キーはデフォルトでは自動連番になります。

つまり、ユーザが登録されるごとに自動で、id=1、id=2と割り振ってくれるわけです。

nameは文字列として保存します。

emailはユニークにします。

ユニークとは他のレコードのemailと重複してはいけないという決まりです。

passwordはハッシュ化した状態でテーブルに保存します。

パスワードを平文で保存するのはセキュリティ的によくないため、暗号化目的のためにハッシュ化します。

admin_flgはユーザが管理者であるかどうかを判定するものです。

次にmicropostsテーブルについてです。

投稿内容を格納するテーブルになります。

user_idは外部キーと呼ばれるものです。

これを設定しておくことで、ある投稿内容がどのユーザのものであるかを特定できます。

contentは投稿内容を保存します。


usersテーブルとmicropostsテーブルは1対多の関係にあります。

これは、一人のユーザが複数の投稿内容を持つことができるということです。

例えば、Twitterでも一人のユーザが複数ツイートすることが可能ですよね。

まさにこれが1対多の関係です。


テーブルの構成を確認したところで、始めていきましょう!

Laravelフレームワークについて簡単に説明します。

LaravelはMVCの構成で基本的には開発していきます。

MVCとはModel、View、Controllerの略です。

スクリーンショット 2020-03-08 15.58.34

Controllerとは...
コントローラはモデルとビューの橋渡しをする存在です。
主に2つの役割を持っています。
・ルートから受け取った情報をモデルに処理をお願いする
・モデルから受け取った情報をビューに表示する
具体的にはモデルからDBにアクセスし、取得したデータをコントローラで加工してビューに渡すと行ったことや、ビューからのデータをモデルに渡し、DBに保存をしたりします。
Viewとは...
表示や入出力といった処理を行います。
Laravelではbladeというテンプレートエンジンを使用しています。
ビュー(Blade)のディレクトリは resources/viewsディレクトリになります。
Modelとは...
DBにアクセスして、あらゆる処理を実行します。
・DBとそれを操作する全て(処理、検証、保存など)
・DBへのアクセス

この辺りの説明はググればいくらでも出てきますね。

ただ、実際に使わないとよくわからないと思いますので、

コードを書きながら掴んでいってください。


第2章 テーブル設計とログイン機能の実装

目次
1. ログインフォームの作成(トップページ)
2. ログイン機能の実装
3. テーブルおよびモデルの作成
4. テストデータの作成
5. ログアウト機能の実装
6. デプロイ


1. ログインフォームの作成(トップページ)

第1章で作成したlaravel-appプロジェクトを使用していきます。

まずはトップページから作成します。

トップページにはログインフォームが表示されるようにしていきます。

routes/web.phpに以下のコードを追加します。

Route::get('/', 'UserController@signin')->name('user.signin');

初期の以下のコードは削除してください。

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

ここではルーティングを設定しています。

ルーティングはweb.phpに記述します。

ファイルを編集した時は、command + s でファイルを保存してください。

今後、ファイル保存についての説明は割愛します。

さて、この1行の意味としては、

http://localhost:8000/ にgetでアクセスするとUserControllerのsigninアクションの処理を実行します。という意味になります。

Controllerはこの後、登場します。

HTTPリクエストにはgetとpostがあります。

ブラウザ上でviewを出力したい場合はgetだと考えてください。

今回はhttp://localhost:8000/ にアクセスした時、ログインフォームを表示したいので、getになります。

詳しくは以下のリンクを参考してください。

「HTTPリクエスト」と「HTTPレスポンス」

また、nameは単に名前をつけています。

名前をつけることであらゆる場所で以下のようにしてこのルーティングを呼び出すことができます。

route('user.signin')

今後、使う機会が出てくるので、頭の片隅のとどめておいてください。


次は、UserController.phpを作成していきます。

ターミナルを開きます。

cdコマンドで第1章で作成したlaravel-appに移動してください。

コンテナ内に入り、UserController.phpを作成します。

[mac]$ pwd
/Users/*********/laravel-docker-workspace/laravel-app

[mac]$ docker-compose up -d

[mac]$ docker-compose exec app bash

[コンテナ]$ cd laravel-app

[コンテナ]$ php artisan make:controller UserController
Controller created successfully.​

作成したUserController.phpを確認します。

(app/Http/Controllers/UserController.php)

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UserController extends Controller
{
   //
}

デフォルトだと上記のようになっていますね。

namespaceは名前空間を意味します。
通常、同じクラス名や同じ関数名を使うとPHPエラーになりますが、名前空間を用いることで、名前の衝突を避けることが可能です。

つまり、UserControllerクラスがどこに存在しているのかを記してあげる必要があるのです。

実際、UserControllerクラス(UserController.php)はControllersディレクト配下に位置していますよね。
use宣言はクラスの中で使うクラスを宣言します。
ControllerではRequestクラスのインスタンスをよく使用するので、
デフォルトでuse宣言されています。

別クラスで定義した関数などを使用したい場合、
そのクラスをuse宣言する必要があると考えて下さい。
extendsは継承を意味します。
今回の場合だと、UserControllerクラスはControllerクラスを継承しています。
この時、Controllerクラスを親クラス、UserControllerクラスを子クラスと呼びます。
子クラスは親クラスからメソッドの内容を引き継ぐことができます。

では、UserController.phpに処理を追加します。

(app/Http/Controllers/UserController.php)

.
.
中略

class UserController extends Controller
{
  /**
   * ログインフォーム表示アクション
   */
  public function signin()
  {
    return view('user.signin');
  }
}

signinアクションでは、viewヘルパ関数を使用しています。

ここではviews/user/signin.blade.phpを返します。

viewヘルパ関数はデフォルトでviewsディレクトリ配下を参照します。

user.signinのように.(ドット)記号を用いることでuserディレクトリ配下のsignin.blade.phpを返すという意味になります。

このようにControllerではViewやModelとやりとりを行います。

http://localhost:8000/ にアクセスすると、このsigninアクションが呼ばれ、userディレクトリ配下のsignin.blade.phpをブラウザ表示します。

今回のsigninアクションではModelとのやりとりは行われていませんが、

今後はModelとのやりとり(DBとのやりとり)を扱っていきます。


さて、signinアクションでviewを返す処理を書きましたが、viewファイルが存在していませんね。

次はviewファイルを作成していきましょう!

cdコマンドでlaravelプロジェクトのviewsディレクトリに移動してください。

layoutsディレクトリとuserディレクトリを作成します。

[mac]$ pwd
/Users/*********/laravel-docker-workspace/laravel-app/resources/views

[mac]$ mkdir layouts
[mac]$ mkdir user

作成したlayoutsディレクトリにapp.blade.php作成します。

cdコマンドでlayoutsディレクトリに移動してください。

移動したら、touchコマンドでファイルを作成します。

[mac]$ pwd
/Users/*********/laravel-docker-workspace/laravel-app/resources/views/layouts

[mac]$ touch app.blade.php

また、userディレクトリにはsignin.blade.phpを作成します。

cdコマンドでuserディレクトリに移動してください。

移動したら、touchコマンドでファイルを作成します。

[mac]$ pwd
/Users/*********/laravel-docker-workspace/laravel-app/resources/views/user

[mac]$ touch signin.blade.php

app.blade.phpファイルを以下のように編集します。

(resources/views/layouts/app.blade.php)

<!DOCTYPE html>
<html lang="ja">
<head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">

 <!-- CSRF Token -->
 <meta name="csrf-token" content="{{ csrf_token() }}">

 <title>{{ config('app.name', 'Laravel') }}</title>

 <!-- Scripts -->
 <script src="{{ asset('js/app.js') }}" defer></script>

 <!-- Fonts -->
 <link rel="dns-prefetch" href="//fonts.gstatic.com">
 <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">

 <!-- Styles -->
 <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</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="{{ url('/') }}">
           {{ config('app.name', 'Laravel') }}
       </a>
       <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
           <span class="navbar-toggler-icon"></span>
       </button>

       <div class="collapse navbar-collapse" id="navbarSupportedContent">
         <!-- Left Side Of Navbar -->
         <ul class="navbar-nav mr-auto">
         </ul>

         <!-- Right Side Of Navbar -->
         <ul class="navbar-nav ml-auto">
           <!-- Authentication Links -->
           @guest
             <li class="nav-item">
                 <a class="nav-link" href="{{route('user.signin')}}">{{ __('ログイン') }}</a>
             </li>
             <li class="nav-item">
                 <a class="nav-link" href="">{{ __('新規登録') }}</a>
             </li>
           @else
             <li class="nav-item">
                 <a class="nav-link" href="">{{ __('ホーム') }}</a>
             </li>
             <li class="nav-item">
               <a class="nav-link" href="">{{ __('ユーザ一覧') }}</a>
             </li>
             <li class="nav-item">
               <a class="nav-link" href="">{{ __('投稿') }}</a>
             </li>
             <li class="nav-item dropdown">
               <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
                 アカウント <span class="caret"></span>
               </a>

               <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
                 <a class="dropdown-item" href="">アカウント変更</a>
                 <a class="dropdown-item" href=""
                   onclick="event.preventDefault();
                                 document.getElementById('logout-form').submit();">
                   {{ __('ログアウト') }}
                 </a>

                 <form id="logout-form" action="" method="POST" style="display: none;">
                   @csrf
                 </form>
               </div>
             </li>
           @endguest
         </ul>
       </div>
     </div>
   </nav>

   <!-- フラッシュメッセージ -->
   @if (Session::has('flash_message'))
     <p class="bg-success">{!! Session::get('flash_message') !!}</p>
   @endif

   @if (Session::has('error_message'))
     <p class="bg-danger">{!! Session::get('error_message') !!}</p>
   @endif

   <main class="py-4">
     @yield('content')
   </main>
 </div>
</body>
</html>

次にsignin.blade.phpを以下のように編集します。

(resources/views/user/signin.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">{{ __('ログイン') }}</div>
       <div class="card-body">
         @if (count($errors) > 0)
         <div class="errors">
           <ul>
             @foreach ($errors->all() as $error)
               <li>{{$error}}</li>
             @endforeach
           </ul>
         </div>
         @endif
         <form action="{{route('user.login')}}" method="POST">
           @csrf
           <div class="form-group">
             <label for="email">E-Mail</label>
             <input type="text" id="email" name="email" value="{{old('email')}}" class="form-control">
           </div>
           <div class="form-group">
             <label for="password">Password</label>
             <input type="password" id="password" name="password" value="{{old('password')}}" class="form-control">
           </div>
           <button type="submit" class="btn btn-primary">ログイン</button>
         </form>
       </div>
     </div>
   </div>
 </div>
</div>
@endsection

いきなりすごいコード量です。笑

順に説明します。

まず、Laravelはbladeテンプレートエンジンを用いていると冒頭で説明しました。

bladeテンプレートでは、@記号を用いてif文やfor文などあらゆる操作をviewファイルで使用することができます。

また、{{ }}記法や{!! !!}記法も登場します。

{{ }}記法 とは...
bladeの{{ }}記法はXSS攻撃を防ぐため、自動的にPHPのhtmlspecialchars関数を通されます。

{!! !!}記法 とは...
自動的にPHPのhtmlspecialchars関数を通されません。

app.blade.phpは共通のviewファイルです。

これは親テンプレートと呼ばれたりします。

例えば、ヘッダー部分は全ファイル共通で使いたい!

という場合が多いです。

親テンプレートでヘッダー部分を記述しておけば、子テンプレートでいちいちヘッダー部分を記述する必要がなくなるわけです。

app.blade.phpに@yield('content')という記述があります。

これは親テンプレートの@yield('content')に子のコンテンツを表示させるという意味になります。


では、子テンプレートであるsignin.blade.phpを見ていきましょう。

@section('content')という記述がありますね。

この部分が親テンプレート(app.blade.php)の@yield('content')になるわけです!

また、@extends('layouts.app')は親テンプレートであるapp.blade.phpを継承しています。

.(ドット)記法は先ほども登場しました。

layouts.appはviews/layoutsディレクトリ配下のapp.blade.phpを示しています。

他にも以下のコード部分はバリデーションのエラーメッセージを表示する部分になります。

@if (count($errors) > 0)
  <div class="errors">
    <ul>
    @foreach ($errors->all() as $error)
      <li>{{$error}}</li>
    @endforeach
    </ul>
  </div>
@endif

バリデーションとは入力チェックのことです。

例えば、パスワードの入力は6文字以上と決めた場合、それ未満の入力であればエラーメッセージを表示する必要がありますね。


それでは、一度dockerを立ち上げてlocalhostにアクセスしてみます。

[mac]$ docker-compose up -d

http://localhost:8000/ にアクセスしてください。

以下のような画面が表示されていればOKです!

スクリーンショット 2020-03-09 6.53.24

ある程度レイアウトが整っているのは、bootstrapというCSSのフレームワークをLaravelではデフォルトで使用しているからです。

例えば、ログインボタンに注目してみると、以下のようになっています。

<button type="submit" class="btn btn-primary">ログイン</button>

classに btn btn-primary をあてるだけで、bootstrapによってCSSが適用され、自前でCSSを書かなくてもいい感じのボタンを作ってくれています。


基本的な流れとしては、以下の3ステップです。

1. ルーティングを決める
2. Controller内のアクションで処理を記述
3. Viewを作成して、ブラウザ表示

ここで、変更したファイルをコミットしておきましょう!

[mac]$ pwd
/Users/*********/laravel-docker-workspace/laravel-app

[mac]$ git status
[mac]$ git add -A
[mac]$ git commit -m 'ログインフォーム表示'
[mac]$ git push

これで、コミット完了です。

コミットしておくことで、いつでもこの時点に戻ることができます。

今後、コミットするアナウンスは行いません。

こまめにコミットするようにしてください。

コミットした時点に戻る方法をググればいくらでも出てきます。

もし、必要になった場合は、自力で調べてみてください。


2. ログイン機能の実装

次は、ログイン処理を書いていきます。

ログイン処理で行う操作は以下の通りです。

1. ログインフォームでメールアドレスとパスワードを入力し、ログインボタンをクリック。

2. クリックすると、メールアドレスとパスワードが正しいか確認し、正しければログイン成功、誤りならログインフォームでエラーメッセージを表示。


では、ルーティングから記述していきます。

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

Route::post('/user/login', 'UserController@login')->name('user.login');

ログインフォームを表示したときのルーティングはgetでしたが、今回はPOSTです。

なにか値を送信したいときはPOSTを検討します。

今回だとメールアドレスとパスワードをユーザに入力してもらい、

その入力値を受け取ってログイン処理を実装します。

つまり、メールアドレスとパスワードの値をサーバー側に送信するのでPOSTになります。

ルーティングを決めたら、resources/views/user/signin.blade.phpを修正します。

中略

<form action="{{route('user.login')}}" method="POST">
  @csrf
  <div class="form-group">
    <label for="email">E-Mail</label>
    <input type="text" id="email" name="email" value="{{old('email')}}" class="form-control">
  </div>
  <div class="form-group">
    <label for="password">Password</label>
    <input type="password" id="password" name="password" value="{{old('password')}}" class="form-control">
  </div>
  <button type="submit" class="btn btn-primary">ログイン</button>
</form>

中略

formタグのactionに先ほどweb.phpで定義したルーティングを設定します。

ルーティングでname()を使って名前をつけておくことで上記コードのように使用できます。

また、ログイン処理はPOSTなので、methodにPOSTと記述しています。

oldヘルパーを使用しているのは、バリデーションエラーがあった場合でも、入力値を入力フィールドに保持するためです。

これでログインボタンをクリックするとhttp://localhost:8000/user/loginにPOSTでアクセスすることになります。

CSRFと呼ばれる不正アクセス対策のため、@csrfはPOSTする時は必須になります。


次にUserController.phpに以下を追加してください。

中略

use Auth; # 追加
 
class UserController extends Controller
{

中略

 /**
  * ログイン処理アクション
  */
  public function login(Request $request)
  {
    $email    = $request->input('email');
    $password = $request->input('password');
    if (!Auth::attempt(['email' => $email, 'password' => $password])) {
      // 認証失敗
      return redirect('/')->with('error_message', 'I failed to login');
    }
    // 認証成功
    return redirect()->route('micropost.index');
  }

}

loginアクション引数に(Request $request)を指定しています。

これでフォームから送信されたリクエストを取得することができます。

Requestインスタンス$requestを用いて、メールアドレスとパスワードを取得しています。

これらの情報を用いて、Auth::attemptで判定し、認証に成功時と失敗時の処理を分けます。

Authはファサードと呼ばれるもので、使用するために、use宣言しています。

ファサードはAuthだけでなく、たくさんの機能を提供しています。


ここで開発に必須なデバックの方法について説明します。

デバックとは...
コンピュータプログラムや電気機器中のバグ・欠陥を発見および修正し、動作を仕様通りのものとするための作業である

その一つとして変数の中身を調べることがよくあります。

例えば、loginアクションでメールアドレスやパスワードが本当に取得できているかを調べるといったことです。

実際の開発ではこの変数の中身を調べる行為はとても重要なので、ぜひデバックすることを覚えてください。

では、やってみます!

UserController.phpのloginアクションにdd()を追加してください。

中略

public function login(Request $request)
{
  $email    = $request->input('email');
  $password = $request->input('password');
  dd($email, $password);

中略

追加したものはdd()の部分ですね。

このdd()で変数の中身を調べられ、処理も中断してくれます。

Dockerを起動してブラウザで操作してみましょう。

[mac]$ docker-compose up -d

http://localhost:8000にアクセスしてください。

以下画像のようにメールアドレスとパスワードを入力してログインボタンをクリックすると、値が取得できていることが確認できますね。

仮にメールアドレスを入力しているのに、dd()で確認すると空になっていることがあれば、​どこかが間違っていると気がつけるわけです。

スクリーンショット 2020-03-11 6.40.12

スクリーンショット 2020-03-11 6.40.22

これでデバックの説明は終了しますので、追加したdd()は削除してください。


さて、次はバリデーションを追加していきます。

現時点だと、メールアドレスもパスワードも空でもリクエストを受け付けているので、入力に制限を与えます。

今回は以下のようなバリデーションを追加していきます。

email
・入力必須
・メールアドレス形式

password
・入力必須
・6文字以上

コンテナ内に入り、artisanコマンドで、UserRequest.phpファイルを生成します。

[mac]$ pwd
/Users/*********/laravel-docker-workspace/laravel-app

[mac]$ docker-compose up -d

[mac]$ docker-compose exec app bash

[コンテナ]$ cd laravel-app

[コンテナ]$ php artisan make:request UserRequest
Request created successfully.

app/Http/Requests/UserRequest.phpに生成されたファイルを確認します。

デフォルトだと以下のようになっています。

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UserRequest extends FormRequest
{
   /**
    * Determine if the user is authorized to make this request.
    *
    * @return bool
    */
   public function authorize()
   {
       return false;
   }

   /**
    * Get the validation rules that apply to the request.
    *
    * @return array
    */
   public function rules()
   {
       return [
           //
       ];
   }
}

バリデーションの制限はrules()に記述していきます。

以下のように書き換えてください。

中略

class UserRequest extends FormRequest
{
   /**
    * Determine if the user is authorized to make this request.
    *
    * @return bool
    */
   public function authorize()
   {
       return true; # trueに変更
   }

   /**
    * Get the validation rules that apply to the request.
    *
    * @return array
    */
   public function rules()
   {
       $rules = [
         'email'     => 'required|email',
         'password'  => 'required|min:6',
       ];

       return $rules;
   }
}

まず、authorizeメソッドはデフォルトではfalseを返していましたが、trueに変更します。

このメソッドでは認証されているユーザーが、指定されたリソースを更新する権限を実際に持っているのかを確認します。

特別な処理を書かない場合は常にtrueにします。

rulesメソッドにバリデーションの制限を記述しています。

メールアドレスは入力必須で、メールアドレス形式である必要があります。

パスワードは入力必須で、最低6文字という制限を与えています。


次に、UserController.phpを以下のように修正します。

use App\Http\Requests\UserRequest; # 追加

class UserController extends Controller
{
  中略

  /**
   * ログイン処理アクション
   */
  public function login(UserRequest $request)
  {
    $email    = $request->input('email');
    $password = $request->input('password');
    if (!Auth::attempt(['email' => $email, 'password' => $password])) {
      // 認証失敗
      return redirect('/')->with('error_message', 'I failed to login');
    }
    // 認証成功
    return redirect()->route('micropost.index');
  }
}

loginアクションの引数を変更しました。

先ほど作成したUserRequestインスタンスを使用することでバリデーションをかけることができます。


エラーメッセージは赤文字で表示するため、public/css/custom.cssを作成します。

コンテナに入っている場合はexitコマンドで抜けてください。

cdコマンドでpublic/cssディレクトリまで移動してください。

[mac]$ pwd
/Users/*********/laravel-docker-workspace/laravel-app/public/css

[mac]$ touch custom.css

public/css/custom.cssに以下を記述します。

.errors {
  color: red;
}

次に、public/css/custom.cssを読み込みます。

resources/views/layouts/app.blade.phpを開き、以下を追加してください。

<head>

 中略

 <!-- Styles -->
 <link href="{{ asset('css/app.css') }}" rel="stylesheet">
 <link href="{{ asset('css/custom.css') }}" rel="stylesheet"> # 追加
</head>

これでバリデーションが働いているか確認してみましょう。​

以下のようにエラーメッセージが表示されればOKです!

custom.cssが読み込まれない場合は、ブラウザのキャッシュを削除してみましょう。

Chromeのキャッシュ削除方法について

スクリーンショット 2020-03-11 21.59.41


次に、認証成功時にリダイレクトするページを作成します。

リダイレクトとは...
ウェブページが、アクセスしたURLではなく別のURLに移動している場合に、移動先のURLに転送処理されること。

今回は認証に成功した場合、投稿一覧ページに遷移させます。

まずはルーティングからです。

routes/web.phpを開き、以下を追加してください。

Route::get('/micropost/index', 'MicropostController@index')->name('micropost.index');

これが投稿一覧のルーティングになります。

ルーティングを見てわかるように、投稿一覧ページはMicropostControllerに記述していきます。

では、コマンドでMicropostControllerを作成していきます。

[mac]$ pwd
/Users/*********/laravel-docker-workspace/laravel-app

[mac]$ docker-compose up -d

[mac]$ docker-compose exec app bash

[コンテナ]$ cd laravel-app

[コンテナ]$ php artisan make:controller MicropostController
Controller created successfully.

app/Http/Controllers/MicropostController.phpを開くとデフォルトだと以下のようになっています。

UserControllerのときとほぼ同じですね。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class MicropostController extends Controller
{
  //
}

これを以下のように編集してください。

中略

class MicropostController extends Controller
{
  
  /**
   * 投稿一覧表示アクション
   */
  public function index()
  {
    return view('micropost.index');
  }
}

index()アクションでは現状、viewを返すだけの処理にしています。

第3章で変更を加えていきます。


次に、投稿一覧を表示するviewファイルを作成していきます。

cdコマンドでviewsディレクトリに移動してください。

コンテナ内に入っている場合はexitコマンドで抜けてください。

[mac]$ pwd
/Users/*********/laravel-docker-workspace/laravel-app/resources/views

[mac]$ mkdir micropost

[mac]$ cd micropost

[mac]$ touch index.blade.php

resources/views/micropost/index.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">{{ __('投稿一覧') }}</div>
       <div class="card-body">
         @if (count($errors) > 0)
         <div class="errors">
           <ul>
             @foreach ($errors->all() as $error)
               <li>{{$error}}</li>
             @endforeach
           </ul>
         </div>
         @endif
       </div>
     </div>
   </div>
 </div>
</div>
@endsection

これでログイン機能の実装は終了です!

ですが、ログインできるユーザがDBに存在していませんね。


3. テーブルおよびモデルの作成

まずはusersテーブルを変更します。

既にusersテーブルは作成していますが、冒頭で説明したテーブル設計通りに変更します。

database/migrations/2014_10_12_000000_create_users_table.phpを以下のように編集してください。

中略

public function up()
{
  Schema::create('users', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('name');
    $table->string('email')->unique();
    $table->string('password');
    $table->boolean('admin_flg')->default(false);
    $table->timestamps();
   });
 }

中略

migrationsディレクトリ配下はマイグレーションファイルが格納されています。

マイグレーションとは...
データベースのテーブル作成や編集などを管理するもの。

上記のソースコードを説明します。

usersテーブルを作成する処理を記述しています。

内容は主キーであるid、string型のnameカラム、string型のemailカラム、string型のpasswordカラム、boolean型のadmin_flgカラムをusersテーブルに作ります。

bigIncrements
主キーでよく使われる型。
1,2のように数値が入ります。

string
文字列の型。
name、email、passwordは文字列で保存します。

boolean
真偽値の型。
false または trueで保存します。(0 または 1)

timestamps
created_atカラムとuodated_atカラムを自動で追加します。
それぞれ作成日時、更新日時が保存されます。

では、マイグレーションの変更を反映します。

[mac]$ pwd
/Users/*********/laravel-docker-workspace/laravel-app

[mac]$ docker-compose up -d

[mac]$ docker-compose exec app bash

[コンテナ]$ cd laravel-app

[コンテナ]$ php artisan migrate:refresh

エラーなくmigrateされればOKです!


micropostsテーブルも一緒に作ってしまいましょう!

今回はマイグレーションファイルから作成します。

コンテナの中に入っている場合は、以下コマンドを実行してください。

コンテナの外にいる場合は、上記のusersテーブル作成時と同様にコンテナの中に入ってlaravel-appディレクトリに移動してから実行してください。

[コンテナ]$ php artisan make:migration create_microposts_table --create=microposts

database/migrations配下にファイルが作成されますので、以下のように編集してください。

中略

public function up()
{
  Schema::create('microposts', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->unsignedBigInteger('user_id');
    $table->foreign('user_id')->references('id')->on('users');
    $table->text('content');
    $table->timestamps();
  });
}

中略

ここではuser_idカラムをusersテーブルの主キー(id)の外部キーとして設定しています。

user_idにはusersテーブルの主キーが入ります。

これでmicropostsテーブルにデータを保存する場合は、必ず親テーブルであるuserの情報が必要になり、だれの投稿データかわかるようにしています。

では、マイグレーションファイルを反映しましょう!

コンテナの中に入っている場合は、以下コマンドを実行してください。

コンテナの外にいる場合は、usersテーブル作成時と同様にコンテナの中に入ってlaravel-appディレクトリに移動してから実行してください。

[コンテナ]$ php artisan migrate

これで、micropostsテーブルが作成されました!


続いて、モデルを作成します。

マイグレーションでテーブルを作成したら、大抵モデルも作成すると考えてください。

こちらもartisanコマンドで作成するため、コンテナの中で実行します。

[mac]$ pwd
/Users/*********/laravel-docker-workspace/laravel-app

[mac]$ docker-compose up -d

[mac]$ docker-compose exec app bash

[コンテナ]$ cd laravel-app

[コンテナ]$ php artisan make:model Micropost

既に作成されているapp/User.phpを以下のように編集してください。

appディレクトリ直下のファイルにモデル情報を記述します。

中略

class User extends Authenticatable
{
   use Notifiable;

   /**
    * ユーザの投稿データを取得
    */
   public function microposts()
   {
     return $this->hasMany('App\Micropost');
   }

   /**
    * The attributes that are mass assignable.
    *
    * @var array
    */
   protected $fillable = [
       'name',
       'email', 
       'password',
       'admin_flg', 
   ];
}

micropostsメソッドではあるユーザの投稿データを全て取得します。

ユーザは投稿データを複数持つことができるので、micropostsと複数系のメソッド名にしています。

$fillableプロパティはホワイトリストになります。

ここではusersテーブルのカラム名を記述することで、定義したカラムのみデータの保存や更新が可能になります。

次にapp/Micropost.phpを以下のように編集してください。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Micropost extends Model
{
   /**
    * 投稿データを所有するユーザを取得
    */
   public function user()
   {
     return $this->belongsTo('App\User');
   }

   /**
    * The attributes that are mass assignable.
    *
    * @var array
    */
   protected $fillable = [
     'user_id',
     'content',
   ];
}

userメソッドでは投稿データからユーザを取得します。

ある投稿データをもつユーザは必ず1ユーザのため、メソッド名も単数形にしています。

これでUser側から投稿データを取得でき、Micropost側からもユーザを取得できるように記述しました。

つまり、これが1対多のリレーション関係になります。


4. テストデータの作成

認証機能のテストができるように、usersテーブルにレコードを保存していきます。

テストデータはシーダーを使って保存していきます。

まずはシーダーファイルを作成します。

シーダーファイルはコンテナ内でコマンドを実行して作成します。

[mac]$ pwd
/Users/*********/laravel-docker-workspace/laravel-app

[mac]$ docker-compose up -d

[mac]$ docker-compose exec app bash

[コンテナ]$ cd laravel-app

[コンテナ]$ php artisan make:seeder UsersTableSeeder

database/seeds/UsersTableSeeder.phpが作成されています。

デフォルトでは以下のようになっています。

<?php

use Illuminate\Database\Seeder;

class UsersTableSeeder extends Seeder
{
   /**
    * Run the database seeds.
    *
    * @return void
    */
   public function run()
   {
       //
   }
}

runメソッドの中にテストデータを登録する処理を記述します。

以下のように、編集してください。

中略
use App\User; # 追加

public function run()
{
  $users = [
    [
      'name'      => 'admin',
      'email'     => 'admin@example.com',
      'password'  => Hash::make('password'),
      'admin_flg' => true,
    ],
    [
      'name'      => 'test0001',
      'email'     => 'test0001@example.com',
      'password'  => Hash::make('password'),
      'admin_flg' => false,
    ],
  ];

  // 登録
  foreach ($users as $user) {
    User::create($user);
  }
}

中略

次にdatabase/seeds/DatabaseSeeder.phpを以下のように編集します。

中略

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

中略

これでシーダーの作成は終了したので、artisanコマンドでシーディングを実行しましょう!

[mac]$ pwd
/Users/*********/laravel-docker-workspace/laravel-app

[mac]$ docker-compose up -d

[mac]$ docker-compose exec app bash

[コンテナ]$ cd laravel-app

[コンテナ]$ php artisan db:seed

Seeding: UsersTableSeeder
Database seeding completed successfully.

php artisan db:seedコマンドでDatabaseSeederクラスのrunメソッドが実行され、テスト用データが生成されます。


さて、ようやく認証機能を試してみます。

先ほど作成したユーザデータでログインしてみます。​

以下画像のように投稿一覧のページに遷移できればOKです!

スクリーンショット 2020-03-13 21.59.27

スクリーンショット 2020-03-13 21.58.46


忘れないうちに、ヘッダーのホームリンクを作成しておきます。

resources/views/layouts/app.blade.phpの52行目付近を以下のように修正してください。

<li class="nav-item">
  <a class="nav-link" href="{{route('micropost.index')}}">{{ __('ホーム') }}</a>
</li>

ホームリンクをクリックすると投稿一覧ページに遷移するようにしています。


5. ログアウト機能の実装

続いてログアウト機能を実装していきましょう!

まずはルーティングからです。

routes/web.phpに以下を追加してください。

Route::post('/user/logout', 'UserController@logout')->name('user.logout');

​こちらがログアウトのパスになります。

/user/logout にPOSTでアクセスするとUserControllerのlogoutアクションに処理が走ります。


次に、resources/views/layouts/app.blade.php74行目あたりを以下のように修正します。

中略

<form id="logout-form" action="{{route('user.logout')}}" method="POST" style="display: none;">
  @csrf
</form>

中略

actionにルーティングで設定したログアウトパスの名前を記述しています。

また、POSTの場合は@csrfが必須でしたね!


次は、UserControllerにlogoutアクションを追加していきます。

UserController.phpのUserControllerクラス内に以下を追加してください。

 /**
  * ログアウト処理アクション
  */
 public function logout()
 {
   Auth::logout();
   return redirect()->route('user.signin');
 }

ログアウトして、ログインフォームに遷移する処理を記述しています。

これでログアウト機能は終了です!

ログイン状態から、ログアウトを試してみましょう!

以下のログアウトリンクからログインフォームに遷移できればOKです。

スクリーンショット 2020-03-13 22.31.49


実は重大な欠点があります。

http://localhost:8000/micropost/index にURLを直打ちすると認証していないにも関わらず、ページ遷移ができてしまいます。

これを防ぐ処理を記述していきます。

また、現在認証済みの状態なら投稿一覧ページにリダイレクトする処理も追加していきましょう。

ページの保護はルーティングで記述します。

今回は認証されていない場合は、ログインフォームでリダイレクトするようにしていきます。

routes/web.phpを以下のように編集してください。

Route::get('/', 'UserController@signin')->name('user.signin');
Route::post('/user/login', 'UserController@login')->name('user.login');

Route::group(['middleware' => 'auth'], function() {
 Route::get('/micropost/index', 'MicropostController@index')->name('micropost.index');
 Route::post('/user/logout', 'UserController@logout')->name('user.logout');
});

保護をかけたいルートにミドルウェアの auth で囲みます。

ミドルウェアとは...
コントローラーのアクションに処理が入る前や後に特定の処理を組み込むことができる仕組みです。

ミドルウェアについて

ミドルウェアのルートへの登録は app/Http/Kernel.php で登録されています。

確認すると、middlewareのauthは app/Http/Middleware/Authenticate.php であることがわかります。

protected $routeMiddleware = [
  'auth' => \App\Http\Middleware\Authenticate::class,

  中略
];

では、app/Http/Middleware/Authenticate.phpを以下のように編集してください。

中略

class Authenticate extends Middleware
{
   /**
    * Get the path the user should be redirected to when they are not authenticated.
    *
    * @param  \Illuminate\Http\Request  $request
    * @return string
    */
   protected function redirectTo($request)
   {
       if (! $request->expectsJson()) {
           return route('user.signin');
       }
   }
}

ここではリダイレクト先をログインフォームに設定しています。

これでページの保護が終わりましたので、試してみます。

ログインしていない状態でhttp://localhost:8000/micropost/indexにURL直打ちしてみましょう。

ログインフォームにリダイレクトされればOKです!


最後に、既に認証済みなら投稿一覧ページにリダイレクトする処理を記述していきます。

routes/web.phpを以下のように編集してください。

Route::group(['middleware' => 'guest'], function() {
 Route::get('/', 'UserController@signin')->name('user.signin');
 Route::post('/user/login', 'UserController@login')->name('user.login');
});

Route::group(['middleware' => 'auth'], function() {
 Route::get('/micropost/index', 'MicropostController@index')->name('micropost.index');
 Route::post('/user/logout', 'UserController@logout')->name('user.logout');
});

対象となるルートにミドルウェアの guest で囲みます。

authの時と同様、ミドルウェアのルートへの登録は app/Http/Kernel.php で登録されています。

protected $routeMiddleware = [
  中略
  'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
  中略     
];

確認すると、middlewareのguestはapp/Http/Middleware/RedirectIfAuthenticated.phpであることがわかります。

では、app/Http/Middleware/RedirectIfAuthenticated.phpを以下のように編集してください。

中略

class RedirectIfAuthenticated
{
   /**
    * Handle an incoming request.
    *
    * @param  \Illuminate\Http\Request  $request
    * @param  \Closure  $next
    * @param  string|null  $guard
    * @return mixed
    */
   public function handle($request, Closure $next, $guard = null)
   {
       if (Auth::guard($guard)->check()) {
           return redirect()->route('micropost.index');
       }

       return $next($request);
   }
}

Auth::guard($guard)->check()で認証済みかどうかチェックしています。

認証済み(ログイン状態)なら、投稿一覧へリダイレクトさせています。

これで終了です。

では、実際に試してみましょう!

テストデータを使ってログインしてください。

(test0001@example.com, password)

ログインしている状態で、http://localhost:8000/を直打ちしてみてください。

ログインフォームではなく、投稿一覧ページにリダイレクトされればOKです!


6. デプロイ

最後にgit commitしてデプロイしておきましょう。

[mac]$ pwd
/Users/*********/laravel-docker-workspace/laravel-app

[mac]$ git status
[mac]$ git add -A
[mac]$ git commit -m '第二章 Complete!'
[mac]$ git push
[mac]$ git push heroku master
[mac]$ heroku run php artisan migrate:refresh
[mac]$ heroku run php artisan migrate --seed
[mac]$ heroku open

この第二章ではテーブルを作成したりと、DBの構成を変更したので、heroku側にも反映する必要があります。

heroku run php artisan migrate:refreshでheroku側のテーブルをまっさらにして、再度構築してくれます。

つまり、micropostsテーブルやusersテーブルのカラム変更情報が反映されます。

その後、heroku run php artisan migrate --seedでusersテーブルにテストデータを登録しています。

本番環境のDBを変更するコマンドを実行するとyes or noの確認が出るので、yesでEnterを押しましょう!

heroku openで本番環境をブラウザ表示して、確認してみましょう!

スクリーンショット 2020-03-14 11.41.46

あれ、、cssが効いていないですね。。

検証モードで確認するとなんかいっぱいエラーが出てる。笑

このようにうまくいかないのは普通のことです。

エラーを読み解いていきましょう!

(問題なく表示されている方はOKです。)

エラーには...

'http://laravel-app-super.herokuapp.com/css/custom.css'. This request has been blocked; the content must be served over HTTPS.

と書いています。

つまり、httpになっているのか問題っぽいですね。

これをhttpsにできれば、解決できそうです!

ググった結果、こちらの記事が参考になりそうです!

Laravel5.7: Herokuにデプロイする

記述してある通りに、変更を加えます。

app/Providers/AppServiceProvider.phpのbootメソッドを以下のように編集してください。

中略

public function boot()
{
  // 本番環境(Heroku)でhttpsを強制する
  if (\App::environment('production')) {
    \URL::forceScheme('https');
  }
}

中略

これで、再度デプロイしてます。

[mac]$ pwd
/Users/*********/laravel-docker-workspace/laravel-app

[mac]$ git status
[mac]$ git add -A
[mac]$ git commit -m 'heroku https化'
[mac]$ git push
[mac]$ git push heroku master
[mac]$ heroku open

ブラウザ表示して、確認してみましょう!

スクリーンショット 2020-03-14 11.59.11

cssが効いていますね!

改善されない場合は、ブラウザのキャッシュを削除してみましょう。

本番環境でログイン、ログアウトが問題なく動作するか確認してみてください。

問題なければ、今度こそ第二章は終了になります!

第三章ではユーザ登録、編集、削除などのCRUDと呼ばれる機能の実装を進めていきます。

ありがとうございました!

第二章 まとめ
・usersテーブルの再作成
・micropostsテーブルの作成
・モデルの作成(1対多のリレーション)
・コントローラーの作成
・ログイン機能実装
・ログアウト機能実装

この記事が気に入ったらサポートをしてみませんか?