takanori-matsushita / laravel-practice

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

rails tutorial 13章をlaravelで実装 #10

Open takanori-matsushita opened 4 years ago

takanori-matsushita commented 4 years ago

branch: user-microposts

takanori-matsushita commented 4 years ago

13.1 Micropostモデル

13.1.1 基本的なモデル

リスト 13.1: Micropostモデルを生成する php artisan make:model Micropost -a -aオプションを付けることでfactory, migration, seeder, controllerが同時に生成される。

takanori-matsushita commented 4 years ago

リスト 13.2: 自動生成されたMicropostモデル Laravelでは自動生成されないので、以下のようにuserメソッドを実装する。

app/Micropost.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Micropost extends Model
{
  public function user()
  {
    return $this->belongsTo('App\User');
  }
}
takanori-matsushita commented 4 years ago

リスト 13.3: インデックスが付与されたMicropostのマイグレーション マイグレーションファイルのupメソッドを以下のように書き換える。

database/migrations/[timestamps]_create_microposts_table.php

  public function up()
  {
    Schema::create('microposts', function (Blueprint $table) {
      $table->id();
      $table->string('content');
      $table->integer('user_id');
      $table->timestamps();
      $table->index(['user_id', 'created_at']);
    });
  }

できたらマイグレーションを実行する。 php artisan migrate

takanori-matsushita commented 4 years ago

