【PHP】動的プロパティをエラーが出ないように扱ってみる

PHP

PHP8.2.0以降で動的プロパティは非推奨となり、error_reporting(E_ALL) とすると、存在しないクラスプロパティへの値代入は実行されますが、エラー出力されます。

意図しないプロパティへの値代入はバグの温床になるので、クラスプロパティは明示的に宣言してくださいとうことでしょう。

▼コード例:

<?php

error_reporting(E_ALL);
$class = new class {};
$class->prop1 = "value1";
var_dump($class);

▼実行結果: PHP 8.2以降

PHP Deprecated:  Creation of dynamic property class@anonymous::$prop1 is deprecated in /home/macocci7/php/dynamic-property/deprecated.php on line 5
PHP Stack trace:
PHP   1. {main}() /home/macocci7/php/dynamic-property/deprecated.php:0
/home/macocci7/php/dynamic-property/deprecated.php:6:
class class@anonymous#1 (1) {
  public $prop1 =>
  string(6) "value1"

しかしながら、この動的プロパティを敢えて使いたい場面もあったります。

例えば、ModelBaseで取得結果をDTOで返したい場合などは、当然テーブル毎にカラム定義が異なるので、いちいちテーブル毎のDTOを事前に宣言しておくというのは現実的ではありません(いや、やりたくありません)。

PDOStatementで fetch したレコードのカラム名をプロパティ名として、動的プロパティとして値を代入したいものです。

というわけで、 error_reporting(E_ALL) でもエラーが出ないように動的プロパティを扱っていきます。

前提条件

  • PHP8.2以降
  • php.ini はいじらない
  • ini_set() も使わない
  • error_reporting(E_ALL) の状態でエラーが出ないようにする
  • アトリビュート #[\AllowDynamicProperties] は使わない

動的プロパティ専用基底クラス

PHP公式サイトに対応方法が書かれています。

警告

動的なプロパティは、PHP 8.2.0 以降は推奨されなくなりました。 代わりに、プロパティを宣言することを推奨します。 任意のプロパティの名前を扱うには、 クラスがマジックメソッド __get() と __set() を実装すべきです。 動的なプロパティを使うための最終手段として、 アトリビュート #[\AllowDynamicProperties] でクラスをマークすることができます。

動的なプロパティ | PHPマニュアル

動的プロパティを許容する専用の基底クラスを準備します。

▼「DynamicProperty.php」

<?php

namespace MyLib;

class DynamicProperty
{
    /**
     * @var array<string, mixed>    $data = []
    */
    private array $data = [];

    /**
     * @param   array<string, mixed>    $data = []
     */
    public function __construct(array $data = [])
    {
        $this->setData($data);
    }

    public function __set(string $key, mixed $value): void
    {
        $this->data[$key] = $value;
    }

    public function __get(string $key): mixed
    {
        return $this->data[$key] ?? null;
    }

    public function __isset(string $key): bool
    {
        return isset($this->data[$key]);
    }

    /**
     * @param   array<string, mixed>    $data = []
     */
    private function setData(array $data = []): void
    {
        foreach ($data as $key => $value) {
            if (is_string($key)) {
                $this->data[$key] = $value;
            }
        }
    }
}

クラスに存在しないプロパティへの代入があった際には、マジックメソッド __set() がコールされます。

クラスに存在しないプロパティの参照があった際には、マジックメソッド __get() がコールされます。

クラスに存在しないプロパティの isset() がコールされた際には、マジックメソッド __isset() がコールされます。

このクラスのインスタンス生成時に、コンストラクタの引数に渡された配列が、クラスプロパティ$data に格納されます。

__get() でこの $data から取得して返し、 __set() でこの $data へ格納し、 __isset() でこの $data 内に存在するかチェックします。

DBのカラム名で利用することを考慮して、渡す配列キーは文字列のみを許容しています。

使用例

▼「DBRecordDTO.php」:DynamicPropertyクラスを継承

<?php

namespace MyLib;

require_once __DIR__ . "/DynamicProperty.php";

use MyLib\DynamicProperty;

class DBRecordDTO extends DynamicProperty
{
}

▼「UserModel.php」:取得結果をDBRecordDTOで返す

<?php

namespace MyLib;

require_once __DIR__ . "/DBRecordDTO.php";

use MyLib\DBRecordDTO;

class UserModel
{
    public function find(int $id): DBRecordDTO|null
    {
        $data = [
            ["id" => 1, "name" => "ユーザー1", "email" => "user1@example.com"],
            ["id" => 2, "name" => "ユーザー2", "email" => "user2@example.com"],
            ["id" => 3, "name" => "ユーザー3", "email" => "user3@example.com"],
        ];
        $result = array_filter($data, fn($record) => $record["id"] === $id);
        return empty($result) ? null : new DBRecordDTO(array_shift($result));
    }
}

※DB接続せずにダミーの配列から返すなんちゃってモデルです。

※本来はModelBaseとかを作る必要がありますが、説明を簡単にするためにすっ飛ばしてます。

▼「find_user.php」:呼出処理

<?php

error_reporting(E_ALL);

require_once __DIR__ . "/UserModel.php";

use MyLib\UserModel;

$model = new UserModel;
foreach ([1, 3, 5] as $id) {
    $user = $model->find($id);
    if (is_null($user)) {
        echo "ID[{$id}]:データが見つかりません。" . PHP_EOL;
        continue;
    }
    echo "ID[{$id}]:" . sprintf("%s <%s>", $user->name, $user->email) . PHP_EOL;
}

▼実行結果:

エラーが出ることなく、動的プロパティを扱うことができました。

以上です。

  • 2
  • 0
  • 0
  • 0

コメント

タイトルとURLをコピーしました