LoginSignup
2
0

CodeIgniterについてアウトプットしてみた~Entity編~

Last updated at Posted at 2024-05-01

はじめに

今回はCodeIgniterで用意されているEntityクラスについて説明します。
Entityクラスは、テーブルの1レコードの各カラムのビジネスロジックを定義するクラスです。
DDDのドメインモデルに近い?のかもしれません。

Entityは、CodeIgniterのversion4から導入されました。

ビジネスロジックをEntityクラスに集約することで、コードの保守性が高まり、品質が高くなりそうです。

Entityクラスを使わない場合の課題

例えば、ユーザテーブルが以下のようなテーブル構成になっているとします。

物理名 論理名
id ID
last_name
first_name
login_id ログインID
password パスワード
is_active 有効フラグ
created_at 登録日時
updated_at 更新日時
deleted_at 削除日時

ここで、以下のような処理をしたいとします。

  • パスワード登録/更新時には、入力されたパスワードをハッシュ化して保存したい
  • ログイン時には、入力されたパスワードが一致するか確認したい
  • full_nameというキーでユーザ情報にアクセスしたら、「姓 + 名]の形式で取りたい
  • 数値は数値型、真偽値はbool型などカラムの型にあった形式でデータを取得したい

このような場合、どこにこういった処理を書くでしょうか?
・パスワード登録に関しては、ユーザ登録APIやユーザ更新APIやパスワード再設定API
・名前に関してはユーザ一覧API、ユーザ詳細API、それ以外のユーザ情報を表示するAPI
などいろいろなところで使うことが想定されます。

このままだと、各ControllerやModelの複数のメソッドに同じ処理を書くことになりそうです :thinking:
そうすると、いざ修正したいとなった時に対応漏れが発生しやすくなります。
共通クラスを作るなどやり方はあるとは思いますが、出来るならそういったのは自分たちで作らず、楽をしたいですよね。

そこでEntityクラスの登場です。

Entityクラスを使ってみる

EntityクラスとModelクラスの作成

ます、Entityクラスを作ります。
以下のようにテーブルの単数名でEntityクラスを作ります。

app/Entities/User.php

<?php

namespace App\Entities;

use CodeIgniter\Entity\Entity;

class User extends Entity
{
    // castするカラムと型を指定
    protected $casts = [
        'id' => 'integer',
        'is_active' => 'boolean',
    ];
    // 入力されたパスワードをハッシュ化
    public function setPassword(string $pass)
    {
        $this->attributes['password'] = password_hash($pass, PASSWORD_BCRYPT);

        return $this;
    }
    // 入力されたパスワードが登録されているパスワードと一致するか検証
    public function passwordVerify($inputPassword): bool
    {
        return password_verify($inputPassword, $this->attributes['password']);
    }
    // フルネーム(姓+名)を取得
    public function getFullName(): string
    {
        return $this->attributes['last_name'] . ' ' . $this->attributes['first_name'];
    }
}

publicメソッドにしていますが、他のクラス(Controllerなど)からアクセスしないのであれば、メソッドはprotectedでもよさそうです

次に、Modelクラスを作りましょう。
以下のようにテーブルの単数名でModelクラスを作ります。

app/Models/UserModel.php

<?php

namespace App\Models;

use CodeIgniter\Model;
use App\Entities\User;

class UserModel extends Model
{
    protected $table = 'users';
    protected $primaryKey = 'id';
    protected $useAutoIncrement = true;

    //protected $returnType = 'array';
    protected $returnType = User::class;

    /**
     * @var array<string>
     */
    protected $allowedFields = [
        'login_id',
        'password',
        'last_name',
        'first_name',
        'is_active',
        'created_at',
        'updated_at',
        'deleted_at',
    ];
    protected $protectFields = true;

    // date property
    protected $useTimestamps = true;
    protected $dateFormat = 'datetime';
    protected $createdField = 'created_at';
    protected $updatedField = 'updated_at';
    protected $deletedField = 'deleted_at';
    protected $useSoftDeletes = true;

Modelクラスを以下のように変更しています。
protected $returnType = 'array';

protected $returnType = User::class
これで、Modelを介した操作はEntityの形式で返されるようになります。

Controllerの作成

では、次にEntityに実装した各メソッドをどのように呼ぶか説明します。
以下のようにユーザ詳細ユーザパスワード更新ログイン処理をするAPIがあるとします。
※説明に不要な処理は省いています

app/Controllers/User.php
<?php

namespace App\Controllers;

use App\Models\UserModel;
use CodeIgniter\RESTful\ResourceController;

class User extends ResourceController
{
    /**
     * User model instance
     * @var UserModel
     */
    private UserModel $userModel;


