🙆

CodeIgniter3ユーザーがCodeIgniter4を触ってみた

2023/12/05に公開

はじめに

この記事は私が勤めている会社の人向けの、CodeIgniter4の紹介です。
以下のWebアプリケーションを作るシナリオで、機能をざっくりと見ていきます。

  • 会員制の掲示板
  • 会員登録、ログイン、閲覧、投稿、削除の機能
  • 投稿は30文字以内、一人5個まで
  • 自分の投稿のみ削除できる

動作確認環境は以下になります。

  • MacOS Sonoma 14.0
  • kool.dev 2.2.0
  • CodeIgniter 4.4.3

ソースコードはこちら。
https://github.com/coffee-r/CodeIgniter4Twitter

kool.devのインストール

Docker開発環境をお手軽に用意するためのツールをインストールしておきます。

https://kool.dev/docs/getting-started/installation

curl -fsSL https://kool.dev/install | bash

プロジェクトの作成と環境構築

https://kool.dev/docs/presets/codeigniter

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ファイルを編集します。

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を追加します。

https://zenn.dev/kenjis/articles/1bd9ceecc76c25

kool stop
docker-compose.yml
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

マイグレーションファイルを編集します。

app/Database/Migrations/2023-12-05-124948_AddPosts.php
<?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クラスの中身はこんな感じになります。

app/Models/Posts.php
<?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    = [];
}

ビューファイルの準備

投稿の一覧と投稿フォームのビューファイルを適当に作成しておきます。

app/Views/post_view.php
<!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

投稿一覧を取得し、ビューに渡すアクションメソッドを実装します。

app/Controllers/Post.php
<?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クラスの読み込み
CodeIgniter3
// モデルクラスの読み込み
$this->load->model('モデルクラス名');

// モデルクラスを使う
$this->モデルクラス名->find(['id' => $id]);

ルーティングの設定を行います。

app/Config/Routes.php
// ログインが必要になるようにフィルターを設定しておく
$routes->get('/posts', 'Post::index', ['filter' => 'session']);

http://localhost/postsにて一覧画面が出せました。
しかし投稿が1件もないので入力フォームしか表示されません。

ダミーデータを作り一覧を表示させましょう。
CodeIgniter4にはSeederの機能があるので、Seederでダミーデータを作ります。

kool run spark make:seeder PostsSeeder
app/Database/Seeds/PostsSeeder.php
<?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 を有効にしておきます。
https://www.codeigniter.com/user_guide/tutorial/create_news_items.html#id1

PostsテーブルのModelクラスのallowedFieldsを編集し、データ挿入時にuser_idmessageを入れられるようにしておきます。

app/Models/Posts.php
    protected $allowedFields    = ['user_id', 'message'];

フォームバリデーション、データベースの値に応じたバリデーションを行なったのち、保存するアクションメソッドを実装します。

app/Controllers/Post.php
~~~
    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パラメタの取得
CodeIgniter3
$field = $this->input->post('field');
CodeIgniter3のバリデーション
CodeIgniter3
// ルールの設定
$this->form_validation->set_rules();

// バリデーションの実行
$result = $this->form_validation->run();

投稿一覧機能と同様にルーティング設定を行います。

app/Config/Routes.php
// ログインが必要になるようにフィルターを設定
$routes->post('/posts', 'Post::create', ['filter' => 'session']);

ここまでで、投稿機能ができました。

投稿削除機能の実装

試しに自作ライブラリを作り、それを読み込んで削除する形で実装してみます。

app/Libraries/PostDeleteAction.php
<?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);
    }
}

コントローラー側

app/Controllers/Post.php
~~~
    public function destroy(int $postId)
    {
        // model()ヘルパーを使ってモデルクラスを読み込む
        $postModel = model(Posts::class);

        // 削除ライブラリをインスタンス化
        $postDeleteAction = new PostDeleteAction();

        // 投稿を削除
        $postDeleteAction($postModel, $postId, auth()->user()->id);

        return redirect()->back();
    }
CodeIgniter3のLibraryの読み込み方
CodeIgniter3
$this->load->library('ライブラリ名');

同様にルーティング設定を行います。

app/Config/Routes.php
$routes->delete('/posts/(:segment)', 'Post::destroy/$1', ['filter' => 'session']);

ここまでで、投稿削除機能が実装できました。

テストコード

CodeIgniter4から、テストを書くためのライブラリを公式が用意してくれています。
いくつか書いてみます。

投稿削除ライブラリのテストコード

Fabricatorを使いダミーの投稿データを作成して、それを削除するテストコードを書いてみます。

tests/feature/PostDeleteActionTest.php
<?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コントローラーのテスト (一部)

ログインユーザーとゲストユーザーの挙動を書いてみます。

tests/feature/PostFeatureTest.php
<?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