本記事の概要
DBに存在しない派生データをスマートに扱うベストプラクティス!
CakePHP の Virtual & Accessor Properties を使うと、データベースに列を追加しなくても “派生値” をエンティティから自然に取得できます。
ビューや API レスポンスにロジックを散らさないための定番テクニックです。
本記事では基本的な使い方から実践例、注意点までをまとめました。
本記事の要約
-
ポイントは 2 ステップだけ
-
protected function _getFoo()
を Entity に実装 -
protected $_virtual = ['foo'];
に列挙
-
- View や JSON で
$entity->foo
と使える。自前でメソッドを呼ぶ必要なし。 - Setter (
_setFoo
) も実装すれば “値を書き戻す” こともできる。
バーチャルプロパティとは?
- 実体のないカラム:テーブルに列を追加せず、エンティティが保持する「計算値」
- 遅延計算:アクセス時に PHP で動的に計算される(キャッシュしない限り毎回実行)
- Getter / Setter のネーミング規約で自動解決
フレームワーク側がマジックメソッドで呼び出してくれるため、利用側は普段のプロパティアクセスと同じ書き味で扱えます。
動作環境
ソフトウェア | バージョン (例) |
---|---|
PHP | 8.1 以上 (記事内コードは 8.2 で動作確認) |
CakePHP | 5.0.x |
データベース | MySQL 8 / MariaDB / PostgreSQL など任意 |
※ CakePHP 4.x 以前でもバーチャルプロパティの基本 API は同じですが、$_virtual
など一部挙動が異なる場合があります。本記事では CakePHP 5 系 を前提に記載しています。
実装手順(基本形)
1. Getter を定義
class User extends Entity
{
// JSON や toArray() に含めたい場合は列挙
protected array $_virtual = [
'full_name',
];
// アクセサ (getter)
protected function _getFullName(): string
{
return trim($this->first_name . ' ' . $this->last_name);
}
}
- メソッド名は
_get
+ CamelCase 変換した仮想カラム名
full_name
→_getFullName()
- 必要なら null ガードを追加
2. View で利用
<h2><?= h($user->full_name) ?></h2>
3. API レスポンスにもそのまま
return $this->response
->withType('json')
->withStringBody(json_encode($user));
// → {"id":1,"full_name":"Taro Yamada", ... }
$_virtual
に列挙しておかないとtoArray()
/jsonSerialize()
の結果に含まれませんが、テンプレートで$user->full_name
と呼ぶ分には列挙しなくても取得できます。
よくある活用例
パターン | 実装例 |
---|---|
氏名の連結 |
full_name = first + last |
消費税込み価格 |
price_with_tax = price * TAX_RATE
|
ステータス表示 |
status_label = データベース値 → バッジ文字列 |
画像URL生成 |
avatar_url = /img/avatars/{$this->id}.png
|
価格に消費税を付与する例
class Product extends Entity
{
protected array $_virtual = ['price_with_tax'];
protected function _getPriceWithTax(): float
{
$rate = 1.1; // 10%
return round($this->price * $rate, 2);
}
}
Setter(Mutator)も使える
書き込み側ロジックを入れたい場合は _setFoo($value)
を実装します。
protected function _setFullName(string $value): void
{
// "姓 名" 形式を分割して first_name / last_name に保存
[$first, $last] = explode(' ', $value, 2) + [1 => ''];
$this->first_name = $first;
$this->last_name = $last;
}
落とし穴 & ベストプラクティス
注意点 | 解説 |
---|---|
select 漏れ | Getter 内で参照するカラムを ->select() で取得し忘れると null になる。 |
N+1 問題 | Getter 内で DB クエリを実行すると大量データ時に遅くなる。計算は軽めに。 |
キャッシュが必要なら |
_properties に結果を保持するか、Trait でキャッシュパターンを共通化すると良い。 |
大量レコードの一覧 | 遅延計算がボトルネックになる場合は ViewModel や JOIN で解決することも検討。 |
まとめ
- 2 ステップで導入できる簡単な拡張ポイント
- テンプレート・API がすっきりし、ロジックの重複を防げる
- 大量データ or 複雑ロジックには注意しつつ活用しよう
バーチャルプロパティは「ちょっとした派生情報」を扱うのに最適です。まだ使ったことがない方はぜひ導入してみてください。