演習

  1. >>> $post = new Micropost()
    => App\Micropost {#3108}
    >>> $post->user_id = 1
    => 1
    >>> $post->content = 'Lorem ipsum'
    => "Lorem ipsum"
    >>> $post->created_at
    => null
    >>> $post->updated_at
    => null
  2. >>> $post->user
    => App\User {#3117
     id: 1,
     name: "Example User",
     email: "example@railstutorial.org",
     email_verified_at: "2020-04-23 08:55:10",
     created_at: "2020-04-23 08:47:10",
     updated_at: "2020-04-23 16:15:35",
    }
  3. >>> $post->save()
    => true
    >>> $post->created_at
    => Illuminate\Support\Carbon @1587700290 {#3109
     date: 2020-04-24 03:51:30.0 UTC (+00:00),
     timezone: "UTC",
    }
    >>> $post->updated_at
    => Illuminate\Support\Carbon @1587700290 {#3121
     date: 2020-04-24 03:51:30.0 UTC (+00:00),
     timezone: "UTC",
    }
takanori-matsushita commented 4 years ago

13.1.2 Micropostのバリデーション

リスト 13.4: 新しいMicropostの有効性に対するテスト green 省略

takanori-matsushita commented 4 years ago

リスト 13.5: マイクロポストのuser_idに対する検証 green リクエストの作成 php artisan make:request MicropostFromRequest

app/Http/Requests/MicropostFormRequest.php rulesメソッドを以下のように編集

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

リスト 13.6: green 省略

takanori-matsushita commented 4 years ago

リスト 13.7: Micropostモデルのバリデーションに対するテスト red 省略

takanori-matsushita commented 4 years ago

リスト 13.8: Micropostモデルのバリデーション green app/Micropost.php

  public function rules()
  {
    return [
      'user_id' => 'required',
      'content' => 'required|max:140'
    ];
  }
takanori-matsushita commented 4 years ago

リスト 13.9: green 省略

takanori-matsushita commented 4 years ago

13.1.3 User/Micropostの関連付け

先のMicropostモデルに記述済み以下のようになっていれば良い。

リスト 13.10: マイクロポストがユーザーに所属する (belongs_to) 関連付け green

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Micropost extends Model
{
  public function user()
  {
    return $this->belongsTo('App\User');
  }
}
takanori-matsushita commented 4 years ago

リスト 13.11: ユーザーがマイクロポストを複数所有する (has_many) 関連付け green app/User.php

  public function micropost()
  {
    $this->hasMany('App\Micropost');
  }
takanori-matsushita commented 4 years ago

リスト 13.12: 慣習的に正しくマイクロポストを作成する green 省略

takanori-matsushita commented 4 years ago

リスト 13.13: green 省略

takanori-matsushita commented 4 years ago

演習

  1. $user = User::find(1)
    => App\User {#3104
     id: 1,
     name: "Example User",
     email: "example@railstutorial.org",
     email_verified_at: "2020-04-23 08:55:10",
     created_at: "2020-04-23 08:47:10",
     updated_at: "2020-04-23 16:15:35",
    }
    >>> $user->micropost->content = "Lorem ipsum"=> "Lorem ipsum"
    >>> $user->save()
    => true
  2. >>> $user->micropost
    => Illuminate\Database\Eloquent\Collection {#3128
     all: [
       App\Micropost {#3126
         id: 1,
         content: "Lorem ipsum",
         user_id: 1,
         created_at: "2020-04-24 03:51:30",
         updated_at: "2020-04-24 03:51:30",
       },
     ],
    }
  3. >>> $user = User::find(1)
    => App\User {#3135
     id: 1,
     name: "Example User",
     email: "example@railstutorial.org",
     email_verified_at: "2020-04-23 08:55:10",
     created_at: "2020-04-23 08:47:10",
     updated_at: "2020-04-23 16:15:35",
    }
    >>> $micropost = Micropost::find(1)
    => App\Micropost {#3129
     id: 1,
     content: "Lorem ipsum",
     user_id: 1,
     created_at: "2020-04-24 03:51:30",
     updated_at: "2020-04-24 03:51:30",
    }
    >>> $user == $micropost->user
    => true
takanori-matsushita commented 4 years ago

13.1.4 マイクロポストを改良する

リスト 13.14: マイクロポストの順序付けをテストする red 省略

takanori-matsushita commented 4 years ago

リスト 13.15: マイクロポスト用のfixture 省略

takanori-matsushita commented 4 years ago

リスト 13.16: red 省略

takanori-matsushita commented 4 years ago

リスト 13.17: default_scopeでマイクロポストを順序付ける green クエリスコープ機能を利用する。グローバルスコープとローカルスコープがあるが、常に降順で取り出したいため、グローバルスコープを利用して実装する。

app/Micropost.php

: 
use Illuminate\Database\Eloquent\Builder;

class Micropost extends Model
{
  /**
   * @return void
   */
  protected static function booted()
  {
    static::addGlobalScope('order', function (Builder $builder) {
      $builder->orderBy('created_at', 'desc');
    });
  }
}

[公式]クエリスコープ

takanori-matsushita commented 4 years ago

リスト 13.18: green 省略

takanori-matsushita commented 4 years ago

リスト 13.19: マイクロポストは、その所有者 (ユーザー) と一緒に破棄されることを保証する Observerを作成する。ObserverとはEloquentのイベント発生によって発火するメソッドの集まりのこと。 php artisan make:observer UserObserver --model=User --modelオプションは指定したモデルのObserverの雛形を作成してくれる。何らかのアクションが起こった際に、各CRUDで関連するモデルへ処理を実行する。 ここでは、Userモデルでユーザーが削除された際に、紐付いているMicropostも合わせて削除するため、以下のように編集する。

app/Observers/UserObserver.php

  public function deleted(User $user)
  {
    $user->microposts->each(function ($post) {
      $post->delete();
    });
  }

app/User.php Observerに記述した処理がそれぞれのCRUDで実行されるように、以下を追記する。

: 
use App\Observers\UserObserver;

class User extends Authenticatable
{
  : 
  /**
   * @return void
   */
  public static function booted()
  {
    User::observe(UserObserver::class);
  }
  : 
}
takanori-matsushita commented 4 years ago

リスト 13.20: dependent: :destroyのテスト green 省略

takanori-matsushita commented 4 years ago

リスト 13.21: green 省略

takanori-matsushita commented 4 years ago

演習 省略

takanori-matsushita commented 4 years ago

13.2 マイクロポストを表示する

13.2.1 マイクロポストの描画

Micropostモデル生成時に-aオプションでコントローラも生成されている。

リスト 13.22: 1つのマイクロポストを表示するパーシャル viewファイルの作成

resources/views/components/micropost.blade.php

<li id="micropost-{{ $micropost->id }}">
  <a href="{{ $micropost->user->id }}">
    <img src="{{ gravator_for($micropost->user, $options = ['size' => 50]) }}">
  </a>
  <span class="user">
    <a href="{{route('users.show', $micropost->user->id)}}">{{ $micropost->user->name }}</a>
  </span>
  <span class="content">{{ $micropost->content }}</span>
  <span class="timestamp">
    Posted {{ $micropost->created_at->diffForHumans() }} ago.
  </span>
</li>

以下に注目する。 Posted {{ $micropost->created_at->diffForHumans() }} ago. ->diffForHumans()メソッドで 〇〇分前、〇〇日前のように表示することができる。

takanori-matsushita commented 4 years ago

リスト 13.23: @micropostsインスタンス変数をshowアクションに追加する app/Http/Controllers/MicropostController.php

  public function show(User $user)
  {
    $grav_url = "http://www.gravatar.com/avatar/" . md5(strtolower(trim($user->email)));
    $microposts = $user->microposts()->paginate(30);
    return view('users.show', ['user' => $user, 'grav_url' => $grav_url, 'microposts' => $microposts]);
  }

$microposts変数は、関連付けされたデータが30件以上あれば、ページネーションとして出力されるようになる。

takanori-matsushita commented 4 years ago

リスト 13.24: マイクロポストをユーザーのshowページ (プロフィール画面) に追加する resources/views/users/show.blade.php

@php
$title = $user->name
@endphp
@extends('layouts.layout')
@section('content')
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <h1>
        <img src={{gravator_for($user)}}>
        {{$user->name}}
      </h1>
    </section>
  </aside>
  <div class="col-md-8">
    @if (!empty($user->microposts))  //もしユーザーの投稿データがあれば、以下の処理を行う
    <h3>Microposts ({{ $user->microposts->count() }})</h3>
    <ol class="microposts">
      @each('components.micropost', $microposts, 'micropost')
    </ol>
    {{$microposts->links()}}  //ページネーションの出力
    @endif
  </div>
</div>
@endsection
takanori-matsushita commented 4 years ago

演習 省略

takanori-matsushita commented 4 years ago

13.2.2 マイクロポストのサンプル

リスト 13.25: サンプルデータにマイクロポストを追加する Micropostファクトリの作成 database/factories/MicropostFactory.php

<?php

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\Micropost;
use Faker\Generator as Faker;

$factory->define(Micropost::class, function (Faker $faker) {
  return [
    'content' => $faker->sentence(20),  //20単語の文章
    'user_id' => rand(1, 6),  //1から6までの数字をランダムで生成
    'created_at' => $faker->dateTime(),
    'updated_at' => $faker->dateTime()
  ];
});

Micropostシーダーの作成 database/seeds/MicropostSeeder.php

<?php

use Illuminate\Database\Seeder;

class MicropostSeeder extends Seeder
{
  /**
   * Run the database seeds.
   *
   * @return void
   */
  public function run()
  {
    factory(\App\Micropost::class, 500)->create();
  }
}

シーダーの登録 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);  //追加
  }
}

