【初心者向け】LaravelでYoutubeのお気に入りCuration(まとめ)アプリを作る【第4回: ログイン＆動画登録】

(この記事は作成中です。)

こんにちは。
ITエンジニアの濱辺(ハマベ)です。

今回は、ログイン＆お気に入り動画登録機能を実装していきます。

↓第3回はこちら
[LaravelでYoutubeのお気に入りCuration(まとめ)アプリを作る【第3回】](https://qiita.com/dogabon20/items/4133b65637dc7f60530d)

↓こちらの画面定義書のものを作っていきます。
Youtube-Curation 画面定義所 (googleスプレッドシート)

## ログイン機能の実装

ログイン機能についても、Laravelに標準で備わっているファイルがあります。
基本機能は最初から用意されているんですね。

#### Route設定

下記3つのrouteを記述しましょう。

```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');
```

それぞれ、
- ログインページ表示
- ログインフォーム情報の送信
- ログアウト処理
のためのrouteです。

#### LoginController確認

`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');
    }
}
```

ログアウト以外のアクションは、guest(非ログイン)ユーザしか実行できない、といったことが記述されています。

上記記述のうち、推移先のページ情報だけ書き換えましょう。

```LoginController.php
protected $redirectTo = RouteServiceProvider::HOME;
↓ //変更
protected $redirectTo = '/';
```

## ログインページの見た目実装

ログインページのviewを作成します。

`view/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
```

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

#### ヘッダーからログインページへ推移できるようにする

ヘッダーを書き換えて、ログイン状態と非ログイン状態で表示分けできるようにしましょう。

```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())`から、`@else`までの間がログイン状態での表示。
`@else`から`@endif`の間が非ログイン状態の表示を表しています。

`Auth::check()`は「ユーザがログイン状態か？」を判断する関数です。

## トップページにログインユーザ名を表示

トップページにログインしたユーザの名前を表示させましょう。

```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
```

さて、一通り実装できたので、ログイン・ログアウトを実行してみましょう。

ログイン画面
![スクリーンショット 2021-10-05 13.19.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1209814/0b763fbd-d0c4-f647-4b2f-23b0050b29a2.png)


ログインした状態
![スクリーンショット 2021-10-05 13.19.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1209814/a34ac8df-ca20-aa8f-6b93-ec6da98643c6.png)

ログアウト実行後(非ログイン状態)
![スクリーンショット 2021-10-05 13.20.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1209814/fc798b03-c464-91d1-d72d-b6da59583e85.png)

無事に動作したでしょうか。
できましたら、動画登録機能の実装をしていきましょう！

## 動画登録の実装

まずは、movie用のmysqlテーブルを作成していきます。

#### Moviesテーブルを作成

まず、Moviesテーブルを作成します。
下記のコマンドを実行しましょう。

```
# php artisan make:migration create_movies_table --create=movies
```

ファイルが作成されるので、中身up()の中身を書き換えましょう。

```[作成日時]_create_movies_table.php(一部抜粋)
    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');
        });
    }
```

５つのカラムを作るように記述しました。

- id：各動画に付ける連番
- user_id：動画を登録したユーザのＩＤ
- url： YouTube動画のURL
- comment： 動画に対するコメント （nullable = nullでも投稿できる)
- timestamps：動画登録日時・動画更新日時

`$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');`は、紐づいているユーザIDを持つユーザが消去されたら、moviesテーブルの該当カラムも一緒に削除する、といった意味になります。

マイグレートを実行してみましょう。

```
# php artisan migrate
```

すると、下記のエラーが発生するかと思います。

```
PDOException::("SQLSTATE[HY000]: General error: 3780 Referencing column 'user_id' and referenced column 'id' in foreign key constraint 'movies_user_id_foreign' are incompatible.")
```

これは、usersテーブルのidと、moviesテーブルのidの型が異なることが原因で発生しています。

下記の部分を修正しましょう。

```2014_10_12_000000_create_users_table.php
Schema::create('users', function (Blueprint $table) {
    $table->increments('id');
    〜略
});
```

## moviesモデルの作成

以下のコマンドにて、Movie モデルを作成しましょう。

```
# php artisan make:model Movie
```

