Так исторически сложилось, что язык программирования PHP порой недолюбливают. Я не встречал ещё ни одного Java-программиста, который бы не смотрел на PHP свысока или хотя бы не ронял фразы типа: «К сожалению, практически вся e-commerce написана на PHP». Наверное, это происходит из-за того, что мы видим «плохой» код на PHP, иногда вынуждены поддерживать этот код и переносим негатив на сам язык. Но тем не менее нельзя отрицать, что PHP популярен — по данным на 2024 год, PHP используется на более чем 75% всех веб-сайтов, где язык программирования известен..

Надеюсь, эта статья может быть полезна не только тем, кто не работает с PHP постоянно, а вынужден лишь иногда что-то время от времени фиксить, но и тем, для кого PHP является «родным» языком. Я собрал некоторые анти-паттерны или плохие практики, из-за которых появляется плохой код. Возможно, вы узнаете здесь свои приёмы и подходы и пересмотрите их.

1. Хук — это не просто функция

Когда мы используем фреймворки или CMS, мы пользуемся специальными hook-функциями. Они могут называться по-разному, но смысл в том, что эта функция будет вызвана при определённом событии, произошедшем в системе. Например, «пользователь добавил товар в корзину» или «пользователь зашёл на определённую страницу». Это «событийно-ориентированная модель» или «event-driven programming» — парадигма программирования, основанная на обработке событий, сигналов или сообщений, возникающих в системе. Например, в Drupal такая функция может выглядеть так:

/**
* Implements hook_form_alter().
*/
function module_name_form_alter(&$form, FormStateInterface $form_state, $form_id) {

}

Технически хук — это, конечно, функция, и проблема в том, что программист пытается поместить весь свой код между фигурными скобками этой функции. В итоге получается следующее:

Пример очень длинной функции.
Пример очень длинной функции.

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

Какая в этом проблема:

  • Этот код трудно читать;

  • Трудно найти правильное место, если нужно что‑то изменить;

  • Реально трудно понять, что здесь происходит, потому что многие независимые части сайта изменяются этой одной функцией;

  • Это яркий пример «спагетти» кода;

Как это можно улучшить?

Необходимо использовать подход «разделяй и властвуй» (Divide and Conquer) или по другому: «модульность» (Modularity). Во‑первых использовать более специфические хуки взамен более общих:

Слева — общий хук; справа — три более специфических.
Слева — общий хук; справа — три более специфических.

Тогда у вас будет несколько небольших узкоспециализированных функций взамен одной общей.

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

Слева — длинная функция; справа — функция, вызывающая другие функции.
Слева — длинная функция; справа — функция, вызывающая другие функции.

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

Вывод: хук — это не просто функция, а точка входа в вашу программу!

Не бойтесь добавлять новые функции, объявлять файлы классов в своем коде, если вам это нужно. Не пытайтесь втиснуть всю необходимую логику между фигурными скобками функции‑события.

2. Используйте силу ООП

Довольно часто бывает нужно собрать и передать некоторый набор данных, и первая идея — использовать ассоциативный массив в качестве хранилища. Подумайте об использовании объектов (классов) вместо ассоциативных массивов, когда это имеет смысл.

Слева — ассоциативный массив; справа — объект.
Слева — ассоциативный массив; справа — объект.

В чем проблема массивов?

  • Ключи ассоциативных массивов не подсвечиваются IDE, поэтому вы не знаете, какие ключи там ожидать;

  • Вы не можете ограничить тип значения элемента ассоциативного массива;

  • Очевидно, что вам нужно проверить, существует ли ключ, прежде чем обращаться к нему, чтобы избежать ошибки;

Преимущества использования классов:

  • IDE показывает назначение и описание элемента класса;

  • Вы можете определить типы свойств;

  • Вы можете проверять и фильтровать значения в одном месте (сеттеры/геттеры);

Этот подход называется «Data Transfer Object (DTO)».

Другая ошибка: даже используя классы, некоторые программисты пишут код, подобный этому:

Class MyNodeProcessor {

 private function processNode($nid, $external_data) {
   $node_array_storage = getStorage();
   $langcode = getLangcode();
   $user = getCurrentUser();

   $node = $this->changeTitleToExternal($node_array_storage, $nid, $langcode, $external_data->title, $user);
   $node = $this->setExternalPerson($node_array_storage, $nid, $langcode, $external_data->person, $user);
   $node = $this->publishIfNeeded($node_array_storage, $nid, $langcode, $external_data->person, $user);

   return $node;
 }

}

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

