Laravel QUEST #4【ログイン機能・Movie登録機能の実装】

前回は、ユーザ登録機能について学習しました。

今回は、ログイン機能の実装、そして、Movie登録機能の実装にも入りますので
前回までの復習も兼ねて、Model/Router/Controller/View全ての構築を行って頂くことになります。

動画の機能に関しては、登録・削除・ユーザごとの個別動画表示など、今回で基本的な機能はほぼ実装できるようになって頂きたいと思います。

少し難しい部分もあるかも知れませんが、できるようになれば間違いなく「楽しい」と思って頂ける内容だと思いますので、一緒にしっかり取り組んでいきましょう。

●今回のキーポイント

  • ログイン機能
  • ファサードについて
  • 一対多の関係
  • 基本ルーティングの省略形
  • ユーザ一覧
  • ページネーション
  • 動画登録フォーム
  • 動画登録
  • 動画削除
  • ユーザごとの個別ページ

前回の内容はこちら

https://prog.quest-academia.com/laravel-quest-3/

ログイン機能の実装

では、ログイン機能を作っていきましょう。

実は、ログイン機能も例によってLaravelがすでに作ってくれています。

Routerの設定

Routerでログイン認証を実装します。
下記のファイルに、3種類のルーティングを記述しましょう。

laravel-quest > routes > web.php

web.php(一部抜粋)

Route::get('login', 'Auth\LoginController@showLoginForm')->name('login');
Route::post('login', 'Auth\LoginController@login')->name('login.post');
Route::get('logout', 'Auth\LoginController@logout')->name('logout');

上から、それぞれ

  • ログインフォームを表示する
  • ログインフォームに入力された内容(メールアドレス・パスワードなどを)を送信する
  • ログアウトを行う

という意味です。

上記記載の通り、LoginControllerで認証の処理を行います。

LoginController

概要

LoginControllerを開いてみましょう。

laravel-quest > app > Http > Controllers > Auth > LoginController 

LoginController.php

<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
class LoginController extends Controller
{
    //(中略)
    use AuthenticatesUsers;
    /**
     * Where to redirect users after login.
     *
     * @var string
     */
    protected $redirectTo = RouteServiceProvider::HOME;
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest')->except('logout');
    }
}

ミドルウェアは前回のテキストで解説した通りですが、
上記コードは 「logoutアクションを除いて、ユーザはguestでないといけない」という条件でないと
ログイン認証ができないという意味です。 

つまり「まだログイン認証されていないユーザだけが、ログインできる」ということです。
この条件に合致しない場合、ログイン認証しようとしても別ページに飛ばされます。

また、ここで、use AuthenticatesUsers; と出てきました。
これも前回テキストで見たトレイトを表すのですが、ログイン認証のルーティングで記述した
showLoginFormなどのアクションは、AuthenticatesUsersトレイトで規定されています。

遷移先ページの設定

ログインページのフォームでログイン認証が成功した際に、リダイレクトされるページを以下の設定から変更することができます。

laravel-quest > app > Http > Controllers > Auth > LoginController.php(抜粋)

LoginController.php(一部抜粋)

    /**
     * Where to redirect users after login.
     *
     * @var string
     */
    protected $redirectTo = RouteServiceProvider::HOME;

リダイレクト先を、下記のように書き換えてください。

     /**
     * Where to redirect users after login.
     *
     * @var string
     */
    protected $redirectTo = '/';

これで、ログイン後は、トップページにリダイレクトされます。

View

ログインページ

続いて、ログインページの作成です。
register.blade.phpと同じく、viewsフォルダのauthフォルダ内に、下記の名前の新規ファイルlogin.blade.phpを作成しましょう。

laravel-quest > resources > views > auth > login.blade.php

login.blade.php

@extends('layouts.app')
@section('content')
    <div class="center jumbotron bg-warning">
        <div class="text-center text-white">
            <h1>YouTubeまとめ × SNS</h1>
        </div>
    </div>
    <div class="text-center">
        <h3 class="login_title text-left d-inline-block mt-5">ログイン</h3>
    </div>
    <div class="row mt-5 mb-5">
        <div class="col-sm-6 offset-sm-3">
            {!! Form::open(['route' => 'login.post']) !!}
                <div class="form-group">
                    {!! Form::label('email', 'メールアドレス') !!}
                    {!! Form::email('email', old('email'), ['class' => 'form-control']) !!}
                </div>
                <div class="form-group">
                    {!! Form::label('password', 'パスワード') !!}
                    {!! Form::password('password', ['class' => 'form-control']) !!}
                </div>
                {!! Form::submit('ログイン', ['class' => 'btn btn btn-primary mt-2']) !!}
            {!! Form::close() !!}
            <p class="mt-3">{!! link_to_route('signup', '新規ユーザ登録する?') !!}</p>
        </div>
    </div>
@endsection
ヘッダー

これでログインができるようになりました。

次にヘッダーに追記して、ユーザがログイン・ログアウトできるようにしましょう。
ログイン状態の場合は、「ログアウト」「マイページ」メニューが表示されるようにして、
逆にログアウトされているときは「新規ユーザ登録」「ログイン」を表示しましょう。

まずは以下のコードに書き換えてください。