    public function __construct()
    {
        $this->userModel = model(UserModel::class);
    }

    /**
     * ユーザ詳細
     *
     **/
    public function detailUser()
    {
        $user = $this->userModel->find(1);
        $response = [
            'full_name' => $user->full_name,
            'is_active' => $user->is_active,
        ];
        return $this->respond(['status' => 200, 'response' => $response]);
    }

    /**
     * ユーザ更新
     *
     **/
    public function updateUser()
    {
        $user = $this->userModel->find(1);
        $user->password = 'hoge';
        $this->userModel->save($user);

        return $this->respond(['status' => 200, 'response' => $user]);
    }
    /**
     * ログイン
     *
     **/
    public function login()
    {
        $requestBody = $this->request->getJSON(true);
        $user = $this->userModel->find(1);

        if (!$user->passwordVerify($requestBody['password'])) {
            return $this->respond(['status' => 401, 'message' => '認証エラー']);
        }

        return $this->respond(['status' => 200, 'token' => 'xxx']);
    }
}

ユーザ詳細を試してみる

ユーザ詳細APIへリクエストするとレスポンスの

  • full_nameがgetFullName()の値
  • is_activeがboolean型へのキャスト処理が行われた後の値
    になっています
レスポンスボディ
{
    "status": 200,
    "response": {
        "full_name": "山田 太郎",
        "is_active": true
    }
}

getFullName()メソッドを明示的に呼んでないし、is_activeのキャスト処理も行っていないのに、どうしてだろう:thinking:と思うかもしれませんが、CodeIgniterのEntityでは、カラム読み込み時に

  • get${カラム名のパスカルケース}というメソッドを作成すると、自動的にメソッドを呼び出し
  • $castsにカラムと型を設定すると、自動的に型変換
    してくれるのです!

ちなみに、$castsでis_activeをキャストしないとレスポンスは以下のように、文字列の1になります。
※Mysqlのtinyint(1)カラムを使っている場合

レスポンスボディ
{
    "status": 200,
    "response": {
        "full_name": "山田 太郎",
        "is_active": "1"
    }
}

ユーザ更新を試してみる

ユーザ更新APIをリクエストしたときに、setPassword()処理が行われています。

CodeIgniterのEntityでは、Entityのカラムに対して値をセットした時に
set${カラム名のパスカルケース}というメソッドを作成すると、自動的にメソッドを呼び出し
てくれます。

ログインを試してみる

getterとsetterだけでも非常に便利になることがわかりました。
ただ、複数のカラムを使った判定処理や入力値を使った比較処理など、実際はもっといろんなことをしたいと思います。

その例として、ログインAPIで行うパスワード検証処理として、passwordVerify()を実装してみます。
このメソッドでは入力されたパスワードと、登録されているパスワードのハッシュ値を比較し、認証を行います。
こういったカスタムメソッドは、$user->passwordVerify($requestBody['password'])のように、自由にメソッド名や引数を定義し、任意のタイミングで呼び出すことが出来ます。

主な機能としては以上になりますが、他の機能も説明していきます。

その他の機能

基本的な機能としては、getter、setter、$castsになると思っていますが、それ以外に出来ることについて解説していきます。

$dates

日付のミューテータ定義をする設定です。
$datesに設定したプロパティは、CodeIgniter\I18n\Timeインスタンスに変換されます。
なので、以下のように読み取り時に必要なフォーマットに変換できます。

protected $dates = ['created_at', 'updated_at', 'deleted_at']; // デフォルトでは、この3つ
// ex)created_at=2024-02-07 00:00:00
var_dump($user->created_at); // インスタンス(CodeIgniter\I18n\Time)
var_dump($user->created_at->humanize()); // 2 months ago
var_dump($user->created_at->format('Y-m-d')); // 2024-02-07

$datamap

カラムとアクセスするプロパティ名を変換する設定です。
例えば現在、ログインには

  • 任意の文字列である、login_id
  • 任意の文字列である、password
    を使っているとします

しかし、途中の仕様変更でlogin_idから、emailに変更されました。

この場合、login_idを使用しているAPIの該当処理を全てemailを利用するように変更しないといけないと思います。
しかし、$datemapに以下のように記述することで、login_idへのアクセスはemailカラムへのアクセスにマッピングされます。

これによって、修正範囲をEntityだけ減らすことが出来ます。

便利ではありますが、開発者が意図しない挙動をする可能性もはらんでいるので、利用はよく検討する必要があるかなとは思います。

app/Entities/User.php
protected $datamap = [
  'login_id' => 'email',
];
$user = $this->userModel->where('id', 1)->first();
echo $user->login_id; // emailカラムの値が表示される

$casts

既に$castsについては紹介はしていますが、boolean以外にどんなキャストがあるのか紹介します。

castはgetter/setter両方で変換する型と、getterのみで変換する型があります。
※下記表で、×になっているものに関しては、setter時にはcast処理は行われません。

名前 getterの処理 setterの処理
int or integer (int)でキャスト ×
float or double (float)でキャスト ×
string (string)でキャスト ×
bool or boolean (bool)でキャスト ×
object (object)でキャスト ×
array (array)でキャスト serialize($value)でシリアライズ
int-bool (bool)でキャスト (int)でキャスト
datetime CodeIgniter\I18n\Timeインスタンスを生成 ×
timestamp strtotimeでUnixタイムスタンプに変換 ×
uri CodeIgniter\HTTP\URIインスタンスを生成 ×
json json_decode($value, false)でjsonをデコード json_encode($value)でエンコード
json-array json_decode($value, true)でjsonをデコード json_encode($value)でエンコード
csv explode(',', $value)で配列形式に変換 implode(',', $value)で文字列に変換

なお、NULL可能なカラムに関しては、?stringのように定義します。

上記がCodeIgniterで用意されているものになりますが、これだけでは対応できないケースがあるかと思います。その場合は独自のキャストを定義することが可能です。

以下はbase64キャストの例です。

  • getterではbase64デコード
  • setterではbase64エンコード
    しています。
App/Entities/Cast/CastBase64.php
namespace App\Entities\Cast;

use CodeIgniter\Entity\Cast\BaseCast;

class CastBase64 extends BaseCast
{
    public static function get($value, array $params = [])
    {
        return base64_decode($value, true);
    }

    public static function set($value, array $params = [])
    {
        return base64_encode($value);
    }
}

作成したキャストは、以下のように$castsに定義するだけで利用可能です。

app/Entities/User.php

<?php

namespace App\Entities;

use CodeIgniter\Entity\Entity;

class User extends Entity
{
    protected $casts = [
        'file' => 'base64',
    ];
}

Special getter/setter

Entityを利用していると一部処理は共通化したいと思ってくると思います。
その場合はEntityクラスを継承した親Entityを作って、親Entityを継承した子Entityを実装することで共通化することが出来ます。

しかし、例えばUserEntityだけgetter/setterの挙動を変えたい場合など出てきた場合は、どうすればいいのでしょうか?

そこで、Special getter/setterの出番です。
まず、親となるParentEntityを作成します。

app/Entities/ParentEntity.php
<?php

namespace App\Entities;

use CodeIgniter\Entity\Entity;

class ParentEntity extends Entity
{
    protected function getFullname()
    {
        return $this->attributes['last_name'] . ' ' . $this->attributes['middle_name'] . ' ' . $this->attributes['first_name'];
    }
    protected function setPassword()
    {
      $this->attributes['password'] = password_hash($pass, PASSWORD_BCRYPT);
        return $this;
    }
}

次に、子となるUserEntityと、ChildEntityを作成します。
メソッド名の先頭に[_]を付けることで、子Entityの処理が実行されるようになります。

app/Entities/UserEntity.php
class User extends ParentEntity
{
    protected function _getPassword()
    {
        return $this->attributes['last_name'] . ' ' . $this->attributes['first_name'];
    }
    protected function _setPassword()
    {
      $this->attributes['password'] = password_hash($pass, PASSWORD_ARGON2I);
        return $this;
    }
}
app/Entities/ChildEntity.php
class Child extends ParentEntity
{
}

これで、getPasswordの呼び出し時に

  • UserEntityでは、定義した_getPassword
  • ChildEntityでは、getPassword
    が呼ばれるようになります。

ちなみに、フレームワークの以下のファイルでどちらを処理するか判定しています。

codeigniter4/framework/system/Entity/Entity.php
   public function __get(string $key)
    {
        $dbColumn = $this->mapProperty($key);

        $result = null;

        // Convert to CamelCase for the method
        $method = 'get' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $dbColumn)));

        // if a getter method exists for this key,
        // use that method to insert this value.
        if (method_exists($this, '_' . $method)) { // 👈子Entityのgetterがあれば、こちらを処理
            // If a "`_get` + $key" method exists, it is a getter.
            $result = $this->{'_' . $method}();
        } elseif (method_exists($this, $method)) {
            // If a "`get` + $key" method exists, it is also a getter.
            $result = $this->{$method}();


            // Otherwise return the protected property
            // if it exists.
        } elseif (array_key_exists($dbColumn, $this->attributes)) {
            $result = $this->attributes[$dbColumn];
        }

        // Do we need to mutate this into a date?
        if (in_array($dbColumn, $this->dates, true)) {
            $result = $this->mutateDate($result);
        }
        // Or cast it as something?
        elseif ($this->_cast) {
            $result = $this->castAs($result, $dbColumn);
        }

        return $result;
    }

toArray()、toRawArray()

userModel->find(1);の返り値はEntity形式になるのですが、配列形式で取得したいこともあると思います。その場合は、 toArray()かtoRawArray()を使います。違いとしては、

  • toArray():getterメソッドを通す
  • toRawArray():getterメソッドを通さない

という違いがあります。

クエリビルダでEntityを利用

クエリビルダーで取得した場合にはResult形式で返されますが、ModelのようにEntity形式で返したいこともあるかもしれません。その場合は、以下のようにすればEntity形式で取得できます。

var_dump($this->builder()->getWhere($where)); // CodeIgniter\Database\MySQLi\Result
var_dump($this->builder()->getWhere($where)->getResult(User:class)); // App\Entities\User

Entitiyの値が変更されたかチェック

Entityのあるプロパティの値が変更されたかチェックしたい場合は、hasChanged()を使います。

$user = new \App\Entities\User();
// 特定の属性の変更チェック
echo $user->hasChanged('name'); // false
$user->name = 'Hoge';
echo $user->hasChanged('name'); // true

// Entitiy全体の変更チェック
echo $user->hasChanged(); // true

おまけ

各CRUDにおけるEntityを使ったバージョンでの実装

ModelクラスでCRUDを行う場合は、以下のようになります。

app/Models/UserModel.php

<?php

namespace App\Models;

use CodeIgniter\Model;
use App\Entities\User;

class UserModel extends Model
{
    protected $table = 'users';
    protected $primaryKey = 'id';
    protected $useAutoIncrement = true;

    protected $returnType = User::class;

    /**
     * @var array<string>
     */
    protected $allowedFields = [
        'login_id',
        'password',
        'last_name',
        'first_name',
        'created_at',
        'updated_at',
        'deleted_at',
    ];
    protected $protectFields = true;

    // date property
    protected $useTimestamps = true;
    protected $dateFormat = 'datetime';
    protected $createdField = 'created_at';
    protected $updatedField = 'updated_at';
    protected $deletedField = 'deleted_at';
    protected $useSoftDeletes = true;

    /**
     * ユーザ登録
     *
     * @param array $request
     * @return bool
     */
    public function register($request): bool
    {
      $user = new User($request); // 各setterメソッドが呼ばれます
      // 以下のように1つずつ設定することも可能です
      // $user = new User();
      // $user->password = $request['password'];
      // $user->last_name = $request['last_name'];
      // $user->first_name = $request['first_name'];
      $this->insert($user);
    }

    /**
     * ユーザ更新
     *
     * @param int $userId
     * @param array $request
     * @return bool
     */
    public function update($userId, $request): bool
    {
      $user = $this->find($userId);
      $user->fill($request); // 各setterメソッドが呼ばれます
      // fill()で設定せずに、以下のように1つずつ設定することも可能です
      // $user->password = $request['password'];
      // $user->last_name = $request['last_name'];
      // $user->first_name = $request['first_name'];
      $this->update($userId, $user);
    }

    /**
     * ユーザ削除
     *
     * @param int $userId
     * @return bool
     */
    public function delete($userId): bool
    {
      $this->delete(userId);
    }

    /**
     * ユーザ取得
     *
     * @param int $userId
     * @return User|null
     */
    public function detail($userId): ?User
    {
      $this->find($userId);
    }
    

fill()メソッドでは、Modelの$allowedFieldsに存在するカラムかをチェックしています。
存在しない場合は、登録/更新対象から除外されます。
new User($request)のようにEntityインスタンス作成時にも、fill()メソッドは実行されます。

$casts、$dates、setter/getterの優先度

$casts、$dates、setter/getterでいろいろな変換処理を行えることがわかりましたが、これらを全て設定している場合、どのように挙動になるのでしょうか?
Entityファイルを見てみましょう。

codeigniter4/framework/system/Entity/Entity.php
    /**
     * Magic method to all protected/private class properties to be
     * easily set, either through a direct access or a
     * `setCamelCasedProperty()` method.
     *
     * Examples:
     *  $this->my_property = $p;
     *  $this->setMyProperty() = $p;
     *
     * @param array|bool|float|int|object|string|null $value
     *
     * @return void
     *
     * @throws Exception
     */
    public function __set(string $key, $value = null)
    {
        $dbColumn = $this->mapProperty($key);

        // Check if the field should be mutated into a date
        if (in_array($dbColumn, $this->dates, true)) {
            $value = $this->mutateDate($value);
        }

        $value = $this->castAs($value, $dbColumn, 'set');

        // if a setter method exists for this key, use that method to
        // insert this value. should be outside $isNullable check,
        // so maybe wants to do sth with null value automatically
        $method = 'set' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $dbColumn)));

        // If a "`_set` + $key" method exists, it is a setter.
        if (method_exists($this, '_' . $method)) {
            $this->{'_' . $method}($value);

            return;
        }

        // If a "`set` + $key" method exists, it is also a setter.
        if (method_exists($this, $method) && $method !== 'setAttributes') {
            $this->{$method}($value);

            return;
        }

        // Otherwise, just the value. This allows for creation of new
        // class properties that are undefined, though they cannot be
        // saved. Useful for grabbing values through joins, assigning
        // relationships, etc.
        $this->attributes[$dbColumn] = $value;
    }

    /**
     * Magic method to allow retrieval of protected and private class properties
     * either by their name, or through a `getCamelCasedProperty()` method.
     *
     * Examples:
     *  $p = $this->my_property
     *  $p = $this->getMyProperty()
     *
     * @return array|bool|float|int|object|string|null
     *
     * @throws Exception
     *
     * @params string $key class property
     */
    public function __get(string $key)
    {
        $dbColumn = $this->mapProperty($key);

        $result = null;

        // Convert to CamelCase for the method
        $method = 'get' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $dbColumn)));

        // if a getter method exists for this key,
        // use that method to insert this value.
        if (method_exists($this, '_' . $method)) {
            // If a "`_get` + $key" method exists, it is a getter.
            $result = $this->{'_' . $method}();
        } elseif (method_exists($this, $method)) {
            // If a "`get` + $key" method exists, it is also a getter.
            $result = $this->{$method}();


            // Otherwise return the protected property
            // if it exists.
        } elseif (array_key_exists($dbColumn, $this->attributes)) {
            $result = $this->attributes[$dbColumn];
        }

        // Do we need to mutate this into a date?
        if (in_array($dbColumn, $this->dates, true)) {
            $result = $this->mutateDate($result);
        }
        // Or cast it as something?
        elseif ($this->_cast) {
            $result = $this->castAs($result, $dbColumn);
        }

        return $result;
    }

setterとgetterで挙動が違うようです。まとめると以下のような順番で処理されます。

  • setter
  1. $datesに設定されているカラムに対して、CodeIgniter\I18n\Timeインスタンスへの変換処理を実施
  2. $castsに設定されているカラムに対して、変換処理を実施
  3. setterメソッド処理
    3-1. Special setterメソッドがあれば、該当のメソッドを実施してreturn
    3-2. setterメソッドがあれば、該当のメソッドを実施してreturn
    3-3. setterメソッドがなければ、何もしない
  • getter
  1. getterメソッド処理
    1-1. Special getterメソッドがあれば、該当のメソッドを実施
    1-2. getterメソッドがあれば、該当のメソッドを実施
    1-3. getterメソッドがなければ、何もしない
  2. $datesに設定されているカラムに対して、CodeIgniter\I18n\Timeインスタンスへの変換処理を実施
  3. $castsに設定されているカラムに対して、変換処理を実施
  4. 1~3で変換した値をreturn

おわりに

CodeIgniterのEntityについて、説明しました。
アウトプットしながら、自身の理解も深められて良かったです!
次は、CodeIgniterのModelクラスに絞って説明していこうと思います:smile:

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