Как это улучшить:

Используйте свойства класса для хранения общих данных вместо передачи их в качестве параметров метода. Используйте конструктор для установки свойств класса. Этот подход называется «Инкапсуляция» (Encapsulation).

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

class MyNodeProcessor {

 private $node;
 private $node_array_storage;
 private $langcode;
 private $current_user;

 private function processNode($nid) {
   $this->setNode($nid);
   $this->changeTitleToExternal();
   $this->setExternalPerson();
   $this->publishIfNeeded();
 }

}

3. Не упускайте новые возможности PHP 8

В последнее время язык обновляется очень интенсивно, новые версии выходят постоянно. Следите за обновлениями! Появились очень крутые и полезные инструменты. Вот некоторые из них:

Null coalescing operator

$username = $result['user'] ?? 'nobody';

// То же что и:

$username = isset($result['user']) ? $result['user'] : 'nobody';

Некоторые подходы устаревают, например динамический свойства классов:

class Post
{
 public string $title;
}
// …
$post->name = 'Name';
// Dynamic properties are deprecated in PHP 8.2, and will throw an ErrorException in PHP 9.0

Вы знали, что теперь есть read-only классы?

readonly class Post
{
 public function __construct(
   public string $title,
   public Author $author,
   public string $body,
   public DateTime $publishedAt,
 ) {}
}
$post->title = 'Other';
Error: Cannot modify readonly property Post::$title

Вместо этого кода, где мы проверяем значение на null:

$country = null;

if ($session !== null) {
    $user = $session->user;

    if ($user !== null) {
      $address = $user->getAddress();

    if ($address !== null) {
      $country = $address->country;
    }
  }
}

Можно использовать «оператор безопасного обращения к null» (Null‑safe operator) или Null‑coalescing chaining:

$country = $session?->user?->getAddress()?->country;
Слева - "классическое" объявление переменных класса;Справа - новое!
Слева - "классическое" объявление переменных класса;
Справа - новое!

Я не могу показывать все новые функции по очевидной причине:) вам придется гуглить и читать о новых функциях PHP8 самостоятельно.

И не бойтесь — попробуйте использовать их в своем коде! И конечно избегайте использования устаревших функций. Быть в курсе последних новшеств — это хороший способ повышать свои скилы как разработчика PHP.

4. Отформатируйте свой код

К сожалению не во всех наших проектах есть git‑линтеры, которые не позволяют коммитить неформатированный код. Выберите определенные правила форматирования и придерживайтесь их.

В чем проблема?

  • Неформатированный код трудно читать. Я видел классы, в которых свойства были объявлены между методами. Метод конструктора был где‑то в середине файла вместо того, чтобы быть перым методом класса;

  • В git будет мусор: среди хаотичных изменений в отступах и пробелах трудно найти настоящие функциональные изменения;

  • Неформатированный код является нарушением лучших практик PHP;

Для устранения лишнего “шума” в коде вы можете самостоятельно выполнить проверку форматирования. Вкратце продемонстрирую, как установить и использовать инструмент проверки кода на примере стандарта Drupal на Mac.

Устанавливаем composer глобально

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === 'e21205b207c3ff031906575712edab6f13eb0b361f2085f1f1237b7126d785e826a450292b6cfd1d64d92e6563bbde02') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"
sudo mv composer.phar /usr/local/bin/composer

Устанавливаем через composer Drupal Coder Sniffer, тоже глобально

composer global require drupal/coder
Edit ~/.zshrc and add export PATH="$HOME/.composer/vendor/bin:<other path>"

Вот и все :)

Теперь можно проверить свой код на соответствие стандартам Drupal:

phpcs --extensions=theme,module,php --standard=Drupal,DrupalPractice  web/modules/custom/module_name/*

Результатом будет что-то вроде этого:

Результат проверки PHP кода на соответствие стандарту.
Результат проверки PHP кода на соответствие стандарту.

Прелесть в том, что этот же инструмент может исправить ваш код автоматически:

phpcbf --extensions=theme,module,php --standard=Drupal,DrupalPractice  web/modules/custom/module_name/*

Результатом будет:

Автоматическое исправление форматирования кода.
Автоматическое исправление форматирования кода.

Если вы раньше не использовали линтеры и вдруг начнете, то однозначно одним хорошим PHP программистом станет больше!

Ну и напоследок еще один антипаттерн и непрошеный совет:

Ниже будет частично псевдокод, но каждый PHP бэкендер может его узнать.

if (isset($node_ids)) {
 if (is_array($node_ids)) {
   foreach ($node_ids as $key => $node_id) {
     $node = LoadNode($node_id);
     if ($node instanceof Node) {
       if ($node->hasField('name')) {
         if (!$node->isEmpty('name')) {
           $result[$key] = $node->get('name')->toString();
         }
       }
     }
   }
 }
}
return $result;

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

Как можно улучшить этот код?

В данном случае его можно улучшить, используя «ранний возврат» (early return), то есть, делая проверки на противоположные условия и останавливая выполнение программы, если условие срабатывает. Например вот так:

if (!isset($node_ids)) {
 return $result;
}

if (!is_array($node_ids)) {
 return $result;
}

foreach ($node_ids as $key => $node_id) {
 $node = LoadNode($node_id);
 if (!$node instanceof Node) {
   continue;
 }
  
 if (!$node->hasField('name')) {
   continue;
 }
  
 if ($node->isEmpty('name')) {
   continue;
 }
  
 $result[$key] = $node->get('name')->toString();
}

return $result;

Меньше вложенных уровней делают код более читабельным. Как видите, также можно пропустить текущую итерацию цикла вместо проверки условий перед выполнением чего‑либо внутри цикла.

Заключение

Мои советы были о том как:

  • Не писать длинных функций (даже если это хук);

  • Использовать силу ООП;

  • Не упускать новые возможности PHP 8;

  • Отформатиовать наконец свой код;

  • Не делать много вложенных проверок;

Эти антипаттерны — часть того, с чем я постоянно сталкиваюсь в процессе работы и то, что портит представление о PHP как о языке в целом. В этот список можно еще много чего добавить, например использование побочных эффектов — когда для приведения типа переменной к числу используют умножение на единицу. Или если не умеют пользоваться авто‑загрузчиком классов и пространствами имен.

Да, на PHP можно писать отвратительный код. Но ведь можно писать и красиво! Этот язык прост для начинающих, что обманчиво, и даже многие состоявшиеся программисты «потрогав» PHP думаю что уже его умеют, но чувствуют, что получается какая то фигня, и тогда предполагают, что это язык какой-то не такой. Язык в порядке — совершенствуйте свои навыки, изучайте и применяйте лучшие практики и не используйте плохие.

Всем добра, и надеюсь, джависты не закидают меня тапками  ;-)

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


  1. FanatPHP
    03.01.2025 14:44

    Довольно сумбурная подборка, но в целом ничего.

    И надо бы освоить канонические названия для описанных практик. Приём "Вместо этого кода" в разделе про read-only классы называется Nullsafe оператор. Запись структуры данных в объект называется не ООП, а DTO. "Меньше вложенных уровней" называется "ранний возврат".


    1. dimas846 Автор
      03.01.2025 14:44

      Большое спасибо за совет! Действительно все уже придумано за нас :)
      Добавил названия паттернов в текст. Интересующиеся смогут загуглить и узнать о них больше.


  1. plFlok
    03.01.2025 14:44

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

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


    1. FanatPHP
      03.01.2025 14:44

      У вас устаревшие сведения. Со скоростью у РНР всё хорошо, многопоточность обеспеспечивается менеджером процессов, строгую типизацию уже подвезли.


      1. plFlok
        03.01.2025 14:44

        Я прямо сейчас поддерживаю проект на php8.1

        Строгая типизация - это declare(strict_types=1)? Ну вот ровно оно и любит прифаталить приложение. Ещё достаточно часто код фаталится на стыке моего кода и кода сторонних библиотек.

        Можно сказать, что линтеры или тесты решают эту проблему. А можно взять компилируемые языки и не иметь эту проблему вообще.

        Про синхронность и многопоточность: как красиво и без приседаний отдать ответ юзеру, закрыть хттп соединение, и поделать какую-то работу после отдачи ответа? Например, посчитать аналитику. Без кипы процессов, стороннего хранилища и часто самописной обвязки для постановки задачи и забирания её это не сделать. Ещё и появляется дополнительная точка отказа в виде этого хранилища.

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

        Ну и скорость: у меня сейчас на работе переезд проекта с php на джаву, сокращаем просто только этим действием потребление цпу на 90%. Как уж более честный бенчмарк придумать, как не написание реальных задач на двух языках.

        P.s.: писал бекенды на php 9 лет с проектами на 100к rps. За всё это время ни разу не проникся любовью к языку.


        1. FanatPHP
          03.01.2025 14:44

          Ради бога. Любовь - штука субъективная. Непонятно только, зачем было утруждаться и делиться с нами этими интимными подробностями. "Три дня я гналась за вами, чтобы сказать, как вы мне безразличны" :-D


          1. posledam
            03.01.2025 14:44

            Не фанат PHP, но есть проекты на PHP и хотелось бы решить вот такую серьёзнейшую проблему. Может подскажете? Во всех наших приложениях на .NET, если клиент отвалился, то обработка запроса моментально прекращается. Что бы там не происходило, если был запрос в БД, то он немедленно прерывается, результата не ждём. Если был запрос к другому сервису по HTTP, то он сразу закрывается. Что угодно, везде действует асинхронная кооперативная отмена всей операции, на всех уровнях.

            Но в приложениях на PHP имеем такую проблему, когда клиент отваливается (сам, или прокси рубит), PHP продолжает работать и готовить ответ клиенту, который уже давно ушёл. Это забирает драгоценный рабочий процесс (воркер), и провоцирует лавинообразную деградацию, если запросов в один момент было сделано много, из-за интеграций, которые порой затягиваются, или запросов в БД, которая начала подтупливать. В итоге имеем периодическую полную неработоспособность приложения, восстанавливается когда кубер грохает контейнер из-за не отвечающей пробы php fpm. Конечно, мы масштабируем, но эта мера не устраняет проблему, а только уменьшает и откладывает последствия, которые неизбежно настигают. Слишком увлекаться репликами не можем, решение становится очень дорогостоящим по ТСО. Наши разработчики PHP разводят руками, проблема не решаема.

            Есть какое-то решение? У нас Yii2. Спасибо!


            1. polyanin
              03.01.2025 14:44

              ignore_user_abort


              1. posledam
                03.01.2025 14:44

                curl и запросы в бд тоже прерывает? Не нашёл информации об этом.

                upd. Спасибо!


            1. dimas846 Автор
              03.01.2025 14:44

              А каким образом клиенты подключаются перед тем как отвалиться? Потому, что сли это просто http post/get и т.п. запрос, то он обработается, и будет отдан ответ в любом случае.


        1. dimas846 Автор
          03.01.2025 14:44

          Сам факт компиляции - это не особое преимущество. PHP ведь тоже парсится и компилируется в опкод, только перед исполнением.

          Падения же при strict_types=1 означает, видимо что данные приходят не того типа. Помимо strict_types=1 надо везде проставлять типы переменных и типы, которые возвращают методы. Сама IDE подскажет, если что не так. Не думаю, что другой язык не упал бы если в строку записать например массив.
          Я хотел донести мысль, что плохой PHP код он действительно плохой и точно может есть память и процессы. Верно, и то, что другие языки могу с конкретными задачами справляться лучше, а с другими - хуже.
          Но умение правильно писать на PHP, позволяет писать хороший код и спокойно использовать и сокеты и hight load data с очередями. Горизонтально тоже прекрасно масштабируется.
          Если совершенствоваться и использовать хорошие практики, то и результат будет хорошим


        1. Andreyika
          03.01.2025 14:44

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


        1. Voenniy
          03.01.2025 14:44

          Про синхронность и многопоточность: как красиво и без приседаний отдать ответ юзеру, закрыть хттп соединение, и поделать какую-то работу после отдачи ответа?

          https://www.php.net/manual/ru/function.fastcgi-finish-request.php


        1. Driver86
          03.01.2025 14:44

          Во-первых, есть fastcgi_finish_request()
          Во-вторых, с менеджером задач лучше, не ясно негодование. Например, нагрузка на сервер более кониролируема. Лучше контроль повторных задач. И т.п.


  1. JBFW
    03.01.2025 14:44

    Конкретно про PHP ничего не скажу - отошел от него примерно в то время, когда для обработки каждого HTTP-вызова запускался интерпретатор соотвествующего скрипта (тяжело, медленно, долго).

    Но вот сами принципы - универсальные для любого языка:
    - если функция не помещается в 1 экран - она плохо написана
    - существуют библиотеки (модули или еще как-то назовут) - всё повторяющееся вынести туда
    - давать осмысленные названия функциям и переменным, и желательно понятные, чтобы не спотыкаться на переменных god и mesyats и не пытаться прочитать слово zashishennyy
    - и не делать простые вещи сложным способом. Бывает, залезешь в чей-то код, а там - "кручу-верчу, запутать хочу!"

    И всё будет хорошо...