laravel-quest > resources > views > commons > header.blade.php

header.blade.php

<header class="mb-5">
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
        <a class="navbar-brand" href="/">YouTube-Curation</a>
        <button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#nav-bar">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="nav-bar">
            <ul class="navbar-nav mr-auto"></ul>
            <ul class="navbar-nav">
                @if (Auth::check())
                    <li class="nav-item">{!! link_to_route('logout', 'ログアウト', [], ['class' => 'nav-link']) !!}</li>
                    <li class="nav-item"><a href="" class="nav-link">マイページ</a></li>
                @else
                    <li class="nav-item">{!! link_to_route('signup', '新規ユーザ登録', [], ['class' => 'nav-link']) !!}</li>
                    <li class="nav-item">{!! link_to_route('login', 'ログイン', [], ['class' => 'nav-link']) !!}</li>
                @endif
            </ul>
        </div>
    </nav>
</header>

if文のところの、Auth::check()は、ユーザがログイン状態にあるかどうかを判定する関数となります。

ファサードとは?

Auth::check()を使いましたが、そもそもAuthとは何でしょうか?
これはファサードと言われるものです。

ファサードは、クラスを使いやすくしたものと思っていただければいいでしょう。
例えば、通常下記のように長々と表現しないといけないクラスを

※サンプルコードにつき追記不要です

Illuminate\Support\Facades\Auth::class

以下のように、短縮して呼び出しやすくしているのです。

Auth

改めて、Authファサードを利用した関数には、下記のようなものがあります。

  • Auth::check() :ユーザがログイン状態にあるかどうかを判定する
  • Auth::user() :ログイン中のユーザを取得する

ちなみに、ファサードは、 下記ファイルの aliases の中で設定をされています。

laravel-quest > config > app.php

app.php(一部抜粋)

    'aliases' => [
        'App' => Illuminate\Support\Facades\App::class,
        'Artisan' => Illuminate\Support\Facades\Artisan::class,
        'Auth' => Illuminate\Support\Facades\Auth::class,
        'Blade' => Illuminate\Support\Facades\Blade::class,
        'Broadcast' => Illuminate\Support\Facades\Broadcast::class,
        'Bus' => Illuminate\Support\Facades\Bus::class,
        'Cache' => Illuminate\Support\Facades\Cache::class,
        'Config' => Illuminate\Support\Facades\Config::class,
        'Cookie' => Illuminate\Support\Facades\Cookie::class,
        'Crypt' => Illuminate\Support\Facades\Crypt::class,
        'DB' => Illuminate\Support\Facades\DB::class,
        'Eloquent' => Illuminate\Database\Eloquent\Model::class,
        'Event' => Illuminate\Support\Facades\Event::class,
        'File' => Illuminate\Support\Facades\File::class,
        'Gate' => Illuminate\Support\Facades\Gate::class,
        'Hash' => Illuminate\Support\Facades\Hash::class,
        'Lang' => Illuminate\Support\Facades\Lang::class,
        'Log' => Illuminate\Support\Facades\Log::class,
        'Mail' => Illuminate\Support\Facades\Mail::class,
        'Notification' => Illuminate\Support\Facades\Notification::class,
        'Password' => Illuminate\Support\Facades\Password::class,
        'Queue' => Illuminate\Support\Facades\Queue::class,
        'Redirect' => Illuminate\Support\Facades\Redirect::class,
        'Redis' => Illuminate\Support\Facades\Redis::class,
        'Request' => Illuminate\Support\Facades\Request::class,
        'Response' => Illuminate\Support\Facades\Response::class,
        'Route' => Illuminate\Support\Facades\Route::class,
        'Schema' => Illuminate\Support\Facades\Schema::class,
        'Session' => Illuminate\Support\Facades\Session::class,
        'Storage' => Illuminate\Support\Facades\Storage::class,
        'URL' => Illuminate\Support\Facades\URL::class,
        'Validator' => Illuminate\Support\Facades\Validator::class,
        'View' => Illuminate\Support\Facades\View::class,
    ],

ファサードは、これまでにも何度も登場してきました。

DB::connection()や、Route::getもDBファサードやRouteファサードを利用しています。
より深く知りたい方は、下記も参考にしてください。

Laravelドキュメント
https://readouble.com/laravel/6.x/ja/facades.html

トップページ

また、トップページですが、ログインが完了した場合は
そのユーザ名を表示させるようにしておきましょう。

laravel-quest > resources > views > welcome.blade.php

welcome.blade.php(一部抜粋)

@extends('layouts.app')
@section('content')
    <div class="center jumbotron bg-warning">
        <div class="text-center text-white">
            <h1>YouTubeまとめ × SNS</h1>
        </div>
    </div>
    <div class="text-right">
        @if(Auth::check())
            {{ Auth::user()->name }}
        @endif
    </div>
@endsection

さて、ここで、ログイン機能とログアウト機能を実行して試して見ましょう。
問題なくログイン・ログアウトが実行されましたでしょうか?

動画登録機能

ではいよいよ、動画登録機能を実装していきましょう。

Model

まず、Modelをつくっていきます。モデルの名前は、Movieにしましょう。

1対多の関係

UserモデルとMovieモデル は 1対多 の関係といわれます。