編集できたら以下のコマンドを実行 php artisan migrate:refresh --seed

takanori-matsushita commented 4 years ago

リスト 13.26: マイクロポスト用のCSS (本章で利用するCSSのすべて) resources/sass/_custom.scss そのままコピペしてyarn devを実行

takanori-matsushita commented 4 years ago

演習 省略

takanori-matsushita commented 4 years ago

13.2.3 プロフィール画面のマイクロポストをテストする

省略

takanori-matsushita commented 4 years ago

13.3 マイクロポストを操作する

リスト 13.30: マイクロポストリソースのルーティング routes/web.php

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

13.3.1 マイクロポストのアクセス制御

リスト 13.31: Micropostsコントローラの認可テスト red 省略 13.32 13.33も省略

takanori-matsushita commented 4 years ago

リスト 13.34: Micropostsコントローラの各アクションに認可を追加する green 今回はすべてのルートに対して認可を与えたいため、ルーティングを定義したファイルに記述する。 routes/web.php

Route::resource('microposts', 'MicropostController', ['only' => ['store', 'destroy']])->middleware('auth');  // ->middleware('auth')とすることでMicropostの全アクションに対して認可処理を実行できる
takanori-matsushita commented 4 years ago

リスト 13.35: green 省略

takanori-matsushita commented 4 years ago

13.3.2 マイクロポストを作成する

リスト 13.36: Micropostsコントローラのcreateアクション app/Http/Controllers/MicropostController.php

use App\Http\Requests\MicropostFormRequest;
: 
: 
  public function store(MicropostFormRequest $request)
  {
    $micropost = new  Micropost;
    $form = $request->all();
    unset($form['_token']);
    $micropost->fill($form)->save();
    session()->flash('success', 'Micropost created!');
    return redirect()->route('root');
  }
takanori-matsushita commented 4 years ago

リスト 13.37: Homeページ (/) にマイクロポストの投稿フォームを追加する resources/views/static_pages/home.blade.php

@auth
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      @include('shared.user_info')
    </section>
    <section class="micropost_form">
      @include('shared.micropost_form')
    </section>
  </aside>
</div>
@else
: 
@endauth
takanori-matsushita commented 4 years ago

リスト 13.38: サイドバーで表示するユーザー情報のパーシャル ここでもコード内でロジックが発生するため、ビューコンポーザを作成する。

app/Http/Composers/CurrentUserComposer.php

<?php

namespace App\Http\Composers;

use Illuminate\View\View;

class CurrentUserComposer
{
  public function compose(View $view)
  {
    $view->with('current_user', \Auth::user());
  }
}

サービスプロバイダの作成 php artisan make:provider CurrentUserServiceProvider

bootメソッドを以下のように定義する app/Providers/CurrentUserServiceProvider.php

  public function boot()
  {
    View::composer(
      'static_pages.home',
      'App\Http\Composers\CurrentUserComposer'
    );
  }

作成したプロバイダーの登録 config/app.php

    /*
        *View Composer Service Providers...
        */
    : 
    App\Providers\CurrentUserServiceProvider::class,  //追加
  ],

