Сделать метод абстрактного класса общедоступным только из дочерних классов

Во-первых, мы говорим о PHP 7.4.10, но любая общая информация приветствуется!

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

Рассмотрим следующий пример (пояснение в комментариях). Я хочу иметь возможность звонить Apple::printName() и Pear::printName(), но не Fruit::printName().

abstract class Fruit
{
    /*
     * Oblige every child class to define a string name, nothing unusual
     */
    protected abstract static function name() : string;

    /*
     * The problem is with the access modifier of this method here
     *** 
     * If it is public, everything is fine with Apple::printName() and Pear::printName(), 
     * but one can call Fruit::printName() from outside,
     * resulting in PHP Error: Cannot call abstract method Fruit::name()
     * this is still sort of okay, since an error will be thrown anyway, 
     * but I don't want the runtime to even enter the method's body
     * I'd like to get an access restriction error.
     *** 
     * If it is protected, then we automatically can't call Apple::printName nor Pear::printName()
     ***
     * So, is there a way to define the parent static method only publicly accessible from child classes without copying code?
     */
    public static function printName()
    {
        return "My name is: " . static::name();
    }
}

class Apple extends Fruit
{
    protected static function name() : string
    {
        return "apple";
    }
}

class Pear extends Fruit
{
    protected static function name() : string
    {
        return "pear";
    }
}

echo Apple::printName(); //prints "My name is: apple"
echo Pear::printName(); //prints "My name is: pear"
echo Fruit::printName(); //PHP Error: Cannot call abstract method Fruit::name() at line...

Я также открыт для любых альтернативных подходов к тому, как можно добиться желаемого поведения.


person D. Petrov    schedule 09.11.2020    source источник


Ответы (3)


Вы можете проверить, является ли ваш экземпляр подклассом или нет, а затем залог, если это не так.

abstract class A1 {
    public static function childrenOnly() {
        return is_subclass_of(new static, 'A1');
    }
}

class A2 extends A1 {
}
Al::childrenOnly(); // errors out
A2::childrenOnly(); // true
person chiliNUT    schedule 09.11.2020
comment
Это может быть хорошим небольшим обходным путем, вы помещаете его один раз в родительский класс и готово. В итоге я сделал Apple и Pear абстрактными (поскольку они предназначены для служебных синглетонов) и реализовал ваше предложение в Fruit. Вы можете увидеть мой ответ для заключения. - person D. Petrov; 10.11.2020

public static function printName() {}

Это определение конкретного метода. Можно подумать, что вызывать этот метод нормально, так как это не абстрактный общедоступный метод. Так почему же он у Fruit, если он ничего с ним не делает?

Таким образом, одно из решений состоит в том, чтобы определить trait и use только внутри тех, кому это действительно нужно?

trait Printer { 
    
    public static function printName()
    {
        return "My name is: " . static::name();
    }   
}

abstract class Fruit
{
    protected abstract static function name() : string;
}

class Apple extends Fruit
{
    use Printer;
    protected static function name() : string
    {
        return "apple";
    }
}

class Pear extends Fruit
{
    use Printer;
    protected static function name() : string
    {
        return "pear";
    }
}

Однако, если ваш Fruit является просто определением для дочерних классов, как вы упомянули в комментарии, и у вас есть PHP 8 :), тогда, возможно, просто не делайте printName статическим

public function printName()
{
    return "My name is: " . static::name();
}

И

$a = new Apple();
$p = new Pear();

echo $a->printName();
echo $p->printName();

Теперь echo Fruit::printName(); выдаст вам прекрасную фатальную ошибку, которую вы искали.

Неустранимая ошибка: Uncaught Ошибка: нестатический метод Fruit::printName() не может быть вызван статически

person Rain    schedule 09.11.2020
comment
Спасибо за ответ! Я тоже думал об этом сценарии, но две основные вещи кажутся мне не столь изощренными и оптимальными: - трейт Printer можно включать в классы, которые не обязательно содержат статический метод name(); - Мне придется добавить use Printer; к каждому классу, который уже расширяет Fruit, что является своего рода формой копирования кода, хотя и довольно минимальным и, возможно, достаточно хорошим. В любом случае спасибо за предложение, это действительно может оказаться лучшей альтернативой, но мне все еще интересно, существуют ли другие способы достижения упомянутого поведения. :) - person D. Petrov; 10.11.2020
comment
И чтобы ответить на ваш первоначальный вопрос: Fruit имеет именно то, что предоставляет его в качестве функциональности по умолчанию для всех своих дочерних классов, что необходимо именно потому, что они реализуют abstract static function name() : string, который, в свою очередь, используется в методе printName(). - person D. Petrov; 10.11.2020
comment
Я понимаю, что вы имеете в виду, хотя принцип включает objects типов, и мой вопрос касается довольно статических реализаций. Я могу согласиться, что это оказывается нарушением принципа Лискова. Но допустим, я хочу иметь 100 дочерних классов фруктов, только 2 из которых потребуют другой реализации для статического printName(). Я бы по-прежнему хотел предоставить им реализацию по умолчанию, а также возможность перезаписать ее. Или это желание само по себе является нарушением принципа Лискова? - person D. Petrov; 10.11.2020
comment
Я видел ваше обновление, но на самом деле меня больше интересуют статические реализации, поскольку это служебные классы, которые я пытаюсь вывести. Большое спасибо за усилия! - person D. Petrov; 10.11.2020

Поэтому, прочитав все ответы, я нашел обходное решение и, наконец, остановился на предложении @chilliNUT. В итоге я поступил следующим образом:

abstract class Fruit
{
    protected abstract static function name() : string;

    /*
     * Basically, if you call the default method from any child (not necessarily a direct child) class, no problems,
     * but if you call it directly from the baseclass, you get the somewhat acceptable exception :)
     */
    public static function printName()
    {
        if(!is_subclass_of(static::class, self::class))
        {
            throw new \BadMethodCallException("Can't call base Fruit class methods directly!");
        }

        return "My name is: " . static::name();
    }
}

//those are abstract now, just to match the intention of them being utility singletons
abstract class Apple extends Fruit
{
    protected static function name() : string
    {
        return "apple";
    }
}

abstract class Pear extends Fruit
{
    protected static function name() : string
    {
        return "pear";
    }
}

В любом случае, из ответа @Rain я понял, почему это, как правило, плохая практика и на самом деле не лучшее направление для дальнейшего развития, поэтому я рассмотрю разные стратегии для того, что я пытаюсь достигать. Мое любопытство было удовлетворено в конце дня! Всем спасибо за участие!

person D. Petrov    schedule 09.11.2020
comment
Пожалуйста . Хорошее решение, но я думал, что вы не хотите, чтобы среда выполнения даже входила в тело метода: D - person Rain; 10.11.2020
comment
Ну, видите ли, это ближе всего к тому, чтобы даже не входить - сразу после входа останавливается! :D Все дело было в том, что я не хотел выполнять непредсказуемое поведение и получать непредвиденные ошибки в зависимости от тела метода и того, что может или не может сломаться внутри него. - person D. Petrov; 10.11.2020