クリエイター:メタボ兔

ウェブやアプリの開発者で利用する色な技術やサーバーや開発環境の設定について共有する場

PHPのDTOの作成:カプセル化

背景

Javaのエンジニアの経験から見るとPhpのクラスの甘さが良いとも悪いとも言えます。その中でDTOで使用するクラスをもうちょっとある属性&関数のみ使用するようにしてカプセル化をしたいところです。

定義してない属性の設定や参照の禁止

    public function __set($key, $value){
        if (property_exists($this, $key)) {
            $this->{$key} = $value;
        }
        throw new \BadMethodCallException('Method "'.$key.'" does not exist.');
    }

    public function __get($key)
    {
        if (property_exists($this, $key)) {
            return $this->{$key};
        }
        throw new \BadMethodCallException('Method "'.$key.'" does not exist.');
    }

初期ソースは簡単に考えましたが、ちょっと待って!! 「property_exists」ってpublicだけじゃない〜〜〜

それでこんな感じにしました。

    public function __set($key, $value){
        if ($this->existsProperty($key)) {
            $this->{$key} = $value;
        }
        throw new \BadMethodCallException('Method "'.$key.'" does not exist.');
    }

    public function __get($key)
    {
        if ($this->existsProperty($key)) {
            return $this->{$key};
        }
        throw new \BadMethodCallException('Method "'.$key.'" does not exist.');
    }

    private function existsProperty($propertyName) {
        $reflection = new ReflectionObject($this);
        $properties = $reflection->getProperties(ReflectionProperty::IS_PUBLIC);
        foreach ($properties as $property) {
            if ($property->getName() === $propertyName) return true;
        }
        return false;
    }

Setter/Getterの自動化

Javaの実装でgetter/setterの作成の面倒です。Lombokのような物もありますね。それで以下のソースを用意しました。

    public function __call($name, $args) {
        if (strncmp($name, 'get', 3) === 0) {
            $pascalCName = substr($name, 3);
            $snakeCName = $this->pascalCase2SnakeCase($pascalCName);

            if (property_exists($this, $snakeCName)) {
                return $this->{$snakeCName};
            }
        } else if (strncmp($name, 'set', 3) === 0) {
            $pascalCName = substr($name, 3);
            $snakeCName = $this->pascalCase2SnakeCase($pascalCName);

            if (property_exists($this, $snakeCName)) {
                return $this->{$snakeCName} = reset($args);
            }
        }
        throw new \BadMethodCallException('Method "'.$name.'" does not exist.');
    }

    private function pascalCase2SnakeCase($input) {
        preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches);
        $ret = $matches[0];
        foreach ($ret as &$match) {
            $match = $match == strtoupper($match) ? strtolower($match) : lcfirst($match);
        }
        return implode('_', $ret);
    }

このソースではpublic以外のも設定可能にしないといけないので「property_exists」を利用しました。

完成

abstract class Dto
{
    public function __call($name, $args) {
        if (strncmp($name, 'get', 3) === 0) {
            $pascalCName = substr($name, 3);
            $snakeCName = $this->pascalCase2SnakeCase($pascalCName);

            if (property_exists($this, $snakeCName)) {
                return $this->{$snakeCName};
            }
        } else if (strncmp($name, 'set', 3) === 0) {
            $pascalCName = substr($name, 3);
            $snakeCName = $this->pascalCase2SnakeCase($pascalCName);

            if (property_exists($this, $snakeCName)) {
                return $this->{$snakeCName} = reset($args);
            }
        }
        throw new \BadMethodCallException('Method "'.$name.'" does not exist.');
    }

    public function __set($key, $value){
        if ($this->existsProperty($key)) {
            $this->{$key} = $value;
        }
        throw new \BadMethodCallException('Method "'.$key.'" does not exist.');
    }

    public function __get($key)
    {
        if ($this->existsProperty($key)) {
            return $this->{$key};
        }
        throw new \BadMethodCallException('Method "'.$key.'" does not exist.');
    }

    private function pascalCase2SnakeCase($input) {
        preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches);
        $ret = $matches[0];
        foreach ($ret as &$match) {
            $match = $match == strtoupper($match) ? strtolower($match) : lcfirst($match);
        }
        return implode('_', $ret);
    }

    private function existsProperty($propertyName) {
        $reflection = new ReflectionObject($this);
        $properties = $reflection->getProperties(ReflectionProperty::IS_PUBLIC);
        foreach ($properties as $property) {
            if ($property->getName() === $propertyName) r[f:id:FattyRabbit:20201230123544j:plain]eturn true;
        }
        return false;
    }
}

クラス名のセンスはないので。。。