生成された`Movie.php`に以下のように記述し、（’user_id’,’url’,’comment’）を登録できるようにしましょう。

```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モデルに、Movieモデルとの関係を記述しましょう。(一対多、一人のuserに複数のmovieの関係)

```User.php

~ 省略　~

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
    
　　　//追加(movieとの一対多の関係)
     public function movies()
    {
        return $this->hasMany(Movie::class);
    }
}
```

ここまで出来たら、一度tinkerを使って動画の登録を試してみましょう。

```
>>> use App\User
>>> $user = User::find(1)
=> App\User {#4104
     id: 1,
     name: "sample1",
     email: "sample1@sample.com",
     email_verified_at: null,
     #password: "$2y$10$vHoB.o8.Gd.KGmkflb06MOVuRTnEU9KihfJ9himiOxjNCDaZ7d4oS",
     #remember_token: null,
     created_at: null,
     updated_at: null,
   }>>> use App\Movie
>>> $user->movies()->get();
=> Illuminate\Database\Eloquent\Collection {#xxx
     all: [],
   }
>>> $user->movies()->create([
... 'url' => 'Gn61Vq9v6GY',
... 'comment' => 'sample movie 1'])
=> App\Movie {#3324
     url: "Gn61Vq9v6GY",
     comment: "sample movie 1",
     user_id: 1,
     updated_at: "2021-10-12 07:51:46",
     created_at: "2021-10-12 07:51:46",
     id: 1,
   }
>>> $user->movies
=> Illuminate\Database\Eloquent\Collection {#4252
     all: [
       App\Movie {#4249
         id: 1,
         user_id: 1,
         url: "Gn61Vq9v6GY",
         comment: "sample movie 1",
         created_at: "2021-10-12 07:51:46",
         updated_at: "2021-10-12 07:51:46",
       },
     ],
   }
```

## MovieのRouter作成

Movie関連のRouterを記述していきます。
今回は`resorce`を使用します。resourceは、主要7つのrouteを自動作成してくれます。

`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
Route::group(['middleware' => 'auth'], function () {
    Route::resource('movies', 'MoviesController', ['only' => ['create', 'store', 'destroy']]);
});
```

## UsersController作成

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

```
# php artisan make:controller UsersController
```

`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' => $users,`は、usersという変数をviewに持っていく、という処理を表しています。

## Viewの作成

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

`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') }}
```

ここで少し、見た目を整えるためにCSSを適用しましょう。
下記のようにlinkを、`app.blade.php`に追記します。

```app.blade.php
    <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>
```

続いて、`public/css/styles.css`を作成し、下記を記述します。

```styles.css
.movie{
    width: 290px;
}

.movie > p{
    height: 72px;
}

.button{
    width: 290px;
}
```

これで、下記のような見た目となるはずです。

![スクリーンショット 2021-10-12 22.15.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1209814/eacf4a7a-f160-dcb9-b8a1-c824e6074ed7.png)

それぞれ、ユーザに登録した最新の動画のサムネが表示され、クリックすると、再生できるはずです。

ここまでで、「動画を保存できる」ようにはなりました。
次はwebページから動画を登録できるようにしていきます。

## MoviesController作成

まずは、下記のコマンドでMoviesControllerを作成しましょう。

```
# php artisan make:controller MoviesController
```

`app/Http/Controllers/MoviesController.php`が作成されるので、
createアクションを記述していきましょう。

#### 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);
    }
}
```

今回は、`$data`という変数に、`$user`と`$movies`を配列として渡して、`$data`をViewに渡すようにしています。

## Viewの作成

まず、headerから動画の新規登録ページへ飛べるようにしましょう。

```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
```

次に、動画登録フォームを作成しましょう。
`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])
```

これは、moviesフォルダのmovies.blade.phpを読み込む記述です。
こうして別ファイルに分けた方が、後々の変更に強くなるのです。
というわけで、`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') }}
```

これで、ヘッダーの「動画を登録する」から遷移した画面がこうなるはずです。


![スクリーンショット 2021-10-14 11.39.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1209814/712ceb75-4a31-f93f-86e9-c3da00cd7171.png)

## MoviesContoroller storeアクション作成