CodeIgniter3ユーザーのためのLaravel10入門:会員制Twitterアプリを作りながら学ぶ
はじめに
この記事では、CodeIgniter3ユーザー向けにLaravel10の機能を紹介します。私が所属する企業のメンバーを対象に、具体的なアプリケーションを通してLaravel10の魅力を解説します。
以下のようなアプリケーションを作成するシナリオで、実際に手を動かしながら、どういった機能があるかを見ていきましょう。
- 会員制のTwitterライクなアプリケーション
- 会員登録、ログイン、閲覧、投稿、削除の機能
- 投稿は30文字以内、一人5個まで
- 自分の投稿のみ削除できる
ソースコードはこちら。
プロジェクトの作成
まず最初に、Laravel10プロジェクトを作成しましょう。Laravelの公式インストールガイドに従い、次のComposerコマンドを実行します。
composer create-project laravel/laravel LaravelTwitter
このコマンドにより、LaravelTwitter
という名前のディレクトリが作成され、その中にはLaravelフレームワークとサンプルコードが含まれています。生成されたディレクトリ構造は以下の通りです。
LaravelTwitter/
├── app/
├── bootstrap/
├── config/
├── database/
├── public/
├── resources/
├── routes/
├── storage/
├── tests/
├── vendor/
├── ...
このディレクトリ構造は、Laravelアプリケーションの主要な要素を含んでおり、開発を始めるための基盤となります。
簡易的なDocker開発環境の構築
Laravel Sailを使用して、簡単にDocker開発環境を構築しましょう。Laravel SailはLaravelに組み込まれた機能で、以下のComposerコマンドを使ってインストールできます。
composer require laravel/sail --dev
次に、artisanコマンドを使用してLaravel Sailのセットアップを行います。artisanはLaravelが提供するコマンドラインインターフェイスです。
php artisan sail:install
これによりプロジェクト内に docker-compose.yml
ファイルが生成されます。データベースの内容を可視化するため、phpMyAdminを組み込んでおきましょう。
~~~
phpmyadmin:
image: phpmyadmin/phpmyadmin
links:
- mysql:mysql
ports:
- 8080:80
environment:
#PMA_USER: "${DB_USERNAME}"
#PMA_PASSWORD: "${DB_PASSWORD}"
PMA_HOST: mysql
networks:
- sail
~~~
そして、以下のコマンドを使用してDocker開発環境を起動できます。
./vendor/bin/sail up -d
./vendor/bin/sail npm install # 初回のみ
./vendor/bin/sail npm run dev
以上の手順が完了すると、ブラウザで localhost を開くと、Laravelのデフォルト画面が表示されます。
認証機能の作成とフロントエンドのセットアップ
Laravel Breezeを使用して、簡単に認証機能とフロントエンドをセットアップしましょう。Laravel Breezeは、以下のComposerコマンドを使用してインストールできます。
composer require laravel/breeze --dev
次に、artisanコマンドを使用してBreezeのインストールを行います。
./vendor/bin/sail artisan breeze:install
コマンドを実行すると、いくつかの選択肢が表示されます。今回は以下のように選択します。
- React with Inertia
- TypeScript (experimental)
- PHPUnit
これで、 localhost を開くと、Laravelのデフォルト画面にログインと会員登録の項目が追加されます。
さっそく会員登録を行ってみましょう。
すると、データベースにusers
テーブルが作成されていないため、以下のようなエラーが表示されるはずです。
データベースにusers
テーブルを作っていないので、データ保存ができないというエラーです。
マイグレーションを使用してローカルの開発環境にusersテーブルを作成する
Laravelのマイグレーション機能は、データベーステーブルの構造を定義・変更するための強力なツールです。マイグレーションを実行することで、CREATE TABLE や ALTER TABLE などのSQLコマンドを発行し、またマイグレーションをロールバックすることで DROP TABLE なども可能です。
Laravel Breezeには既にいくつかのマイグレーションファイルが用意されています。その中に users
テーブルのマイグレーションファイルが含まれているので、その内容を確認してみましょう。
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
}
};
このマイグレーションファイルでは、users
テーブルに対して各カラムの定義を行っています。
次に、以下のコマンドを使用してマイグレーションを実行してみましょう。
./vendor/bin/sail artisan migrate
実行後、phpMyAdminでローカルデータベースの中身を確認すると、users
テーブルが作成されていることがわかります。
また、実行したマイグレーションの履歴は migrations
テーブルに保存されており、phpMyAdminで確認できます。
ここには各マイグレーションの実行履歴が保存され、バージョン管理のような機能が提供されています。
私が勤めている会社でのCodeIgniter3のModelクラスとLaravel10のModelクラスの使い方の違い
データの参照について
$query = $this->db->query('SELECT id, name FROM users');
// 基本的にresult_array()を使用していて連想配列で結果を受け取っている
foreach ($query->result_array() as $row)
{
echo $row['id'];
echo $row['name'];
}
$users = User::all();
foreach ($users as $user) {
echo $user->id;
echo $user->name;
}
データの保存
$data = array(
'name' => $name,
);
$this->db->insert('users', $data);
$user = new User();
$user->name = 'name';
$user->save();
ツイート閲覧機能の実装
ツイートを永続化するため、マイグレーション機能を使用して posts
テーブルを作成し、データベース操作を行う Post
モデルクラスも作成します。以下のartisanコマンドを使用して、ModelとMigrationの作成を行います。
./vendor/bin/sail artisan make:model Post --migration
生成されたマイグレーションファイルを編集します。
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->text('message');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('posts');
}
};
その後、マイグレーションコマンドを使用してデータベースに posts
テーブルを作成します。
./vendor/bin/sail artisan migrate
次に、ツイート閲覧機能のためのコントローラー (PostController
) を作成します。
./vendor/bin/sail artisan make:controller PostController
生成されたコントローラーファイル (app/Http/Controllers/PostController.php
) を編集します。
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Inertia\Inertia;
use Inertia\Response;
class PostController extends Controller
{
public function index(): Response
{
// 投稿を取得する
$posts = Post::join('users', 'users.id', '=', 'posts.user_id')
->select(['posts.id as post_id', 'user_id', 'message', 'users.name as user_name'])
->orderBy('posts.id', 'desc')
->get();
// 投稿データをビューに渡す
return Inertia::render('Posts', [
'posts' => $posts,
]);
}
}
次に、ルーティング設定を行います。
~~~
Route::middleware('auth')->group(function () {
~~~
Route::get('/posts', [PostController::class, 'index'])->name('posts');
});
~~~
以上で、投稿一覧画面が表示されるようになりました。しかし、現時点ではまだ投稿が存在していません。そのため、ダミーデータを作成するためにFactoryとSeederを使用します。
./vendor/bin/sail artisan make:factory Post
./vendor/bin/sail artisan make:seeder PostSeeder
PostFactory
でダミーデータの定義を行います。
<?php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Post>
*/
class PostFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'user_id' => User::factory(),
'message' => fake()->text(30)
];
}
}
その後、PostSeeder
でダミーデータを作成します。
<?php
namespace Database\Seeders;
use App\Models\Post;
use Illuminate\Database\Seeder;
class PostSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
Post::factory()->count(5)->create();
}
}
./vendor/bin/sail artisan db:seed --class=PostSeeder
ツイート投稿機能の実装
投稿一覧画面に入力フォームを用意します。
ツイート投稿機能を実装するために、まずはフォームバリデーションを行います。フォームバリデーションをコントローラーに直接記述することも可能ですが、コードの構造をより明確にするため、FormRequest
を使用します。以下のartisanコマンドを使用して、StorePostRequest
フォームリクエストを作成します。
./vendor/bin/sail artisan make:request StorePostRequest
生成された StorePostRequest
ファイルを編集してフォームバリデーションのルールを定義します。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'message' => 'required|max:30',
];
}
}
次に、データベースの値に応じたバリデーションおよび投稿データの永続化を行うため、このユースケースを満たすクラスを新たに作成します。これにより、コントローラーの実装がより薄くなり、コードの見通しが良くなると考えています。
<?php
namespace App\UseCases;
use App\Models\Post;
class PostStoreAction
{
public function __invoke(int $user_id, Post $post)
{
// 投稿ユーザーの投稿数を取得
$countPosts = Post::where('user_id', $user_id)->count();
// 投稿数が5つ以上は投稿できない
if ($countPosts >= 5) {
abort(400, '投稿数の上限に達しました。');
}
// 投稿データを保存する
$post->save();
}
}
コントローラー内でフォームリクエストおよびPostStoreAction
クラスを使用してツイート投稿機能を実現します。
~~~
public function store(StorePostRequest $request, PostStoreAction $postStoreAction): RedirectResponse
{
// モデルクラスをインスタンス化 (バリデーションした値を同時に入れる)
$post = new Post($request->validated());
// ログインユーザーのユーザーIDをセット
$post->user_id = Auth::user()->id;
// データベースの値に応じたバリデーション
// 及び永続化
$postStoreAction(Auth::user()->id, $post);
// フラッシュメッセージと共に一覧画面に遷移
return back()->with('message', '投稿しました');
}
~~~
なお、コントローラーのメソッドの引数にあるフォームリクエスト、別レイヤーとして切り出した処理のクラスはLaravelがよしなにDIしてくれます。
最後にルーティングの設定をします。
Route::get('/posts', [PostController::class, 'index'])->name('posts');
Route::post('/posts', [PostController::class, 'store'])->name('posts.store'); // 追加
投稿を試してみましょう。
これで、ツイート投稿機能が実装されました。フォームバリデーションやデータベースの値に応じたバリデーションも適切に行われています。
ツイート削除機能の実装
投稿一覧画面に削除ボタンを追加します。
削除機能については先にコントローラーを実装してみましょう。
public function destroy(Post $post): RedirectResponse
{
// 投稿を削除
$post->delete();
// フラッシュメッセージと共に一覧画面に遷移
return back()->with('message', '投稿を削除しました');
}
アクションメソッドの引数にモデルクラスが入ってきていますが、これはLaravelのルートモデルバインディングという機能を使用しており、ルーティング設定に一手間かけることで画面からフォームリクエストされた投稿IDを元に投稿モデルを取得することをしています。
// 通常はパスパラメーターがコントローラーのアクションメソッドに渡るが、モデル名を指定するとモデルのインスタンス化までしてくれる
Route::delete('/posts/{post:id}', [PostController::class, 'destroy'])->name('posts.destroy');
これで投稿の削除ができるようになりました。しかし、他のユーザーの投稿も削除できる状態です。
自身が投稿した投稿のみ削除できるように、いわゆる認可処理を書きましょう。これを実装するにはLaravelのPolicyという機能を使うのが便利です。
まずはartisanコマンドでPolicyクラスを作成します。
./vendor/bin/sail artisan make:policy PostPolicy
Policyクラスを編集します。
<?php
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class PostPolicy
{
public function delete(User $user, Post $post): Response
{
if ($post->user_id === $user->id) {
return Response::allow();
}
return Response::deny('この投稿の削除権限がありません。');
}
}
さらにコントローラークラスに1行追加します。
public function destroy(Post $post): RedirectResponse
{
// 認可 (先ほど作ったPolicyクラスが内部で動作する)
$this->authorize('delete', $post);
// 投稿を削除
$post->delete();
// フラッシュメッセージと共に一覧画面に遷移
return back()->with('message', '投稿を削除しました');
}
試しに自分の投稿とダミーデータの投稿を削除してみましょう。ダミーデータの場合は自身のユーザーIDとダミーユーザーIDが一致しないため、削除権限がないというメッセージが表示されます。
5個しか投稿できないというテストのコードを書く
データベースの値に応じたバリデーションはPostStoreAction
に記述していました。このクラスに対して
- 5つまで投稿できる
- 6つは投稿できない
機能テストを書いてみましょう。
<?php
namespace Tests\Feature\UseCases;
use App\Models\Post;
use App\Models\User;
use App\UseCases\PostStoreAction;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Tests\TestCase;
class PostStoreActionTest extends TestCase
{
use RefreshDatabase;
/**
* @doesNotPerformAssertions
*/
public function testユーザーは5つまで投稿できる()
{
$user = User::factory()->create();
Post::factory()->count(4)->create(['user_id' => $user->id]);
$post = new Post();
$post->user_id = $user->id;
$post->message = 'test';
$postStoreAction = new PostStoreAction();
$postStoreAction($user->id, $post);
}
public function test6つ目の投稿は失敗する()
{
$user = User::factory()->create();
Post::factory()->count(5)->create(['user_id' => $user->id]);
$post = new Post();
$post->user_id = $user->id;
$post->message = 'test';
$postStoreAction = new PostStoreAction();
$this->expectException(HttpException::class);
$postStoreAction($user->id, $post);
}
}
RefreshDatabase
というtraitが出てきました。これは公式ドキュメントから引用すると
以前のテストデータが後続のテストに干渉しないように、各テストの後にデータベースをリセットする
機能になります。
また、テストコードでは初めの方に登場していたFactoryを活用することで一時的にダミーデータを作ることができます。
テストコードの実行は以下のコマンドから行うことができます。
./vendor/bin/sail test
自分の投稿のみ削除できるというテストのコードを書く
コントローラーのテストコードを書いてみましょう。
<?php
namespace Tests\Feature\Http\Controllers;
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PostControllerTest extends TestCase
{
use RefreshDatabase;
public function testDelete投稿者自身が削除()
{
$post = Post::factory()->create();
$user = User::find($post->user_id);
$this->actingAs($user)
->delete('/posts/'.$post->id)
->assertStatus(302);
}
public function testDelete投稿者以外が削除()
{
$post = Post::factory()->create();
$user = User::factory()->create();
$this->actingAs($user)
->delete('/posts/'.$post->id)
->assertStatus(403);
}
}
actingAs($user)
というのは、特定のユーザーとしてログインしているのをシュミレートするようなメソッドになります。
終わりに
バックエンド開発者の皆さんに向けてLaravelの魅力を紹介しました。新しいフレームワークへの乗り換えは一歩が大きいかもしれませんが、その先にはより柔軟で効率的な開発が広がっています。CodeIgniter3からの転換が、新しいプロジェクトのスタートになりますように。
Discussion