Документирование возвращаемых типов абстрактных фабричных методов в PHP с помощью docblocks

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

Контекст:

    class AbstractBuildObject {}
    class Hammer extends AbstractBuildObject{}
    class Nail extends AbstractBuildObject{}

    class AbstractFactory{    
        /**
          * return $type
          */
        public function build1(string $type): AbstractBuiltObject {
            return new $type();
        }

        /**
          * return any(AbstractBuiltObject)
          */
        public function build2(string $someArg): AbstractBuiltObject {
            $type = $this->decideTypeBasedOnArgsAndState($someArg);
            return new $type();
        }
    }

Я попытался представить то, что мне нужно, с помощью аннотаций над билдерами.

return $type (или в идеале return $type of AbstractBuiltObject должно намекать на то, что тип возвращаемого значения указан во входном параметре.

Во втором случае any(AbstractBuiltObject) означает, что может быть возвращена любая производная конкретизация абстрактного класса.

Поэтому мне нужна какая-то аннотация для достижения описанных мной эффектов. Эти аннотации, очевидно, не работают, я просто использовал их для иллюстрации концепции.

Я знаю, что у кого-то может возникнуть соблазн использовать соединения типа конвейера, такие как return Hammer|Nail, но в моем случае фабричный класс должен изменяться каждый раз, когда в проект добавляется новая конкретная реализация, он также недостаточно специфичен в случае build1, где я точно знать, каким должен быть возвращаемый тип.

Короче говоря, мне нужно, чтобы это работало хотя бы в PhpStorm:

    (new AbstractFactory())->build1(Hammer::class)-> // I should have Hammer autocomplete here
    (new AbstractFactory())->build2('foo')-> // I should have autocomplete of any concretion of the abstract here

person Dinu    schedule 04.03.2021    source источник
comment
Чем any(AbstractBuiltObject) должен отличаться от AbstractBuiltObject? Также обратите внимание, что то, что вы описываете, по сути, предназначено для интерфейсов. Сделайте так, чтобы ваши конкреции реализовали интерфейс, а затем введите интерфейс.   -  person Alex Howansky    schedule 04.03.2021
comment
@AlexHowansky any(AbstractBuiltObject) должен быть эквивалентом статического анализатора жестко введенного Hammer|Nail. То есть я должен получить автозаполнение и дальнейший тип iferrence для любого т.е. метода, который реализован на Hammer, но не на реферате.   -  person Dinu    schedule 04.03.2021
comment
Для этого в PhpStorm есть функция Расширенные метаданные. Он используется такими вещами, как помощник Laravel IDE, плагин Symfony и т. д. и т. д. -- jetbrains.com/help/phpstorm/ide-advanced-metadata.html   -  person LazyOne    schedule 04.03.2021
comment
В противном случае посмотрите на PHPStan/Psalm — PhpStorm имеет некоторую интеграцию, но я не уверен, что эти конкретные случаи охвачены: jetbrains.com/help/phpstorm/using-psalm.html / jetbrains.com/help/phpstorm/using-phpstan.html   -  person LazyOne    schedule 04.03.2021


Ответы (2)


Философские разговоры о нарушении D в SOLID в сторону, если вы хотите что-то вроде этого для автозаполнения методов, доступных только на Hammer:

(new AbstractFactory())->build1(Hammer::class)->

Тогда вы уже взяли на себя обязательство написать этот блок кода специально для класса Hammer. И если вы собираетесь это сделать, то вы могли бы также сделать это:

$hammer = (new AbstractFactory())->build1(Hammer::class);

И если вы делаете это, то вы могли бы также сделать это:

/**
 * @var Hammer
 */
$hammer = (new AbstractFactory())->build1(Hammer::class);

И тогда ваше автозаполнение на $hammer-> должно работать.

person Alex Howansky    schedule 04.03.2021
comment
Хотя это догматически правильно, для меня это имеет серьезные практические последствия (разве не всегда что-то есть?). А именно, то, что я делаю прямо сейчас, — это замена некоторых статически сгенерированных экземпляров сгенерированными на заводе с помощью большой кодовой базы; упомянутые объекты используются так часто, что, очевидно, они появляются в длинных катенах плавного кода. Это приводит к довольно уродливому рефакторингу и преобразует то, что заняло бы 1 секунду и не требовало обучения (например, написание $factory->get(Instance::class)), во что-то более сложное; поэтому я действительно надеюсь, что смогу вытащить его из своей IDE... - person Dinu; 04.03.2021
comment
С точки зрения доктрины, моя проблема наиболее правильно решается с использованием DI для экземпляров, но опять же, это будет означать такой жестокий рефакторинг, что на этот раз я просто ищу быстрый выход :) - person Dinu; 04.03.2021
comment
После хорошего ночного сна я склоняюсь к использованию такой конструкции: php class AbstractBuiltObject { /** @return static */ public static from(self $instance): self { // :static from 7.4+ if(!($instance instanceof static)){ throw new Exception('wrong'); } return $instance; } } Hammer::from($factory->get('find me a hammer'))->hammerMethod(); Кажется, работает как шарм с точки зрения статического анализа, не слишком сильно ломает шаблоны. Видите ли вы какие-либо недостатки в этом? - person Dinu; 05.03.2021
comment
Я не знаю, как форматировать этот код... - person Dinu; 05.03.2021
comment
pastebin.com/E2KjGJ1N - person Dinu; 05.03.2021
comment
Затем используйте как Hammer::from($factory->get(...))->hammerMethod(), это позволит сохранить плавные последовательности кода и минимизировать накладные расходы на рефакторинг. Я думаю, что это также максимальный уровень полиморфизма, который PHP может обеспечить на данный момент... - person Dinu; 05.03.2021
comment
Другим решением может быть использование шаблонов psalm.dev/docs/annotating_code/templated_annotations для моделирования чего-то вроде Factory::get<Hammer>(): Hammer но, похоже, Storm еще не полностью поддерживает это - person Dinu; 05.03.2021

Решение, которое мы приняли, таково:

<?php
class AbstractBuiltClass {
    /**
      * @return static
      */
    public static function type(self $instance)
    // change return type to :static when #PHP72 support is dropped and remove explicit typecheck
    :self
    {
        if(!($instance instanceof static)){
            throw new Exception();
        }
        return $instance;
    }
}

class Hammer extends AbstractBuiltClass{
    function hammerMethod(){}
}

Hammer::type($factory->get(Hammer::class))->hammerMethod();

Претенденты на жизнеспособное решение:

  1. Аннотации шаблонов псалмов: https://psalm.dev/docs/annotating_code/templated_annotations/ очень многообещающий, но еще не получивший широкой поддержки
  2. Переменный docblock (см. ответ Алекса Ховански)
person Dinu    schedule 07.03.2021