Привет, меня зовут Алексей и я должен признаться, я PHP разработчик. Последние несколько лет плотно занимаюсь проектами на symfony и решил поделиться с сообществом практиками, которые стараюсь соблюдать при работе.

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

1. Зависимости в di указываем явно

Явное лучше неявного. При описании классов не полагаемся на магию symfony di. Вообще мне кажется, что autowiring довольно вредная практика. При явном описании гораздо проще и быстрее при чтении кода понимать какие зависимости использует класс.

Плохо:

App\Services\MyService: ~

Хорошо:

app.services.my_service:
    class: App\Services\MyService
    arguments:
        - '@doctrine.orm.entity_manager'
        - '@app.services.my_service.dependency'

2. В DI подставляем реализации, а не интерфейсы

Так повышается читабельность и поощряется множественные имплементации интерфейсов.

Плохо:

app.services.my_service:
    class: App\Services\MyService
    arguments:
        - '@App\OtherService\Producer\ProducerInterface'

App\OtherService\Producer\ProducerInterface:
    alias: App\OtherService\Producer\Amqp

Хорошо:

app.services.my_service:
    class: App\Services\MyService
    arguments:
        - '@app.other_service.producer'

app.other_service.producer:
    class: App\OtherService\Producer\Amqp

3. Не надо пробрасывать в классы весь контейнер целиком

Плохо:

final class MyService implements MyServiceInterface
{
    public function __construct(private readonly ContainerInterface $container) {}
    public function doSomething()
    {
        $producer = $this->container->get('app.other_service.producer');
        $producer->publish();
    }
}

Хорошо:

final class MyService implements MyServiceInterface
{
    public function __construct(private readonly ProducerInterface $producer) {}
    public function doSomething()
    {
        $producer->publish();
    }
}

4. Каждый класс закрывается интерфейсом

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

final class MyService implements MyServiceInterface
{
    // ...
}

5. Чем меньше методов в интерфейсе, тем лучше.

Большие интерфейсы сложнее поддерживать и реализовать. Нарушается принцип единственной ответственности.

Плохо:

interface SomeEntityInterface
{
    public function getId(): int;
    public function getName(): int;
    public function getAge(): int;
}

Хорошо:

interface IdAwareInterface
{
    public function getId(): int;
}
interface NameAwareInterface
{
    public function getName(): int;
}
interface AgeAwareInterface
{
    public function getAge(): int;
}

6. В интерфейсах указываем какие exception могут быть выброшены

Кроме этих exceptions никакие другие выбрасываться не должны. Если у интерфейса нет блока @throws - значит в реализации все exception должны быть обработаны.

7. Максимально строгая типизация

Используем strict_types=1. Типы указываем максимально жестко.

Плохо:

public function doSomething(mixed $id)
{
    return $id;
}

Хорошо:

public function doSomething(int $id)
{
    return $id;
}

8. Флаги в аргументах функций

Нарушается принцип единственной ответственности. Отдельные классы проще тестировать, можно использовать в разных сценариях с помощью композиции.

Плохо:

final class SomeService {
    public function doSomething($force = false)
    {
        if ($force) {
            return $this->doForce()
        }
        
        return $this->do()
    }
}

Хорошо:

final class ForceService {
    public function doSomething()
    {
        return $this->doForce();
    }
}

9. Количество аргументов в функции максимум 3

Если аргументов много - значит метод выполняет слишком много действий, нарушается принцип единственной ответственности. Это сложно тестировать и поддерживать.

Плохо:

final class SomeService {
    public function doSomething($own, $two, $three, $four)
    {
    }
}

10. Не мутировать объекты переданные в метод/функцию

Это неочевидное поведение для клиентского кода и легко приводит к ошибкам в процессе развития и изменения кода.

Плохо:

$item = new Item();
$this->calculateCount($item);

public function calculateCount(Item $item): void
{
    $count = 0;
    // some logic
    $item->setCount($count);
}

Хорошо:

$item = new Item();
$item->setCount($this->calculateCount());