1対多の関係とは、1つのモデルのインスタンスが、複数のモデルのインスタンスを所有しているということです。

本講義のアプリケーションを例に取ると、 1人の利用者(User)はたくさんの動画(Movie)を登録して保持することが可能であるということです。
逆に言うと、個々の動画 (Movie) は1人の利用者(User)に属しているともいえます。

つまり、下記のような主従関係となります。

  • 動画 (Movie)は、利用者(User)に所属(belongsTo)している
  • 利用者(User)は、動画(Movie)を所有(hasMany)している

この「1対多の関係」や今後出てくる「多対多の関係」を複数つくることで、アプリケーションを完成に近づけていきます。 belongsToとhasManyに関しては後ほど説明します。

以下で解説する1対多の関係を使ったモデル操作の方法をしっかりと確認していくようにしましょう。

テーブルをつくろう

マイグレーション

マイグレーションファイルをつくって、moviesテーブルを作成していきましょう。

ターミナル

$ php artisan make:migration create_movies_table --create=movies

上記コマンド実行後、以下のフォルダに下記ファイルが出来上がりますので、
up()関数の中身をこのように書き換えましょう。

laravel-quest > database > migrations > xxxxx_create_movies_table.php

xxxxx_create_movies_table.php(一部抜粋)

class CreateMoviesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('movies', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('user_id')->unsigned()->index();
            $table->string('url');
            $table->string('comment')->nullable();
            $table->timestamps();
            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('movies');
    }
}

moviesテーブルには、以下のようなカラムを作ります。

  • id:各動画に付ける連番
  • user_id:動画を登録したユーザのID
  • url: YouTube動画のURL
  • comment: 動画に対するコメント (nullableでコメントなしでも投稿できる仕様にしています)
  • timestamps:動画登録日時・動画更新日時

後ほど登録フォームを作成するのですが、
YouTube動画を登録する際には、YouTube動画ID(URLに記載されている動画の識別文字列)というものが必要になってきます。
そのIDをView上のURLに当てはめて、動画を表示させることになります。

コメントは、動画登録するユーザが、動画に添えたいコメントを自由に書くことができる機能として
付けています。

また、user_idについている unsigned() , index()とは何でしょうか。

unsigned() は、マイナスの値は保存できないように制限を掛けているということです。
index() は、検索速度を早めることができる関数です。カラムに付けることで、 index() が付けられたカラムのみを抽出して素早く対象動画の情報を得ることができます。
動画は特にユーザとの関わりが深いため(どのユーザが所有しているかが重要であるので)、user_idにindex()を付けています。

外部キー制約とは?

xxxxx_ create_movies_table.php(一部抜粋)

マイグレーションファイルのうち、下記コードは「外部キー制約」といわれるコードです。

$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');

このコードの意味合いは、
「このテーブルのカラム(‘user_id’)と、usersテーブルのカラム(‘id’)が一致していて、別テーブルはusersテーブルのことを示す。また、idカラムを持っているレコード(ユーザ自体)が削除された場合は、この動画情報も一緒に削除される」
ということです。

この外部キー制約によって、テーブルどうしの相互性を高めることができます。
例えば、1つの動画が存在しないユーザの所有物として登録された場合、動画自体はテーブル上に保存されるのですが、存在しないユーザに属するということはユーザに所属していない扱いになってしまいます。
その場合、動画は存在するにも関わらず表示されない可能性等があり、その動画はデータとして不十分なものとなります。

つまり、外部キー制約を設けることによって、そのような中途半端なデータを生み出さないように対策を打っているわけです。

では、続いてマイグレーションを実行しましょう。

ターミナル

$ php artisan migrate

マイグレーションの際にエラーが発生した場合

ターミナル でコマンドを実行した際、このようなエラーが出たのではないでしょうか。

SQLSTATE[HY000]: General error: 1005 Can't create table 'laravel-quest.#sql-b55_11' (errno: 150)

こちらは外部キー制約で参照しようとしているデータの型が異なるため発生するエラーです。

xxxxx_create_users_table.php (一部抜粋)

Schema::create('users', function (Blueprint $table) {
    $table->bigIncrements('id');
    //(中略)
});

こちらを以下のコードに書き換えた後、もう一度マイグレーションを実行するコマンドを打ちましょう。

Schema::create('users', function (Blueprint $table) {
    $table->increments('id');
    //(中略)
});

Movie Model

次は以下のコマンドにて、Movie モデルを作成していきます。

ターミナル

$ php artisan make:model Movie

まず、下記の様にモデルの中身に、fillable変数を定義することで
下記3つのカラム(’user_id’,’url’,’comment’)を一度に入力→保存できるようにしましょう。

また、ここでモデルに1対多の関係を定義するために、user() という関数を定義します。
関数の中身としては、return $this->belongsTo(User::class);という値を書き込みます。
この内容は「MovieモデルがUserモデルに所属している」ということを明示する役割があり、
実際のコード記述上でも、下記のようなシンプルなコードで「Movieインスタンスが属しているユーザを取得」できることになります。

  • $movie->user()->get();
  • $movie->user;

laravel-quest > app > Movie.php

Movie.php

