Laravel で Ruby on Rails チュートリアル【10章】更新・削除

## 環境
- Laravel 8.6
- PHP 8.0
- Bootstrap 5.1

## 10.1 ユーザーを更新する

### 10.1.1 編集フォーム

```php:/app/Http/Controllers/UsersController.php
    public function edit(User $user)
    {
        return view('users.edit')->with('user', $user);
    }
```
```html:/resources/views/users/edit.blade.php
@extends('layouts.application')

@section('title', 'Edit user')

@section('content')
    <h1>Update your profile</h1>

    <div class="row">
        <div class="col-md-6 offset-md-3">
            {{ Form::model($user, ['route' => ['users.update', $user->id], "method" => "patch"]) }}
            @include('shared.error_messages')
            <div class="mb-3">
                {{ Form::label('name', 'Name', ['class' => 'form-label fw-bold']) }}
                {{ Form::text('name', $user->name, ['class' => 'form-control']) }}
            </div>
            <div class="mb-3">
                {{ Form::label('email', 'Email', ['class' => 'form-label fw-bold']) }}
                {{ Form::email('email', $user->email, ['class' => 'form-control']) }}
            </div>
            <div class="mb-3">
                {{ Form::label('password', 'Password', ['class' => 'form-label fw-bold']) }}
                {{ Form::password('password', ['class' => 'form-control']) }}
            </div>
            <div class="mb-3">
                {{ Form::label('password_confirmation', 'Password confirmation', ['class' => 'form-label fw-bold']) }}
                {{ Form::password('password_confirmation', ['class' => 'form-control']) }}
            </div>
            {{ Form::submit("Save changes", ["class" => "btn btn-primary w-100"]) }}
            {{ Form::close() }}

            <div class="gravatar_edit">
                <img src="{{ gravatar_url($user) }}" class="gravatar" alt="Example User">
                <a href="http://gravatar.com/emails" target="_blank">change</a>
            </div>
        </div>
    </div>
@endsection
```
```html:/retources/views/layouts/header.blade.php
<li>{{ link_to_route("users.edit", "Settings", [auth()->id()], ["class" => "dropdown-item"]) }}</li>
```

#### 課題

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

```html:/resources/views/users/form.blade.php
{{ Form::model($user, $options) }}
@include('shared.error_messages')
<div class="mb-3">
    {{ Form::label('name', 'Name', ['class' => 'form-label fw-bold']) }}
    {{ Form::text('name', $user->name, ['class' => 'form-control']) }}
</div>
<div class="mb-3">
    {{ Form::label('email', 'Email', ['class' => 'form-label fw-bold']) }}
    {{ Form::email('email', $user->email, ['class' => 'form-control']) }}
</div>
<div class="mb-3">
    {{ Form::label('password', 'Password', ['class' => 'form-label fw-bold']) }}
    {{ Form::password('password', ['class' => 'form-control']) }}
</div>
<div class="mb-3">
    {{ Form::label('password_confirmation', 'Password confirmation', ['class' => 'form-label fw-bold']) }}
    {{ Form::password('password_confirmation', ['class' => 'form-control']) }}
</div>
{{ Form::submit($button_text, ["class" => "btn btn-primary w-100"]) }}
{{ Form::close() }}
```
```html:/resources/views/users/edit.blade.php
@extends('layouts.application')

@section('title', 'Edit user')

@section('content')
    <h1>Update your profile</h1>

    <div class="row">
        <div class="col-md-6 offset-md-3">
            @include("users.form", ["options" => ['route' => ['users.update', $user->id], "method" => "patch"], "button_text" => "Save changes"])

            <div class="gravatar_edit mt-3">
                <img src="{{ gravatar_url($user) }}" class="gravatar" alt="Example User">
                <a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a>
            </div>
        </div>
    </div>
@endsection
```
```html:/resources/views/users/create.blade.php
@extends('layouts.application')

@section('title', 'Sign up')

@section('content')
    <h1>Sign up</h1>

    <div class="row">
        <div class="col-md-6 offset-md-3">
            @include("users.form", ["options" => ['route' => 'signup'], "button_text" => "Create my account"])
        </div>
    </div>
@endsection
```

