Собственно в результате общего мозгового штурма и компиляции всех предложенных идей нарисовалось вот такое решение: использование объекта обертки
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