🐘

[PHP] privateなプロパティをオーバーライドするときはgetter/setterも子クラスに書かないとハマるという話

2020/07/22に公開

PHP歴8年にもなるのに今さらこんなのでめっちゃハマってしまったので反省文です😵

OKパターン

まずはこちらをご覧ください。

<?php
// test.php
class Base
{
    private $privateProperty = 'base';
    protected $protectedProperty = 'base';
    public $publicProperty = 'base';

    private function privateMethod() { return 'base'; }
    protected function protectedMethod() { return 'base'; }
    public function publicMethod() { return 'base'; }
}

class Child extends Base
{
    private $privateProperty = 'child';
    protected $protectedProperty = 'child';
    public $publicProperty = 'child';

    private function privateMethod() { return 'child'; }
    protected function protectedMethod() { return 'child'; }
    public function publicMethod() { return 'child'; }

    public function __get($name) { return $this->$name; }
    public function __call($name, $arguments) { return $this->$name(); }
}

$child = new Child();

echo $child->privateProperty.PHP_EOL;
echo $child->protectedProperty.PHP_EOL;
echo $child->publicProperty.PHP_EOL;
echo '---'.PHP_EOL;
echo $child->privateMethod().PHP_EOL;
echo $child->protectedMethod().PHP_EOL;
echo $child->publicMethod().PHP_EOL;

このコードの実行結果は以下のようになります。

$ php test.php
child
child
child
---
child
child
child

プロパティもメソッドもすべて子クラスでオーバーライドしているので、当然の結果ですね。

NGパターン

では今度は __get() __call() マジックメソッドを親に移動させてみましょう。

  <?php
  // test.php
  class Base
  {
      private $privateProperty = 'base';
      protected $protectedProperty = 'base';
      public $publicProperty = 'base';
  
      private function privateMethod() { return 'base'; }
      protected function protectedMethod() { return 'base'; }
      public function publicMethod() { return 'base'; }
+ 
+     public function __get($name) { return $this->$name; }
+     public function __call($name, $arguments) { return $this->$name(); }
  }
  
  class Child extends Base
  {
      private $privateProperty = 'child';
      protected $protectedProperty = 'child';
      public $publicProperty = 'child';
  
      private function privateMethod() { return 'child'; }
      protected function protectedMethod() { return 'child'; }
      public function publicMethod() { return 'child'; }
- 
-     public function __get($name) { return $this->$name; }
-     public function __call($name, $arguments) { return $this->$name(); }
  }
  
  $child = new Child();
  
  echo $child->privateProperty.PHP_EOL;
  echo $child->protectedProperty.PHP_EOL;
  echo $child->publicProperty.PHP_EOL;
  echo '---'.PHP_EOL;
  echo $child->privateMethod().PHP_EOL;
  echo $child->protectedMethod().PHP_EOL;
  echo $child->publicMethod().PHP_EOL;

実行結果はこうなります。

$ php test.php
base
child
child
---
base
child
child

privateプロパティとprivateメソッドだけ、親の持つ値が出力されました😵

原因(ものすごく当たり前の話ですが)

__get() が親にある場合、例えば $child->privateProperty にアクセスしたときの処理の流れは以下のようになります。

まったく厳密ではありません。あくまでイメージです🙏

  1. Child::privateProperty は存在するけどprivateなので、 __get() が探される
  2. Child::__get() は存在しないので Base::__get() が呼ばれる
  3. Base::__get() から Child::privateProperty は取得できない
  4. Base::privateProperty の値が取得される

一方、 $child->protectedProperty にアクセスしたときはというと、

  1. Child::protectedProperty は存在するけどprotectedなので、 __get() が探される
  2. Child::__get() は存在しないので Base::__get() が呼ばれる
  3. Base::__get() から Child::protectedProperty が取得できる(protected以上なので)
  4. Child::protectedProperty の値が取得される

となります。

これが原因です。メソッドアクセスの場合もまったく同じ理屈ですね。

実務でハマりそうなケース

  • 複数の派生クラスがあり
  • privateプロパティのオーバーライドを使っていて
  • 面倒なのでgetter/setterを基底クラスに書いちゃう

ということをするとこの問題が発生します。

NGパターン

class Base
{
    private $name = 'base';
    
    public function getName()
    {
        return $this->name;
    }
    
    public function setName($name)
    {
        $this->name = $name;
    }
}
class Child1
{
    private $name = 'child1';
}
class Child2
{
    private $name = 'child2';
}

ついやっちゃいそうじゃないですか?😓

OKパターン1

privateのままオーバーライドするなら、getter/setterはちゃんと子クラスに移しましょう。

class Base
{
    private $name = 'base';
}
class Child1
{
    private $name = 'child1';

    public function getName()
    {
        return $this->name;
    }
    
    public function setName($name)
    {
        $this->name = $name;
    }
}
class Child2
{
    private $name = 'child2';

    public function getName()
    {
        return $this->name;
    }
    
    public function setName($name)
    {
        $this->name = $name;
    }
}

OKパターン2

あるいは、getter/setterを親に持たせておきたいなら、privateではなくprotectedにしましょう。

class Base
{
    protected $name = 'base';
    
    public function getName()
    {
        return $this->name;
    }
    
    public function setName($name)
    {
        $this->name = $name;
    }
}
class Child1
{
    protected $name = 'child1';
}
class Child2
{
    protected $name = 'child2';
}

まとめ

当たり前ですが、privateプロパティやprivateメソッドには親自身からしかアクセスできないということを改めて脳に刻み込んでおきましょう…😓

GitHubで編集を提案

Discussion