PHP

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

PHPに、c#のpropertyとほぼ(?)等価な機能を実装しました。

https://github.com/strictphp/property
composer install strictphp/property

PHPのプロパティにあった問題点

  • publicなプロパティはカプセル化を阻害する
  • カプセル化を維持して安全にpublicなプロパティを書こうと思うと、__get__setが巨大化する

このライブラリは以上の問題を一掃します!

使い方

Strict\Property\StrictPropertyContainerを継承し、registerPropertyメソッドを実装してください。registerPropertyメソッドの第一引数にあるPropertyRegisterは以下のメソッドを持ちます。

class PropertyRegister
{
    /**
     * 新たに、実際に値を持つプロパティを登録します。
     *
     * @param string $propertyName
     * @param bool   $enableGet
     * @param bool   $enableSet
     * @return PropertyRegister
     */
    public function newProperty(string $propertyName, bool $enableGet, bool $enableSet): self
    {
        $this->propertyDefinition->newProperty($propertyName, $enableGet, $enableSet);
        return $this;
    }

    /**
     * 新たに、実際には値を持たないプロパティを登録します。
     *
     * @param string       $propertyName
     * @param Closure|null $getter = function (): mixed;
     * @param Closure|null $setter = function ($value): void;
     * @return PropertyRegister
     */
    public function newVirtualProperty(string $propertyName, ?Closure $getter, ?Closure $setter): self
    {
        $this->propertyDefinition->newVirtualProperty($propertyName, $getter, $setter);
        return $this;
    }

    /**
     * セッターにフック関数を追加します。フック関数は複数追加可能で、先入れ後出しの順で処理されます。
     * すでに登録されている関数を呼び出したくない場合、フック関数の第二引数$nextを無視してください。
     *
     * @param string  $propertyName
     * @param Closure $hook = function ($value, Closure $next): void;
     * @return PropertyRegister
     */
    public function addSetterHook(string $propertyName, Closure $hook): self
    {
        $this->propertyDefinition->addSetterHook($propertyName, $hook);
        return $this;
    }

    /**
     * ゲッターにフック関数を追加します。フック関数は複数追加可能で、先入れ後出しの順で処理されます。
     * すでに登録されている関数を呼び出したくない場合、フック関数の第二引数$nextを無視してください。
     *
     * @param string  $propertyName
     * @param Closure $hook = function (Closure $next): mixed;
     * @return PropertyRegister
     */
    public function addGetterHook(string $propertyName, Closure $hook): self
    {
        $this->propertyDefinition->addGetterHook($propertyName, $hook);
        return $this;
    }
}

利用例

/**
 * @property-read float $r
 * @property      float $x
 * @property      float $y
 */
class Vector
    extends StrictPropertyContainer
{
    protected function registerProperty(PropertyRegister $propertyRegister): void
    {
        /*
         * PropertyRegister::newProperty(
         *     string $propertyName,
         *     bool $enableGet,
         *     bool $enableSet
         * ): self;
         */
        $propertyRegister
            ->newProperty('x', true, true)
            ->newProperty('y', true, true);
        // プロパティxとyを読み取り書き込み両方OKで登録

        $validator = function (float $value, Closure $next) {
            $next($value);
        };

        $propertyRegister
            ->addSetterHook('x', $validator)
            ->addSetterHook('y', $validator);
        // プロパティxとyについて、書き込み時に$validatorを通す

        /*
         * PropertyRegister::newVirtualProperty(
         *     string $propertyName,
         *     ?Closure $getter,
         *     ?Closure $setter
         * ): self;
         */
        $propertyRegister
            ->newVirtualProperty('r', function (): float {
                return ($this->x ** 2 + $this->y ** 2) ** 0.5;
            }, null);
        // プロパティrを読み取りのみで登録
    }

    public function __construct(float $x = 0.0, float $y = 0.0)
    {
        parent::__construct();
        $this->x = $x;
        $this->y = $y;
    }
}

$vec = new Vector(0.0, 4.0);
3.99 <= $vec->r && $vec->r <= 4.01;    // true

$vec->x = 3;
4.99 <= $vec->r && $vec->r <= 5.01;    // true

// $vec->x = '3.00';   // TypeError ($validatorの引数タイプヒントと合致しない)

// そもそもphpってこういう言語だったっけ