### 10.1.2 編集の失敗

```php:/app/Http/Controllers/UsersController.php
    public function update(Request $request, User $user)
    {
        $request->email = strtolower($request->email);
        $request->validate([
            "name"                  => "required|max:50",
            "email"                 => "required|max:255|email|unique:users",
            "password"              => "nullable|confirmed|min:6",
            "password_confirmation" => "nullable|min:6",
        ]);
        $user->name = $request->name;
        $user->email = $request->email;
        if (!empty($request->password)) {
            $user->password = bcrypt($request->password);
        }
        $user->save();
        return redirect()->route("users.show", [$user->id]);
    }
```

### 10.1.3 編集失敗時のテスト

```php:/tests/Feature/UsersEditTest.php
class UsersEditTest extends TestCase
{
    private $user;

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

    public function testUnsuccessfulEdit()
    {
        $response = $this->get(route("users.edit", $this->user->id));
        $response->assertViewIs("users.edit");
        $response = $this->followingRedirects()
            ->patch(route("users.update", $this->user->id), [
                "name"                  => " ",
                "password"              => "foo",
                "password_confirmation" => "bar",
            ]);
        $response->assertViewIs("users.edit");
    }
}
```

### 10.1.4 TDDで編集を成功させる

```php:/tests/Feature/UsersEditTest.php
    public function testSuccessfulEdit()
    {
        $response = $this->get(route("users.edit", $this->user->id));
        $response->assertViewIs("users.edit");
        $name = "Foo Bar";
        $email = "foo@bar.com";
        $response = $this->followingRedirects()
            ->patch(route("users.update", $this->user->id), [
                "name"                  => $name,
                "email"                 => $email,
                "password"              => "",
                "password_confirmation" => "",
            ]);
        $response->assertViewIs("users.show");
        $response = $this->get(route("users.show", $this->user->id));
        $this->assertEquals($name, $response->original->user->name);
        $this->assertEquals($email, $response->original->user->email);
    }
```
```php:/app/Http/Controllers/UsersController.php
session()->flash("message", ['success' => 'Profile updated']);
```

emailが変わらないケースに対応

```php:/app/Http/Requests/UserUpdatePost.php
        $request->validate([
            "name" => "required|max:50",
            "email" => ['required', 'max:255', 'email', Rule::unique('users')->ignore(Auth::id())],
            "password" => "nullable|confirmed|min:6",
            "password_confirmation" => "nullable|min:6"
        ]);
```

## 10.2 認可

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

```php:/app/Http/Middleware/Authenticate.php
class Authenticate
{
    public function handle($request, Closure $next, ...$gurded)
    {
        if (!Auth::check()) {
            session()->flash('message', ['danger' => 'Please login.']);
            return redirect()->route('login');
        }

        return $next($request);
    }
}
```

特定アクションにログインを強要

```php:/app/Http/Controllers/UsersController.php
    public function __construct()
    {
        $this->middleware('auth')->only(["edit", "update"]);
    }
```
```php:/tests/Feature/UsersEditTest.php
    public function testUnsuccessfulEdit()
    {
        $this->actingAs($this->user);
```
```php:/tests/Feature/UsersEditTest.php
    public function testSuccessfulEdit()
    {
        $this->actingAs($this->user);
```
```php:/tests/Unit/UsersControllerTest.php
    private $user;

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

...

    public function testRedirectEditWithGuest()
    {
        $response = $this->get(route("users.edit", $this->user->id));
        $response->assertSessionHas("message");
        $response->assertRedirect(route("login"));
    }

    public function testRedirectUpdateWithGuest()
    {
        $response = $this->patch(route("users.update", $this->user->id), [
                        "name" => $this->user->name,
                        "email" => $this->user->email
                    ]);
        $response->assertSessionHas("message");
        $response->assertRedirect(route("login"));
    }
```

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

