takanori-matsushita / laravel-practice

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

rails tutorial 10章をlaravelで実装 #8

Open takanori-matsushita opened 4 years ago

takanori-matsushita commented 4 years ago

branch: updating-users

takanori-matsushita commented 4 years ago

10.1 ユーザーを更新する

10.1.1 編集フォーム

リスト 10.1: ユーザーのeditアクション app/Http/Controllers/UsersController.php

  public function edit($id)
  {
    $user = User::find($id);
    return view('users.edit', ["user" => $user]);
  }
takanori-matsushita commented 4 years ago

リスト 10.2: ユーザーのeditビュー

viewファイルの作成 resources/views/users/edit.blade.php

@php
$title = 'Edit user';
@endphp
@extends('layouts.layout')
@section('content')
<div class="row">
  <div class="col-md-6 offset-md-3">
    <form action="{{route('users.update', ['user' => $user->id])}}" method="post">
      @method('patch')
      @csrf
      @include('shared.error_messages')
      <label for="name">Name</label>
      <input type="text" name="name" class="form-control @error('name') is-invalid @enderror" value="{{old('name', $user->name)}}">
      <label for="email">Email</label>
      <input type="text" name="email" class="form-control @error('email') is-invalid @enderror" value="{{old('email', $user->email)}}">
      <label for="password">Password</label>
      <input type="password" name="password" class="form-control @error('password') is-invalid @enderror">
      <label for="password_confirmation">Password Confirmation</label>
      <input type="password" name="password_confirmation" class="form-control">
      <input type="submit" value="Save changes" class="btn btn-primary">
    </form>
    <div class="gravatar_edit">
      <img src={{gravator_for($user)}}>
      <a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a>
    </div>
  </div>
</div>
@endsection

oldメソッドは、エラーだった際に前回の入力値を保持するものだったが、第二引数にデフォルトで表示させたい値を入力することができる。 上記のようにすることで、編集ページにアクセスした際、データベースに保存されている値が表示される。 バリデーションに引っかかった際は、フォームへ送った値が保持されて再度レンダリングされる。

takanori-matsushita commented 4 years ago

リスト 10.4: レイアウトの “Settings” リンクを更新する resources/views/layouts/header.blade.php Settingsのhref部分の実装

<a class="dropdown-item" href="{{route('users.edit', ['user'=>Auth::id()])}}">Settings</a>
takanori-matsushita commented 4 years ago

ここで、['user'=>Auth::id()]に注目する。 基本的にMVCアーキテクチャーでviewの中に、ロジックを組み込むのは、メンテナンス性が良くない。 そのため、Laravelではビューコンポーザを利用する。手順としては、

  1. ビューコンポーザクラスの作成
  2. サービスプロバイダの作成
  3. サービスプロバイダの登録

となる。いかに手順を記述する。

  1. ビューコンポーザクラスの作成 app/Http/Composersの中にAuthIdComposer.phpを作成する。Composersフォルダはないため新規作成。

app/Http/Composers/AuthIdComposer.php

<?php

namespace App\Http\Composers;

use Illuminate\View\View;

class AuthIdComposer
{
  public function compose(View $view)
  {
    $view->with('auth_id', \Auth::id());
  }
}
  1. サービスプロバイダの作成 php artisan make:provider GetAuthIdServiceProvider app/Providers/GetAuthIdServiceProvider.php VIewファサードの利用と、bootメソッドを追加する。
    use Illuminate\Support\Facades\View;
    :
    :  
    public function boot()
    {
    View::composer(
      'layouts.header',
      'App\Http\Composers\AuthIdComposer'
    );
    }
  2. サービスプロバイダの登録 config/app.php 'providers' の配列の最後に以下を追加する。今後ビューコンポーザをまとめて見やすくするため、コメントも追加。
    /*
        *View Composer Service Providers...
        */
    App\Providers\GetAuthIdServiceProvider::class,
takanori-matsushita commented 4 years ago