<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Movie extends Model
{
    protected $fillable = ['user_id','url','comment'];
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

User Model

続いて、Userモデル にも1対多の関係を定義しましょう。
Movieモデルの user() は単数形で表現していたと思うのですが、Userインスタンスが所有するMovieは複数あるので、movies() という複数形を使って関数を作りましょう。
関数の中身としては、return $this->hasMany(Movie::class); という値を書き込みます。
この内容は「 User モデルがMovieモデルを所有している」ということを明示する役割があり、
実際のコード記述上でも、下記のようなコードで「Userインスタンスが所有しているMovieを取得」できることになります。

  • $user->movies()->get();
  • $user->movies;

laravel-quest > app > User.php

User.php(一部抜粋)

<?php
namespace App;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
    use Notifiable;
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];
    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];
    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
     public function movies()
    {
        return $this->hasMany(Movie::class);
    }
}

動画登録をtinkerでやってみよう

Userモデルの新規作成でも、create()関数を使って作成することができましたが
Movieモデルでも同様にcreate()関数を使うことが可能です。

Userモデルの時と同様に、Movieモデルも連想配列形式で「動画情報を登録して保存する」という流れを取ります。

Laravelドキュメント
https://readouble.com/laravel/6.x/ja/eloquent-relationships.html#the-create-method

試しに、tinker を使って新規動画情報をDBに保存させてみましょう。

ターミナル

>>> use App\User
>>> $user = User::find(1)
=> App\User {#xxx
     id: 1,
     name: "sample1",
     email: "sample1@sample.com",
     created_at: "2020-04-01 09:25:07",
     updated_at: "2020-04-01 09:25:07",
   }
>>> use App\Movie
>>> $user->movies()->get();
=> Illuminate\Database\Eloquent\Collection {#xxx
     all: [],
   }
>>> $user->movies()->create([
... 'url' => 'lLQQwW0SySo',
... 'comment' => 'Laravel Quest 1'])
=> App\Movie {#xxx
     id: 1,
     user_id: 1,
     url: "lLQQwW0SySo",
     comment: "Laravel Quest 1",
     created_at: "2020-04-01 09:25:07",
     updated_at: "2020-04-01 09:25:07",
   }
>>> $user->movies
=> Illuminate\Database\Eloquent\Collection {#xxx
     all: [
       App\Movie {#xxx
         id: 1,
         user_id: 1,
         url: "lLQQwW0SySo",
         comment: "Laravel Quest 1",
         created_at: "2020-04-01 09:25:07",
         updated_at: "2020-04-01 09:25:07",
       },
     ],
   }

Router

次は、ログイン(新規ユーザ登録)した場合のみに利用できる、Moviesのルーティングをつくっていきます。
これにより、ログインしているユーザのみがMoviesControllerを使って、動画登録・削除できるようになります。

これまでは、トップページを表示させるに当たって、今まで下記の様にルーティングを書いていたと思いますが、こちらも変更していきます。

laravel-quest > routes > web.php

web.php(一部抜粋)

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

今後は、下記の様に記述を書き換えて、UsersController@indexアクションを経由して、下記の様にトップページを表示させるようにしたいと思います。

※まだ記述不要です。

Route::get('/', 'UsersController@index');

ルーティングの記述省略

実は、前回のテキストで書いていたような「7つのルーティング」記述を省略する方法があります。下記の通りです。

※サンプルコードにつき、まだ記述不要です。

Route::resource('movies', 'MoviesController');

前回までは1つ1つのルーティングを記述してきましたが、上記は1つの記述で7つのルーティングを準備できる、ルーティング記述の短縮バージョンです。

今回から、この短縮バージョンも利用して記述していきましょう。
ルーティングを以下の様に書き換えます。

laravel-quest > routes > web.php

web.php(一部抜粋)

Route::get('/', 'UsersController@index'); //書き換え
Route::get('signup', 'Auth\RegisterController@showRegistrationForm')->name('signup');
Route::post('signup', 'Auth\RegisterController@register')->name('signup.post');
Route::get('login', 'Auth\LoginController@showLoginForm')->name('login');
Route::post('login', 'Auth\LoginController@login')->name('login.post');
Route::get('logout', 'Auth\LoginController@logout')->name('logout');
// 追記分
Route::resource('users', 'UsersController', ['only' => ['show']]);
Route::group(['middleware' => 'auth'], function () {
    Route::resource('movies', 'MoviesController', ['only' => ['create', 'store', 'destroy']]);
});

最後に記述しているMoviesControllerに注目して下さい。
ここでは、Route::groupでルーティングのグループを作成して、その時に[‘middleware’ => ‘auth’]として、ログイン認証を通ったユーザのみが、その内部のルーティングにアクセスできるようにしています。

また、Route::resource()で、7つのルーティングの短縮形となるのですが
あえて[‘only’ => [‘create’, ‘store’, ‘destroy’]]と記述して、実際にルートとして設定するアクションを限定しています。

UsersControllerも、同様にアクションを制限して記述しています。

では次に、コントローラの各アクションを記述していきます。

UsersController indexアクション

UsersControllerを作り、トップページを表示する「indexアクション」を記述していきます。

indexアクション

ターミナル

$ php artisan make:controller UsersController