```php:/database/seeds/TestSeeder.php
    public function run()
    {
        DB::table('users')->insert([
            'name'       => "Michael Example",
            'email'      => strtolower("michael@example.com"),
            'password'   => bcrypt('password'),
            'created_at' => Carbon::now(),
            'updated_at' => Carbon::now(),
        ]);

        DB::table('users')->insert([
            'name'       => "Sterling Archer",
            'email'      => strtolower("duchess@example.gov"),
            'password'   => bcrypt('password'),
            'created_at' => Carbon::now(),
            'updated_at' => Carbon::now(),
        ]);
    }
```
```php:/tests/Unit/UsersControllerTest.php
    private $user;
    private $other_user;

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

...

    public function testRedirectEditWrongUser()
    {
        $this->actingAs($this->other_user);
        $response = $this->get(route("users.edit", $this->user->id));
        $response->assertSessionMissing("message");
        $response->assertRedirect("/");
    }

    public function testRedirectUpdateWrongUser()
    {
        $this->actingAs($this->other_user);
        $response = $this->patch(route("users.update", $this->user->id), [
            "name" => $this->user->name,
            "email" => $this->user->email
        ]);
        $response->assertSessionMissing("message");
        $response->assertRedirect("/");
    }
```
```php:/app/Http/Controllers/UserController.php
        $this->middleware('auth')->only(["edit", "update"]);
        $this->middleware(function ($request, $next) {
            if ($request->user->id != Auth::id()) {
                return redirect('/');
            }
            return $next($request);
        })->only(["edit", "update"]);
```

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

```php:/tests/Feature/UsersEditTest.php
    private $user_pass;

    protected function setUp(): void
    {
        parent::setUp();
        Artisan::call('migrate:fresh');
        $this->seed('TestSeeder');
        $this->user = User::find(1);
        $this->user_pass = "password";
    }
        
...

    public function testSuccessfulEdit()
    {
        $this->get(route("users.edit", $this->user->id));
        $response = $this->followingRedirects()
            ->post(route("login"), [
                "email"    => $this->user->email,
                "password" => $this->user_pass,
            ]);
        $response->assertViewIs("users.edit");
        $name = "Foo Bar";
        $email = "foo@bar.com";
        $response = $this->followingRedirects()
            ->patch(route("users.update", $this->user->id), [
                "name"                  => $name,
                "email"                 => $email,
                "password"              => "",
                "password_confirmation" => "",
            ]);
        $response->assertSeeText("Profile updated");
        $response->assertViewIs("users.show");
        $response = $this->get(route("users.show", $this->user->id));
        $this->assertEquals($name, $response->original->user->name);
        $this->assertEquals($email, $response->original->user->email);
    }
```
```php:/app/Http/Middleware/Authenticate.php
        if (!Auth::check()) {
            session()->flash('message', ['danger' => 'Please login.']);
            session(["url.intended" => url()->current()]);
            return redirect('/login');
        }
```
```php:/app/Http/Controllers/SessionController.php
    public function store(Request $request)
    {
        $user = User::where("email", Str::lower($request->email))->first();
        if ($user && Hash::check($request->password, $user->password)) {
            Auth::login($user, $request->remember_me === "1");
            return redirect()->intended(route("users.show", $user->id));
        } else {
            session()->flash('message', ['danger' => 'Invalid email/password combination']);
            return back()->withInput();
        }
    }
```

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

### 10.3.1 ユーザーの一覧のページ

```php:/tests/Unit/UsersControllerTest.php
    public function testRedirectIndexGuest()
    {
        $response = $this->get(route("users.index"));
        $response->assertRedirect(route("login"));
    }
```
```php:/app/Http/Controllers/UsersController.php
$this->middleware('authenticate')->only(["index", "edit", "update"]);
...
    public function index()
    {
        $users = User::all();
        return view("users.index")->with("users", $users);
    }
```
```html:/resources/views/users/index.blade.php
@extends('layouts.application')

@section('title', "All users")

@section('content')
<h1>All users</h1>

<ul class="users">
    @foreach ($users as $user)
    <li>
        <img src="{{ gravatar_url($user, ["size" => 50]) }}" class="gravatar" alt="Example User">
        {{ link_to_route("users.show", $user->name, [$user->id]) }}
    </li>
    @endforeach
</ul>
@endsection
```
```html:/resources/views/layouts/header.blade.php
<li class="nav-item">{{ link_to_route("users.index", "Users", [], ["class" => "nav-link"]) }}</li>
```
```scss:/resources/sass/app.scss
/* Users index */

.users {
    list-style: none;
    margin: 0;

    li {
        overflow: auto;
        padding: 10px 0;
        border-bottom: 1px solid #eeeeee;
    }
}
```