これで['user'=>Auth::id()]$auth_id変数に代入される。最終的には下記コードになる。 resources/views/layouts/header.blade.php

<header>
  <nav class="navbar fixed-top  navbar-expand-lg navbar-dark bg-dark">
    <div class="container">
      <a href={{route('root')}} id="logo">Sample app</a>
      <ul class="navbar-nav">
        <li class="nav-item"><a href={{route('root')}} class="nav-link">Home</a></li>
        <li class="nav-item"><a href={{route('help')}} class="nav-link">Help</a></li>
        @auth
        <li class="nav-item dropdown" dusk="Users">
          <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
            Users
          </a>
          <div class="dropdown-menu" aria-labelledby="navbarDropdown">
            <a class="dropdown-item" href="{{route('users.show', $auth_id)}}">Profile</a>  //ビューコンポーザを利用した値の取得
            <a class="dropdown-item" href="{{route('users.edit', $auth_id)}}">Settings</a>  //ビューコンポーザを利用した値の取得
            <div class="dropdown-divider"></div>
            <form action="{{route('logout')}}" method="post">
              @csrf
              <button type="submit" class="dropdown-item" dusk="Logout">Logout</button>
            </form>
          </div>
        </li>
        @else
        <li class="nav-item"><a href="{{route('login')}}" class="nav-link">Login</a></li>
        @endauth
      </ul>
    </div>
  </nav>
</header>
takanori-matsushita commented 4 years ago

演習

  1. resources/views/users/edit.blade.php <a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a>

takanori-matsushita commented 4 years ago

10.1.2 編集の失敗

app/Http/Controllers/UsersController.php updateアクションの追加

use App\Http\Requests\UserFormRequest;
use Illuminate\Support\Facades\Hash;
: 
class UsersController extends Controller
{
  public function update(UserFormRequest $request, User $user)
  {
    $user->name = $request->name;
    $user->email = $request->email;
    $user->password = Hash::make($request->password);
    $user->password_confirmation = Hash::make($request->password_confirmation);
    $user->save();
  }

以前のユーザーのテストで作成したバリデーションを読み込むために、UserFormRequestを引数として指定。

takanori-matsushita commented 4 years ago

このバリデーションのままだと、メールアドレスが既に存在するエラーがキャッチされるため、以下のように変更。 app/Http/Requests/UserFormRequest.php

use Illuminate\Validation\Rule;

class UserFormRequest extends FormRequest
{
: 
public function rules()
  {
    return [
      "name" => "required|max:50",
      "email" => [
        "required",
        "max:255",
        "email:filter",
        Rule::unique('users')->ignore(\Auth::id()),
      ],
      "password" => "required|min:6"
    ];
  }

Ruleのignoreメソッドで、ログインしているidの重複を除外する。

takanori-matsushita commented 4 years ago

10.1.3 編集失敗時のテスト

リスト 10.9: 編集の失敗に対するテストgreen テストファイルの作成 php artisan dusk:make UsersEditTest tests/Browser/UsersEditTest.php

<?php

namespace Tests\Browser;

use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;

class UsersEditTest extends DuskTestCase
{
  use DatabaseMigrations;  //テスト前のマイグレーションを実行する
  /**
   * A Dusk test example.
   *
   * @return void
   */
  public function testUnsuccessfulEdit()
  {
    $user = $this->registerUser();  //DuskTestCaseに記述したメソッドの呼び出し
    $this->browse(function (Browser $browser) use ($user) {
      $browser->visit('/login')
        ->assertSeeLink('Sign up now!')
        ->type('email', $user->email)
        ->type('password', 'password')
        ->press('Log in!')
        ->click('@Users')
        ->click('@Settings')
        ->type('name', '')
        ->type('email', 'foo@invalid')
        ->type('password', 'foo')
        ->press('Save changes')
        ->type('password_confirmation', 'bar')
        ->assertPathIs('/users/1/edit');
    });
  }
}
takanori-matsushita commented 4 years ago

リスト 10.10: green php artisan dusk

takanori-matsushita commented 4 years ago

演習

