CodeIgniter3ユーザーがCodeIgniter4を触ってみた
はじめに
この記事は私が勤めている会社の人向けの、CodeIgniter4の紹介です。
以下のWebアプリケーションを作るシナリオで、機能をざっくりと見ていきます。
- 会員制の掲示板
- 会員登録、ログイン、閲覧、投稿、削除の機能
- 投稿は30文字以内、一人5個まで
- 自分の投稿のみ削除できる
動作確認環境は以下になります。
- MacOS Sonoma 14.0
- kool.dev 2.2.0
- CodeIgniter 4.4.3
ソースコードはこちら。
kool.devのインストール
Docker開発環境をお手軽に用意するためのツールをインストールしておきます。
curl -fsSL https://kool.dev/install | bash
プロジェクトの作成と環境構築
kool create codeigniter CodeIgniter4Twitter
? Which PHP version do you want to use PHP 8.2
⇒ PHP 8.2
? Which database service do you want to use MySQL 8.0
⇒ MySQL 8.0
? Which cache service do you want to use None - do not use a key/value cache
⇒ None - do not use a key/value cache
? Which Javascript package manager do you want to use None
⇒ None
環境変数の定義がまとめられているenvファイルを編集します。
app.baseURL = 'http://localhost/'
CI_ENVIRONMENT = development
DB_DATABASE = ci4
DB_USERNAME = user
DB_PASSWORD = pass
database.default.hostname = database
database.default.database = "${DB_DATABASE}"
database.default.username = "${DB_USERNAME}"
database.default.password = "${DB_PASSWORD}"
database.default.DBDriver = MySQLi
database.default.DBPrefix =
database.default.port = 3306
koolのセットアップコマンドを実行します。
cd CodeIgniter4Twitter/
kool run setup
docker-compose.ymlを編集してphpMyAdminを追加します。
kool stop
version: "3.8"
#
# Services definitions
#
services:
app:
image: kooldev/php:8.2-nginx
ports:
- "${KOOL_APP_PORT:-80}:80"
environment:
ASUSER: "${KOOL_ASUSER:-0}"
UID: "${UID:-0}"
volumes:
- .:/app:delegated
networks:
- kool_local
- kool_global
database:
image: mysql/mysql-server:8.0
command: --default-authentication-plugin=mysql_native_password
ports:
- "${KOOL_DATABASE_PORT:-3306}:3306"
environment:
MYSQL_ROOT_HOST: "%"
MYSQL_ROOT_PASSWORD: "${DB_PASSWORD-rootpass}"
MYSQL_DATABASE: "${DB_DATABASE-database}"
MYSQL_USER: "${DB_USERNAME-user}"
MYSQL_PASSWORD: "${DB_PASSWORD-pass}"
MYSQL_ALLOW_EMPTY_PASSWORD: 1
volumes:
- database:/var/lib/mysql:delegated
networks:
- kool_local
healthcheck:
test: ["CMD", "mysqladmin", "ping"]
phpmyadmin:
image: phpmyadmin/phpmyadmin
depends_on:
- database
environment:
PMA_HOST: database
PMA_PORT: ${DB_PORT:-3306}
PMA_USER: root
PMA_PASSWORD: ${DB_PASSWORD-pass}
ports:
- 81:80
networks:
- kool_local
#
# Networks definitions
#
networks:
kool_local:
kool_global:
external: true
name: "${KOOL_GLOBAL_NETWORK:-kool_global}"
volumes:
database:
コンテナを立ち上げます。
kool start
kool status
+------------+---------+-----------------------------------------+---------------------------------+
| SERVICE | RUNNING | PORTS | STATE |
+------------+---------+-----------------------------------------+---------------------------------+
| app | Running | 0.0.0.0:80->80/tcp, 9000/tcp | Up 2 seconds |
| database | Running | 0.0.0.0:3306->3306/tcp, 33060-33061/tcp | Up 2 seconds (health: starting) |
| phpmyadmin | Running | 0.0.0.0:81->80/tcp | Up 2 seconds |
+------------+---------+-----------------------------------------+---------------------------------+
http://localhost
が見れるようになります。
CodeIgniter Shieldのインストール
CodeIgniter Shieldは公式の認証パッケージです。
会員登録、ログインの認証機能をお手軽に作ることができます。
kool run composer require codeigniter4/shield:dev-develop
kool run spark shield:setup
The required Config\Email::$fromEmail is not set. Do you set now? [y, n]: n
The required Config\Email::$fromName is not set. Do you set now? [y, n]: n
Run `spark migrate --all` now? [y, n]: y
以下URLで、ログイン・ログアウト・会員登録の機能が追加されます。
http://localhost/login
http://localhost/logout
http://localhost/register
DebugバーのAuthの項目でログイン状況を確認できます。
投稿テーブルとModelクラスの準備
CodeIgniter4にはマイグレーションの機能があるので使用します。
コマンドでマイグレーションファイルを作成します。
kool run spark make:migration AddPosts
マイグレーションファイルを編集します。
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPosts extends Migration
{
public function up()
{
//
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 5,
'unsigned' => true,
'auto_increment' => true,
],
'user_id' => [
'type' => 'INT',
'constraint' => 5,
'unsigned' => true,
],
'message' => [
'type' => 'TEXT',
],
]);
$this->forge->addKey('id', true);
$this->forge->createTable('posts');
}
public function down()
{
//
$this->forge->dropTable('posts');
}
}
マイグレーションを実行してPostsテーブルを作成します。
kool run spark migrate
phpMyAdminで確認するとpostsテーブルが出来上がっていることが確認できます。
続いて、Postsテーブルに対応するModelクラスの作成をします。
こちらもコマンドからファイルを作成します。
kool run spark make:model Posts
コマンドから自動生成されたModelクラスの中身はこんな感じになります。
<?php
namespace App\Models;
use CodeIgniter\Model;
class Posts extends Model
{
protected $table = 'posts';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $allowedFields = [];
// Dates
protected $useTimestamps = false;
protected $dateFormat = 'datetime';
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $deletedField = 'deleted_at';
// Validation
protected $validationRules = [];
protected $validationMessages = [];
protected $skipValidation = false;
protected $cleanValidationRules = true;
// Callbacks
protected $allowCallbacks = true;
protected $beforeInsert = [];
protected $afterInsert = [];
protected $beforeUpdate = [];
protected $afterUpdate = [];
protected $beforeFind = [];
protected $afterFind = [];
protected $beforeDelete = [];
protected $afterDelete = [];
}
ビューファイルの準備
投稿の一覧と投稿フォームのビューファイルを適当に作成しておきます。
<!DOCTYPE html>
<html lang="en">
<body>
<form action="/posts" method="POST">
<?= csrf_field() ?>
<textarea name='message'></textarea>
<button type="submit">
投稿
</button>
</form>
<?= validation_show_error('message') ?>
<?php if(session()->has('error')): ?>
<?= session('error') ?>
<?php endif; ?>
<?php foreach ($posts as $key => $post) : ?>
<div>
<div>
<div>
<div>
post_id : <?php echo $post->id; ?> <br/>
message : <?php echo $post->message; ?>
</div>
<form action="/posts/<?php echo $post->id; ?>" method="POST">
<?= csrf_field() ?>
<input type="hidden" name="_method" value="DELETE">
<button type="submit">
削除
</button>
</form>
</div>
</div>
</div>
<?php endforeach; ?>
</body>
投稿閲覧機能の実装
コントローラークラスをコマンドから作成します。
kool run spark make:controller Post
投稿一覧を取得し、ビューに渡すアクションメソッドを実装します。
<?php
namespace App\Controllers;
use App\Controllers\BaseController;
use App\Models\Posts;
class Post extends BaseController
{
public function __construct()
{
helper('form');
}
public function index()
{
// model()ヘルパーを使ってモデルクラスを読み込む
$postModel = model(Posts::class);
// 投稿を取得する
$posts = $postModel->asObject()->findAll();
// ビューを表示
return view('post_view', ['posts' => $posts]);
}
}
CodeIgniter3のModelクラスの読み込み
// モデルクラスの読み込み
$this->load->model('モデルクラス名');
// モデルクラスを使う
$this->モデルクラス名->find(['id' => $id]);
ルーティングの設定を行います。
// ログインが必要になるようにフィルターを設定しておく
$routes->get('/posts', 'Post::index', ['filter' => 'session']);
http://localhost/posts
にて一覧画面が出せました。
しかし投稿が1件もないので入力フォームしか表示されません。
ダミーデータを作り一覧を表示させましょう。
CodeIgniter4にはSeederの機能があるので、Seederでダミーデータを作ります。
kool run spark make:seeder PostsSeeder
<?php
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
class PostsSeeder extends Seeder
{
public function run()
{
$data = [
[
'user_id' => '99991',
'message' => 'testmessage1',
],
[
'user_id' => '99992',
'message' => 'testmessage2',
],
[
'user_id' => '99993',
'message' => 'testmessage3',
]
];
$this->db->table('posts')->insertBatch($data);
}
}
Seederを実行してダミーデータを作ります。
kool run spark db:seed PostsSeeder
ここまでで、一覧画面に投稿が表示されるようになりました。
投稿機能の実装
CSRF Filter を有効にしておきます。
PostsテーブルのModelクラスのallowedFieldsを編集し、データ挿入時にuser_id
とmessage
を入れられるようにしておきます。
protected $allowedFields = ['user_id', 'message'];
フォームバリデーション、データベースの値に応じたバリデーションを行なったのち、保存するアクションメソッドを実装します。
~~~
public function create()
{
$data = [
'user_id' => auth()->user()->id,
'message' => $this->request->getPost('message')
];
$rules = [
'message' => 'required|max_length[30]'
];
if ($this->request->is('post') && $this->validateData($data, $rules) == false) {
// model()ヘルパーを使ってモデルクラスを読み込む
$postModel = model(Posts::class);
// 投稿を取得する
$posts = $postModel->asObject()->findAll();
// ビューを表示
return view('post_view', ['posts' => $posts]);
}
// model()ヘルパーを使ってモデルクラスを読み込む
$postModel = model(Posts::class);
// 現在の投稿数を取得
$countPosts = $postModel->where('user_id', auth()->user()->id)->countAllResults();
// 6つ以上は投稿できない
if ($countPosts >= 5) {
return redirect()->back()->with('error', '投稿は1ユーザーにつき最大5件までです。');
}
// 投稿を保存
$postModel->save($data);
return redirect()->back();
}
CodeIgniter3のPOSTパラメタの取得
$field = $this->input->post('field');
CodeIgniter3のバリデーション
// ルールの設定
$this->form_validation->set_rules();
// バリデーションの実行
$result = $this->form_validation->run();
投稿一覧機能と同様にルーティング設定を行います。
// ログインが必要になるようにフィルターを設定
$routes->post('/posts', 'Post::create', ['filter' => 'session']);
ここまでで、投稿機能ができました。
投稿削除機能の実装
試しに自作ライブラリを作り、それを読み込んで削除する形で実装してみます。
<?php
namespace App\Libraries;
use App\Models\Posts;
use Exception;
class PostDeleteAction
{
public function __invoke(Posts $postModel, int $postId, int $loginUserId)
{
// 投稿を取得
$post = $postModel->asObject()->find($postId);
// 投稿がない場合はエラー
if (empty($post)) {
throw new Exception('投稿がありません', 404);
}
// 自分の投稿でない場合はエラー
if ($post->user_id != $loginUserId) {
throw new Exception('他人の投稿は削除できません', 403);
}
// 投稿を削除
$postModel->delete($postId);
}
}
コントローラー側
~~~
public function destroy(int $postId)
{
// model()ヘルパーを使ってモデルクラスを読み込む
$postModel = model(Posts::class);
// 削除ライブラリをインスタンス化
$postDeleteAction = new PostDeleteAction();
// 投稿を削除
$postDeleteAction($postModel, $postId, auth()->user()->id);
return redirect()->back();
}
CodeIgniter3のLibraryの読み込み方
$this->load->library('ライブラリ名');
同様にルーティング設定を行います。
$routes->delete('/posts/(:segment)', 'Post::destroy/$1', ['filter' => 'session']);
ここまでで、投稿削除機能が実装できました。
テストコード
CodeIgniter4から、テストを書くためのライブラリを公式が用意してくれています。
いくつか書いてみます。
投稿削除ライブラリのテストコード
Fabricator
を使いダミーの投稿データを作成して、それを削除するテストコードを書いてみます。
<?php
namespace Tests\Feature;
use App\Libraries\PostDeleteAction;
use App\Models\Posts;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use CodeIgniter\Test\Fabricator;
use Exception;
class PostDeleteActionTest extends CIUnitTestCase
{
use DatabaseTestTrait;
protected $refresh = true; // setUp時にデータベースをリフレッシュ
protected $namespace = 'App'; // マイグレーションファイルの名前空間
/**
* @doesNotPerformAssertions
*/
public function test正常系()
{
$fabricator = new Fabricator(Posts::class);
$fabricator->setOverrides(['user_id' => 1, 'message' => 'Post Message']);
$fabricator->create();
$postModel = model(Posts::class);
$postDeleteAction = new PostDeleteAction();
$postDeleteAction($postModel, 1, 1);
}
public function test存在しない投稿()
{
$postModel = model(Posts::class);
$postDeleteAction = new PostDeleteAction();
$this->expectException(Exception::class);
$postDeleteAction($postModel, 9999, 1);
}
public function test他人の投稿は削除できない()
{
$fabricator = new Fabricator(Posts::class);
$fabricator->setOverrides(['user_id' => 1, 'message' => 'Post Message']);
$fabricator->create();
$postModel = model(Posts::class);
$postDeleteAction = new PostDeleteAction();
$this->expectException(Exception::class);
$postDeleteAction($postModel, 1, 2);
}
}
テストコードを実行します。
./vendor/bin/phpunit --testdox
Postコントローラーのテスト (一部)
ログインユーザーとゲストユーザーの挙動を書いてみます。
<?php
namespace Tests\Feature;
use CodeIgniter\Shield\Models\UserModel;
use CodeIgniter\Shield\Test\AuthenticationTesting;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use CodeIgniter\Test\FeatureTestTrait;
class PostFeatureTest extends CIUnitTestCase
{
use FeatureTestTrait;
use DatabaseTestTrait;
use AuthenticationTesting;
protected $refresh = true; // setUp時にデータベースをリフレッシュ
protected $namespace = ['App', 'CodeIgniter\Shield', 'CodeIgniter\Settings']; // マイグレーションファイルの名前空間 CodeIgniter Shieldを動かすためにいくつか追加
protected function setUp(): void
{
parent::setUp();
helper(['auth', 'setting']);
}
public function testIndexゲストユーザーはloginにリダイレクト()
{
$response = $this->get('/posts')
->assertRedirect('/login');
}
public function testIndexログインユーザーは普通に閲覧できる()
{
$user = fake(UserModel::class);
$response = $this->actingAs($user)->get('/posts')
->assertOK();
}
}
Discussion