見出し画像

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

こんにちは!ゆうきです。(@RubyPHP2

この第3章が最後になります!

第2章の続きから始めていきますので、第1章と第2章がまだだよって方は第1章から覗いていただけると嬉しいです。

第2章ではテーブルを作成して、ログイン、ログアウト機能の実装まで進めましたね!

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

このCRUDの実装はWebアプリケーションの基礎になり、とても重要なのでぜひ習得していただきたいですね。

CRUDとは...
ほとんど全てのコンピュータソフトウェアが持つ永続性の4つの基本機能のイニシャルを並べた用語。 その4つとは、Create(生成)、Read(読み取り)、Update(更新)、Delete(削除)である。

では、さっそく始めていきましょう!


第3章  掲示板アプリの作成

目次
1. ユーザ登録機能の実装
2. ユーザ編集/更新 機能の実装
3. ユーザ一覧ページの作成
4. ユーザ削除機能の実装
5. 投稿データの表示
6. 投稿機能の実装
7. デプロイ

1. ユーザ登録機能の実装

まずはユーザ登録機能を実装していきます。

ルーティングから定義していきます。

CRUDのルーティングを一気に定義していきます。

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::resource('user', 'UserController', ['only' => ['create', 'store']]);
});

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

追加したのは、Route::resourceの部分ですね。

Route::resourceとすることで、CRUDを実装する上で必要なルーティングを自動で定義してくれます。

今回はログイン前、ログイン後で使うルーティングを分けているので、

onlyを指定することで必要なルーティングを定義しています。

ただ、これだけだと、だんなパスが設定されているかわからないですよね。

artisanコマンドで現在定義されているルーティングを確認できるので、確認してみましょう!

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 route:list

php artisan route:listを実行すると、以下のように現在定義されているルーティングを確認できます。

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

ユーザ登録ページはuser.create、ユーザ登録処理はuser.storeになります。

対応するアクションも既に決まっています。

php artisan route:listの実行結果を見ると、

ユーザ登録ページはUserControllerのcreateアクション、

ユーザ登録処理はUserControllerのstoreアクションに割り振られていますね。

では、UserController.phpにcreateアクションとstoreアクションを追加していきましょう!

中略

class UserController extends Controller
{
  中略

  /**
   * ユーザ登録ページ表示アクション
   */
  public function create()
  {
    return view('user.create');
  }

  /**
   * ユーザ登録処理アクション
   */
  public function store()
  {
    // ここに処理を書く
  }
}

createアクションはユーザ登録ページを表示するだけの処理です。

viewファイルはuserディレクトリに作成します。

storeアクションは後ほど処理を書いていきます。

まずはユーザ登録ページを表示させていきましょう!

では、viewファイルを作成していきます。

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

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

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

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

[mac]$ touch create.blade.php

作成したresources/views/user/create.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.store')}}" method="POST">
           @csrf
           <div class="form-group">
             <label for="name">Name</label>
             <input type="text" id="name" name="name" value="{{old('name')}}" class="form-control">
           </div>
           <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>
           <div class="form-group">
             <label for="password">Password Confirmation</label>
             <input type="password" id="password_confirmation" name="password_confirmation" value="{{old('password_confirmation')}}" class="form-control">
           </div>
           <button type="submit" class="btn btn-primary">新規登録</button>
         </form>
       </div>
     </div>
   </div>
 </div>
</div>
@endsection

ログインフォームを作成したときと作りは似ていますね。

formのactionにはユーザ登録処理のrouteを記述しています。(user.store)

php artisan route:listで確認した通り、ユーザ登録処理はPOSTなので、methodはPOSTを指定しています。

POST時は@csrfは必須でしたね。忘れないようにセットしておきます。

また、新規登録時は名前とパスワード確認を入力項目として置いておきます。

これで、新規登録ボタンをクリックすることで、route('user.store')に、

つまりユーザ登録処理が走るようになりました。


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

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

<li class="nav-item">
  <a class="nav-link" href="{{route('user.create')}}">{{ __('新規登録') }}</a>
</li>


では、ユーザ登録処理を書いていきます。

UserController.phpのstoreアクションを以下のように編集してください。

中略
use App\User; # 追加
use Hash; # 追加

class UserController extends Controller
{
  中略

  /**
   * ユーザ登録処理アクション
   */
  public function store(Request $request)
  {
    $user     = new User;
    $name     = $request->input('name');
    $email    = $request->input('email');
    $password = $request->input('password');
    $params   = [
      'name'      => $name,
      'email'     => $email,
      'password'  => Hash::make($password),
    ];
    if (!$user->fill($params)->save()) {
      return redirect()->route('user.create')->with('error_message', 'User registration failed');
    }
    if (!Auth::attempt(['email' => $email, 'password' => $password])) {
      return redirect()->route('user.signin')->with('error_message', 'I failed to login');
    }
    return redirect()->route('micropost.index');
  }
}