  1. ->assertSee('The form contains 3 errors');
takanori-matsushita commented 4 years ago

10.1.4 TDDで編集を成功させる

リスト 10.11: 編集の成功に対するテストred tests/Browser/UsersEditTest.php

  public function testSuccessfulEdit()
  {
    $user = $this->registerUser();
    $this->browse(function (Browser $browser) use ($user) {
      $browser
        ->click('@Users')
        ->click('@Settings')
        ->type('name', 'Foo Bar')
        ->type('email', 'foo@bar.com')
        ->type('password', '')
        ->type('password_confirmation', '')
        ->press('Save changes')
        ->assertPathIs('/users/1')
        ->assertSee('Profile updated')
        ->script('location.reload();');
      $browser
        ->assertDontSee('Profile updated');
    });
  }

scriptメソッドでページをリロードしている。これはフラッシュメッセージのテストでページ更新後に指定のテキストがなくなっていることをチェックしている。 scriptメソッドを使うと、メソッドチェーンが使えなくなるため、再度$browserを使っている。

takanori-matsushita commented 4 years ago

リスト 10.12: ユーザーのupdateアクション red app/Http/Controllers/UsersController.php

  public function update(UserFormRequest $request, User $user)
  {
    $user->name = $request->name;
    $user->email = $request->email;
    $user->password = Hash::make($request->password);
    $user->save();
    session()->flash('success', 'Profile updated');  //フラッシュメッセージ
    return redirect()->route('users.show', \Auth::id());  //詳細ページへリダイレクト
  }
takanori-matsushita commented 4 years ago

リスト 10.13: パスワードが空のままでも更新できるようにする green 複数のファイルを編集して実装する。 app/Http/Requests/UserFormRequest.php

  public function rules(Request $request)
  {
    $password = [];
    if ($request->method() === 'POST') {
      $password = [
        "min:6",
        "required"
      ];
    }
    return [
      "name" => "required|max:50",
      "email" => [
        "required",
        "max:255",
        "email:filter",
        Rule::unique('users')->ignore(\Auth::id()),
      ],
      "password" => $password
    ];
  }

HTTPメソッドがPOST(新規登録)の場合は、passwordのバリデーションを設定し、PATCH(更新)の場合は、passwordのバリデーションを外す処理を記述している。

しかし、このままだと、空のパスワードがデータベースに登録されてしまうため、updateアクションの処理を以下のように変更する。 app/Http/Controllers/UsersController.php

  public function update(UserFormRequest $request, User $user)
  {
    $user->name = $request->name;
    $user->email = $request->email;
    if (!empty($user->password)) {
      $user->password = Hash::make($request->password);
    }
    $user->save();
    session()->flash('success', 'Profile updated');
    return redirect()->route('users.show', \Auth::id());
  }

emptyメソッドで値が存在するかチェックする。値がある場合はfalseとなるが、!を使っているのでtrueとなり、パスワードの更新処理が走る。

これに伴い、バリデーションエラーが変わるため、テストファイルを再度編集。 tests/Browser/UsersEditTest.php

  public function testUnsuccessfulEdit()
  {
    : 
      ->assertSee('The form contains 2 errors');  //3を2に変更する。
  }
takanori-matsushita commented 4 years ago

リスト 10.13: パスワードが空のままでも更新できるようにする green 複数のファイルを編集して実装する。 app/Http/Requests/UserFormRequest.php

  public function rules(Request $request)
  {
    $password = [];
    if ($request->method() === 'POST') {
      $password = [
        "min:6",
        "required"
      ];
    }
    return [
      "name" => "required|max:50",
      "email" => [
        "required",
        "max:255",
        "email:filter",
        Rule::unique('users')->ignore(\Auth::id()),
      ],
      "password" => $password
    ];
  }

HTTPメソッドがPOST(新規登録)の場合は、passwordのバリデーションを設定し、PATCH(更新)の場合は、passwordのバリデーションを外す処理を記述している。

しかし、このままだと、空のパスワードがデータベースに登録されてしまうため、updateアクションの処理を以下のように変更する。 app/Http/Controllers/UsersController.php