### 10.3.2 サンプルのユーザー

```php:/database/factories/UserFactory.php
    public function definition()
    {
        return [
            'name'     => $this->faker->name(),
            'email'    => "example-{$this->faker->unique()->randomNumber}@railstutorial.org",
            'password' => bcrypt("password"),
        ];
    }
```
```php:/database/seeds/DatabaseSeeder.php
class DatabaseSeeder extends Seeder
{
    public function run()
    {
        User::create([
            "name"     => "Example User",
            "email"    => "example@railstutorial.org",
            "password" => bcrypt("foobar"),
        ]);

        User::factory()
            ->count(99)
            ->create();
    }
}
```

### 10.3.3 ページネーション

```php:/app/Http/Controllers/UsersController.php
    public function index()
    {
        $users = User::paginate(30);
        return view('users.index')->with('users', $users);
    }
```
```html:/resources/views/users/index.blade.php
@extends('layouts.application')

@section('title', "All users")

@section('content')
    <h1>All users</h1>

    {{ $users->links() }}

    <ul class="users">
        @foreach ($users as $user)
            <li>
                <img src="{{ gravatar_url($user, ["size" => 50]) }}" class="gravatar" alt="Example User">
                {{ link_to_route("users.show", $user->name, [$user->id]) }}
            </li>
        @endforeach
    </ul>

    {{ $users->links() }}
@endsection
```
```php:/app/Providers/AppServiceProvider.php
    public function boot()
    {
        Paginator::useBootstrap();
    }
```

### 10.3.4 ユーザー一覧のテスト

```php:/database/seeds/TestSeeder.php
class TestSeeder extends Seeder
{
    public function run()
    {
        User::create(["name" => "Michael Example", "email" => "michael@example.com", "password" => bcrypt("password")]);
        User::create(["name" => "Sterling Archer", "email" => "duchess@example.gov", "password" => bcrypt("password")]);
        User::create(["name" => "Lana Kane",       "email" => "hands@example.gov",   "password" => bcrypt("password")]);
        User::create(["name" => "Malory Archer",   "email" => "boss@example.gov",    "password" => bcrypt("password")]);

        User::factory()
            ->count(30)
            ->create();
    }
}
```
```php:/tests/Unit/UsersIndexTest.php
class UsersIndexTest extends TestCase
{
    private $user;

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

    public function testIndexPagination()
    {
        $this->actingAs($this->user);
        $response = $this->get(route("users.index"));
        $response->assertViewIs("users.index");
        $dom = $this->dom($response->content());
        $this->assertSame(2, $dom->filter("ul.pagination")->count());
        foreach (User::paginate(30) as $user) {
            $this->assertSame(route("users.show", $user->id), $dom->filter("a:contains(\"{$user->name}\")")->attr("href"));
        }
    }
}
```

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

```html:/resoueces/views/users/index.blade.php
<ul class="users">
    @foreach ($users as $user)
        @include('users.user')
    @endforeach
</ul>
```
```html:/resources/views/users/user.blade.php
<li>
    <img src="{{ gravatar_url($user, ["size" => 50]) }}" class="gravatar" alt="Example User">
    {{ link_to_route("users.show", $user->name, [$user->id]) }}
</li>
```

## 10.4 ユーザーを削除する

### 10.4.1 管理ユーザー
```php:/database/migrations/[timestamp]_add_admin_to_users.php
class AddAdminToUsers extends Migration
{
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->boolean('admin')->default(false);
        });
    }
    
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('admin');
        });
    }
}
```
```php:/database/seeds/DatabaseSeeder.php
        User::create([
            "name" => "Example User", 
            "email" => "example@railstutorial.org", 
            "password" => bcrypt("foobar"), 
            "admin" => true
        ]);
```

アドミンを更新出来ないようモデルを修正

```php:/app/Http/Models/User.php
    protected $guarded = ['id', 'admin'];
...
    protected $casts = [
        'email_verified_at' => 'datetime',
        'admin'             => 'boolean',
    ];
```

#### 課題

```php:/tests/Unit/UsersControllerTest.php
    public function testEditAdminAttributeViaWeb()
    {
        $this->actingAs($this->other_user);
        $this->assertSame(false, $this->other_user->admin);
        $this->patch(route("users.update", $this->other_user->id), [
                "name" => $this->other_user->name,
                "email" => $this->other_user->email,
                "admin" => true
        ]);
        $this->assertSame(false, User::find(2)->admin);
    }
```

### 10.4.2 destroyアクション

```html:/resources/views/users/user.blade.php
<li>
    <img src="{{ gravatar_url($user, ["size" => 50]) }}" class="gravatar" alt="Example User">
    {{ link_to_route("users.show", $user->name, [$user->id]) }}
    @if (auth()->user()->admin === true && auth()->user() != $user)
        | <a href="javascript:if(window.confirm('Yes Sure?')){document.deleteform{{$user->id}}.submit()}">delete</a>
        {{ Form::open(["route" => ["users.destroy", $user->id], "method" => "delete", "name" => "deleteform{$user->id}"]) }}
        {{ Form::close() }}
    @endif
</li>
```
```php:/app/Http/Controllers/UsersController.php
        $this->middleware('auth')->only(["index", "edit", "update", "destroy"]);
...
    public function destroy(User $user)
    {
        $user->delete();
        session()->flash("message", ["success" => "User deleted"]);
        return redirect()->route("users.index");
    }
```
```php:/app/Http/Controllers/UsersController.php
        $this->middleware(function ($request, $next) {
            if (Auth::user()->admin === false) {
                return redirect('/');
            }
            return $next($request);
        })->only(["destroy"]);
```

### 10.4.3 ユーザー削除のテスト

```php:/database/seeds/TestSeeder.php
User::create(["name" => "Michael Example", "email" => "michael@example.com", "password" => bcrypt("password"), "admin" => true]);
```
```php:/tests/Unit/UsersControllerTest.php
    public function testRedirectDestroyGuest()
    {
        $count = User::all()->count();
        $response = $this->delete(route("users.destroy", $this->user->id));
        $this->assertEquals($count, User::all()->count());
        $response->assertRedirect(route("login"));
    }

    public function testRedirectDestroyNonadmin()
    {
        $this->actingAs($this->other_user);
        $count = User::all()->count();
        $response = $this->delete(route("users.destroy", $this->user->id));
        $this->assertEquals($count, User::all()->count());
        $response->assertRedirect("/");
    }
```
```php:/tests/Feature/UsersIndexTest.php
class UsersIndexTest extends TestCase
{
    private $admin;
    private $non_admin;

    protected function setUp(): void
    {
        parent::setUp();
        Artisan::call('migrate:fresh');
        $this->seed('TestSeeder');
        $this->admin = User::find(1);
        $this->non_admin = User::find(2);
    }

    public function testIndexAsAdmin()
    {
        $this->actingAs($this->admin);
        $response = $this->get(route("users.index"));
        $response->assertViewIs("users.index");
        $dom = $this->dom($response->content());
        $this->assertSame(2, $dom->filter("ul.pagination")->count());
        foreach (User::paginate(30) as $user) {
            $this->assertSame(route("users.show", $user->id), $dom->filter("a:contains(\"{$user->name}\")")->attr("href"));
            if ($user != $this->admin) {
                $this->assertSame(route("users.destroy", $user->id), $dom->filter("form[name=deleteform{$user->id}]")->attr("action"));
            }
        }
        $count = User::all()->count();
        $this->delete(route("users.destroy", $this->non_admin->id));
        $this->assertSame($count - 1, User::all()->count());
    }

    public function testIndexAsNonadmin()
    {
        $this->actingAs($this->non_admin);
        $response = $this->get(route("users.index"));
        $dom = $this->dom($response->content());
        $this->assertSame(0, $dom->filter('a:contains("delete")')->count());
    }
}
```

## 10.5 最後に

本番でもfakerを使う場合はインストールする

```bash
composer require fakerphp/faker
```

# 参考