環境
- 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