  public function update(UserFormRequest $request, User $user)
  {
    $user->name = $request->name;
    $user->email = $request->email;
    if (isset($user->password)) {
      $user->password = Hash::make($request->password);
    }
    $user->save();
    session()->flash('success', 'Profile updated');
    return redirect()->route('users.show', \Auth::id());
  }

issetメソッドで変数の値が存在するかチェックする。値がある場合はtrueとなるので、パスワードの更新処理が走る。

takanori-matsushita commented 4 years ago

リスト 10.14: green php artisan dusk

takanori-matsushita commented 4 years ago

10.2 認可

10.2.1 ユーザーにログインを要求する

リスト 10.15: beforeフィルターにlogged_in_userを追加する green app/Http/Controllers/UsersController.php laravel/uiのAuthのmiddlewareが用意されているためそちらを使用する。

class UsersController extends Controller
{
  public function __construct()
  {
    $this->middleware('auth', [
      'only' => ['edit', 'update']
    ]);
  }
  : 
}

コンストラクタで、指定のアクションに対して、処理を実装する。

takanori-matsushita commented 4 years ago

リスト 10.16: green php artisan dusk duskテストでは、greenとなり、アクセス制限のテストができないため、以下のテストを追加で記述する。

リスト 10.17: テストユーザーでログインする green duskテストでは、ログインしていない際に編集ページにアクセスするテストを別途記述する。これは、ログインする前に実装したほうが良いので、testUnsuccessfulEditメソッドの上に記述する。 tests/Browser/UsersEditTest.php

  public function testAuthenticateCantAccessEdit()
  {
    $this->browse(function (Browser $browser) {
      $browser->click('@Logout')
        ->visit('/users/1/edit')
        ->assertPathIs('/login');
    });
  }
takanori-matsushita commented 4 years ago

以下省略

takanori-matsushita commented 4 years ago

10.2.2 正しいユーザーを要求する

リスト 10.23: fixtureファイルに2人目のユーザーを追加する tests/DuskTestCase.php

  protected function anyUsers()
  {
    $user1 = $this->registerUser();
    $user2 = factory(User::class)->create([
      "name" => "Sterling Archer",
      "email" => "duchess@example.gov",
      "password" => Hash::make('password')
    ]);
    return [$user1, $user2];
  }

複数のユーザーを登録する関数を定義する。

takanori-matsushita commented 4 years ago

リスト 10.24: 間違ったユーザーが編集しようとしたときのテスト red tests/Browser/UsersControllerTest.php

class UsersControllerTest extends DuskTestCase
{
  use DatabaseMigrations;
  : 
  public function testShouldRedirectEditWhenLoggedInAsWrongUser()
  {
    $users = $this->anyUsers();
    $this->browse(function (Browser $browser) use ($users) {
      $browser->visit('login')
        ->type('email', $users[0]->email)
        ->type('password', 'password')
        ->press('Log in!')
        ->visit('/users/2/edit')
        ->assertPathIs('/');
    });
  }
}
takanori-matsushita commented 4 years ago

リスト 10.25: beforeフィルターを使って編集/更新ページを保護する green Larabelでフィルターを作るには、policyを作成する。 php artisan make:policy UserPolicy --model=User --model=UserでUserモデルポリシーの雛形を作成する。

app/Policies/UserPolicy.php

  public function update(User $auth, User $user)
  {
    return $auth->id == $user->id;
  }

ログイン中のユーザーIDとページのIDを比較する。

ポリシーの登録をする。 app/Providers/AuthServiceProvider.php

  protected $policies = [
    'App\User' => 'App\Policies\UserPolicy',
  ];

コントローラーに認可の処理を記述する。 app/Http/Controllers/UsersController.php

