Laravel で Ruby on Rails チュートリアル【14章】

## 環境
- Laravel 5.5
- PHP 7.2

# この章でやること
- [14.1 Relationshipモデル](#14-1-)
 - [14.1.1 データモデルの問題(および解決策)](#14-1-1-)
 - [14.1.2 User/Relationshipの関連付け](#14-1-2-)
 - [14.1.3 Relationshipのバリデーション](#14-1-3-)
 - [14.1.4 フォローしているユーザー](#14-1-4-)
 - [14.1.5 フォロワー](#14-1-5-)
- [14.2 Follow のWebインターフェイス](#14-2-)
 - [14.2.1 フォローのサンプルデータ](#14-2-1-)
 - [14.2.2 統計と Follow フォーム](#14-2-2-)
 - [14.2.3 Following と Followers ページ](#14-2-3-)
 - [14.2.4 Follow ボタン (基本編)](#14-2-4-)
 - [14.2.5 Follow ボタン (Ajax編)](#14-2-5-)
 - [14.2.6 フォローをテストする](#14-2-6-)
- [14.3 ステータスフィード](#14-3-)
 - [14.3.1 動機と計画](#14-3-1-)
 - [14.3.2 フィードを初めて実装する](#14-3-2-)
 - [14.3.3 サブセレクト](#14-3-3-)
- [14.4 最後に](#14-4-)
 - [14.4.1 サンプルアプリケーションの機能を拡張する](#14-4-1-)
 - [14.4.2 読み物ガイド](#14-4-2-)
 - [14.4.3 本省のまとめ](#14-4-3-)
 - [14.4.4 役者あとがき](#14-4-4-)

# 14.1 Relationshipモデル

# 14.1.1 データモデルの問題(および解決策)

(/app/Relationship.php)
```php
class Relationship extends Model
{
    protected $guarded = ['id'];
}
```
(/database/migrations/[timestamp]_createrelationships.php)
```php
class CreateRelationships extends Migration
{
    public function up()
    {
        Schema::create('relationships', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('follower_id');
            $table->integer('followed_id');
            $table->timestamps();

            $table->index('follower_id');
            $table->index('followed_id');
            $table->unique(['follower_id', 'followed_id']);

            $table->foreign('follower_id')->references('id')->on('users')->onDelete('cascade');
            $table->foreign('followed_id')->references('id')->on('users')->onDelete('cascade');
        });
    }

    public function down()
    {
        Schema::dropIfExists('relationships');
    }
}
```

# 14.1.2 User/Relationshipの関連付け

(/app/User.php)
```php
    public function activeRelationships()
    {
        return $this->hasMany("App\Relationship", "follower_id");
    }
```
(/app/Relationship.php)
```php
    public function follower()
    {
        return $this->belongsTo("App\User", "id", "follower_id");
    }

    public function followed()
    {
        return $this->belongsTo("App\User", "id", "followed_id");
    }
```

# 14.1.3 Relationshipのバリデーション

# 14.1.4 フォローしているユーザー

(/app/User.php)
```php
    public function following()
    {
        return $this->hasManyThrough("App\User", "App\Relationship", "follower_id", "id", "id", "followed_id");
    }

    public function isFollowing($user_id)
    {
        return (bool) Relationship::where("follower_id", $this->id)->where("followed_id", $user_id)->count();
    }
```
(/tests/Unit/UserTest.php)
```php
    public function testFollowAndUnfollow()
    {
        $user_1 = User::find(1);
        $user_2 = User::find(2);
        $this->assertFalse($user_1->isFollowing($user_2->id));
        $user_1->follow($user_2->id);
        $this->assertTrue($user_1->isFollowing($user_2->id));
        $user_1->unfollow($user_2->id);
        $this->assertFalse($user_1->isFollowing($user_2->id));
    }
```
(/app/User.php)
```php
    public function follow($user_id)
    {
        $this->activeRelationships()->create(["followed_id" => $user_id]);
    }

    public function unfollow($user_id)
    {
        $this->activeRelationships()->where("followed_id", $user_id)->delete();
    }

    public function isFollowed($user_id)
    {

        return (bool) Relationship::where("follower_id", $user_id)->where("followed_id", $this->id)->count();
    }
```

# 14.1.5 フォロワー

(/app/User.php)
```php
    public function passiveRelationships()
    {
        return $this->hasMany("App\Relationship", "followed_id");
    }

    public function followers()
    {
        return $this->hasManyThrough("App\User", "App\Relationship", "followed_id", "id", "id", "follower_id");
    }
```
(/tests/Unit/UserTest.php)
```php
$this->assertEquals(1, $user_1->following()->where("followed_id", $user_2->id)->count());
```

# 14.2 Follow のWebインターフェイス

# 14.2.1 フォローのサンプルデータ

(/database/seeds/UsersTableSeeder.php)
```php
        $user = User::first();
        $following = User::where("id",  ">=", 2)->where("id", "<=", 50);
        $followers = User::where("id",  ">=", 3)->where("id", "<=", 40);
        $following->each(function ($following) use ($user) { $user->follow($following->id); });
        $followers->each(function ($follower)  use ($user) { $follower->follow($user->id);  });
```

# 14.2.2 統計と Follow フォーム

(/routes/web.php)
```php
Route::prefix('users')->group(function () {
    Route::get('{user}/following', "UsersController@following")->name("following");
    Route::get('{user}/followers', "UsersController@followers")->name("followers");
});
```
(/resources/views/shared/stats.blade.php)
```html
@php empty($user) && $user = Auth::user() @endphp
<div class="stats">
    <a href="{{ route("following", $user->id) }}">
        <strong id="following" class="stat">
            {{ $user->following()->count() }}
        </strong>
        following
    </a>
    <a href="{{ route("followers", $user->id) }}">
        <strong id="followers" class="stat">
            {{ $user->followers()->count() }}
        </strong>
        followers
    </a>
</div>
```
(/resources/views/static_pages/home.blade.php)
```html
            <section class="stats">
                @include ("shared.stats")
            </section>
```
(/routes/web.php)
```php
Route::resource('relationships', "RelationshipsController", ["only" => ['store', 'destroy']]);
```
(/resources/views/users/follow_form.blade.php)
```html
@unless ($user == Auth::user())
    <div id="follow_form">
        @if (Auth::user()->isFollowing($user->id))
            @include("users.unfollow")
        @else
            @include("users.follow")
        @endif
    </div>
@endunless
```
(/resources/views/users/follow.blade.php)
```html
{{ Form::open(["route" => "relationships.store"]) }}
{{ Form::hidden('followed_id', $user->id) }}
{{ Form::submit("Follow", ["class" => "btn btn-primary"]) }}
{{ Form::close() }}
```
(/resources/views/users/unfollow.blade.php)
```html
{{ Form::open(["route" => ["relationships.destroy", $user->id], "method" => "delete"]) }}
{{ Form::submit("Unfollow", ["class" => "btn"]) }}
{{ Form::close() }}
```
(/resources/views/users/show.blade.php)
```html
@extends('layouts.application')

@section('title', $user->name)

@section('content')
<div class="row">
    <aside class="col-md-4">
        <section class="user_info">
            <h1>
              {!! gravatar_for($user) !!}
              {{ $user->name }}
            </h1>
        </section>
        <section class="stats">
            @include("shared.stats")
        </section>
    </aside>
    <div class="col-md-8">
        @includeWhen (Auth::check(), "users.follow_form")
        @if ($user->microposts())
            <h3>Microposts ({{ $user->microposts()->count() }})</h3>
            <ol class="microposts">
                @foreach ($microposts as $micropost)
                    @include ("microposts.micropost")
                @endforeach
            </ol>
            {{ $microposts->links() }}
        @endif
    </div>
</div>
@endsection
```

# 14.2.3 Following と Followers ページ

(/tests/Unit/UsersControllerTest.php)
```php
    public function testRedirectFollowing()
    {
        $response = $this->get(route("following", 1));
        $response->assertRedirect(route("login"));
    }

    public function testRedirectFollowers()
    {
        $response = $this->get(route("followers", 1));
        $response->assertRedirect(route("login"));
    }
```
(/app/Http/Controllers/UsersController.php)
```php
    public function __construct()
    {
        $this->middleware('guest')->only(["index", "edit", "update", "destroy", "following", "followers"]);
    }

    public function following($user)
    {
        $title = "Following";
        $user  = User::find($user);
        $users = $user->following()->paginate(30);
        return view("users.show_follow")->with(["title" => $title, "user" => $user, "users" => $users]);
    }

    public function followers($user)
    {
        $title = "Following";
        $user  = User::find($user);
        $users = $user->followers()->paginate(30);
        return view("users.show_follow")->with(["title" => $title, "user" => $user, "users" => $users]);
    }
```
(/resources/views/users/show_follow.blade.php)
```html
@extends('layouts.application')

@section('title', $title)

@section('content')
<div class="row">
    <aside class="col-md-4">
        <section class="user_info">
            {!! gravatar_for($user) !!}
                <h1>{{ $user->name }}</h1>
            <span>{{ Html::linkRoute("users.show", "view my profile", $user->id) }}</span>
            <span><b>Microposts:</b> {{ $user->microposts()->count() }}</span>
        </section>
        <section class="stats">
            @include("shared.stats")
            @if ($users)
                <div class="user_avatars">
                    @foreach ($users as $user)
                        <a href="{{ route("users.show", $user->id) }}" >{!! gravatar_for($user, ["size" => 30]) !!}</a>
                    @endforeach
                </div>
            @endif
        </section>
    </aside>
    <div class="col-md-8">
        <h3>{{ $title }}</h3>
        @if ($users)
            <ul class="users follow">
                @foreach ($users as $user)
                    @include("users.user", ["user" => $user])
                @endforeach
            </ul>
            {{ $users->links() }}
        @endif
    </div>
</div>
@endsection
```
(/database/seeds/test/TestSeeder.php)
```php
        Relationship::create(["follower_id" => 1, "followed_id" => 3]);
        Relationship::create(["follower_id" => 1, "followed_id" => 4]);
        Relationship::create(["follower_id" => 3, "followed_id" => 1]);
        Relationship::create(["follower_id" => 2, "followed_id" => 1]);
```
(/tests/Feature/FollowingTest.php)
```php
class FollowingTest extends TestCase
{
    private $user;

    protected function setUp()
    {
        parent::setUp();
        Artisan::call('migrate:refresh');
        $this->seed('TestSeeder');
        $this->user = User::find(1);
        $this->be($this->user);
    }

    public function testFollowingPage()
    {
        $response = $this->get(route("following", $this->user->id));
        $this->assertNotEmpty($this->user->following());
        $response->assertSeeText((string) $this->user->following()->count());
        $dom = $this->dom($response->content());
        foreach ($this->user->following as $following) {
            $this->assertSame(route("users.show", $following->id), $dom->filter("a:contains(\"{$following->name}\")")->attr("href"));
        }
    }

    public function testFollowersPage()
    {
        $response = $this->get(route("followers", $this->user->id));
        $this->assertNotEmpty($this->user->followers());
        $response->assertSeeText((string) $this->user->followers()->count());
        $dom = $this->dom($response->content());
        foreach ($this->user->followers as $follower) {
            $this->assertSame(route("users.show", $follower->id), $dom->filter("a:contains(\"{$follower->name}\")")->attr("href"));
        }
    }
}
```

# 14.2.4 Follow ボタン (基本編)

(/tests/Unit/RelationshipsControllerTest.php)
```php
class RelationshipsControllerTest extends TestCase
{
    public function testRedirectCreatePage()
    {
        $count = Relationship::all()->count();
        $response = $this->post(route("relationships.create"));
        $this->assertEquals($count, Relationship::all()->count());
        $response->assertRedirect(route("login"));
    }

    public function testRedirectDestroyPage()
    {
        $count = Relationship::all()->count();
        $response = $this->delete(route("relationships.destory", 1));
        $this->assertEquals($count, Relationship::all()->count());
        $response->assertRedirect(route("login"));
    }
}
```
(/app/Http/Controllers/RelationshipsController.php)
```php
class RelationshipsController extends Controller
{
    public function __construct()
    {
        $this->middleware('authenticate');
    }

    public function store(Request $request)
    {
        Auth::user()->follow($request->followed_id);
        return redirect()->route("users.show", Auth::id());
    }

    public function destroy($id)
    {
        Auth::user()->unfollow($id);
        return redirect()->route("users.show", Auth::id());
    }
}
```

# 14.2.5 Follow ボタン (Ajax編)

# 14.2.6 フォローをテストする

(/tests/Feature/FollowingTest.php)
```php
class FollowingTest extends TestCase
{
    private $user;
    private $other_user;

    protected function setUp()
    {
        parent::setUp();
        Artisan::call('migrate:refresh');
        $this->seed('TestSeeder');
        $this->user = User::find(1);
        $this->other_user = User::find(2);
        $this->be($this->user);
    }

    public function testFollowUser()

    {
        $count = Relationship::all()->count();
        $this->post(route("relationships.store"), ["followed_id" => $this->other_user->id]);
        $this->assertEquals($count + 1, Relationship::all()->count());
    }

    public function testUnfollowUser()
    {
        $this->user->follow($this->other_user->id);
        $count = Relationship::all()->count();
        $this->delete(route("relationships.destroy", $this->other_user->id));
        $this->assertEquals($count - 1, Relationship::all()->count());
    }
}
```

# 14.3 ステータスフィード

# 14.3.1 動機と計画

(/tests/Unit/UserTest.php)
```php
    public function testStatusFeed()
    {
        $user_1 = User::find(1);
        $user_2 = User::find(2);
        $user_3 = User::find(3);
        foreach ($user_3->microposts as $micropost) {
            $this->assertTrue($user_1->feed()->get()->contains($micropost));
        }
        foreach ($user_1->microposts as $micropost) {
            $this->assertTrue($user_1->feed()->get()->contains($micropost));
        }
        foreach ($user_2->microposts as $micropost) {
            $this->assertFalse($user_1->feed()->get()->contains($micropost));
        }
    }
```

# 14.3.2 フィードを初めて実装する

(/app/User.php)
```php
    public function feed()
    {
        $relations = $this->activeRelationships()->get()->toArray();
        $followed_ids = array_pluck($relations, "followed_id");
        return Micropost::whereIn("user_id", $followed_ids)->orWhere("user_id", $this->id);
    }
```
(/app/Http/Controllers/StaticPagesController.php)
```php
    public function home()
    {
        $feed_items = null;
        if (Auth::check()) {
            $feed_items = Auth::user()->feed()->paginate(30);
        }
        return view('static_pages/home')->with("feed_items", $feed_items);
    }
```

# 14.3.3 サブセレクト

# 14.4 最後に

# 14.4.1 サンプルアプリケーションの機能を拡張する

# 14.4.2 読み物ガイド

# 14.4.3 本省のまとめ

# 14.4.4 役者あとがき