takanori-matsushita / laravel-practice

http://laraveltutorial.herokuapp.com/
0 stars 0 forks source link

rails tutorial 14章をlaravelで実装 #12

Open takanori-matsushita opened 4 years ago

takanori-matsushita commented 4 years ago

branch: following-users

takanori-matsushita commented 4 years ago

14.1 Relationshipモデル

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

Relationshipモデルの作成 php artisan make:model Relationship -a

リスト 14.1: relationshipsテーブルにインデックスを追加する database/migrations/[time_stamps]_create_relationships_table.php

  public function up()
  {
    Schema::create('relationships', function (Blueprint $table) {
      $table->unsignedInteger('follower_id');
      $table->unsignedInteger('followed_id');
      $table->timestamps();
      $table->index('follower_id');
      $table->index('followed_id');
      $table->index(['follower_id', 'followed_id']);
      $table->unique(['follower_id', 'followed_id']);

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

インクリメント(id)を使用しない宣言とプライマリーキーの指定をする。 app/Relationship.php

class Relationship extends Model
{
  protected $primaryKey = [
    'follower_id', 'followed_id'
  ];

  public $incrementing = false;
}
takanori-matsushita commented 4 years ago

演習 省略

takanori-matsushita commented 4 years ago

14.1.2 User/Relationshipの関連付け

リスト 14.2: 能動的関係に対して1対多 (has_many) の関連付けを実装する app/User.php

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

app/Observers/UserObserver.php

  public function deleted(User $user)
  {
    $user->microposts->each(function ($post) {
      $post->delete();
    });
// 紐付いているfollower_idのデータをすべて削除
    $user->active_relationships->each(function ($follower) {
      $follower->delete();
    });
  }
takanori-matsushita commented 4 years ago

リスト 14.3: リレーションシップ/フォロワーに対してbelongs_toの関連付けを追加する app/Relationship.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Relationship extends Model
{
  public function follower()
  {
    $this->belongsTo('App\User');
  }

  public function followed()
  {
    $this->belongsTo('App\User');
  }
}
takanori-matsushita commented 4 years ago

演習

  1. >>> $user = User::find(1)
    => App\User {#3133
     id: 1,
     name: "Example User",
     email: "example@railstutorial.org",
     email_verified_at: null,
     created_at: "2020-04-26 10:09:32",
     updated_at: "2020-04-26 10:09:32",
    }
    >>> $other_user = User::find(2)
     id: 2,
     name: "Johnson McClure MD",
     email: "fsenger@example.net",
     email_verified_at: null,
     created_at: "2020-04-26 10:09:37",
     updated_at: "2020-04-26 10:09:37",
    }
    >>> $user->active_relationships()->create(['followed_id'=> $other_user->id])
    => App\Relationship {#3138
     followed_id: 2,
     follower_id: 1,
     updated_at: "2020-04-26 10:14:18",
     created_at: "2020-04-26 10:14:18",
     id: 1,
    }
  2. 上記参照
takanori-matsushita commented 4 years ago

14.1.3 Relationshipのバリデーション

リスト 14.4: Relationshipモデルのバリデーションをテストする 省略

takanori-matsushita commented 4 years ago

リスト 14.5: Relationshipモデルに対してバリデーションを追加する リクエストの作成 php artisan make:requiest RelationshipRequest

app/Http/Requests/RelationshipRequest.php

  public function rules()
  {
    return [
      'follower_id' => 'required',
      'followed_id' => 'required'
    ];
  }
takanori-matsushita commented 4 years ago

リスト 14.6: Relationship用のfixtureを空にする green 省略

takanori-matsushita commented 4 years ago

14.1.4 フォローしているユーザー リスト 14.8: Userモデルにfollowingの関連付けを追加する app/User.php

class User extends Authenticatable
{
  : 
  public function following()
  {
    return $this->belongsToMany(self::class, 'relationships', 'follower_id', 'followed_id');
  }
}
takanori-matsushita commented 4 years ago

リスト 14.9: “following” 関連のメソッドをテストする red 省略

takanori-matsushita commented 4 years ago

リスト 14.10: "following" 関連のメソッド green app/User.php

class User extends Authenticatable
{
  : 
  public function follow(Int $other_user)
  {
    return $this->following()->attach($other_user->id);
  }

  public function unfollow(Int $other_user)
  {
    return $this->following()->detach($other_user->id);
  }

  public function is_following(Int $other_user)
  {
    return (boolean) $this->following()->where('followed_id', $other_user->id)->first(['id'])
  }
}
takanori-matsushita commented 4 years ago

リスト 14.11: green 省略

takanori-matsushita commented 4 years ago

14.1.5 フォロワー

リスト 14.12: 受動的関係を使ってuser.followersを実装する app/User.php

class User extends Authenticatable
{
  : 
  public function passive_relationships()
  {
    return $this->hasMany('App\Relationship', 'followed_id');
  }
  : 
  public function followers()
  {
    return $this->belongsToMany(self::class, 'relationships', 'followed_id', 'follower_id');
  }
}

app/Observers/UserObserver.php

  : 
  public function deleted(User $user)
  {
    $user->microposts->each(function ($post) {
      $post->delete();
    });
    $user->active_relationships->each(function ($follower) {
      $follower->delete();
    });
    $user->passive_relationships->each(function ($followed) {  //追加
      $followed->delete();
    })
  }
takanori-matsushita commented 4 years ago

リスト 14.13: followersに対するテスト green 省略

takanori-matsushita commented 4 years ago

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

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

リスト 14.14: サンプルデータにfollowing/followerの関係性を追加する database/seeds/RelationshipSeeder.php

<?php

use App\User;
use App\Relationship;
use Illuminate\Database\Seeder;

class RelationshipSeeder extends Seeder
{
  /**
   * Run the database seeds.
   *
   * @return void
   */
  public function run()
  {
    $users = User::all();
    $user = $users->first();
    $following = range(3, 51);
    $followers = range(4, 41);

    foreach ($followers as $follower) {
      factory(Relationship::class)->create([
        'followed_id' => $user->id,
        'follower_id' => $follower
      ]);
    }

    foreach ($following as $followed) {
      factory(Relationship::class)->create([
        'followed_id' => $followed,
        'follower_id' => $user->id
      ]);
    }
  }
}

RelationshipSeederの登録 database/seeds/DatabaseSeeder.php

<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
  /**
   * Seed the application's database.
   *
   * @return void
   */
  public function run()
  {
    $this->call(UsersTableSeeder::class);
    $this->call(MicropostSeeder::class);
    $this->call(RelationshipSeeder::class);  //追加
  }
}
takanori-matsushita commented 4 years ago

演習 1, 2.

>>> $user = User::first()
>>> count($user->followers()->get())
=> 38
>>> count($user->following()->get())
=> 49
takanori-matsushita commented 4 years ago

14.2.2 統計と [Follow] フォーム

リスト 14.15: Usersコントローラにfollowingアクションとfollowersアクションを追加する routes/web.php

Route::get('/users/{user}/following', 'UsersController@following')->name('users.following');
Route::get('/users/{user}/followers', 'UsersController@followers')->name('users.followers');
takanori-matsushita commented 4 years ago

リスト 14.16: フォロワーの統計情報を表示するパーシャル resources/views/shared/stats.blade.php

<div class="stats">
  <a href="{{route('users.following', $user)}}">
    <strong id="following" class="stat">
      {{$user->following()->count()}}
    </strong>
    following
  </a>
  <a href="{{route('users.followers', $user)}}">
    <strong id="followers" class="stat">
      {{$user->followers()->count()}}
    </strong>
    followers
  </a>
</div>

このままだとundefined userとエラーが出るため、コントローラで$userを渡す app/Http/Controllers/StaticPagesController.php

class StaticPagesController extends Controller
{
  public function home()
  {
    // \Auth::user() ? $feed_items = \Auth::user()->microposts()->where('user_id', \Auth::id())->paginate(30) : $feed_items = [];
    \Auth::user() ? $feed_items = \Auth::user()->feed()->paginate(30) : $feed_items = [];
    $user = \Auth::user();
    return view('static_pages.home', ['user' => $user, 'feed_items' => $feed_items]);
  }
  : 
}
takanori-matsushita commented 4 years ago

リスト 14.17: Homeページにフォロワーの統計情報を追加する resources/views/static_pages/authorize/true.blade.php

<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      @include('shared.user_info')
    </section>
    <section class="stats">  //追加
      @include('shared.stats')  //追加
    </section>  //追加
    <section class="micropost_form">
      @include('shared.micropost_form')
    </section>
  </aside>
  <div class="col-md-8">
    <h3>Micropost Feed</h3>
    @include('shared.feed')
  </div>
</div>
takanori-matsushita commented 4 years ago

リスト 14.18: Homeページのサイドバー用のSCSS resources/sass/_custom.scss

.gravatar {
  float: left;
  margin-right: 10px;
}

.gravatar_edit {
  margin-top: 15px;
}

.stats {
  overflow: auto;
  margin-top: 0;
  padding: 0;
  a {
    float: left;
    padding: 0 10px;
    border-left: 1px solid $gray-light;
    color: gray;
    &:first-child {
      padding-left: 0;
      border: 0;
    }
    &:hover {
      text-decoration: none;
      color: blue;
    }
  }
  strong {
    display: block;
  }
}

.user_avatars {
  overflow: auto;
  margin-top: 10px;
  .gravatar {
    margin: 1px 1px;
  }
  a {
    padding: 0;
  }
}

.users.follow {
  padding: 0;
}

以下コマンドを実行 yarn dev

takanori-matsushita commented 4 years ago

リスト 14.19: フォロー/フォロー解除フォームのパーシャル resources/views/users/follow_form.blade.php

@auth
<div id="follow_form">
  @if (Auth::user()->is_following($user))
  <%= render 'unfollow' %>
  @include('users.unfollow')
  @else
  <%= render 'follow' %>
  @include('users.follow')
  @endif
</div>
@endauth
takanori-matsushita commented 4 years ago

リスト 14.20: Relationshipリソース用のルーティングを追加する routes/web.php

Route::resource('relationships', 'RelationshipController')->only(['store', 'destroy']);
takanori-matsushita commented 4 years ago

リスト 14.21: ユーザーをフォローするフォーム resources/views/users/follow.blade.php

<form action="{{route('relationships.store')}}" method="post">
  @csrf
  <div><input type="hidden" name="followed_id" value="{{$user->id}}"></div>
  <input type="submit" class="btn btn-primary" value="Follow">
</form>
takanori-matsushita commented 4 years ago

リスト 14.22: ユーザーをフォロー解除するフォーム resources/views/users/unfollow.blade.php

<form action="{{route('relationships.destroy', $user->id)}}" method="post">
  @csrf
  @method('delete')
  <div><input type="hidden" name="followed_id" value="{{$user->id}}"></div>
  <input type="submit" class="btn" value="Unfollow">
</form>
takanori-matsushita commented 4 years ago

演習 省略

takanori-matsushita commented 4 years ago

14.2.3 [Following] と [Followers] ページ リスト 14.24: フォロー/フォロワーページの認可をテストする red 省略

takanori-matsushita commented 4 years ago

リスト 14.25: followingアクションとfollowersアクション red app/Http/Controllers/UsersController.php

{
  public function __construct()
  {
    $this->middleware('auth', [
      'only' => ['index', 'edit', 'update', 'destroy', 'following', 'followers']  //following, followers追加
    ]);
  : 省略
  }
  : 省略
  public function following(User $user)
  {
    $title = 'Following';
    $users = $user->following()->paginate(30);
    return view('users.show_follow', ['title' => $title, 'users' => $users, 'user' => $user]);
  }

  public function followers(User $user)
  {
    $title = 'Followers';
    $users = $user->followers()->paginate(30);
    return view('users.show_follow', ['title' => $title, 'users' => $users, 'user' => $user]);
  }
takanori-matsushita commented 4 years ago

リスト 14.26: フォローしているユーザーとフォロワーの両方を表示するshow_followビュー green resources/views/users/show_follow.blade.php

@extends('layouts.layout')
@section('content')
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <img src="{{ gravator_for($user) }}">
      <h1>{{ $user->name }}</h1>
      <span>
        <a href="{{route('users.show', $user)}}">view my profile</a>
      </span>
      <span><b>Microposts:</b> {{ $user->microposts()->count() }}</span>
    </section>
    <section class="stats">
      @include('shared.stats')
      @if(!empty($users))
      <div class="user_avatars">
        @foreach($users as $user)
        <a href="{{route('users.show', $user)}}">
          <img src="{{ gravator_for($user, ['size'=>30]) }}">
        </a>
        @endforeach
      </div>
      @endif
    </section>
  </aside>
  <div class="col-md-8">
    <h3>{{ $title }}</h3>
    @if(!empty($users))
    <ul class="users follow">
      @each('components.user', $users, 'user')
    </ul>
    {{$users->links()}}
    @endif
  </div>
</div>
@endsection
takanori-matsushita commented 4 years ago

リスト 14.27: green 省略

takanori-matsushita commented 4 years ago

リスト 14.28: following/followerをテストするためのリレーションシップ用fixture 省略

takanori-matsushita commented 4 years ago

リスト 14.29: following/followerページのテスト green 省略

takanori-matsushita commented 4 years ago

リスト 14.30: green 省略

takanori-matsushita commented 4 years ago

14.2.4 [Follow] ボタン (基本編)

リスト 14.31: リレーションシップの基本的なアクセス制御に対するテスト red 省略

takanori-matsushita commented 4 years ago

リスト 14.32: リレーションシップのアクセス制御 green routes/web.php

Route::resource('relationships', 'RelationshipController')->only(['store', 'destroy'])->middleware('auth');

これでも良いが、これまでauthミドルウェアを使ったルーティングをまとめることができる。 routes/web.php

Route::get('/', 'StaticPagesController@home')->name('root');
Route::get('/help', 'StaticPagesController@help')->name('help');
Route::get('/about', 'StaticPagesController@about')->name('about');
Route::get('/contact', 'StaticPagesController@contact')->name('contact');
Route::resource('users', 'UsersController')->except(['create', 'store']);
Route::get('/signup', 'Auth\RegisterController@showRegistrationForm')->name('users.signup');
Route::get('/users/{user}/following', 'UsersController@following')->name('users.following');
Route::get('/users/{user}/followers', 'UsersController@followers')->name('users.followers');
//ミドルウェアのauthを使用するグループのルーティング
Route::group(['middleware' => 'auth'], function () {
  Route::resource('microposts', 'MicropostController',)->only(['store', 'destroy']);
  Route::resource('relationships', 'RelationshipController')->only(['store', 'destroy']);
});
Auth::routes();
takanori-matsushita commented 4 years ago

リスト 14.33: Relationshipsコントローラ app/Http/Controllers/RelationshipController.php

class RelationshipController extends Controller
{
  /**
   * Store a newly created resource in storage.
   *
   * @param  \Illuminate\Http\Request  $request
   * @return \Illuminate\Http\Response
   */
  public function store(Request $request)
  {
    $user = User::find($request->followed_id);
    \Auth::user()->follow($user);
    return back();
  }

  /**
   * Remove the specified resource from storage.
   *
   * @param  \Illuminate\Http\Request  $request
   * @return \Illuminate\Http\Response
   */
  public function destroy(Request $request)
  {
    \Auth::user()->unfollow($request->followed_id);
    return back();
  }
}
takanori-matsushita commented 4 years ago

14.2.5 [Follow] ボタン (Ajax編)

CSRFの対策処理 resources/views/layouts/laravel_default.blade.php

<meta name="csrf-token" content="{{csrf_token()}}">  //追加
<link rel="stylesheet" href={{ asset('/css/app.css') }}>
<script src={{'/js/app.js'}} defer></script>
takanori-matsushita commented 4 years ago

14.3 ステータスフィード

14.3.1 動機と計画

リスト 14.42: ステータスフィードのテスト red 省略

takanori-matsushita commented 4 years ago

リスト 14.43: red 省略

takanori-matsushita commented 4 years ago

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

リスト 14.44: とりあえず動くフィードの実装 green リスト 14.45: green 省略

takanori-matsushita commented 4 years ago

14.3.3 サブセレクト

リスト 14.46: whereメソッド内の変数に、キーと値のペアを使う green 省略

takanori-matsushita commented 4 years ago

リスト 14.47: フィードの最終的な実装 green app/User.php

  public function feed()
  {
    $follower_id = \Auth::id();
    $following_ids = Relationship::where('follower_id', $follower_id)->get('followed_id');
    return Micropost::whereIn('user_id', $following_ids)->orWhere('user_id', $follower_id);

  }
takanori-matsushita commented 4 years ago

リスト 14.48: green 省略