@shasoftX

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

Есть такой PHP код
class Node
{
   protected function func1() { }
}
class NodeA extends Node
{
   public function FUNC2() { }
}
class NodeB extends Node
{
    // Функция инициализации
    protected function onInit(ApiInit $api): void
    {
        // Добавить зависимость
        $api->dependency(function (?NodeA $a) {
            $a->FUNC2(); // Метод успешно вызывется так как он public
            $a->func1();  // Метод также успешно вызывается хотя он и protected
        });
    }

Собственно понятно почему удается вызвать функцию func1 (из-за общего предка). Но вот этого как раз и не хотелось бы. Можно как-то реализовать такое но чтобы нельзя было вызвать защищенные функции базового класса?
Вариант сделать их приватными не подходит, так как они должны вызываться в самих наследниках.

Т.е. чтобы функцию Node()->func1 можно было вызывать в NodeA, но при этом нельзя было вызывать это в контексте NodeB
  • Вопрос задан
  • 1049 просмотров
Решения вопроса 1
@shasoftX Автор вопроса
Собственно в результате общего мозгового штурма и компиляции всех предложенных идей нарисовалось вот такое решение: использование объекта обертки
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
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 3
Красиво сделать не получится. Но можно сделать так, чтобы хотя бы на тестах оно упало и показало, что нельзя так делать.

<?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
Ответ написан
alestro
@alestro
Как вариант можно сбрасывать скоп внутри метода $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 методы.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы