0
0

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 チュートリアル【10章】更新・削除

Last updated at Posted at 2020-11-14

環境

  • Laravel 8.6
  • PHP 8.0
  • Bootstrap 5.1

10.1 ユーザーを更新する

10.1.1 編集フォーム

/app/Http/Controllers/UsersController.php
    public function edit(User $user)
    {
        return view('users.edit')->with('user', $user);
    }
/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
/retources/views/layouts/header.blade.php
<li>{{ link_to_route("users.edit", "Settings", [auth()->id()], ["class" => "dropdown-item"]) }}</li>

課題

/resources/views/users/edit.blade.php
<a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a>
/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() }}
/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
/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 編集の失敗

/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 編集失敗時のテスト

/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で編集を成功させる

/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);
    }
/app/Http/Controllers/UsersController.php
session()->flash("message", ['success' => 'Profile updated']);

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

/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 ユーザーにログインを要求する

/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);
    }
}

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

/app/Http/Controllers/UsersController.php
    public function __construct()
    {
        $this->middleware('auth')->only(["edit", "update"]);
    }
/tests/Feature/UsersEditTest.php
    public function testUnsuccessfulEdit()
    {
        $this->actingAs($this->user);
/tests/Feature/UsersEditTest.php
    public function testSuccessfulEdit()
    {
        $this->actingAs($this->user);
/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 正しいユーザーを要求する

/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(),
        ]);
    }
/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("/");
    }
/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 フレンドリーフォワーディング

/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);
    }
/app/Http/Middleware/Authenticate.php
        if (!Auth::check()) {
            session()->flash('message', ['danger' => 'Please login.']);
            session(["url.intended" => url()->current()]);
            return redirect('/login');
        }
/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 ユーザーの一覧のページ

/tests/Unit/UsersControllerTest.php
    public function testRedirectIndexGuest()
    {
        $response = $this->get(route("users.index"));
        $response->assertRedirect(route("login"));
    }
/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);
    }
/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
/resources/views/layouts/header.blade.php
<li class="nav-item">{{ link_to_route("users.index", "Users", [], ["class" => "nav-link"]) }}</li>
/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 サンプルのユーザー

/database/factories/UserFactory.php
    public function definition()
    {
        return [
            'name'     => $this->faker->name(),
            'email'    => "example-{$this->faker->unique()->randomNumber}@railstutorial.org",
            'password' => bcrypt("password"),
        ];
    }
/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 ページネーション

/app/Http/Controllers/UsersController.php
    public function index()
    {
        $users = User::paginate(30);
        return view('users.index')->with('users', $users);
    }
/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
/app/Providers/AppServiceProvider.php
    public function boot()
    {
        Paginator::useBootstrap();
    }

10.3.4 ユーザー一覧のテスト

/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();
    }
}
/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 パーシャルのリファクタリング

/resoueces/views/users/index.blade.php
<ul class="users">
    @foreach ($users as $user)
        @include('users.user')
    @endforeach
</ul>
/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 管理ユーザー

/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');
        });
    }
}
/database/seeds/DatabaseSeeder.php
        User::create([
            "name" => "Example User", 
            "email" => "example@railstutorial.org", 
            "password" => bcrypt("foobar"), 
            "admin" => true
        ]);

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

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

課題

/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アクション

/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>
/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");
    }
/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 ユーザー削除のテスト

/database/seeds/TestSeeder.php
User::create(["name" => "Michael Example", "email" => "michael@example.com", "password" => bcrypt("password"), "admin" => true]);
/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("/");
    }
/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を使う場合はインストールする

composer require fakerphp/faker

参考

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?