laravel-quest > app > Http > Controllers > UsersController.php

UsersController.php

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\User; //追記
class UsersController extends Controller
{
    public function index()
    {
        $users = User::orderBy('id','desc')->paginate(9);
        return view('welcome', [
            'users' => $users,
        ]);
    }
}

$users = orderBy(‘id’,’desc’)は、
「すべてのユーザをIDが新しい順(降順)に並び替える」という意味です。orderBy()という順番を並び替える関数を使っています。

->paginate(9);は、ページネーションを行っています。
上記の場合、「9名のユーザを取得する」という意味です。

view() の ( ‘welcome’ ) は、welcome.blade.phpを表示させるという意味ですが、
‘users’ => $users は何でしょうか。

※サンプルコードにつき記述不要です。

return view('welcome', [
    'users' => $users,
]);

これは、「$usersという変数をViewに持っていきます」という宣言です。
このように宣言された変数しか、ControllerからViewに持って行って表示させることはできません。記述形式としては連想配列の形を取っています。

より詳しく説明しますと、左側の「users」がViewで呼び出す変数の名前を示しており、右側の「$users」がControllerで作った変数を意味しています。
なので、 Viewで呼び出したい変数が仮に「sample」なら、下記のように宣言することになります。

※サンプルコードにつき記述不要です。

return view('welcome', [
    'sample' => $users,
]);

View

Movies の一覧を表示する共通の View として、 users.blade.php を作成します。
まずviewフォルダ内に、usersフォルダを作成し、その中に下記ファイルを作っていきましょう。

laravel-quest > resources > views > users  > users.blade.php

users.blade.php

<h2 class="mt-5 mb-5">users</h2>
<div class="movies row mt-5 text-center">
    @foreach ($users as $key => $user)
        @php
            $movie=$user->movies->last();
        @endphp
        @if($loop->iteration % 3 == 1 && $loop->iteration != 1)
            </div>
            <div class="row text-center mt-3">
        @endif
            <div class="col-lg-4 mb-5">
                <div class="movie text-left d-inline-block">
                    @{{ $user->name }}
                    <div>
                        @if($movie)
                            <iframe width="290" height="163.125" src="{{ 'https://www.youtube.com/embed/'.$movie->url }}?controls=1&loop=1&playlist={{ $movie->url }}" frameborder="0"></iframe>
                        @else
                            <iframe width="290" height="163.125" src="https://www.youtube.com/embed/" frameborder="0"></iframe>
                        @endif
                    </div>
                    <p>
                        @if(isset($movie->comment))
                               {{ $movie->comment }}
                        @endif
                    </p>
                </div>
            </div>
    @endforeach
</div>
{{ $users->links('pagination::bootstrap-4') }}

ここで行っているのは、foreach文で$usersから各ユーザを抽出して
そこから更に、各ユーザの動画情報 $movies の一番最近登録された動画情報を抜き出しています。
Laravelでは @php ~ @endphp で、<?php ~?>と同じくPHPの記述の開始と終了の宣言を表します。

下記部分の記述も見ていきましょう。

    @if($loop->iteration % 3 == 1 && $loop->iteration != 1)
            </div>
            <div class="row text-center mt-3">
       @endif

これは、foreach文で繰り返して抽出されるユーザ数を「3で割った場合に余りが1」となった場合(4番目のユーザを表示する前)に、<div>タグを設けて改行させる意味合いがあります。
(ただし、ユーザが1番目の場合は除く)
つまり「4番目のユーザを表示する前や、8番目のユーザを表示する前に改行させて、1行に3ユーザずつ表示させる」ということです。

@if($movie)~@endifまでの記述は、ユーザが登録動画を所有していた場合に、
<iframe>というページ上に Webページや動画を埋め込む仕組みを使って
urlカラム(YouTube動画ID)を変数($movie->url)として代入することによって、ユーザの登録動画を表示させようとしています。

また、@if(isset($movie->comment))という記述のところで、動画のcommentカラムが存在すれば
そのコメント内容を表示させるようにしています。

また、最後の{{ $users->links(‘pagination::bootstrap-4’) }}は、「1ページ目に9名までのユーザを表示」して、10名以上のユーザ情報があったら「2ページ以上に渡って表示させる」場合の「次ページリンク」を表します。

このusers.blade.phpを、welcome.blade.phpの中で@includeすると、ログイン後に表示されるトップページに、ユーザ名やユーザの最新の動画情報が一覧として表示されるようになります。

laravel-quest > resources > views > welcome.blade.php

welcome.blade.php(一部抜粋)

@extends('layouts.app')
@section('content')
    <div class="center jumbotron bg-warning">
        <div class="text-center text-white">
            <h1>YouTubeまとめ × SNS</h1>
        </div>
    </div>
    <div class="text-right">
        @if(Auth::check())
            {{ Auth::user()->name }}
        @endif
    </div>
    @include('users.users', ['users'=>$users])
@endsection

ControllerからViewに変数を持っていく場合と同様に、Viewから別のViewに変数を持ち込む場合も、$usersという変数をViewに渡すための宣言をここでしなければなりません。

上記コードのまま表示させると、少し見た目がいびつな部分があるので
CSSファイルを作って見た目を整えたいと思います。

