PHP
オブジェクト指向

PHP に @readonly を実装してみた

概要

この記事について

こちらの記事を拝見して、興味深いなぁと思ったので、似たような機能を実装してみました。

PHPにC#のpropertyを実装してみた - Qiita

実際にプロダクトに取り入れるつもりはあまりないですが(普段は Laravel を使っていますが、Eloquent は無法地帯なので)、考え方としてはアリかな、くらいの気持ちです。

環境

  • PHP 7.1.12

方針

  • class ではなく trait で実装します。
  • マジックメソッド( __get() , __set() )を使います。

詳細

HasReadonlyProperties.php

<?php
declare(strict_types=1);

namespace App\Traits;

trait HasReadonlyProperties
{
    private $properties;
    private $readonlyProperties;

    public function __get($key) //: mixed
    {
        if (array_key_exists($key, $this->properties)) {
            return $this->properties[$key];
        }
        throw new \InvalidArgumentException('no attribute with the given key: ' . $key);
    }

    public function __set($key, $value): void
    {
        if (in_array($key, $this->readonlyProperties)) {
            throw new \BadMethodCallException('setter is prohibited');
        }
        if (array_key_exists($key, $this->properties)) {
            $this->properties[$key] = $value;
            return;
        }
        throw new \InvalidArgumentException('no attribute with the given key: ' . $key);
    }

    private function initProperties(array $properties): void
    {
        $this->properties = $properties;
        $class = new \ReflectionClass(static::class);
        $comment = $class->getDocComment();
        $this->readonlyProperties = $this->extractReadonlyProperties($comment);
    }

    private function extractReadonlyProperties(string $comment): array
    {
        $lines = explode(PHP_EOL, $comment);
        $readonlyProperties = [];
        foreach ($lines as $line) {
            if (preg_match('/@readonly\s(.+)/', $line, $matches)) {
                $readonlyProperties[] = $matches[1];
            }
        }

        return $readonlyProperties;
    }
}

  • リフレクションで DocComment を読み、 @readonly と指定された疑似プロパティを識別します。
  • プロパティへの書き込みがあった場合 __set() が呼ばれるので、Readonly なら例外を投げます。

Model.php

<?php
declare(strict_types=1);

namespace App;

use App\Traits\HasReadonlyProperties;

/**
* @property x
* @readonly x
* @property y
*/
class Model
{
    use HasReadonlyProperties;

    public function __construct(array $properties)
    {
        self::initProperties($properties);
    }
}
  • クラスの DocComment に @property やら @readonly やらを指定します。
  • Trait を初期化します

使い方

<?php
declare(strict_types=1);

require './vendor/autoload.php';

use App\Model;

$x = 100;
$y = 200;
$model = new Model(['x' => $x, 'y' => $y]);
echo 'x=' . $model->x . PHP_EOL; // 100
$model->y = 250;
echo 'y=' . $model->y . PHP_EOL; // 250
// $model->x = 150; // BadMethodCallException
// echo $model->z . PHP_EOL; // InvalidArgumentException
  • プロパティへのアクセスなのに BadMethodCallException はフィットしないような気がしますが、組み込み/SPL の中ではこれがいちばんしっくりきたので使いました
  • 同じくプロパティへのアクセスなのに InvalidArgumentException は(ry

なんとなくもうちょっといい実装がありそうな気がしてるんですが、思い浮かびません。

こうするといいんじゃない?みたいなのがあればコメント欄にてご意見いただければ助かります :bow: