はじめに
今回は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の複数のメソッドに同じ処理を書くことになりそうです
そうすると、いざ修正したいとなった時に対応漏れが発生しやすくなります。
共通クラスを作るなどやり方はあるとは思いますが、出来るならそういったのは自分たちで作らず、楽をしたいですよね。
そこでEntityクラスの登場です。
Entityクラスを使ってみる
EntityクラスとModelクラスの作成
ます、Entityクラスを作ります。
以下のようにテーブルの単数名でEntityクラスを作ります。
<?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クラスを作ります。
<?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があるとします。
※説明に不要な処理は省いています
<?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のキャスト処理も行っていないのに、どうしてだろうと思うかもしれませんが、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だけ減らすことが出来ます。
便利ではありますが、開発者が意図しない挙動をする可能性もはらんでいるので、利用はよく検討する必要があるかなとは思います。
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エンコード
しています。
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に定義するだけで利用可能です。
<?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を作成します。
<?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の処理が実行されるようになります。
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;
}
}
class Child extends ParentEntity
{
}
これで、getPasswordの呼び出し時に
- UserEntityでは、定義した
_getPassword
- ChildEntityでは、
getPassword
が呼ばれるようになります。
ちなみに、フレームワークの以下のファイルでどちらを処理するか判定しています。
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を行う場合は、以下のようになります。
<?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ファイルを見てみましょう。
/**
* 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
- $datesに設定されているカラムに対して、
CodeIgniter\I18n\Time
インスタンスへの変換処理を実施 - $castsに設定されているカラムに対して、変換処理を実施
- setterメソッド処理
3-1. Special setterメソッドがあれば、該当のメソッドを実施してreturn
3-2. setterメソッドがあれば、該当のメソッドを実施してreturn
3-3. setterメソッドがなければ、何もしない
- getter
- getterメソッド処理
1-1. Special getterメソッドがあれば、該当のメソッドを実施
1-2. getterメソッドがあれば、該当のメソッドを実施
1-3. getterメソッドがなければ、何もしない - $datesに設定されているカラムに対して、
CodeIgniter\I18n\Time
インスタンスへの変換処理を実施 - $castsに設定されているカラムに対して、変換処理を実施
- 1~3で変換した値をreturn
おわりに
CodeIgniterのEntityについて、説明しました。
アウトプットしながら、自身の理解も深められて良かったです!
次は、CodeIgniterのModelクラスに絞って説明していこうと思います