【全6回】Laravel5.8でTwitterっぽいSNSツールを作る（第4回ツイートのCRUD機能 一覧と新規作成）

# Laravelで始めるTwitter風（Twitterクローン）のSNSツール開発チュートリアル

## 概要

スクールとかの課題だったりLaravelを初めてみたいけど何を作ろうって迷ってる人向けによくあるTwitter風のWEBサイトを作ってみます。

### 前回

- 第1回 [【全6回】Laravel5.8でTwitterっぽいSNSツールを作る（第1回DB設計とMigration）](https://qiita.com/namizatop/items/d79017aa474966244073)
- 第2回 [【全6回】Laravel5.8でTwitterっぽいSNSツールを作る（第2回Seeder->ログイン/新規登録）](https://qiita.com/namizatop/items/3c20c0d0565906cc6a1a)
- 第3回 [【全6回】Laravel5.8でTwitterっぽいSNSツールを作る（第3回ユーザ関連とフォロー機能）](https://qiita.com/namizatop/items/0c81b0a94a1084cda6de)
- 第4回 [【全6回】Laravel5.8でTwitterっぽいSNSツールを作る（第4回ツイートのCRUD機能）](https://qiita.com/namizatop/items/c9ed67f98fc3e5ce67c7)
- 第5回 [【全6回】Laravel5.8でTwitterっぽいSNSツールを作る（第5回ツイートのCRUD機能 編集と削除）](https://qiita.com/namizatop/items/2273bd0dcc817785b131)
- 第6回 [【全6回】Laravel5.8でTwitterっぽいSNSツールを作る（第6回コメントといいね機能）](https://qiita.com/namizatop/items/0f3f8783c93f3dfa277d)

第4回はツイートのCRUD機能を追加していきます。
この回は少し長くなってしまったので前半(4回)と後半(5回)で分けていきたいと思います。

## 前提

- PHPをある程度理解している
- Laravelが使える環境がある
- MVC構造をある程度理解している

## 環境

- Mac
- Homestead
- Laravel 5.8

## ルーティング

まずはtweetsのCRUDをやっていくのでControllerを作成していきましょう。

```terminal
php artisan make:controller TweetsController --resource
```

Controllerファイルを生成したらルーティングの設定を追加しましょう。

```php:routes/web.php
// ログイン状態
Route::group(['middleware' => 'auth'], function() {

    // ユーザ関連
    Route::resource('users', 'UsersController', ['only' => ['index', 'show', 'edit', 'update']]);

    // フォロー/フォロー解除を追加
    Route::post('users/{user}/follow', 'UsersController@follow')->name('follow');
    Route::delete('users/{user}/unfollow', 'UsersController@unfollow')->name('unfollow');

    // ツイート関連
    Route::resource('tweets', 'TweetsController', ['only' => ['index', 'create', 'store', 'show', 'edit', 'update', 'destroy']]);

});
```

## Read（ツイート一覧表示機能）

ではCRUDのRead機能から始めます。
ツイート一覧表示機能から実装していきます。

#### Model

ここではフォローしているユーザのツイートのみを取得します。
そのためModelは`Follower.php`と`Tweet.php`を使用していきます。

##### Follower

ログインしているユーザIDを引数で渡してフォローしているユーザIDを取得します。

```php:app/Models/Follower.php
    // フォローしているユーザのIDを取得
    public function followingIds(Int $user_id)
    {
        return $this->where('following_id', $user_id)->get('followed_id');
    }
```

##### Tweet

上の`followingIds（）`で取得したフォローしているユーザIDをControllerを介して取得したと仮定してそのデータを引数で渡します。

```php:app/Models/Tweet.php
    // 一覧画面
    public function getTimeLines(Int $user_id, Array $follow_ids)
    {
        // 自身とフォローしているユーザIDを結合する
        $follow_ids[] = $user_id;
        return $this->whereIn('user_id', $follow_ids)->orderBy('created_at', 'DESC')->paginate(50);
    }
```

#### Controller

ここでは先ほど設定したそれぞれのModelにデータを渡していきます。

> 後で使うので色々useしておきます。

```php:app/Http/Contollers/TweetsController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use App\Models\Tweet;
use App\Models\Comment;
use App\Models\Follower;

class TweetsController extends Controller
{
    public function index(Tweet $tweet, Follower $follower)
    {
        $user = auth()->user();
        $follow_ids = $follower->followingIds($user->id);
        // followed_idだけ抜き出す
        $following_ids = $follow_ids->pluck('followed_id')->toArray();

        $timelines = $tweet->getTimelines($user->id, $following_ids);

        return view('tweets.index', [
            'user'      => $user,
            'timelines' => $timelines
        ]);
    }
}
```

#### View

ではtweetsのViewファイルを作りましょう。
後で必要なViewファイルもついでに作っちゃいます。

```terminal
mkdir resources/views/tweets
touch resources/views/tweets/index.blade.php
touch resources/views/tweets/show.blade.php
touch resources/views/tweets/create.blade.php
touch resources/views/tweets/edit.blade.php
```

ログインしているユーザの場合のみ編集と削除アイコンが表示される様にしているのと、
コメント数といいね数を表示する様にしました。

> コメントのアイコンを押すと詳細画面に飛ぶようにしていますが、次で詳細画面を実装していくのでまだ機能しません。

```php:resources/views/tweets/index.blade.php
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8 mb-3 text-right">
            <a href="{{ url('users') }}">ユーザ一覧 <i class="fas fa-users" class="fa-fw"></i> </a>
        </div>
        @if (isset($timelines))
            @foreach ($timelines as $timeline)
                <div class="col-md-8 mb-3">
                    <div class="card">
                        <div class="card-haeder p-3 w-100 d-flex">
                            <img src="{{ asset('storage/profile_image/' .$timeline->user->profile_image) }}" class="rounded-circle" width="50" height="50">
                            <div class="ml-2 d-flex flex-column">
                                <p class="mb-0">{{ $timeline->user->name }}</p>
                                <a href="{{ url('users/' .$timeline->user->id) }}" class="text-secondary">{{ $timeline->user->screen_name }}</a>
                            </div>
                            <div class="d-flex justify-content-end flex-grow-1">
                                <p class="mb-0 text-secondary">{{ $timeline->created_at->format('Y-m-d H:i') }}</p>
                            </div>
                        </div>
                        <div class="card-body">
                            {!! nl2br(e($timeline->text)) !!}
                        </div>
                        <div class="card-footer py-1 d-flex justify-content-end bg-white">
                            @if ($timeline->user->id === Auth::user()->id)
                                <div class="dropdown mr-3 d-flex align-items-center">
                                    <a href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                                        <i class="fas fa-ellipsis-v fa-fw"></i>
                                    </a>
                                    <div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
                                        <form method="POST" action="{{ url('tweets/' .$timeline->id) }}" class="mb-0">
                                            @csrf
                                            @method('DELETE')

                                            <a href="{{ url('tweets/' .$timeline->id .'/edit') }}" class="dropdown-item">編集</a>
                                            <button type="submit" class="dropdown-item del-btn">削除</button>
                                        </form>
                                    </div>
                                </div>
                            @endif
                            <div class="mr-3 d-flex align-items-center">
                                <a href="{{ url('tweets/' .$timeline->id) }}"><i class="far fa-comment fa-fw"></i></a>
                                <p class="mb-0 text-secondary">{{ count($timeline->comments) }}</p>
                            </div>
                            <div class="d-flex align-items-center">
                                <button type="" class="btn p-0 border-0 text-primary"><i class="far fa-heart fa-fw"></i></button>
                                <p class="mb-0 text-secondary">{{ count($timeline->favorites) }}</p>
                            </div>
                        </div>
                    </div>
                </div>
            @endforeach
        @endif
    </div>
    <div class="my-4 d-flex justify-content-center">
        {{ $timelines->links() }}
    </div>
</div>
@endsection
```

### Read(ツイート一覧表示画面)

これで`/tweets`にアクセスするとフォローしている人と自身のツイートのみ編集出来る様になったと思います。

<img width="1440" alt="スクリーンショット 2019-09-18 0.54.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293438/d22293d9-38bb-b382-7410-9c04642fbc29.png">

続いて詳細画面を作っていきます。

### Read（ツイート詳細表示機能）

一覧表示に続いてツイート詳細画面も作っていきます。
機能的にはさほど変わらないので駆け足で書いていきます。

#### Model

詳細画面ではコメントが閲覧できるように`comments`テーブルからもデータを取得します。
そのため`Tweet`と`Comment`両方のModelを書いていきます。

##### Tweet

取得したツイートidを引数にしてツイート情報を取得します。

```php:app/Models/Tweet.php
    // 詳細画面
    public function getTweet(Int $tweet_id)
    {
        return $this->with('user')->where('id', $tweet_id)->first();
    }
```

##### Comment

`getTweet()`で取得した情報に紐づいたコメント情報を取得します。

```php:app/Models/Comment.php
    public function getComments(Int $tweet_id)
    {
        return $this->with('user')->where('tweet_id', $tweet_id)->get();
    }
```

#### Controller

では先ほどのメソッドに引数を渡してViewに渡す処理を書いていきます。

```php:app/Http/Controllers/TweetsController.php
    public function show(Tweet $tweet, Comment $comment)
    {
        $user = auth()->user();
        $tweet = $tweet->getTweet($tweet->id);
        $comments = $comment->getComments($tweet->id);

        return view('tweets.show', [
            'user'     => $user,
            'tweet' => $tweet,
            'comments' => $comments
        ]);
    }
```

#### View


```php:resources/views/tweets/show.blade.php
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center mb-5">
        <div class="col-md-8 mb-3">
            <div class="card">
                <div class="card-haeder p-3 w-100 d-flex">
                    <img src="{{ asset('storage/profile_image/' .$tweet->user->profile_image) }}" class="rounded-circle" width="50" height="50">
                    <div class="ml-2 d-flex flex-column">
                        <p class="mb-0">{{ $tweet->user->name }}</p>
                        <a href="{{ url('users/' .$tweet->user->id) }}" class="text-secondary">{{ $tweet->user->screen_name }}</a>
                    </div>
                    <div class="d-flex justify-content-end flex-grow-1">
                        <p class="mb-0 text-secondary">{{ $tweet->created_at->format('Y-m-d H:i') }}</p>
                    </div>
                </div>
                <div class="card-body">
                    {!! nl2br(e($tweet->text)) !!}
                </div>
                <div class="card-footer py-1 d-flex justify-content-end bg-white">
                    @if ($tweet->user->id === Auth::user()->id)
                        <div class="dropdown mr-3 d-flex align-items-center">
                            <a href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                                <i class="fas fa-ellipsis-v fa-fw"></i>
                            </a>
                            <div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
                                <form method="POST" action="{{ url('tweets/' .$tweet->id) }}" class="mb-0">
                                    @csrf
                                    @method('DELETE')

                                    <a href="{{ url('tweets/' .$tweet->id .'/edit') }}" class="dropdown-item">編集</a>
                                    <button type="submit" class="dropdown-item del-btn">削除</button>
                                </form>
                            </div>
                        </div>
                    @endif
                    <div class="mr-3 d-flex align-items-center">
                        <a href="{{ url('tweets/' .$tweet->id) }}"><i class="far fa-comment fa-fw"></i></a>
                        <p class="mb-0 text-secondary">{{ count($tweet->comments) }}</p>
                    </div>
                    <div class="d-flex align-items-center">
                        <button type="" class="btn p-0 border-0 text-primary"><i class="far fa-heart fa-fw"></i></button>
                        <p class="mb-0 text-secondary">{{ count($tweet->favorites) }}</p>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div class="row justify-content-center">
        <div class="col-md-8 mb-3">
            <ul class="list-group">
                @forelse ($comments as $comment)
                    <li class="list-group-item">
                        <div class="py-3 w-100 d-flex">
                            <img src="{{ asset('storage/profile_image/' .$comment->user->profile_image) }}" class="rounded-circle" width="50" height="50">
                            <div class="ml-2 d-flex flex-column">
                                <p class="mb-0">{{ $comment->user->name }}</p>
                                <a href="{{ url('users/' .$comment->user->id) }}" class="text-secondary">{{ $comment->user->screen_name }}</a>
                            </div>
                            <div class="d-flex justify-content-end flex-grow-1">
                                <p class="mb-0 text-secondary">{{ $comment->created_at->format('Y-m-d H:i') }}</p>
                            </div>
                        </div>
                        <div class="py-3">
                            {!! nl2br(e($comment->text)) !!}
                        </div>
                    </li>
                @empty
                    <li class="list-group-item">
                        <p class="mb-0 text-secondary">コメントはまだありません。</p>
                    </li>
                @endforelse
            </ul>
        </div>
    </div>
</div>
@endsection
```

### ツイート詳細表示画面

これでツイート投稿画面にコメントが表示されるところまで実装できました。

<img width="1440" alt="スクリーンショット 2019-09-18 21.14.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293438/00fbf0b6-62c5-fdb2-8dfd-b3072f34276e.png">

## Create（ツイート投稿機能）

ではツイート投稿機能を実装していきます！
まずは作成画面から作っていきましょう。

### Controller

```php:app/Http/Controllers/TweetsController.php
    public function create()
    {
        $user = auth()->user();

        return view('tweets.create', [
            'user' => $user
        ]);
    }
```

### View

```php:resources/views/tweets/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">Create</div>

                <div class="card-body">
                    <form method="POST" action="{{ route('tweets.store') }}">
                        @csrf

                        <div class="form-group row mb-0">
                            <div class="col-md-12 p-3 w-100 d-flex">
                                <img src="{{ asset('storage/profile_image/' .$user->profile_image) }}" class="rounded-circle" width="50" height="50">
                                <div class="ml-2 d-flex flex-column">
                                    <p class="mb-0">{{ $user->name }}</p>
                                    <a href="{{ url('users/' .$user->id) }}" class="text-secondary">{{ $user->screen_name }}</a>
                                </div>
                            </div>
                            <div class="col-md-12">
                                <textarea class="form-control @error('text') is-invalid @enderror" name="text" required autocomplete="text" rows="4">{{ old('text') }}</textarea>

                                @error('text')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row mb-0">
                            <div class="col-md-12 text-right">
                                <p class="mb-4 text-danger">140文字以内</p>
                                <button type="submit" class="btn btn-primary">
                                    ツイートする
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection
```

### Create(ツイート新規作成画面)

`/tweets/create`
ツイート投稿画面ができました。

<img width="1440" alt="スクリーンショット 2019-09-18 21.31.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293438/abfa952b-8159-e886-1753-964cff1d7693.png">


### Create(ツイート投稿機能)

では実際に投稿されたツイートをバリデーションして保存する処理をやっていきます。

#### Model

ここではControllerでバリデーション通った前提でDBに保存する処理を書いていきます。

```php:app/Models/Tweet.php
    public function tweetStore(Int $user_id, Array $data)
    {
        $this->user_id = $user_id;
        $this->text = $data['text'];
        $this->save();

        return;
    }
```

#### Controller

リクエストから取得したデータをバリデーションして先ほどの`tweetStore()`にデータを渡します。

```php:app/Http/Controllers/TweetsController.php
    public function store(Request $request, Tweet $tweet)
    {
        $user = auth()->user();
        $data = $request->all();
        $validator = Validator::make($data, [
            'text' => ['required', 'string', 'max:140']
        ]);

        $validator->validate();
        $tweet->tweetStore($user->id, $data);

        return redirect('tweets');
    }
```

これでツイート投稿は出来るようになりましたが、直接URLで`/tweets/create`と入力するのは大変なのでナビバーに投稿ボタンをつけましょう。

`<!--追加-->`と記入されている箇所だけ貼り付けてください

```php:resources/views/layouts/app.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <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') }}"></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">
        <!-- Font Awesome -->
        <link href="https://use.fontawesome.com/releases/v5.6.1/css/all.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 align-items-center">
                            <!-- Authentication Links -->
                            @guest
                                <li class="nav-item">
                                    <a class="nav-link" href="{{ route('login') }}">{{ __('Login') }}</a>
                                </li>
                                @if (Route::has('register'))
                                    <li class="nav-item">
                                        <a class="nav-link" href="{{ route('register') }}">{{ __('Register') }}</a>
                                    </li>
                                @endif
                            @else
                                <!-- 追加 -->
                                <li class="nav-item mr-5">
                                    <a href="{{ url('tweets/create') }}" class="btn btn-md btn-primary">ツイートする</a>
                                </li>
                                <li class="nav-item">
                                    <img src="{{ asset('storage/profile_image/' .auth()->user()->profile_image) }}" class="rounded-circle" width="50" height="50">
                                </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>
                                        {{ auth()->user()->name }} <span class="caret"></span>
                                    </a>

                                    <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
                                        <a href="{{ url('users/' .auth()->user()->id) }}" class="dropdown-item">プロフィール</a>
                                        <a href="{{ route('logout') }}" class="dropdown-item"
                                        onclick="event.preventDefault();
                                                        document.getElementById('logout-form').submit();">
                                            {{ __('Logout') }}
                                        </a>

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

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

これでどの画面からでもツイートを投稿できるようになりました！

<img width="1440" alt="スクリーンショット 2019-09-19 1.42.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293438/9169dc13-7665-c0c8-d74a-5dfe465c107b.png">

以上。

次回 -> [【全6回】Laravel5.8でTwitterっぽいSNSツールを作る（第5回ツイートのCRUD機能 編集と削除）](https://qiita.com/namizatop/items/2273bd0dcc817785b131)