  public function edit(User $user)
  {
    $auth = auth()->user();
//ログイン中のユーザーが自身のユーザーページと一致するかチェックし、処理を切り替える。
    return ($user->can('update', $auth)) ? view('users.edit', compact('user')) : redirect()->route('root');
  }

  public function update(UserFormRequest $request, User $user)
  {
    $auth = auth()->user();
    if($user->can('update', $auth)) {
      $user->name = $request->name;
      $user->email = $request->email;
      if (!empty($request->password)) {
        $user->password = Hash::make($request->password);
      }
      $user->save();
      session()->flash('success', 'Profile updated');
      return redirect()->route('users.show', \Auth::id());
    }
    return redirect()->route('/')
  }

authorizeメソッドでポリシーのメソッドを第一引数に指定する。第二引数で現在アクセスしているページのパラメータをポリシーに引数として渡す。

takanori-matsushita commented 4 years ago

リスト 10.26: green php artisan dusk

takanori-matsushita commented 4 years ago

10.2.3 フレンドリーフォワーディング

LaravelではLaravel/uiですでに実装されているため、テストコードもgreenとなる。

リスト 10.29: フレンドリーフォワーディングのテスト green tests/Browser/UsersEditTest.php ブラウザテストでの実装のため、一度ログアウト処理を記述する。

  public function testLogout()
  {
    $user = $this->registerUser();
    $this->browse(function (Browser $browser) {
      $browser->click('@Users')
        ->press('Logout')
        ->assertPathIs('/');
    });
  }

  public function testSuccessfulEditWithFriendlyForwarding()
  {
    $user = $this->registerUser();
    $this->browse(function (Browser $browser) use ($user) {
      $browser->visit('/users/1/edit')
        ->assertPathIs('/login')
        ->type('email', $user->email)
        ->type('password', 'password')
        ->press('Log in!')
        ->assertPathIs('/users/1/edit');
    });
  }
takanori-matsushita commented 4 years ago

以下省略

takanori-matsushita commented 4 years ago

10.3 すべてのユーザーを表示する

10.3.1 ユーザーの一覧ページ

リスト 10.34: indexアクションのリダイレクトをテストする red

  public function testShouldRedirectIndexWhenNotLoggedIn()
  {
    $users = $this->anyUsers();
    $this->browse(function (Browser $browser) use ($users) {
      $browser->visit(route('users.index'))
        ->assertPathIs('/login');
    });
  }
takanori-matsushita commented 4 years ago

リスト 10.35: indexアクションにはログインを要求する green app/Http/Controllers/UsersController.php