順に説明していきます。

new UserでUserクラスのインスタンスを生成しています。

クラスを設計図とすると、インスタンスは実体のようなものです。

ここではUserの情報を持つものをインスタンスと捉えてください。

$user->fill($params)->save()でusersテーブルにインサート処理をしています。

インサート処理とは、テーブルにレコードを保存する処理のことです。

!(エクスクラメーション)をつけることで、意味が逆転します。

つまり、ユーザの保存に失敗した時の処理をif文内に記述しています。

fill()を使うことで、app/User.phpの$fillableプロパティの値を確認してくれます。

$fillableプロパティは定義したカラムのみ、値が代入されるものでしたね。

例えば、$fillableプロパティのemailの部分を削除してユーザ登録を行うと、エラーが出るようになります。

これは、フォームからメールアドレスを入力しても、$fillableプロパティで定義されていないからです。

また、インサートに成功した場合、ログイン認証して、投稿一覧にリダイレクトさせています。

好みはあるので、絶対ではありませんが、処理を書くときはエラーになるパターンを先に記述することで読みやすいコードになります。

これで、ユーザ登録を実際に行ってみましょう!

適当にメールアドレスとパスワードを入力して、新規登録ボタンをクリックしてください。

⚠︎この時点ではバリデーションを実装していないため、正しい入力値を入力してください。

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

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

投稿一覧ページが表示されれば、usersテーブルにユーザが保存され、認証もOKということになります!

一般的に、Controllerのアクション内に長い処理をだらだらと書くのは良いとされていません。

今回はそう長くないのですが、試しにusersテーブルへのインサート処理はモデルに記述するようにしてみます。

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

中略

class User extends Authenticatable
{
   中略

   /**
    * ユーザ登録/更新
    */
   public function userSave($params)
   {
     $isRegist = $this->fill($params)->save();
     return $isRegist;
   }
}

ユーザ登録に成功した場合はtrue、失敗した場合はfalseを返す単純なメソッドです。

これをUserControllerのstoreアクションで使うと以下のようになります。

 /**
  * ユーザ登録処理アクション
  */
 public function store(Request $request)
 {
   中略

   if (!$user->userSave($params)) {
     return redirect()->route('user.create')->with('error_message', 'User registration failed');
   }
   
   中略
 }

Userモデルに記述したメソッドはUserインスタンスを通して使用できます。

このように処理をモデルに分けたことで、Userインスタンスを生成すれば、どこからでもuserSaveメソッドを呼び出せるようになりました。

よく使いそうな処理はメソッドにまとめてどこからでも呼び出せるようにしておくと後々便利です。


次は、バリデーションを追加していきましょう!

app/Http/Requests/UserRequest.phpのrulesメソッドを以下のように編集してください。

中略

class UserRequest extends FormRequest
{
   /**
    * Get the validation rules that apply to the request.
    *
    * @return array
    */
   public function rules()
   {
       $rules = [
         'email'                 => 'required|email',
         'password'              => 'required|min:6',
         'password_confirmation' => 'filled', # 追加
       ];

       # 追加
       if ($this->password_confirmation) {
         $rules['name']      = 'required|max:20';
         $rules['email']     = 'required|email|unique:users';
         $rules['password']  = 'required|min:6|confirmed';
       }

       return $rules;
   }
}

nameは最大20文字で制限を与えています。

password_confirmationにfilledという制限を加えました。

filledはフィールドが存在する場合は空でないかどうかを判定し、存在していなければ許可するというものです。

つまり、ログイン時のようにパスワード確認フィールドが存在しない時は許可し、新規登録時のようにパスワード確認フィールドが存在している時は空でないかをチェックしてくれます。

パスワード確認はパスワードと一致している必要があるため、パスワード確認の入力値が存在する時のみパスワードのバリデーションにconfirmedを追加しています。