resources/views/shared/user_info.blade.php

@php
$count = $current_user->load('microposts')->microposts()->count(); //currentにmicropostsのuser_idを紐付けてカウントする
@endphp
<a href="{{route('users.show', $current_user->id)}}">
  <img src="{{gravator_for($current_user, ['size' => 50])}}">
</a>
<h1>{{ $current_user->name }}</h1>
<span>
  <a href="{{ route('users.show', $auth_id) }}">view my profile</a>
</span>
@if ($count > 1)
<span>{{ $count }} microposts</span>
@else
<span>{{ $count }} micropost</span>
@endif
takanori-matsushita commented 4 years ago

リスト 13.39: マイクロポスト投稿フォームのパーシャル resources/views/shared/micropost_form.blade.php

<form action="{{route('microposts.store')}}" method=post>
  @csrf
  @include('shared.error_messages')
  <div class="field">
    <input type="hidden" name="user_id" value="{{$current_user->id}}">
    <textarea name="content" id="" cols="30" rows="10" placeholder="Compose new micropost...">{{old('content')}}</textarea>

    <input type="submit" value="Post" class="btn btn-primary">
  </div>
</form>
takanori-matsushita commented 4 years ago

リスト 13.40: homeアクションにマイクロポストのインスタンス変数を追加する 実装済みのため省略

takanori-matsushita commented 4 years ago

実際に投稿してみると、403 | this action is unauthorized.が表示される。 これは、storeアクションで利用しているMicropostFormRequestのauthorizeメソッドが関係している。

app/Http/Requests/MicropostFormRequest.php

  public function authorize()
  {
    return false;
  }

これを以下のように変更する。

  public function authorize()
  {
    return true;
  }

これで、無事に投稿できるようになる。

takanori-matsushita commented 4 years ago

リスト 13.41 〜 13.45 省略

takanori-matsushita commented 4 years ago

演習 resources/views/static_pages/home.blade.php

@php
$title = ''
@endphp
@extends('layouts.layout')
@section('content')
@auth
@include('static_pages.authorize.true')
@else
@include('static_pages.authorize.false')
@endauth
@endsection

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="micropost_form">
      @include('shared.micropost_form')
    </section>
  </aside>
</div>

resources/views/static_pages/authorize/false.blade.php

<div class="center jumbotron">
  <h1>Welcome to the Sample App</h1>

  <h2>
    This is the home page for the
    <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
    sample application.
  </h2>

  <a href={{route('users.signup')}} class="btn btn-lg btn-primary">Sign up now!</a>
</div>

<a href="http://rubyonrails.org/'"><img src="/images/rails.png" alt="Rails logo"></a>
takanori-matsushita commented 4 years ago

13.3.3 フィードの原型

リスト 13.46: マイクロポストのステータスフィードを実装するための準備

takanori-matsushita commented 4 years ago

リスト 13.47: homeアクションにフィードのインスタンス変数を追加する app/Http/Controllers/StaticPagesController.php

  public function home()
  {
    \Auth::user() ? $feed_items = \Auth::user()->feed()->paginate(30) : $feed_items = [];
    return view('static_pages.home', compact('feed_items'));
  }
takanori-matsushita commented 4 years ago

リスト 13.48: ステータスフィードのパーシャル resources/views/shared/feed.blade.php

@if (!empty($feed_items))
<ol class="microposts">
  @each('components.micropost', $feed_items, 'micropost')
</ol>
{{ $feed_items->links() }}
@endif
takanori-matsushita commented 4 years ago

リスト 13.49: 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="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

リスト 13.50: createアクションに空の@feed_itemsインスタンス変数を追加する 特にエラーが出ないため、省略

takanori-matsushita commented 4 years ago

13.3.4 マイクロポストを削除する

リスト 13.51: マイクロポストのパーシャルに削除リンクを追加する if文のところを追加

resources/views/components/micropost.blade.php

<li id="micropost-{{ $micropost->id }}">
  <a href="{{ $micropost->user->id }}">
    <img src="{{ gravator_for($micropost->user, $options = ['size' => 50]) }}">
  </a>
  <span class="user">
    <a href="{{route('users.show', $micropost->user->id)}}">{{ $micropost->user->name }}</a>
  </span>
  <span class="content">{{ $micropost->content }}</span>
  <span class="timestamp">
    Posted {{ $micropost->created_at->diffForHumans() }} ago.
    @if (Auth::id() === $micropost->user->id)
    <form action="{{route('microposts.destroy', $micropost->id)}}" method="post" class="delete">
      @method('delete')
      @csrf
      <button type="submit" onclick="return confirm('You sure?')">delete</button>
    </form>
    @endif
  </span>
</li>