CSSを導入するには、まず下記のようにCSSのリンクを<head>要素に書き加えましょう。

laravel-quest > resources > views > layouts > app.blade.php(抜粋)

app.blade.php(<head>要素のみ抜粋)

    <head>
        <meta charset="utf-8">
        <title>YouTubeまとめ×SNS</title>
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
        <link rel="stylesheet" href="{{ asset('/css/styles.css') }}">
    </head>

asset()はヘルパー関数といわれるもので、CSSやJavaScript等へのリンクを生成してくれます。
多くの場合、CSSや画像ファイルなどはpublicフォルダ内に置きますので、publicフォルダ内のファイルを簡単に指定できるasset関数を利用しましょう。

Laravel ドキュメント
https://readouble.com/laravel/6.x/ja/helpers.html#method-route

次に、publicフォルダ内の cssフォルダの中に、CSSファイルを新規に作ります。
内容的には下記のような形で記述すれば、綺麗に表示されるはずです。

laravel-quest > public > css > styles.css

styles.css

.movie{
    width: 290px;
}
.movie > p{
    height: 72px;
}
.button{
    width: 290px;
}

MoviesController createアクション

次は、動画を登録できるように、登録フォームをつくっていきましょう。

createアクション

ターミナル

$ php artisan make:controller MoviesController

まず、上記コマンドでMoviesControllerをつくって、その中身を記述していきます。

ここで作るのは、movies/createというURLにアクセスした場合に表示される画面になります。

laravel-quest > app > Http > Controllers > MoviesController.php の create アクション

MoviesController.php

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\User;
use App\Movie;
class MoviesController extends Controller
{
    public function create()
    {
        $user = \Auth::user();
        $movies = $user->movies()->orderBy('id', 'desc')->paginate(9);
        $data=[
            'user' => $user,
            'movies' => $movies,
        ];
        return view('movies.create', $data);
    }
}

今回は、2つの変数をViewに渡しますので、$dataという変数に2つの変数を配列形式で代入します。

View

メニューから新規登録フォームへ移動できるリンクを、ヘッダー部分に記述しましょう。
ここでもLaravel Collectiveの link_to_route() を使います。

laravel-quest > resources > views > commons > header.blade.php(抜粋)

header.blade.php(一部抜粋)

@if (Auth::check())
       <li class="nav-item">{!! link_to_route('logout', 'ログアウト', [], ['class' => 'nav-link']) !!}</li>
       <li class="nav-item"><a href="" class="nav-link">マイページ</a></li>
       <li class="nav-item">{!! link_to_route('movies.create','動画を登録する',['id'=>Auth::id()],['class'=>'nav-link']) !!}</li>
           <!--↑追記-->
@else

次に、moviesフォルダを作成して、その中に下記ファイルを作って、
動画登録フォームをつくっていきましょう。

laravel-quest > resources > views > movies > create.blade.php

create.blade.php

@extends('layouts.app')
@section('content')
    <div class="text-right">
        {{ Auth::user()->name }}
    </div>
        <h2 class="mt-5">動画を登録する</h2>
        {!! Form::open(['route'=>'movies.store']) !!}
            <div class="form-group mt-5">
                {!! Form::label('url','新規登録YouTube動画 "ID" を入力する',['class'=>'text-success']) !!}
                    <br>例)登録したいYouTube動画のURLが <span>https://www.youtube.com/watch?v=-bNMq1Nxn5o なら</span>
                    <div>  "v="の直後にある "<span class="text-success">-bNMq1Nxn5o</span>" を入力</div>
                {!! Form::text('url',null,['class'=>'form-control']) !!}
                {!! Form::label('comment','登録動画へのコメント',['class'=> 'mt-3']) !!}
                {!! Form::text('comment',null,['class'=>'form-control']) !!}
                {!! Form::submit('新規登録する?',['class'=> 'button btn btn-primary mt-5 mb-5']) !!}
            </div>
        {!! Form::close() !!}
        <h2 class="mt-5">あなたの登録済み動画</h2>
        @include('movies.movies', ['movies' => $movies])
@endsection

上記のうちの、下記のコードに注目しましょう。

        <h2 class="mt-5">あなたの登録済み動画</h2>
        @include('movies.movies', ['movies' => $movies])

こちらで、動画情報を別のViewに分けています。
こうすることで、後々の変更などにも強くなります。

では、moviesフォルダの中に、下記ファイルを作成して
「登録済み動画」の表示をつくっていきましょう。

laravel-quest > resources > views > movies > movies.blade.php

movies.blade.php

<div class="movies row mt-5 text-center">
    @foreach ($movies as $key => $movie)
        @if($loop->iteration % 3 == 1 && $loop->iteration != 1)
            </div>
            <div class="row text-center mt-3">
        @endif
            <div class="col-lg-4 mb-5">
                <div class="movie text-left d-inline-block">
                    <div>
                        @if($movie)
                            <iframe width="290" height="163.125" src="{{ 'https://www.youtube.com/embed/'.$movie->url }}?controls=1&loop=1&playlist={{ $movie->url }}" frameborder="0"></iframe>
                        @else
                            <iframe width="290" height="163.125" src="https://www.youtube.com/embed/" frameborder="0"></iframe>
                        @endif
                    </div>
                    <p>
                        @if(isset($movie->comment))
                            {{ $movie->comment }}
                        @endif
                    </p>
                </div>
            </div>
    @endforeach
