2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

本記事の概要

DBに存在しない派生データをスマートに扱うベストプラクティス!
CakePHP の Virtual & Accessor Properties を使うと、データベースに列を追加しなくても “派生値” をエンティティから自然に取得できます。
ビューや API レスポンスにロジックを散らさないための定番テクニックです。
本記事では基本的な使い方から実践例、注意点までをまとめました。

本記事の要約

  • ポイントは 2 ステップだけ
    1. protected function _getFoo() を Entity に実装
    2. 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 を定義

src/Model/Entity/User.php
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 で利用

templates/Users/view.php
<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

価格に消費税を付与する例

src/Model/Entity/Product.php
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 で解決することも検討。

まとめ

  1. 2 ステップで導入できる簡単な拡張ポイント
  2. テンプレート・API がすっきりし、ロジックの重複を防げる
  3. 大量データ or 複雑ロジックには注意しつつ活用しよう

バーチャルプロパティは「ちょっとした派生情報」を扱うのに最適です。まだ使ったことがない方はぜひ導入してみてください。

参考リンク

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?