public function calculateCount(): int
{
    $count = 0;
    // some logic
    return $count;
}

11. Заполнение DTO через конструктор с именованными аргументами, readonly свойствами, без get/set методов

Такая запись читабельна за счёт именованных аргументов, нет лишнего кода get/set, такая dto иммутабельна за счёт readonly.

Хорошо:

final class Item
{
    public function __construct(
        public readonly int $id,
        public readonly int $count,
        public readonly string $originId,
    ) {
    }
}

$item = new Item(
    id: 1,
    count: 2,
    originId: 'aaa-bbb-ccc',
);

12. Разбиваем большие классы и методы

Класс меньше ~150 строк, метод меньше ~30 строк. Это значительно упрощает чтение кода.

13. Unit тесты всегда

На любой код обязательно пишем unit тест (кроме dto). Если есть skiptests - то обязательно добавляем ссылку на то место, где это протестировано.

/**
 * @skiptests tested in {@see \App\SomeOthreService}
 */
final class SomeServiceTest extends TestCase {
}

14. Тестируем поведение, а не детали реализации

Тест должен проверять все варианты поведения модуля, но не должен тестировать внутреннюю реализацию этого поведения.
Не надо в тесте проверять вызов логгера, аргументы исходящих вызовов, вызовы приватных методов, если только в этом не заключается основная логика работы класса. Stubs и dummy заглушки намного предпочтительнее mocks и fakes.

Плохо:

final class SomeServiceTest extends TestCase {
    public function testDoSomething()
    {
        $logger = $this->createMock(LoggerInterface::class);
        $logger->expects($this->once())
            ->method('info')
            ->with('some message');
        $service = new SomeService($logger);
        
        $service->doSomething();
    }
}

Хорошо:

final class SomeServiceTest extends TestCase {
    public function testDoSomething()
    {
        $service = new SomeService(
            $this->createMock(LoggerInterface::class),
        );
        
        $service->doSomething();
    }
}

15. Композиция, а не наследование

Наследование это максимально сильная связь между классами.
Композиция в подавляющем большинстве случаев предпочтительнее.

Плохо:

final class SomeService extends BaseLogic {
}

Хорошо:

final class SomeService {
    public function __construct(private readonly BaseLogic $logic) {}
    public function execute()
    {
        $this->logic->doSomething();
    }
}

16. По умолчанию указываем final для классов

Такое правило позволяет избежать проблем с наследованием и поощряет использование композиции.

final class SomeService {
}

17. Не работаем с ассоциативными массивами, только с объектами

Приходится писать много проверок, сложно расширять. Используем JMS\Serializer\SerializerInterface и
JMS\Serializer\ArrayTransformerInterface.

Плохо:

$id = $data['id'];

Хорошо:

$id = $data->getId();

18. Соблюдаем закон Деметры

Класс должен взаимодействовать только с известными ему модулями, не взаимодействовать с незнакомцами.

Плохо:

$client = $this->clientFactory->create();
$client->send();

Хорошо:

$this->clientDecorator->send();

19. Минимальная цикломатическая сложность

Код должен быть максимально плоским. Чем меньше сложность, тем проще читать код и понимать, что он делает.
Иногда можно жертвовать микрооптимизациями ради читаемости.

Плохо:

foreach ($items as $item) {
    if ($item->isAvailable()) {
        $availableItems[] = $item;
    }
    if ($item->isAcvite()) {
        $activeItems[] = $item;
    }
}

Хорошо:

$availableItems = $this->filterAvailableItems($items);
$activeItems = $this->filterActiveItems($items);