それでは、UserControllerのstoreアクションを以下のように修正して、動作を確認していきましょう!

 /**
  * ユーザ登録処理アクション
  */
 public function store(UserRequest $request)
 {
 中略

パスワード確認を空で新規登録ボタンをクリックしてみましょう。

エラーメッセージが表示されればOKです!

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

また、パスワードとパスワード確認で異なる入力値を入力して新規登録ボタンをクリックしてみましょう。

こちらもエラーメッセージが表示されればOKです!

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

これでユーザ新規登録については実装完了です!

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

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

[mac]$ git status
[mac]$ git add -A
[mac]$ git commit -m 'ユーザ新規登録 実装'
[mac]$ git push

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

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

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

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


2. ユーザ編集/更新 機能の実装

次は、ユーザ編集機能を実装していきます。

再度、ルーティングを確認してみましょう。

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

上記の画像において、ユーザ編集ページはuser.edit、ユーザ更新処理はuser.updateになります。

Actionを見ると、それぞれUserControllerのeditアクションとupdateアクションに割り振られていますね。


まずはユーザ編集ページへのリンクを作成します。

resources/views/layouts/app.blade.phpの68行目あたりを以下のように修正してください。

中略

<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
  <a class="dropdown-item" href="{{route('user.edit', ['user' => Auth::user()->id])}}">アカウント変更</a>

中略

アカウント変更をクリックでユーザ編集ページに遷移するようにリンクを記述しています。

上記画像のルーティングによると、ユーザ編集ページへのパスはuser/{user}/editとなっています。

これは実際のURLとしてはhttp://localhost:8000/user/2/editのように{user}部分にはログインしているユーザのidが入ります。

そのため、リンクを指定するときにログインユーザのidを渡す必要があります。

route()の第二引数に['user' => Auth::user()->id]を指定することでログインユーザのidを渡しています。


では、UserController.phpにアクションを追加していきましょう。

中略

class UserController extends Controller
{
  中略

  /**
   * ユーザ編集表示アクション
   */
  public function edit($id)
  {
    $user       = User::find($id);
    $viewParams = [
      'user' => $user,
    ];
    return view('user.edit', $viewParams);
  }

  /**
   * ユーザ更新アクション
   */
  public function update(UserRequest $request, $id)
  {
    // ここに処理を書く
  }
}

editアクションの引数の$idに注目してください。

例えば、http://localhost:8000/user/2/editでアクセスがあった場合、「2」を引数の$idで取得しています。

引数の$idを用いて、User::find($id)でログインしているユーザの情報を取得しています。

その後、viewヘルパーでviewファイルを指定しています。

ここではuserディレクトリ配下のedit.blade.phpを指定しています。

今までと異なる点として、viewヘルパーの第二引数に注目してください。

第二引数に連想配列を渡すことでbladeテンプレートで変数を扱えるようになります。


では、実際にbladeテンプレートで試してみます。

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

touchコマンドでedit.blade.phpを作成します。

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

[mac]$ touch edit.blade.php

作成したedit.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.update', ['user' => $user->id])}}" method="POST">
           @csrf
           @method('PATCH')
           <div class="form-group">
             <label for="name">Name</label>
             <input type="text" id="name" name="name" value="{{old('name', $user->name)}}" class="form-control">
           </div>
           <div class="form-group">
             <label for="email">E-Mail</label>
             <input type="text" id="email" name="email" value="{{old('email', $user->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>
           <div class="form-group">
             <label for="password">Password Confirmation</label>
             <input type="password" id="password_confirmation" name="password_confirmation" value="{{old('password_confirmation')}}" class="form-control">
           </div>
           <button type="submit" class="btn btn-primary">変更する</button>
         </form>
       </div>
     </div>
   </div>
 </div>
</div>
@endsection

ユーザ新規登録と構成は似ています。

上記コードを確認すると$userという変数を使用している箇所があります。

これはeditアクションのviewヘルパーの第二引数に連想配列で$userを渡しているため、bladeで使用できます。

$userには現在ログインしているユーザの情報が格納されているため、

「->」を用いてnameやemailといった情報にアクセスできます。

formのactionに注目してください。

前述でも説明している通り、php artisan route:listでルーティングを確認すると、更新処理はuser/{user}でPATCHリクエストになっています。

{user}に値を渡す必要があるので、route()の第二引数にログインユーザのidを渡しています。

また、GET、POST以外の場合は、明示的に指定する必要があります。

ここでは@method('PATCH')がそれにあたります。

名前とメールアドレスについてはログインユーザの名前とメールアドレスを初期値としてセットしています。


では、ユーザ編集ページが表示されるか確認してみましょう。

ログイン後、アカウント変更から遷移します。

スクリーンショット 2020-03-15 15.09.49

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

名前とメールアドレスの入力項目に現在の名前とメールアドレスが入っているか確認してください。

確認できれば一旦OKです。

しかし、このままでは重大な不具合があるので、これから修正していきます。

重大な不具合とは、URLを直打ちすることで別ユーザの編集ページに遷移できてしまいます。

例えば、id1のユーザでログインしていれば、http://localhost:8000/user/1/edit が編集ページのURLになっているはずです。

これをhttp://localhost:8000/user/2/editに変更してみてください。

遷移できてしまいましたね。。修正していきましょう!

このような認可に関することはGateまたはPolicyを用いて制限していきます。

今回はPolicyを用います。

気になる方は調べてみてください。

Laravel Gate(ゲート)、Policy(ポリシー)を完全理解

Policyを使うには、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:policy UserPolicy --model=User

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

メソッドがいくつか自動生成されています。

viewメソッドとupdateメソッド以外は削除してください。

中略

use Auth; # 追加

class UserPolicy
{
   use HandlesAuthorization;

   /**
    * Determine whether the user can view the model.
    *
    * @param  \App\User  $user
    * @param  \App\User  $model
    * @return mixed
    */
   public function view(User $model, User $user)
   {
       return $model->id == $user->id;
   }

   /**
    * Determine whether the user can update the model.
    *
    * @param  \App\User  $user
    * @param  \App\User  $model
    * @return mixed
    */
   public function update(User $model, User $user)
   {
     return $model->id == $user->id;
   }
}

viewメソッドは編集ページに、updateメソッドはこれから実装する更新処理で使用します。

どちらもモデルオブジェクトのidとログインユーザのidが一致するか確認しています。

UserPolicyを有効にするために、app/Providers/AuthServiceProvider.phpを以下のように修正してください。

中略

class AuthServiceProvider extends ServiceProvider
{
   /**
    * The policy mappings for the application.
    *
    * @var array
    */
   protected $policies = [
       'App\User' => 'App\Policies\UserPolicy',
   ];

   中略
}

これでUserPolicyが有効になります。

あとはコントローラーのアクション内を修正します。

UserController.phpのeditアクションを以下のように修正してください。

 /**
  * ユーザ編集表示アクション
  */
 public function edit($id)
 {
   $user       = User::find($id);
   $viewParams = [
     'user' => $user,
   ];
   $this->authorize('view', $user); # 追加
   return view('user.edit', $viewParams);
 }

authorizeメソッドを記述することでPolicyでアクセス制限を行うことができます。

authorizeの最初の引数である’view’がUserPolicy.phpファイル内に記述したviewメソッドに対応します。2番目の引数には、Userモデルの変数$userを指定しています。

これで、別ユーザの編集ページに遷移することはできないようになりました!

スクリーンショット 2020-03-25 22.16.18



次に、更新処理に入ります。

UserController.phpのupdateアクションを以下のように編集してください。

 /**
  * ユーザ更新アクション
  */
 public function update(UserRequest $request, $id)
 {
   $user     = User::find($id);
   $name     = $request->input('name');
   $email    = $request->input('email');
   $password = $request->input('password');
   $params   = [
     'name'      => $name,
     'email'     => $email,
     'password'  => Hash::make($password),
   ];
   $this->authorize('update', $user);
   if (!$user->userSave($params)) {
     // 更新失敗
     return redirect()
            ->route('user.edit', ['user' => $user->id])
            ->with('error_message', 'Update user failed');
   }
   return redirect()->route('micropost.index')->with('flash_message', 'update success!!');
 }

新規登録処理とすごく似てますね。

異なる点は、updateアクションの引数に$idが渡されている点ですね。

update処理はPATCHリクエストでuser/{user}でアクセスされます。

実際のURLでいうと、例えばhttp://localhost:8000/user/2という感じです。

この「2」はログインユーザのidが入り、updateアクションの引数である$idに渡されています。

editアクションと同じですが、User::find($id)で上記の例でいうと、id2のユーザの情報をusersテーブルから取得しています。

authorizeメソッドで編集ページの時と同様に、別ユーザの更新処理は不可にしています。

$user->userSave($params)はユーザ新規登録時と全く同じです。

コードは同じですが、今回は更新を意味します。

なぜなら、今回は$userにユーザ情報が格納されているからです。(例でいうとid2のユーザ情報)

更新に成功した場合は、投稿一覧にリダイレクトさせています。

バリデーションについては、変更を加えなくてもそのまま使えそうです。


では、実際に動作させてみましょう。

名前、メールアドレス、パスワードを適当な値で変更してください。

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

スクリーンショット 2020-03-15 16.16.47

上記のように、投稿一覧にリダイレクトされればOKです!

さらに、念入りに確認するなら、一度ログアウトして、変更したメールアドレスとパスワードでログインしてみてください。

問題なくログインできれば、OKです!

これで、ユーザ編集、更新の実装は終了です!


3. ユーザ一覧ページの作成

次は、ユーザ一覧を表示していきます。

php artisan route:listで再度ルーティングを確認してみましょう。

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

ユーザ一覧ページはuser.indexになります。

アクションはUserControllerのindexアクションになっていますね。

それでは、UserController.phpにindexアクションを追加して以下のように記述してください。

中略

class UserController extends Controller
{
  中略

  /**
   * ユーザ一覧表示アクション
   */
  public function index()
  {
    $users = User::all();
    $viewParams = [
      'users' => $users,
    ];
    return view('user.index', $viewParams);
  }
}

User::all()でusersテーブルのレコードを全て取得しています。

つまり、全ユーザのデータを$usersに格納しています。

viewヘルパーの第二引数に連想配列を渡しています。

これでuserディレクトリ配下のindex.blade.phpで$usersが使用できます。


次に、resources/views/layouts/app.blade.php57行目あたりを修正してリンクを作成します。

中略

<li class="nav-item">
  <a class="nav-link" href="{{route('user.index')}}">{{ __('ユーザ一覧') }}</a>
</li>

中略

それでは、userディレクトリ配下にindex.blade.phpを作成します。

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

touchコマンドでファイルを作成します。

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

[mac]$ touch index.blade.php

作成した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
         <table class="table">
           <thead>
             <tr>
               <th>id</th>
               <th>ユーザ名</th>
               <th>削除</th>
             </tr>
           </thead>
           <tbody>
             @foreach ($users as $user)
               <tr>
                 <td>{{$user->id}}</td>
                 <td>{{$user->name}}</td>
                 <td>
                   @if (Auth::user()->admin_flg)
                     @if (Auth::user()->id != $user->id)
                       <a href="" class="btn btn-danger">削除</a>
                     @endif
                   @endif
                 </td>
               </tr>
             @endforeach
           </tbody>
         </table>
       </div>
     </div>
   </div>
 </div>
</div>
@endsection

以下のコードに注目してください。

@foreach ($users as $user)
  <tr>
    <td>{{$user->id}}</td>
    <td>{{$user->name}}</td>
    <td>
      @if (Auth::user()->admin_flg)
        @if (Auth::user()->id != $user->id)
          <a href="" class="btn btn-danger">削除</a>
        @endif
      @endif
    </td>
  </tr>
@endforeach

@foreachを使ってループ処理を使っています。

今、$usersには複数のユーザ情報が格納されています。

気になる方はアクション内でデバックしてみましょう。

デバックは第2章で説明しています。

格納されているユーザの数だけループして1ユーザごとに表示しています。

削除ボタンは管理者のみ表示するよう処理を入れています。

また、ログインユーザのidとループしているユーザのidを比べることで、管理者は自身を削除することができないようにしています。


では、ブラウザでユーザ一覧が表示されているか確認していきます。

ヘッダーのユーザ一覧から遷移できます。

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

スクリーンショット 2020-03-18 21.27.15

管理者と管理者以外のユーザで表示が変わっていることが確認できますね。

第2章で作成したテストデータ通りなら、以下アカウントが管理者になります。

(メールアドレス: admin@example.com パスワード: password)

ユーザ一覧の表示はこれで終了です!


4. ユーザ削除機能の実装

次は、ユーザの削除機能を実装していきます。

まずはいつも通り、ルーティングを確認しましょう!

コンテナ内でphp artisan route:listで確認できましたね。

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

ユーザの削除処理はuser.destroyになります。

UserControllerのdestroyアクションに割り振られていますね。

MethodはDELETEでuser/{user}にアクセスすると削除処理を行うようにする必要がありますね。

UserController.phpにdestroyアクションを追加して、以下のように編集してください。

また、管理者であるかを判定するprivateメソッドも追加してください。

class UserController extends Controller
{
  中略

  /**
   * ユーザ削除処理アクション
   */
  public function destroy($id)
  {
    $this->adminCheck();
    $user = User::find($id);
    if (!$user->delete()) {
      return redirect()->route('user.index')->with('error_message', 'Delete user failed');
    }
    return redirect()->route('user.index')->with('flash_message', 'delete success!!');
  }


  // private
 
  // ログインユーザが管理者であるかチェック
  private function adminCheck()
  {
    $adminFlg = Auth::user()->admin_flg;
    if (!$adminFlg) {
      abort(404);
    }
    return true;
  }
}

削除機能は管理者のみが使えるようにするため、ログインユーザが管理者であるか確認する必要があります。

ログインユーザが管理者かどうかはadminCheckメソッドで行なっています。

private functionとすることで、このクラス内でのみadminCheckメソッドは使用可能になります。

このクラスとはここではUserControllerクラスですね。

クラス内のメソッドは「$this->メソッド名」で呼び出すことができます。

このようなチェックはアクションに処理が入る前に確認しておくのも良いですね。

その場合、ミドルウェアを用いるのも一つの手です。

気になる方は一度調べてみてください。

また、前述でも使用したPolicyを使って制限することもできそうです。

今回はクラス内でメソッドを呼び出すという方法でやってみました!

ユーザ削除処理アクションでは、adminCheckメソッドで管理者か確認した後、ユーザの削除を行なっています。

ユーザの削除に成功した場合、ユーザ一覧にリダイレクトしてメッセージを表示します。


次に、resources/views/user/index.blade.php27行目付近から以下のように編集してください。

中略

<tbody>
  @foreach ($users as $user)
    <tr>
      <td>{{$user->id}}</td>
      <td>{{$user->name}}</td>
      <td>
        @if (Auth::user()->admin_flg)
          @if (Auth::user()->id != $user->id)
            <form name="delete_form" action="{{route('user.destroy', ['user' => $user->id])}}" method="POST">
              @csrf
              @method('delete')
              <input type="submit" class="btn btn-danger" value="削除" onclick='return confirm("本当に削除してよろしいですか?");'>
            </form>
          @endif
        @endif
      </td>
    </tr>
  @endforeach
</tbody>

中略

ルーティングを確認したところ、MethodはDELETEでしたね。

GETとPOST以外は@methodを記述する必要があります。

user/{user}でアクセスするので、ユーザのidを指定する必要があり、route()の第二引数にユーザのidを渡しています。

また、onclickを使うことで、削除ボタンをクリックすると確認モーダル が表示されるようにしています。


では、実際にユーザ削除が動作するか確認していきましょう!

スクリーンショット 2020-03-19 6.48.03

スクリーンショット 2020-03-19 6.48.22

選択したユーザが削除され、ユーザ一覧にリダイレクトしていればOKです!

と言いたいところですが、実は大きな欠点があります。。

この時点ではユーザは投稿データを所有していないので、問題ないのですが、ユーザが投稿データを持っていた場合、エラーになります。

usersテーブルが親テーブル、micropostsテーブルが子テーブルの1対多のリレーション関係であることは以前説明しました。

ユーザの削除とは、親テーブルのデータを削除することになるので、子テーブルのデータも一緒に削除する必要があるのです。

ユーザが削除されているのに、そのユーザが以前投稿した投稿データだけ残っていたら少しこわいですよね笑

つまり、ユーザを削除するとき、そのユーザが持つ投稿データも一緒に削除する必要があるということです。

では、app/User.phpにbootメソッドを追加してください。

中略

class User extends Authenticatable
{
   use Notifiable;

   protected static function boot() 
   {
       parent::boot();
       static::deleting(function($model) {
           foreach ($model->microposts()->get() as $child) {
               $child->delete();
           }
       });
   }

中略

これで、ユーザを削除するとき、ユーザが持つ投稿データも一緒に削除してくれます。

Laravelでは削除するとき、deletingというイベントを発火します。

イベントが発火したら、$model->microposts()->get()で削除対象のユーザが持つ投稿データを取得し、ループをまわして削除しています。

これでユーザの削除機能は実装完了です!


5. 投稿データの表示

次は、投稿データを表示させます。

投稿機能を未実装なので、まだ表示するデータが一つもないですね。

このような場合は、ユーザの時と同様にシーダーを使ってDBにデータを保存しておきます。

では、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:seeder MicropostsTableSeeder

作成したdatabase/seeds/MicropostsTableSeeder.phpを以下のように編集してください。

use App\Micropost; # 追加

class MicropostsTableSeeder extends Seeder
{
   /**
    * Run the database seeds.
    *
    * @return void
    */
   public function run()
   {
       $microposts = [
         [
           'user_id'   => 1,
           'content'   => 'これはテスト投稿1',
         ],
         [
           'user_id'   => 1,
           'content'   => 'これはテスト投稿2',
         ],
         [
           'user_id'   => 2,
           'content'   => 'これはテスト投稿3',
         ],
       ];

       foreach ($microposts as $micropost) {
         Micropost::create($micropost);
       }
   }
}

idが1と2のユーザに投稿データを持たせています。

続いて、database/seeds/DatabaseSeeder.phpを以下のように編集してください。

中略

class DatabaseSeeder extends Seeder
{
   /**
    * Seed the application's database.
    *
    * @return void
    */
   public function run()
   {
       $this->call([
         UsersTableSeeder::class,
         MicropostsTableSeeder::class,
       ]);
   }
}

これで準備が整いました。

コンテナ内で以下のコマンドを実行します。

[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 --seed

これで、Micropostsテーブルにもデータが保存されました。

では、投稿データを全て表示する処理を書いていきましょう!

投稿一覧ページ自体は既に表示できているので、MicropostControllerを修正していきます。

MicropostController.phpのindexアクションを以下のように編集してください。

use App\Micropost; # 追加

中略

 /**
  * 投稿一覧表示アクション
  */
 public function index()
 {
   $microposts = Micropost::getAll();
   $viewParams = [
     'microposts' => $microposts,
   ];
   return view('micropost.index', $viewParams);
 }

indexアクションでは投稿データを全て取得し、bladeテンプレートへ渡しています。

ここでいうbladeテンプレートとは、micropostディレクトリ配下のindex.blade.phpになりますね。

さて、以下のコードに注目してください。

$microposts = Micropost::getAll();

このような書き方は初めてですね。

これはMicropostクラスのクラスメソッドを呼び出しています。

クラスメソッドは「クラス名::メソッド名」で呼び出せます。


では、Micropostクラス、すなわちMicropostモデルにクラスメソッドを定義していきましょう!

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

use App\Micropost; # 追加

class Micropost extends Model
{
   中略

   /**
    * 投稿データを降順で全て取得
    */
   public static function getAll()
   {
     $microposts = Micropost::all()->sortByDesc('id');
     return $microposts;
   }
}

クラスメソッドはstaticをつけます。

このクラスメソッドではMicropostのデータをidで降順に並び替えて全て取得しています。


次に、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
       <table class="table">
         <thead>
           <tr>
             <th>ユーザ名</th>
             <th>内容</th>
             <th>投稿日時</th>
           </tr>
         </thead>
         <tbody>
           @foreach ($microposts as $micropost)
             <tr>
               <td>{{$micropost->user->name}}</td>
               <td>{{$micropost->content}}</td>
               <td>{{$micropost->created_at}}</td>
             </tr>
           @endforeach
         </tbody>
       </table>
     </div>
    </div>
  </div>
</div>
</div>
@endsection

$micropostsには複数の投稿データが格納されています。

これを、一つ一つ取り出すループ処理を行なっています。

投稿内容と投稿日時はmicropostsテーブルから値を取得できますが、ユーザ名はusersテーブルから取得する必要がありますよね。

第2章でも説明しましたが、usersテーブルとmicropostsテーブルは1対多の関係です。

つまり、投稿データからユーザを特定することが可能です。

以下のコードに注目してください。

{{$micropost->user->name}}

これは、まさに投稿データからユーザを特定し、そのユーザの名前を取得しています。

なぜ、こんなことができるか、思い出してください。

app/Micropost.phpで以下のコードを記述していますね。

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

このuserを呼び出すことで投稿データからユーザを特定することが可能になっています。


では、ブラウザで投稿データが表示できているか確認しましょう!

スクリーンショット 2020-03-19 22.03.30

このように全て表示できていれば、OKです!


6. 投稿機能の実装

では、最後に投稿機能を実装していきます。

投稿機能は以下のようなステップで実装します。

1. 投稿フォームを用意する
2. 投稿内容を入力して、投稿ボタンをクリック
3. バリデーションチェックをして、問題なければ投稿一覧にリダイレクト

まずは、投稿フォームを作成していきましょう!

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

Route::group(['middleware' => 'auth'], function() {
  中略

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

投稿フォームを表示するルーティングと投稿内容を登録する処理を行うルーティングを追加しています。

投稿機能はログイン後の画面で行うので、authミドルウェア内に記述します。


次に、MicropostControllerにinputアクションを追加していきます。

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

class MicropostController extends Controller
{
  中略

  /**
   * 投稿フォーム表示アクション
   */
  public function input()
  {
    return view('micropost.input');
  }
}

micropostディレクトリ配下のinput.blade.phpを返す単純な処理ですね。


viewファイルを作成するまえに、ヘッダーにリンクを作成しておきましょう。

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

<li class="nav-item">
  <a class="nav-link" href="{{route('micropost.input')}}">{{ __('投稿') }}</a>
</li>

これでヘッダー部分にリンクを貼ることができました。


では、input.blade.phpを作成していきます。

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

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

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

[mac]$ touch input.blade.php

作成したinput.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('micropost.post')}}" method="POST">
           @csrf
           <div class="form-group">
             <label for="content">投稿内容</label>
             <textarea name="content" class="form-control">{{old('content')}}</textarea>
           </div>
           <input type="submit" class="btn btn-primary" value="投稿する">
         </form>
       </div>
     </div>
   </div>
 </div>
</div>
@endsection

actionには投稿内容を登録するルーティングを指定しています。

今回は投稿内容を入力するので、inputタグではなく、textareaを使っています。


では、ブラウザでページが表示されるか確認してみます。

ヘッダーの「投稿」から遷移可能です。

スクリーンショット 2020-03-20 11.11.33

このようにページが表示されればOKです!


続いて、投稿処理を書いていきます。

MicropostController.phpにpostアクションを追加していきましょう!

use Auth; # 追加

class MicropostController extends Controller
{
 
  中略

  /**
   * 投稿処理アクション
   */
  public function post(Request $request)
  {
    $content    = $request->input('content');
    $micropost  = new Micropost;
    $params = [
      'user_id' => Auth::id(),
      'content' => $content,
    ];
    if (!$micropost->micropostSave($params)) {
      // 登録失敗
      return redirect()->route('micropost.input')->with('error_message', 'Regist micropost failed');
    }
    return redirect()->route('micropost.index')->with('flash_message', 'regist success!!');
  }
}

以下のコードに注目してください。

$micropost->micropostSave($params)

ユーザの新規登録などでも同じような実装をやりましたね。

Micropostモデルのメソッドを呼び出して、投稿内容をDBに登録しています。


では、Micropostモデルに投稿データを登録するメソッドを用意しましょう。

app/Micropost.phpにメソッドを追加してください。

class Micropost extends Model
{
  中略

   /**
    * 投稿データを登録する
    */
   public function micropostSave($params)
   {
     $isSave = $this->fill($params)->save();
     return $isSave;
   }
}

micropostSaveメソッドは引数に$paramsを渡して、呼び出します。

このメソッドでは受け取った$paramsを使って、DBに登録、戻り値である真偽値を返します。

以下のようにアクションでmicropostSaveを用いる際、以下のように引数を渡して呼び出していますね。

$micropost->micropostSave($params)


次に、バリデーションをかけていきます。

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:request MicropostRequest

作成したapp/Http/Requests/MicropostRequest.phpを以下のように編集してください。

中略

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

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

       return $rules;
   }
}

ここでは、投稿内容は入力必須、最大200文字と制限を与えることにしています。


次に、MicropostController.phpを以下のように修正してバリデーションを反映させます。

use App\Http\Requests\MicropostRequest; # 追加
 
 /**
  * 投稿処理アクション
  */
 public function post(MicropostRequest $request)
 {

投稿処理の実装は以上になります。

では、実際にブラウザで確認していきましょう!​

以下のように改行を含んで、投稿内容を入力してください。

スクリーンショット 2020-03-22 19.33.14

スクリーンショット 2020-03-22 19.33.34

このように投稿されていればOKと言いたいところですが、改行が反映されていませんね。

改行を反映させるにはresources/views/micropost/index.blade.phpを以下のように修正します。

<tbody>
  @foreach ($microposts as $micropost)
    <tr>
      <td>{{$micropost->user->name}}</td>
      <td>{!! nl2br(e($micropost->content)) !!}</td>
      <td>{{$micropost->created_at}}</td>
    </tr>
  @endforeach
</tbody>

e() でエスケープ、nl2br() で改行を<br>に置き換えています。

{!! !!}で、<br>だけエスケープをせずに表示することで改行が反映されます。

では、確認しましょう!

スクリーンショット 2020-03-22 19.42.49

改行が反映されていればOKです!

これでCRUDをメインとした全ての機能を実装しました!

7. デプロイ

最後に、コミットしてデプロイまでしておきましょう!

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

[mac]$ git status
[mac]$ git add -A
[mac]$ git commit -m '第3章 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

本番環境で一通り動作を確認してください。

特に問題なければ、OKです!!

第3章まとめ
・ユーザ新規登録機能実装
・ユーザ編集 / 更新機能実装
・ユーザ一覧表示
・ユーザ削除機能実装
・投稿一覧表示
・投稿機能実装

第1章〜第3章を通して環境構築から掲示板アプリを作成、デプロイまでの全てを終了しました。

ここまで、お付き合いいただいた方、本当にありがとうございます。

本教材の内容はいかがだったでしょうか。

おそらく全てを理解できたという方はほとんどいらっしゃらないでしょう。

Webアプリの開発がどのようなものか少しでも体感できたなら、この教材の意味があったというものです。

このアプリケーションを自分なりに改修して、良質なポートフォリオにするものありだと思います!

よろしければTwitterで感想を聞かせてください。(@RubyPHP2

最後に手をつけやすいおすすめの機能を紹介してこの教材は終わりにしますね。

・投稿内容のいいね機能
・返信機能
・自分の投稿内容については 編集/ 更新 を可能にする
・投稿の削除機能
・ページネーション機能


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

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