Как можно решить проблему доступа к методам класса PHP, унаследованным от другого класса?

Как можно ограничить доступ к защищенным методам родительского класса, чтобы они были доступны только в классах-наследниках?
  • 29 марта 2024 г. 18:40
Ответы на вопрос 5
В PHP можно ограничить доступ к защищенным методам родительского класса, чтобы они были доступны только в классах-наследниках с помощью ключевого слова `protected`.
Пример:

```php
class ParentClass {
    protected function protectedMethod() {
        // код метода
    }
}

class ChildClass extends ParentClass {
    public function childMethod() {
        $this->protectedMethod(); // Защищенный метод родительского класса доступен в классе-наследнике
    }
}

$child = new ChildClass();
$child->childMethod(); // Вызываем метод childMethod() из класса-наследника
```

Если нужно ограничить доступ к защищенным методам только внутри класса-наследника, то необходимо создать защищенный метод с тем же именем в классе-наследнике. В этом случае защищенный метод родительского класса не будет доступен извне.

Пример:

```php
class ParentClass {
    protected function protectedMethod() {
        // код метода
    }
}

class ChildClass extends ParentClass {
    protected function protectedMethod() {
        // Переопределяем защищенный метод родительского класса
        parent::protectedMethod(); // Вызываем защищенный метод родительского класса
    }
}

$child = new ChildClass();
$child->protectedMethod(); // Ошибка, защищенный метод родительского класса не доступен извне
```
Собственно в результате общего мозгового штурма и компиляции всех предложенных идей нарисовалось вот такое решение: использование объекта обертки 
abstract class Node
{
   protected function onInit(): void
   {
      echo __METHOD__ . PHP_EOL;
   }
}
spl_autoload_register(function ($classname) {
   // Если $classname оканчивается на Dependency и является наследником Node, 
   // то сгенерировать динамический класс который в конструкторе принимает исходный объект
   // и содержит только публичные методы этого объекта
   // ..
});

class NodeA extends Node
{
   public function FUNC2()
   {
      echo __METHOD__ . PHP_EOL;
   }
   protected function onInit(): void
   {
      parent::onInit();
      echo __METHOD__ . PHP_EOL;
   }
}
class NodeB extends Node
{
   // Функция инициализации
   protected function onInit(): void
   {
      parent::onInit();
      echo __METHOD__ . PHP_EOL;
      // Добавить зависимость
      (function (NodeADependency $a) {
         $a->FUNC2();   // Метод успешно вызывается так как он public
         $a->onInit();  // Метод не получится вызвать так как его тут просто НЕТ!
      })(new NodeA);
   }
   public function run()
   {
      $this->onInit();
   }
}

(new NodeB)->run();


Тут конечно есть некоторая проблема с подсказками в IDE, но можно сгенерированные классы обертки сохранять в директорию в виде класса и, как минимум VSCode, подхватит эти классы для IDE
Красиво сделать не получится. Но можно сделать так, чтобы хотя бы на тестах оно упало и показало, что нельзя так делать. 

<?php

class Node
{
    protected function func1() {
        print "NODE PARENT; ";
    }
}
class NodeA extends Node
{
    public function FUNC2() {
        print "NODE A; ";
    }

    // Используем метод родителя внутри этого класса
    public function func1Overrided() {
        print "From parent: " . parent::func1();
    }

    // Переопределяем метод так, чтобы его нельзя было использовать
    protected function func1() {
        throw new \Exception("Нельзя вызывать этот метод из NodeB");
    }
}

class NodeB extends Node
{
    // Функция инициализации
    public function onInit(NodeA $a): void
    {
        // Сделал так, чтобы не мокать api )
        (function (?NodeA $a) {
            $a->FUNC2(); // Метод успешно вызывется так как он public
            $a->func1();  // Метод теперь кидает исключение, использовать не получится
        })($a);
    }
}

$nodeA = new NodeA;
$nodeA->func1Overrided(); // Работает вызов метода funс1 из родителя

$nodeB = new NodeB;
$nodeB->onInit($nodeA); // Выдаёт ошибку, нельзя использовать метод func1 из класса NodeB


Но у меня для вас совет: старайтесь как можно меньше использовать наследование. Из моего опыта могу сказать, что всякий раз, когда я с помощью наследования пытаюсь решить хоть сколько-нибудь сложную задачу, постоянно вылазят такие подводные камни, что жить не хочется. Используйте композицию и интерфейсы, и вам не придётся заниматься подобной ерундой.

Вот минимально-инвазивное решение, которое позволит и наследование сохранить (если оно прям ну вот сильно надо), и решить проблему при помощи композиции, основанной на трейте.

<?php
// Делаем общий трейт для всех классов
trait Func1 {
    private function func1() {
        print "FUNC1; ";
    }
}

class Node
{
    // Включаем трейт
    use Func1;
}

class NodeA extends Node
{
    // Включаем трейт
    use Func1;

    public function FUNC2() {
        print "FUNC 2 NODEA; ";
    }
}

class NodeB extends Node
{
    // Включаем трейт
    use Func1;

    // Функция инициализации
    public function onInit(NodeA $a): void
    {
        // Добавить зависимость
        (function (?NodeA $a) {
            $a->FUNC2(); // Метод успешно вызывется так как он public
            $a->func1();  // Метод использовать не получится, т.к. он private
        })($a);
    }
}

$nodeA = new NodeA;

$nodeB = new NodeB;
$nodeB->onInit($nodeA); // Выдаёт ошибку, нельзя использовать метод func1 из класса NodeB
Как вариант можно сбрасывать скоп внутри метода $api->dependency 

class Node
{
    protected function func1() {
    }
}
class NodeA extends Node
{
    public function FUNC2() {}
}
class NodeB extends Node
{
    // Функция инициализации
    public function __construct()
    {
        (new Api())->dependency(
            function (?NodeA $a) {
                $a->FUNC2(); // Метод успешно вызывется так как он public
                $a->func1();  // Call to protected method Node::func1() from global scope
            }
        );
    }
}

class Api {
    function dependency(callable $dependency) {
        if ($dependency instanceof Closure) {
            $dependency = Closure::bind($dependency, null, null);
            $dependency(new NodeA());
        }
    }
}


Либо можно использовать белый список и проверить стек вызовов, если вызывающего класса нет в списке, то кидать исключение.

class Node
{
    private array $whiteList = [
        NodeA::class
    ];

    protected function func1() {
        $scope = debug_backtrace(2, limit: 2)[1]['class'];
        if (!in_array($scope, $this->whiteList)) {
            throw new Exception('Нельзя вызвать метод для данного класса '. $scope);
        }
    }
}
class NodeA extends Node
{
    public function FUNC2() {}
}
class NodeB extends Node
{
    // Функция инициализации
    public function __construct()
    {
        (function (?NodeA $a) {
            $a->FUNC2(); // Метод успешно вызывется так как он public
            $a->func1();  // Нельзя вызвать метод для данного класса NodeB
        })(new NodeA());
    }
}
Можно в качестве зависимости указывать не сам клас NodeA, а интерфейс NodeAInterface в котором будут описаны только нужные public методы.
Похожие вопросы