Комментарии (42)


  1. fo_otman
    17.05.2024 06:31
    +5

    п.2 нарушает буковку D в аббревиатуре SOLID. Что побудило вас настолько явно нарушать наши религиозные догмы?


    1. Sau
      17.05.2024 06:31

      Еретик-с.


    1. lelikPtz Автор
      17.05.2024 06:31

      Не нарушает, в пункте речь про конфигурацию di, а не код, видимо не очень чётко сформулировал мысль


  1. Cels
    17.05.2024 06:31
    +7

    читал бегло, но
    4. Каждый класс закрывается интерфейсом - сильно перегрузит код. Интерфейсы лучше использовать, когда это действительно нужно.
    5. Чем меньше методов в интерфейсе, тем лучше.- плохой пример, т.е. в данном случае - то что плохо, как раз нормально, а что хорошо - плохо.
    7. Максимально строгая типизация. - у php нет перегрузки функций, поэтому mixed вполне можно использовать где нужно.
    8. Флаги в аргументах функций - они обычно появляются в процессе доработки, чтобы избежать дублирования кода. Если там не куча флагов, то ничего страшного не вижу.
    17. Не работаем с ассоциативными массивами, только с объектами. - а почему, чем плохо? Мне кажется массивы более "легкие", чем объекты..
    15. Композиция, а не наследование - тоже можно поспорить - смотря как и где использовать.

    т.е. вы написали что хорошо делать так, а так делать плохо - а почему?


    1. LaoSan
      17.05.2024 06:31

      т.е. вы написали что хорошо делать так, а так делать плохо - а почему?

      Думаю "так делать плохо" потому, что иначе у автора ломается внутренний комфорт и чувство прекрасного.

      9. Количество аргументов в функции максимум 3

      А, что если у меня в функции описана сложная формула для расчета результата по огромной массе входящих значений?
      Все? Закрывать лавочку и уходить из программирования?


      1. FanatPHP
        17.05.2024 06:31
        +1

        А что если у меня в функции описана сложная формула для расчета результата по огромной массе входящих значений?

        Разбить эти аргументы на группы, каждая в своем DTO/VO.


    1. ILDAR_BAHTIGOZIN
      17.05.2024 06:31

      а почему, чем плохо? Мне кажется массивы более "легкие", чем объекты..

      если массив $data получен из клиента $_POST, $_GET итд то лучше всё таки объект, т.к в большинстве фреймворков он хотя бы проверяется на инъекции.


      1. FanatPHP
        17.05.2024 06:31

        в большинстве фреймворков он хотя бы проверяется на инъекции

        Эээ... А можно чуть подробнее? Как именно проверяется и на какие конкретно "инъекции"?


    1. FanatPHP
      17.05.2024 06:31
      +6

      сильно перегрузит код

      Не очень понятный аргумент. интерфейс сидит в своем файле и никому не мешает. В каком смысле "перегрузит"?

      Максимально строгая типизация. - у php нет перегрузки функций, поэтому mixed вполне можно использовать где нужно.

      Эти два утверждения вообще никак не противоречат друг другу. Да, должна быть максимально строгая типизация. Да, где нужно, mixed вполне можно использовать.

       Если там не куча флагов, то ничего страшного не вижу.

      Это понимание приходит со временем. Которое потратил разбор кода, который сам же писал полгода назад.

      Мне кажется массивы более "легкие", чем объекты..

      Да при чем здесь "легкость". Уже одного автодополнения достаточно. Не говоря уже про автоматические проверки целостности. Если вбил в массив неправильный ключ, то никто не заметит. А объект уже (теперь) такого не даст. Плюс типизация элементов. Тут целый мир разницы, между старым шаляй-валяй и прекрасным новым пхп Никиты Попова.


      1. Cels
        17.05.2024 06:31

        "Не очень понятный аргумент. интерфейс сидит в своем файле и никому не мешает. В каком смысле "перегрузит"? "

        а зачем он тогда нужен?

        "Эти два утверждения вообще никак не противоречат друг другу. Да, должна быть максимально строгая типизация. Да, где нужно, mixed вполне можно использовать."

        кому должна, вам? вообще-то я тоже самое написал, кроме "должен"

        "Это понимание приходит со временем. Которое потратил разбор кода, который сам же писал полгода назад."

        приходит с опытом, а не временем.

        "Да при чем здесь "легкость". Уже одного автодополнения достаточно... "

        тут как бы вообще не про это. Там где нужно используются массивы, там где нужно используются объекты.

        вы чего своим комментарием сказать-то хотели?


        1. MihaOo
          17.05.2024 06:31
          +1

          Интерфейс нужен что бы определить контракт. Не понимаю как фраза "сидит в своём файле" могла вызвать вопрос "зачем он нужен?"


          1. Cels
            17.05.2024 06:31
            +3

            такое ощущение, что общаюсь с детьми, которые нахватались умных слов, "модных" методик и теперь пытаются их везде впихнуть, просто потому, что это "модно и правильно" не разбираясь в сути. Интерфейсы, если простыми словами, нужны для передачи/получения определенных данных от разных источников (н-р: подключение базы, где метод connect может быть описан в интерфейсе, а разные классы от него уже делают подключение для mysql, mssql, postgesql и т.д.), также для передачи классов по интерфейсу, где также определяется поведение, в зависимости что передается.

            зачем интерфейс этому классу:


            class A(){
            public Print(string $msg){
            echo $msg;
            }

            чтобы что? и таких классов большинство. интерфейсы нужны там, где нужны и не нужны там где не нужны.


            1. MihaOo
              17.05.2024 06:31
              +2

              Что за скрытая агрессия не понимаю.

              При чём тут модно/не модно? Есть терминология и люди ей пользуются. Вы же не говорите "эта штука при помощи которой ты дышишь". Вы говорите нос, лёгкие и т.д. Так и тут.

              Никто вас переубедить не пытается потому что это на**р никому не надо. Уж я тем более.

              Касательно класса. Там дальше давали ссылку на комментарий где в ветке говорится про стабильные и волатильные зависимости. Советую почитать. Лично я противник interface obsession, но использовать их безусловно необходимо.

              И помните, только ситхи всё возводят в абсолют.


              1. Cels
                17.05.2024 06:31

                Да нет никакой агрессии - есть чувство раздражения. Ваш комментарий ни о чем. зачем каждому классу нужен интерфейс, как утверждает автор? - вы взялись на него ответить, но внятного ответа я так и не получил.

                фанатPHP - ребенок, который минусит тех, кто с ним не согласен и наоборот. думал вы лучше объясните.


                1. lelikPtz Автор
                  17.05.2024 06:31

                  Если каждый класс реализует интерфейс, а использующие его классы зависят от него, а не от реализации, то:

                  1) Легче писать тесты замокав эти интерфейсы.
                  2) По мере развития кода очень просто на основе контракта, описанного в интерфейсе, подменить реализацию на новую или использовать несколько разных реализаций.

                  Как и везде есть исключения, но как мне кажется доказывать надо именно их необходимость в конкретных случаях, а в общем случае лучше интерфейс


                  1. Cels
                    17.05.2024 06:31

                    а чем это расходится с моимии рссужудениями.


            1. DmitriyGordinskiy
              17.05.2024 06:31
              +2

              Интерфейсы, если простыми словами, нужны для передачи/получения определенных данных от разных источников

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


              1. Cels
                17.05.2024 06:31

                Да, плюсую. А теперь попробуйте это объяснить молодым программистам, которые всё знают.. потому что.. бам-бам - они всё знают..) так дядя Федор сказал..


      1. Kahelman
        17.05.2024 06:31
        +1

        Всем читать A Philosophie of Software Design by Ousterhout.

        Задача программиста создавать абстракции. Причём высокоуровневые. Когда вы один интерфейс растащили по 5 вы убили высокоуровневую абстракцию.

        Чем более высокий уровень абстракций тем легче работать с кодом, поскольку его можешь соответствует бизнес модели/ бизнес процессам.


        1. Tony-Sol
          17.05.2024 06:31

          Когда вы один интерфейс растащили по 5 вы убили высокоуровневую абстракцию.

          Не совсем понял что здесь подразумевается, но если имеется ввиду, что «если сделать из 1 интерфейса на 5 методов, 5 интерфейсов по 1 методу, то это убьет высокоуровневую абстракцию», то в корне с этим не согласен


    1. ironviper
      17.05.2024 06:31

      1. Массивы передаются по значению, а объекты по ссылкеДумаю причина в этом.


      1. FanatPHP
        17.05.2024 06:31

        Это как раз не имеет значения.


  1. saag
    17.05.2024 06:31

    Я заметил одну вещь - здесь на хабре чаще можно встретить упоминание PHP, Go, Java, JavaScript Python, Kotlin, С/C++ и даже прости господи 1С, но вот С# остается как бы в тени, с чем это связано? Его не часто используют?


    1. FanatPHP
      17.05.2024 06:31
      +2

      Я бы сказал, что сейчас про вообще любой язык днем с огнем не сыщешь. В основном про выращивание пельменей и кассетные магнитофоны.

      А если хотите читать про шарпы, то подпишитесь на соответствующий хаб, трафик там сравним с пхпешным.


  1. Apv__013
    17.05.2024 06:31
    +1

    Сначала придумываем себе проблемы, потом героически их решаем. Всё по маркетологовски, как учил святой скилбокс.


  1. el_kex
    17.05.2024 06:31
    +5

    В принципе, неплохой свод правил для себя. Но, как и написано в начале статьи, есть вещи как спорные, так и имеющие не совсем правильное обоснование.

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

    В DI подставляем реализации, а не интерфейсы

    Выше уже написали об этой проблеме. DI на то и DI, чтобы была возможность легко подменять реализации. Конечно, при наличии небольшого набора зависимостей оно проще читается.

    Не надо пробрасывать в классы весь контейнер целиком

    Почему? Тут есть утверждение без доказательства.

    5. Чем меньше методов в интерфейсе, тем лучше.

    Большие интерфейсы сложнее поддерживать и реализовать. Нарушается принцип единственной ответственности.

    Тут нарушается не SRP, а ISP. SOLID явно определяет принцип разделения интерфейсов.

    В интерфейсах указываем какие exception могут быть выброшены

    Весьма общее требование. Например, в слое адаптеров реализуется обращение к мессенджерам. По-хорошему, мессенджеров может быть много, они будут добавляться. Допустим, у нас есть задача рассылать в Slack, Telegram, WhatsApp и еще куда-то. У каждого мессенджера может быть, скажем, внешняя библиотека со своими кастомными исключениями. Перечислять их все в интерфейсе клиента и пополнять каждый раз?

    Используем strict_types=1. Типы указываем максимально жестко.

    Чтобы что? Опять же, утверждение без доказательства. Понятно, что это полезно, но те, кто будут читать статью, могут не понимать причин. Получается, что будет просто бездумный копипастинг правила.

    Если аргументов много - значит метод выполняет слишком много действий

    Эта штука идет из "Чистого кода" Роберта Мартина. Принятие более 3 аргументов еще не означает, что метод обязательно нарушает SRP. Тут утверждение очень общее. Присмотреться к такому методу при рефакторинге, разумеется, стоит. Тут уже речь о code smell. Вероятно, стоит заменить параметры вызовами внутри или передачей объекта.

    Но иногда избавление от параметров может привести к усилению связанность между классами.

    Не мутировать объекты переданные в метод/функцию

    Так тут проблема скорее в том, что мутировать объекты в принципе можно. Свойства и методы в публичном доступе - это доступный контракт. Значит, либо его забыли инкапсулировать, либо к нему таки можно обращаться.

    Заполнение DTO через конструктор с именованными аргументами

    Вот тут про читабельность могу поспорить. Пихать в конструктор сразу же и определение свойств - такое себе. Имхо, спорное нововведение. Ведь так в одном полотне и определение свойств, и типы, и дефолтные значения, и конструктор. И вообще, что мешает навесить readonly вне конструктора?

    Класс меньше ~150 строк, метод меньше ~30 строк. Это значительно упрощает чтение кода.

    Классы и методы не должны определять свое качество количеством строк. Они должны руководствоваться принципами, такими, как, например, SOLID, GRASP. Да, как я писал выше, размер класса - это повод к нему присмотреться. Но не повод сразу идти его рефакторить.

    На любой код обязательно пишем unit тест

    Тут рекомендую почитать вот эту статью. "Обязательно" - термин огульный. 100% покрытия в хотя бы среднем проекте не будет. Да оно и не требуется. Особенно, если помимо unit-тестов есть другие составляющие пирамиды тестирования.

    15. Композиция, а не наследование

    Собственно, тут бы читателям статьи привести ссылки на примеры, когда это плохо.

    По умолчанию указываем final для классов

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


    1. pqbd
      17.05.2024 06:31
      +1

      Про пункт DI. Мне кажется автор некорректно описал то, что имел ввиду. Я вот вижу, что его конструкторы принимают интерфейсы (что и требуется). А вот, в конфигурации контейнера он предпочитает ставить реализации (это конфигурация кода, а не сам код). И его можно понять - если везде прокидывать интерфейсы, то не получится в разные классы прокинуть разные реализации.


      1. el_kex
        17.05.2024 06:31

        Да, я потому и пишу, что тут решение из разряда it depends. Не хватает более широкого обоснования, почему принимается именно такой выбор.

        Понятно, что тут больше книга рецептов, но вероятно из нее надо убрать общепринятые принципы, а сосредоточиться на персональных, но более глубоко. Либо писать несколько статей.


    1. lelikPtz Автор
      17.05.2024 06:31
      +2

      Не хотелось очень подробно расписывать обоснования каждого пункта, так как отдельно это обсуждалось много раз и решил попробовать изложить как можно короче, видимо не очень получилось, наброс принят)


    1. Tony-Sol
      17.05.2024 06:31

      У каждого мессенджера может быть, скажем, внешняя библиотека со своими кастомными исключениями. Перечислять их все в интерфейсе клиента и пополнять каждый раз?

      Над реализациями «мессенджеров» должен быть слой абстракции, которому интерфейс будет принадлежать и логично по соседству (с том же пакете, родительском нэймспэйсе) иметь набор исключений, в которые будут каститься все нижележащие исключения внешних библиотек. А иначе течет абстракция, потому что вышележащий код будет знать про исключения зависимостей.


  1. nivorbud
    17.05.2024 06:31
    +2

    Количество аргументов в функции максимум 3 Если аргументов много - значит метод выполняет слишком много действий, нарушается принцип единственной ответственности. Это сложно тестировать и поддерживать.

    Предположим, есть функция, вычисляющая значение кубического многочлена (для простоты на питоне напишу):

    def calc_polynom(a, b, c, d):
      return a * x**3 + b * x**2 + c * x + d

    Аж четыре аргумента! А максимум допустимо три. Что предлагается сделать? Несколько дополнительных абстракций ввести? Интересно глянуть на рефакторинг.


    1. Mihaelc
      17.05.2024 06:31
      +1

      Можно написать функцию, которая считает значение многочлена степени n и передавать один массив коэфов:)


      1. nivorbud
        17.05.2024 06:31

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

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

        Просто многие с фанатизмом относятся к подобным рекомендациям из книжек, тем более они сформулированы так, как будто это жесткие постулаты (5 строк кода в функции и точка!).


        1. lelikPtz Автор
          17.05.2024 06:31

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

          Предложенное ограничение заставляет задуматься об альтернативах. И само собой бывают исключения.


      1. Kahelman
        17.05.2024 06:31

        Точно, предлагаю пойти дальше и во все функции передавать один аргумент foo(…) - который произвольное количество аргументов. :)


        1. SergeRod
          17.05.2024 06:31

          можно вообще не заморачиваться с определением аргументов. func_get_args никто не отменял.


          1. FanatPHP
            17.05.2024 06:31

            Давно отменили. Есть же оператор распаковки аргументов.


  1. michael_v89
    17.05.2024 06:31
    +3

    Позволяет разрабатывать отталкиваясь от интерфейса.

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

    С интерфейсом не работает функция IDE "Go to definition", приходится постоянно еще раз нажимать сочетание для "Go to implementation", чтобы перейти к реализации. Еще при изменениях сигнатуры методов надо в интерфейсе тоже обновлять. Это усложнение поддержки, а не упрощение.

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

    Если в месте использования класса по какой-то причине не подходит его реализация, его легко заменить другой.

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

    Чем меньше методов в интерфейсе, тем лучше.

    Категоричные утверждения неверны.
    Интерфейс с одним методом не имеет смысла. Теряется цель выделения абстракции, сложность переносится в вызывающий код. Так же как и класс с одним методом.

    Вот есть у меня какая-то библиотека для HTTP запросов, они сделали себе MyHttpClient\NameAwareInterface, и что, я должен в бизнес-логике использовать зависимость от MyHttpClient? А если нет, у меня будет 2 интерфейса NameAwareInterface, а может и 10, фиг поймешь где какой использовать.

    Интерфейс должен описывать методы управления и взаимодействия. Принципы группировки методов в интерфейс такие же, как и группировки методов в класс.

    В интерфейсах указываем какие exception могут быть выброшены

    Говорят, это пробовали в Java, и решили, что это была ошибка.
    Драйвер локальной файловой системы не может выбрасывать NetworkException, а драйвер Amazon S3 может. Что в интерфейсе писать?

    Если вам нужно что-то указывать, значит это возвращаемое значение, и лучше его возвращать явно. Исключения на то и исключения, что они возникают при редкой неожиданной ситуации. Поэтому большинство исключений должно быть RuntimeException или его наследником. В вызывающем коде ловим общий Throwable, откатываем действие, бросаем дальше. Другие варианты только по необходимости, когда понятно, что мы хотим этим достичь.

    Если аргументов много - значит метод выполняет слишком много действий

    Нет, не значит.

    Класс меньше ~150 строк, метод меньше ~30 строк. Это значительно упрощает чтение кода.

    Нет, не упрощает. Я читаю не просто так, а чтобы понять логику. Если она разбросана по разным местам, то понимать ее сложнее. Разбивать надо по логическим границам, а не по количеству.

    По умолчанию указываем final для классов. Такое правило позволяет избежать проблем с наследованием и поощряет использование композиции.

    Зато делает невозможным моки в тестах, из-за чего надо использовать интерфейсы где они не нужны.
    Лучше сделать правило для code sniffer, что наследование идет только от абстрактного класса.

    Соблюдаем закон Деметры
    $this->clientDecorator->send();

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

    Любой дополнительный класс надо поддерживать, любая абстракция усложняет понимание кода, поэтому такой подход усложняет поддержку. Абстракция упрощает поддержку когда она содержит логику и используется в нескольких местах (или будет использоваться), тогда сложность из этих мест переносится в абстракцию.

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


  1. ddruganov
    17.05.2024 06:31

    Вот вроде все хорошо, но от первого пункта хочется пулю пустить в лоб

    Зачем тогда вообще использовать контейнер, если не использовать автовайринг? Создавай все сам тогда при инициализации приложения

    Если мне надо сделать сервис, в который я проталкиваю логгер, то на кой черт я должен лезть в конфиг и явно указывать какой логгер прокидывать? Мне любой сойдет, дефолтного будет достаточно в 99.9% случаев. Та же история с прокидыванием репозиториев.

    Хотя, если вам платят за кол-во строк, то пожалуйста


    1. FanatPHP
      17.05.2024 06:31
      +1

      Это классическая дилемма пишем vs. читаем. Для борзописания конечно лучше когда всё само. Но вот для чтения куда лучше, когда всё явно расписано...


      1. slavcopost
        17.05.2024 06:31
        +1

        Чем это читается хуже?

        class MyService {
          __constructor(
            private EntityManager $manager,
            private Dependency $dependency,
          ) {}
        }
        

        зачем это дважды читать? в коде и еще раз в yaml настройках?


  1. SuperKozel
    17.05.2024 06:31

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