Edited at

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


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


概要

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


前回

第4回はツイートのCRUD機能を追加していきます。

この回は少し長くなってしまったので前半(4回)と後半(5回)で分けていきたいと思います。


前提


  • PHPをある程度理解している

  • Homesteadをインストールしている

  • MVC構造をある程度理解している


環境


  • Mac

  • Homestead

  • Laravel 5.8


ルーティング

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

php artisan make:controller TweetsController --resource

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


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.phpTweet.phpを使用していきます。


Follower

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


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を介して取得したと仮定してそのデータを引数で渡します。


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しておきます。



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ファイルもついでに作っちゃいます。

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

ログインしているユーザの場合のみ編集と削除アイコンが表示される様にしているのと、

コメント数といいね数を表示する様にしました。


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



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にアクセスするとフォローしている人と自身のツイートのみ編集出来る様になったと思います。

スクリーンショット 2019-09-18 0.54.20.png

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


Read(ツイート詳細表示機能)

一覧表示に続いてツイート詳細画面も作っていきます。

機能的にはさほど変わらないので駆け足で書いていきます。


Model

詳細画面ではコメントが閲覧できるようにcommentsテーブルからもデータを取得します。

そのためTweetComment両方のModelを書いていきます。


Tweet

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


app/Models/Tweet.php

    // 詳細画面

public function getTweet(Int $tweet_id)
{
return $this->with('user')->where('id', $tweet_id)->first();
}


Comment

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


app/Models/Comment.php

    public function getComments(Int $tweet_id)

{
return $this->with('user')->where('tweet_id', $tweet_id)->get();
}


Controller

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


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


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



ツイート詳細表示画面

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

スクリーンショット 2019-09-18 21.14.07.png


Create(ツイート投稿機能)

ではツイート投稿機能を実装していきます!

まずは作成画面から作っていきましょう。


Controller


app/Http/Controllers/TweetsController.php

    public function create()

{
$user = auth()->user();

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



View


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

ツイート投稿画面ができました。

スクリーンショット 2019-09-18 21.31.56.png


Create(ツイート投稿機能)

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


Model

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


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()にデータを渡します。


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と入力するのは大変なのでナビバーに投稿ボタンをつけましょう。

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


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>


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

スクリーンショット 2019-09-19 1.42.21.png

以上。

次回 -> 【全6回】Laravel5.8でTwitterっぽいSNSツールを作る(第5回ツイートのCRUD機能 編集と削除)