1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

環境

  • Laravel 5.5
  • PHP 7.2

この章でやること

14.1 Relationshipモデル

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

(/app/Relationship.php)

class Relationship extends Model
{
    protected $guarded = ['id'];
}

(/database/migrations/[timestamp]_createrelationships.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)

    public function activeRelationships()
    {
        return $this->hasMany("App\Relationship", "follower_id");
    }

(/app/Relationship.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)

    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)

    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)

    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)

    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)

$this->assertEquals(1, $user_1->following()->where("followed_id", $user_2->id)->count());

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

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

(/database/seeds/UsersTableSeeder.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)

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)

@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)

            <section class="stats">
                @include ("shared.stats")
            </section>

(/routes/web.php)

Route::resource('relationships', "RelationshipsController", ["only" => ['store', 'destroy']]);

(/resources/views/users/follow_form.blade.php)

@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)

{{ 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)

{{ Form::open(["route" => ["relationships.destroy", $user->id], "method" => "delete"]) }}
{{ Form::submit("Unfollow", ["class" => "btn"]) }}
{{ Form::close() }}

(/resources/views/users/show.blade.php)

@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)

    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)

    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)

@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)

        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)

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)

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)

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)

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)

    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)

    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)

    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 役者あとがき

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?