</div>
{{ $movies->links('pagination::bootstrap-4') }}

基本、先ほど作ったusers.blade.phpと同様に、「1行に3つの動画が表示」されるようになっていて
かつ、Controllerで->paginate(9)と記述して上記Bootstrapリンクでページネーションを使っている通り、「1ページに9つの動画までを表示」させるように処理しています。

MoviesController storeアクション

storeアクション

動画登録フォームから「動画登録」のアクションを実行できるように実装しましょう。
ここまで、tinkerを用いて登録を行いましたが、同じやり方でcreate関数を使って動画情報を保存していきます。

laravel-quest > app > Http > Controllers > MoviesController.php

MoviesController.php(追記分のみ)

    public function store(Request $request)
    {
        $this->validate($request,[
            'url' => 'required|max:11',
            'comment' => 'max:36',
        ]);
        $request->user()->movies()->create([
            'url' => $request->url,
            'comment' => $request->comment,
        ]);
        return back();
    }

ここでは、バリデーションを掛けることで、URL入力を必須とし、URL・コメントのそれぞれの最大入力文字数を制限しています。

また、下記記述で、フォームに入力されたURL・コメントを、動画のそれぞれのカラムに入れ込む処理を行っています。

'url' => $request->url,
'comment' => $request->comment,

また、最後にreturn back();と表記がありますが、このように記述すると投稿が完了すると直前のページが自動的に表示されます。
特定のViewなどを指定しなくとも、統一した記述内容で簡単に実装できる利点があります。

MoviesController destroyアクション

登録済み動画の削除機能を実装してきましょう。

destroyアクション

laravel-quest > app > Http > Controllers > MoviesController.php

MoviesController.php(追記分のみ)

    public function destroy($id)
    {
        $movie = Movie::find($id);
        if (\Auth::id() == $movie->user_id) {
            $movie->delete();
        }
        return back();
    }

削除実行処理を意味する、$movie->delete(); は、if文で囲むことにより
自分以外の他のユーザの動画を勝手に削除されてしまうことがないように、ログインしているユーザIDと動画を所有しているユーザのIDが一致している場合のみ、削除処理を実行するように記述しています。

View

では、削除ボタンを表示動画の下に付けていきましょう。

laravel-quest > resources > views > movies > movies.blade.php

movies.blade.php

<div class="movies row mt-5 text-center">
    @foreach ($movies as $key => $movie)
        @if($loop->iteration % 3 == 1 && $loop->iteration != 1)
            @php
                echo '</div><div class="row text-center mt-3">';
            @endphp
        @endif
            <div class="col-lg-4 mb-5">
                <div class="movie text-left d-inline-block">
                    <div>
                       @if($movie)
                            <iframe width="290" height="163.125" src="{{ 'https://www.youtube.com/embed/'.$movie->url }}?controls=1&loop=1&playlist={{ $movie->url }}" frameborder="0"></iframe>
                        @else
                            <iframe width="290" height="163.125" src="https://www.youtube.com/embed/" frameborder="0"></iframe>
                        @endif
                    </div>
                    <p>
                        @if(isset($movie->comment))
                            {{ $movie->comment }}
                        @endif
                    </p>
                    @if(Auth::id() == $movie->user_id)
                        {!! Form::open(['route' => ['movies.destroy', $movie->id], 'method' => 'delete']) !!}
                            {!! Form::submit('この動画を削除する?', ['class' => 'button btn btn-danger']) !!}
                        {!! Form::close() !!}
                    @endif
                </div>
            </div>
    @endforeach
</div>
{{ $movies->render('pagination::bootstrap-4') }}

ここでも、ログインしているユーザIDと動画を所有しているユーザのIDが一致している場合のみ
「削除ボタンを表示する」ように条件付けをしています。
当然、その動画を登録したユーザが自分の動画を見た場合しか、削除ボタンは表示されません。

UsersController showアクション

カウント機能

後ほど記述します UsersController@showアクションで、Movieの登録数を表示させていきます。
表示させるためには、まずController.phpに登録動画の数を数える処理を記述します。

下記のように書くことで、全てのControllerでcounts関数を使えば、動画の登録数を取得できるようになります。これは、全てのControllerが以下のController.phpを継承しているためです。

laravel-quest > app > Http > Controllers > Controller.php

Controller.php(一部抜粋)

class Controller extends BaseController
{
    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
    public function counts($user) {
        $count_movies = $user->movies()->count();
        return [
            'count_movies' => $count_movies,
        ];
    }
}
 showアクション

このcounts関数をshowアクションの$dataに足していきます。

まずはshowアクションをindexアクションの下に記述していきましょう。
$data += $this->counts($user);と記述すれば、これだけでcounts($user)の戻り値が、$dataに追加されるというわけです。

'count_movies' => $count_movies,

laravel-quest > app > Http > Controllers > UsersController.php showアクションのみ抜粋

