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; } }
クラス名のセンスはないので。。。