  public function __construct()
  {
    $this->middleware('auth', [
      'only' => ['index', 'edit', 'update']  //indexを追加
    ]);
  }
takanori-matsushita commented 4 years ago

リスト 10.36: ユーザーのindexアクション app/Http/Controllers/UsersController.php

class UsersController extends Controller
{
  : 
  public function index()
  {
    $users = User::all();
    return view('users.index', compact('users'));
  }
  : 
}
takanori-matsushita commented 4 years ago

リスト 10.37: ユーザーのindexビュー resources/views/users/index.blade.php

@php
$title = 'All users';
@endphp
@extends('layouts.layout')
@section('content')
<h1>All users</h1>

<ul class="users">
  @foreach($users as $user)
  <li>
    <img src={{gravator_for($user, ['size'=>50])}}>
    <a href="{{route('users.show', compact('user'))}}">{{$user->name}}</a>
  </li>
  @endforeach
</ul>
@endsection
takanori-matsushita commented 4 years ago

リスト 10.39: ユーザーのindexページ用のCSS resources/sass/_custom.scss

/* Users index */

.users {
    list-style: none;
    margin: 0;
    li {
        overflow: auto;
        padding: 10px 0;
        border-bottom: 1px solid $gray-medium-light;
    }
}

以下のコマンドを実行 yarn dev

takanori-matsushita commented 4 years ago

リスト 10.40: ユーザー一覧ページへのリンクを更新する resources/views/layouts/header.blade.php

<header>
        : 
        : 
        @auth
        <li class="nav-item"><a href="{{route('users.index')}}" class="nav-link">Users</a></li>  //追加
        <li class="nav-item dropdown" dusk="Users">
          <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
            Account
          </a>
          <div class="dropdown-menu" aria-labelledby="navbarDropdown">
            <a class="dropdown-item" href="{{route('users.show', $auth_id)}}">Profile</a>
            <a class="dropdown-item" href="{{route('users.edit', $auth_id)}}" dusk="Settings">Settings</a>
            <div class="dropdown-divider"></div>
            <form action="{{route('logout')}}" method="post">
              @csrf
              <button type="submit" class="dropdown-item" dusk="Logout">Logout</button>
            </form>
          </div>
        </li>
        @else
        <li class="nav-item"><a href="{{route('login')}}" class="nav-link">Login</a></li>
        @endauth
        : 
        : 
</header>
takanori-matsushita commented 4 years ago

リスト 10.41: green php artisan dusk

takanori-matsushita commented 4 years ago

演習 tests/Browser/SiteLayoutBrowserTest.php

class SiteLayoutBrowserTest extends DuskTestCase
{
  use DatabaseMigrations;
  : 
  public function testAuthorizeHeaderMenu()
  {
    $user = $this->registerUser();
    $this->browse(function (Browser $browser) use ($user) {
      $browser->visit(route('root'))
        ->assertSeeLink('Home')
        ->assertSeeLink('Help')
        ->assertSeeLink('Login')
        ->assertDontSeeLink('Users')
        ->assertDontSee('Account')
        ->visit(route('login'))
        ->type('email', $user->email)
        ->type('password', 'password')
        ->press('Log in!')
        ->assertDontSeeLink('Login')
        ->assertSeeLink('Users')
        ->assertSee('Account');
    });
  }
}
takanori-matsushita commented 4 years ago

10.3.2 サンプルのユーザー

リスト 10.43: データベース上にサンプルユーザーを生成するRailsタスク Laravelでは、factoryという機能が標準で搭載されているため、それを利用する。UserFactoryは、初めから用意されているため、以下のコマンドは打たなくても良い。 factoryの作成 php artisan make:factory 作成したい名前

database/factories/UserFactory.php

$factory->define(User::class, function (Faker $faker) {
  return [
    'name' => $faker->name,
    'email' => $faker->unique()->safeEmail,
    'password' => Hash::make('password'),
  ];

シーダーの作成 php artisan make:seeder UsersTableSeeder

<?php

use Illuminate\Database\Seeder;

class UsersTableSeeder extends Seeder
{
  /**
   * Run the database seeds.
   *
   * @return void
   */
  public function run()
  {
    factory(\App\User::class)->create([
      "name" => "Example User",
      "email" => "example@railstutorial.org",
      "password" => Hash::make('password')
    ]);
    factory(\App\User::class, 99)->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);
  }
}
takanori-matsushita commented 4 years ago

ファクトリー・シーダーの作成が完了したら以下のコマンドでシーダーを実行する。 php artisan migrate:refresh --seed

takanori-matsushita commented 4 years ago

10.3.3 ページネーション

リスト 10.45: indexページでpaginationを使う resources/views/users/index.blade.php

@php
$title = 'All users';
@endphp
@extends('layouts.layout')
@section('content')
<h1>All users</h1>

{{$users->links()}}  //ページネーションを表示する
<ul class="users">
  @foreach($users as $user)
  <li>
    <img src={{gravator_for($user, ['size'=>50])}}>
    <a href="{{route('users.show', compact('user'))}}">{{$user->name}}</a>
  </li>
  @endforeach
</ul>
{{$users->links()}}  //ページネーションを表示する
@endsection
takanori-matsushita commented 4 years ago

リスト 10.46: indexアクションでUsersをページネートする app/Http/Controllers/UsersController.php

  public function index()
  {
    $users = User::paginate(30);  //1ページに30件のデータを表示する
    return view('users.index', compact('users'));
  }
takanori-matsushita commented 4 years ago

10.3.4 ユーザー一覧のテスト

リスト 10.48: ページネーションを含めたUsersIndexのテストgreen テストファイルの作成 php artisan dusk:make UsersIndexTest

tests/Browser/UsersIndexTest.php

<?php

namespace Tests\Browser;

use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;

class UsersIndexTest extends DuskTestCase
{
  use DatabaseMigrations;
  /**
   * A Dusk test example.
   *
   * @return void
   */
  public function testIndexIncludingPagination()
  {
    $users = $this->anyUsers();
    $this->browse(function (Browser $browser) use ($users) {
      foreach ($users as $user) {
        $browser->visit(route('login'))
          ->type('email', $user->email)
          ->type('password', 'password')
          ->press('Log in!')
          ->visit(route('users.index'))
          ->assertPathIs('/users')
          ->assertPresent('ul.pagination')
          ->assertSeeLink($user->name)
          ->assertSee($user->name)
          ->click('@Users')
          ->press('Logout');
        if ($user->id >= 30) {
          break;
        }
      }
    });
  }
}
takanori-matsushita commented 4 years ago

リスト 10.49: green php artisan dusk

takanori-matsushita commented 4 years ago

10.3.5 パーシャルのリファクタリング

Laravelでは、コンポーネントを利用することで、デザインの使い回しをすることができる。

resources/views/users/index.blade.php

@php
$title = 'All users';
@endphp
@extends('layouts.layout')
@section('content')
<h1>All users</h1>

{{$users->links()}}
<ul class="users">
  @each('components.user', $users, 'user')  //ここを書き換える
</ul>
{{$users->links()}}
@endsection

@eachディレクティブで@foreachのようなことができる。第一引数に表示したいコンポネント、第二引数にデータをまとめた配列(ここでは、Userモデルのデータ)、第三引数にコンポーネント側で配列から取り出し、格納するための変数名を指定する。

takanori-matsushita commented 4 years ago

コンポーネントの作成 php artisan make:component user

リスト 10.51: 各ユーザーを表示するパーシャル resources/views/components/user.blade.php

<li>
  <img src={{gravator_for($user, ['size'=>50])}}>
  <a href="{{route('users.show', compact('user'))}}">{{$user->name}}</a>
</li>
takanori-matsushita commented 4 years ago

リスト 10.53: green php artisan dusk

takanori-matsushita commented 4 years ago

10.4 ユーザーを削除する

10.4.1 管理ユーザー

マイグレーションの作成 php artisan make:migration add_admin_to_users

リスト 10.54: boolean型のadmin属性をUserに追加するマイグレーション database/migrations/[日付]_add_admin_to_users.php

  public function up()
  {
    Schema::table('users', function (Blueprint $table) {
      $table->boolean('admin')->default(false);
    });
  }

boolean型はデフォルトで値を代入しないとマイグレーション実行時にエラーになるため、falseを指定する。 MySQLの場合は、指定ののカラムの後に追加できるafterメソッドが利用できる。 $table->boolean('admin')->default(false)->after('カラム名'); これで、カラム名の後に追加される。

以下コマンドでマイグレーションを実行 php artisan migrate

takanori-matsushita commented 4 years ago

リスト 10.55: サンプルデータ生成タスクに管理者を1人追加する database/seeds/UsersTableSeeder.php

class UsersTableSeeder extends Seeder
{
  : 
  public function run()
  {
    factory(\App\User::class)->create([
      "name" => "Example User",
      "email" => "example@railstutorial.org",
      "password" => Hash::make('password'),
      "admin" => true  //adminの追加
    ]);
    factory(\App\User::class, 99)->create();
  }
takanori-matsushita commented 4 years ago

以下のコマンドでシーダーを実行する php artisan migrate:refresh --seed

takanori-matsushita commented 4 years ago

Laravelでは$guard・$fillableで編集をしてもよい属性の指定ができる。

laravelで初めから用意されているUserモデルファイルには、初めから設定されているため、ここでは気にしない。$hiddenはtinkerでデータを取得した際に指定したカラムの値を表示させない設定のため、ここでadminを指定する。

app/User.php

class User extends Authenticatable
{
  use Notifiable;

  /**
   * The attributes that are mass assignable.
   *
   * @var array
   */
  protected $fillable = [
    'name', 'email', 'password',
  ];

  /**
   * The attributes that should be hidden for arrays.
   *
   * @var array
   */
  protected $hidden = [
    'password', 'remember_token', 'admin',  //adminの追加
  ];

  /**
   * The attributes that should be cast to native types.
   *
   * @var array
   */
  protected $casts = [
    'email_verified_at' => 'datetime',
  ];
}
takanori-matsushita commented 4 years ago

10.4.2 destroyアクション

リスト 10.57: ユーザー削除用リンクの実装 (管理者にのみ表示される) resources/views/components/user.blade.php

<li>
  <img src={{gravator_for($user, ['size'=>50])}}>
  <a href="{{route('users.show', compact('user'))}}">{{$user->name}}</a>
  @auth
  @if (auth()->user()->admin === true)
  <form action="{{route('users.destroy', compact('user'))}}" method="post" class="delete" style="display: inline">
    @method('DELETE')
    @csrf
    |<button type="submit" value="delete" onclick="return confirm('You sure?')">delete</button>
  </form>
  @endif
  @endauth
</li>

resources/sass/_custom.scss

: 
.delete {
    display: inline;
    button[type="submit"] {
        background: none;
        border: none;
        color: #3490dc;
        &:hover {
            color: #1d68a7;
            text-decoration: underline;
        }
        &:focus {
            outline: none;
        }
    }
}

yarn dev

takanori-matsushita commented 4 years ago

リスト 10.58: 実際に動作するdestroyアクションを追加する app/Http/Controllers/UsersController.php

class UsersController extends Controller
{
  public function __construct()
  {
    $this->middleware('auth', [
      'only' => ['index', 'edit', 'update', 'destroy']  //destoryメソッドの追加
    ]);
  }
  : 
  : 
  public function destroy(Request $request, User $user)
  {
    $user = $request->user;
    $user->delete();
    session()->flash('success', 'User deleted');
    return redirect()->route('users.index');
  }
takanori-matsushita commented 4 years ago

リスト 10.59: beforeフィルターでdestroyアクションを管理者だけに限定する Laravelではプロバイダを使ってbefore,afterのアクションを実行することができる。 php artisan make:middleware AdminUser

app/Http/Middleware/AdminUser.php

<?php

namespace App\Http\Middleware;

use Closure;

class AdminUser
{
  /**
   * Handle an incoming request.
   *
   * @param  \Illuminate\Http\Request  $request
   * @param  \Closure  $next
   * @return mixed
   */

  public function handle($request, Closure $next)
  {
    if (auth()->user()->admin !== true) {
      return redirect()->route('root');
    }
    return $next($request);
  }
}

app/Http/Controllers/UsersController.php

: 
use app\Http\Middleware\AdminUser;

class UsersController extends Controller
{
  public function __construct()
  {
    $this->middleware('auth', [
      'only' => ['index', 'edit', 'update', 'destroy']
    ]);

    $this->middleware(AdminUser::class, [
      'only' => ['destroy']
    ]);
  }
  : 
  : 
}
takanori-matsushita commented 4 years ago

10.4.3 ユーザー削除のテスト リスト 10.60: fixture内の最初のユーザーを管理者にする tests/DuskTestCase.php

  protected function registerUser()
  {
    return  factory(User::class)->create([
      "name" => "Michael Example",
      "email" => "michael@example.com",
      "password" => Hash::make('password')
      "admin" => true  //追加
    ]);
  }