UsersController.php(追記分のみ)

    public function show($id)
    {
        $user = User::find($id);
        $movies = $user->movies()->orderBy('id', 'desc')->paginate(9);
        $data=[
            'user' => $user,
            'movies' => $movies,
        ];
        $data += $this->counts($user);
        return view('users.show',$data);
    }

View

では、各ユーザの詳細ページであるshowアクションの先のViewを作って、記述していきましょう。
このshow.blade.phpでも、各ユーザが所有している動画を表示させるようにします。

laravel-quest > resources > views > users > show.blade.php

show.blade.php

@extends('layouts.app')
@section('content')
<h1>{{ $user->name }}</h1>
<ul class="nav nav-tabs nav-justified mt-5 mb-2">
        <li class="nav-item nav-link {{ Request::is('users/' . $user->id) ? 'active' : '' }}"><a href="{{ route('users.show',['user'=>$user->id]) }}">動 画<br><div class="badge badge-secondary">{{ $count_movies }}</div></a></li>
        <li class="nav-item nav-link"><a href="" class="">フォロワー<br><div class="badge badge-secondary"></div></a></li>
        <li class="nav-item nav-link"><a href="" class="">フォロー中<br><div class="badge badge-secondary"></div></a></li>
</ul>
@include('movies.movies', ['movies' => $movies])
@endsection

ユーザ名の下に「タブ」を表示させることで
ユーザ所有の動画情報タブや、後ほど付け加えていくフォロワータブ等が切替えできるようになります。
タブ部分について解説します。

        <li class="nav-item nav-link {{ Request::is('users/' . $user->id) ? 'active' : '' }}"><a href="{{ route('users.show',['user'=>$user->id]) }}">動 画<br><div class="badge badge-secondary">{{ $count_movies }}</div></a></li>
        <li class="nav-item nav-link"><a href="" class="">フォロワー<br><div class="badge badge-secondary"></div></a></li>
        <li class="nav-item nav-link"><a href="" class="">フォロー中<br><div class="badge badge-secondary"></div></a></li>

{{ Request::is(‘users/’ . $user->id) ? ‘active’ : ” }}
 これは、/users/{id} というURLにアクセスされた時に、class=”active” にする意味があります。
 Bootstrapでは、class=”active” にすると「今開いているタブ」が表示上ハッキリ示されます。

Laravel Request@is
https://laravel.com/api/6.x/Illuminate/Http/Request.html#method_is

また、{{ route(‘users.show’,[‘user’=>$user->id]) }}は、ヘルパー関数といわれるものです。
link_to_route関数で記述しても良いのですが、Laravelの仕様上、 <div class=”badge badge-secondary”> {{ $count_movies }}</div>  を含めたリンクが上手く表示されないということがあり、ヘルパー関数を利用しています。

Laravel ドキュメント
https://readouble.com/laravel/6.x/ja/helpers.html#method-route

上記ができましたら、トップページの各ユーザの名前をクリックすると
各ユーザの個別詳細ページに飛べるように、リンクを貼っておきましょう。下記の追記分の1行を書き換えて下さい。

laravel-quest > resources > views > users  > users.blade.php

users.blade.php

<h2 class="mt-5 mb-5">users</h2>
<div class="movies row mt-5 text-center">
    @foreach ($users as $key => $user)
        @php
            $movie=$user->movies->last();
        @endphp
        @if($loop->iteration % 3 == 1 && $loop->iteration != 1)
            </div>
            <div class="row text-center mt-3">
        @endif
            <div class="col-lg-4 mb-5">
                <div class="movie text-left d-inline-block">
                    @{!! link_to_route('users.show',$user->name,['user'=>$user->id]) !!}
                       <!--↑追記-->
                    <div>
                        @if($movie)
                            <iframe width="290" height="163.125" src="{{ 'https://www.youtube.com/embed/'.$movie->url }}?controls=1&loop=1&playlist={{ $movie->url }}" frameborder="0"></iframe>
                        @else
                            <iframe width="290" height="163.125" src="https://www.youtube.com/embed/" frameborder="0"></iframe>
                        @endif
                    </div>
                    <p>
                        @if(isset($movie->comment))
                            {{ $movie->comment }}
                        @endif
                    </p>
                </div>
            </div>
    @endforeach
</div>
{{ $users->render('pagination::bootstrap-4') }}

これでトップページから各ユーザの個別詳細ページに行けるようになったはずです。

また、ヘッダーメニューの「マイページ」にも、ログイン済みユーザが自分の詳細ページを見に行けるようにリンクを貼っておきましょう。

laravel-quest > resources > views > commons > header.blade.php(抜粋)

header.blade.php(一部抜粋)

@if (Auth::check())
       <li class="nav-item">{!! link_to_route('logout', 'ログアウト', [], ['class' => 'nav-link']) !!}</li>
       <li class="nav-item">{!! link_to_route('users.show','マイページ',['user'=>Auth::id()],['class'=>'nav-link']) !!}</li>
           <!--↑追記-->
       <li class="nav-item">{!! link_to_route('movies.create','動画登録する',['id'=>Auth::id()],['class'=>'nav-link']) !!}</li>
@else

以上で、今回の講義は終わりです。
お疲れ様でした!

次回はこちら

https://prog.quest-academia.com/laravel-quest-5