Перейти к основному содержимому

5.07. Рекомендации по разработке на PHP

Разработчику Архитектору

Рекомендации по разработке на PHP

1. Общие принципы разработки

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

1.1. Понимание задачи

Любая разработка начинается с задачи. Это может быть запрос от заказчика, идея собственного проекта или поручение внутри команды. Первый и один из самых важных шагов — правильное понимание требований:

  • Уточняйте требования, задавайте вопросы, исключайте двусмысленность
  • Определяйте цели — чего хочет заказчик, какова основная функция
  • Собирайте контекст — узнайте о существующих ограничениях

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

1.2. Стратегия разработки и оценка затрат

После понимания задачи наступает время стратегического планирования. Этот этап определяет дальнейший ход действий и помогает ответить на ключевые вопросы:

  • Сколько времени потребуется на реализацию
  • Какие ресурсы будут задействованы
  • Какой бюджет необходим

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

1.3. Предварительное планирование

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

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

Документация поможет не забыть о внесённых изменениях, какие были названия у сущностей и элементов, и позволит избежать проблем в будущем.

1.4. Процесс написания кода

Это центральный этап разработки. Код должен быть:

  • Читаемым — другие разработчики должны понимать вашу логику
  • Поддерживаемым — добавление новых функций не должно вызывать сложностей
  • Тестируемым — код должен быть разделён на модули, готовые к проверке

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

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

1.5. Сдача разработки

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

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

2. Требования по именованию

2.1. Нотация для элементов языка

Элемент языкаНотацияПример
Класс, интерфейсPascalCaseAppDomain
Перечисление (тип)PascalCaseErrorLevel
Перечисление (значение)PascalCaseFatalError
Константа классаPascalCaseMAX_ITEMS
Приватное свойствоcamelCase_listItem
Защищённое свойствоcamelCasemainPanel
Публичное свойствоcamelCasebackgroundColor
ПеременнаяcamelCaselistOfValues
МетодcamelCasetoString
Пространство имёнPascalCaseApp\Services
ПараметрcamelCasetypeName
СвойствоcamelCasebackgroundColor

2.2. Избегайте коротких имен

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

$boolValue = ($lo == $l0) ? ($I1 == $11) : ($lOl != $101);

Лучше давать осмысленные имена переменным — код выглядит чуть более громоздким, зато читаемость повышается значительно:

$isPublished = ($positionModificationCode == PositionDecision::PURCHASE_CANCELED)
? ($commonIKZ == $checkCommonIKZ)
: ($commonPositionNumber !== null);

2.3. Названия свойств

  • Именуйте свойства с использованием существительных или словосочетаний с существительными
  • Называйте свойство логического типа с использованием утвердительных фраз. Например, canSeek вместо cantSeek
  • В наименованиях свойств логического типа используйте приставки is, has, can, allows или supports

2.4. Названия методов

Называйте методы с использованием связки глагол-существительное. Хороший пример — showDialog.

Хорошее имя должно давать подсказку, что делает этот метод и, если возможно, почему. Не используйте слово And в названии метода.

2.4.1. Асинхронные методы

Добавляйте суффикс Async к названиям асинхронных методов. Общие требования для методов, которые возвращают Promise — добавлять к ним суффикс Async.

2.5. Цепочки методов

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

public function getNames(): array
{
return $this->dataContext
->contacts()
->where(fn($contact) => $contact->name->startsWith('Super'))
->loadWith(fn($contact) => $contact->contactCommunication)
->toArray();
}

2.6. Тернарные выражения

Записывайте тернарное выражение в три строки. Если они вложены, отделяйте табуляцией:

public function getValidationMessage(string $name): string
{
return empty($name)
? 'Empty name'
: strlen($name) < 3
? 'Name too short'
: 'Ok';
}

2.7. Избегайте остроумия

Если имена будут излишне остроумными, их смысл будет понятен только людям, разделяющим чувство юмора автора — и только если они помнят шутку.

3. Требования по оформлению

3.1. Общие правила оформления

  • В качестве отступов используйте 4 пробела
  • Вставляйте один пробел между выражениями и ключевыми словами
  • Не используйте пробелы после ( и перед )
  • Добавляйте пробел перед и после операторов (+, -, ==)
  • Открывайте и закрывайте парные скобки всегда в новой строке
  • Добавляйте пустую строку между многострочными выражениями, членами класса, после закрытия парных скобок

3.2. Стиль Олмана

Используйте стиль Олмана при расставлении фигурных скобок:

  • Открывающая фигурная скобка располагается на новой строке с тем же отступом, что и выражение на предшествующей строке
  • Первое выражение внутри фигурных скобок располагается на новой строке с отступом 4 пробела
  • Последующие выражения внутри фигурных скобок располагаются с тем же отступом
  • Закрывающая фигурная скобка располагается с отступом, равным отступу соответствующей открывающей фигурной скобке

Пример кода, отформатированного в стиле Олмана:

if ($condition) {
$body;
}

4. Требования по проектированию классов

4.1. Принцип единственной ответственности

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

Класс со словом And в названии — это явное нарушение данного правила.

Для взаимодействия между классами используйте паттерны проектирования. Если не удаётся применить ни один из паттернов к классу, возможно, он берёт на себя слишком большую ответственность.

4.2. Создание экземпляров класса

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

4.3. Использование интерфейсов

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

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

4.4. Слабая связанность

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

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

4.5. Избегайте статических классов

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

4.6. Не скрывайте унаследованные элементы

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

4.7. Не ссылайтесь на производные классы из базового

Наличие зависимостей в родительском классе от его дочерних классов нарушает принципы объектно-ориентированного программирования и не даёт возможности другим разработчикам наследоваться от базового класса.

4.8. Принципы SOLID

При проектировании классов придерживайтесь принципов SOLID:

  • S — Принцип единой ответственности (SRP)
  • O — Принцип открытости/закрытости (OCP)
  • L — Принцип подстановки Лисков (LSP)
  • I — Принцип разделения интерфейсов (ISP)
  • D — Принцип инверсии зависимостей (DIP)

4.9. Избегайте двунаправленной зависимости

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

Наиболее очевидное решение — создание интерфейса для одного из этих классов и использование внедрения зависимостей.

5. Требования по проектированию членов класса

5.1. Свойства класса

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

5.2. Методы вместо свойств

Используйте метод вместо свойства если:

  • Производится более дорогостоящая работа, чем настройка значения поля
  • Свойство представляет собой конвертацию. Например, метод toString
  • Свойство возвращает различные значения для каждого вызова, даже если аргументы при этом не изменяются
  • Использование свойства вызывает побочный эффект

5.3. Возвращаемые значения

Методы никогда не должны возвращать null в случае, если метод возвращает тип, производный от iterable. Всегда возвращайте пустую коллекцию или пустую строку вместо нулевой ссылки.

6. Требования по проектированию методов

6.1. Проверка аргументов

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

public class A
{
private string $_nameA;

public function __construct(string $nameA)
{
if (empty($nameA)) {
throw new InvalidArgumentException('Name cannot be empty');
}
$this->_nameA = $nameA;
}
}

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

Не забывайте, что проверка аргументов — это не только проверка на null, но ещё и на недопустимые значения.

6.2. Генерация исключений

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

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

6.3. Сообщения об исключениях

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

6.4. Обработка общих исключений

Не игнорируйте ошибку путём обработки общих исключений с пустым catch. Только обработчик ошибок самого верхнего уровня должен отлавливать общие исключения с целью логирования и корректного завершения работы приложения.

6.5. Логирование исключений

При логировании исключения всегда используйте полную информацию. Для этого используйте метод __toString() у класса исключения или неявное приведение к типу string.

try {
// ...
} catch (Exception $ex) {
// Правильно
$logger->error("Unable to touch the stars: " . $ex->__toString());
// Тоже правильно
$logger->error("Unable to touch the stars: " . $ex);
// Неправильно
// $logger->error("Unable to touch the stars: " . $ex->getMessage());
}

6.6. Проверка делегатов событий

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

6.7. Возвращаемые типы коллекций

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

Если метод должен вернуть коллекцию, выбирайте из следующих вариантов:

  • Неизменяемые типы — массивы, ArrayObject
  • Изменяемые типы — ArrayList

Исключение: если в методе используется конструкция yield. В этом случае допускается использовать iterable в качестве возвращаемого значения, но название метода должно начинаться с enumerate:

public function enumerateSuitableContracts(): iterable
{
$recentContracts = $this->dataContext
->contracts()
->where(fn($contract) => $contract->createdOn >= (new DateTime())->modify('-7 days'));

foreach ($recentContracts as $contract) {
$isSuitable = $this->getIsContractSuitable($contract);
if ($isSuitable) {
yield $contract;
}
}
}

7. Требования по улучшению сопровождаемости кода

7.1. Магические числа

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

class Whatever
{
public const MAX_NUMBER_OF_WHEELS = 18;
public const COLOR_PAPAYA_WHIP = '#FFEFD5';
}

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

Если значение одной константы зависит от значения другой, укажите это в коде:

class SomeSpecialContainer
{
public const MAX_ITEMS = 32;
public const HIGH_WATER_MARK = self::MAX_ITEMS * 3 / 4; // 75%
}

7.2. Присваивание значений переменным

Присваивайте значение каждой переменной в отдельном объявлении. Никогда не делайте так:

$result = $someField = $this->getSomeMethod();

7.3. Сравнение логических значений

Не производите явного сравнения с true или false. Сравнение логического значения с true или false — это плохой стиль программирования.

while ($condition == false) // неправильно
while ($condition != true) // тоже неправильно
while ($condition) // правильно

7.4. Фигурные скобки в управляющих конструкциях

Всегда используйте конструкции if, else, while, for, foreach и case с фигурными скобками.

if ($b1) {
if ($b2) {
foo();
} else {
bar();
}
}

7.5. Блок default в switch

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

function foo(string $answer): void
{
switch ($answer) {
case 'no':
echo 'You answered with No';
break;
case 'yes':
echo 'You answered with Yes';
break;
default:
// Not supposed to end up here.
throw new LogicException('Unexpected answer ' . $answer);
}
}

7.6. Завершение блока if-else-if

Заканчивайте каждый блок if-else-if объявлением else:

function foo(string $answer): void
{
if ($answer == 'no') {
echo 'Вы ответили Нет';
} else if ($answer == 'yes') {
echo 'Вы ответили Да';
} else {
// Что должно случиться, когда этот блок выполнится?
// Если нет, то сгенерировать исключение LogicException.
}
}

7.7. Условное присваивание

Не используйте блок if-else вместо простого (условного) присваивания. Выражайте свои намерения прямо:

// Вместо этого:
$pos = false;
if ($val > 0) {
$pos = true;
}

// Делайте так:
$pos = ($val > 0);

7.8. Инкапсуляция повторяющегося кода

Следуйте принципу Don't Repeat Yourself. Если код повторяется 2 и более раз, выносите его в отдельный метод.

7.9. Настройки приложения

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

7.10. Выражения запросов

Не используйте операторы запросов кроме случаев, когда выражение можно записать только с их помощью. Вместо:

$query = from($items)->where(fn($item) => $item->length > 0);

Лучше воспользоваться методами расширений:

$query = $items->where(fn($i) => $i->length > 0);

7.11. Лямбда-выражения

Используйте лямбда-выражения вместо анонимных функций. Лямбда-выражения служат более красивой альтернативой анонимным функциям.

7.12. Ключевое слово mixed

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

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

8. Требования к методам

8.1. Количество параметров

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

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

8.2. Двойное отрицание

Избегайте двойного отрицания. Двойное отрицание более сложно для понимания, чем простое выражение.

8.3. Параметры byRef

Не используйте передачу параметров по ссылке в параметрах. Они делают код менее понятным и создают предпосылки для ошибок. Вместо этого возвращайте составные объекты в качестве результата выполнения функции.

8.4. Порядок членов класса

Располагайте члены класса в строго определённом порядке:

  • Приватные свойства, константы и приватные методы
  • Публичные константы
  • Публичные статические свойства
  • Фабричные методы
  • Конструкторы
  • События
  • Публичные свойства
  • Прочие методы

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

8.5. Инкапсуляция сложных выражений

Инкапсулируйте сложное выражение в методе или свойстве:

// Вместо этого:
if ($member->hidesBaseClassMember && ($member->nodeType != NodeType::INSTANCE_INITIALIZER)) {
// что-то делаем
}

// Делайте так:
if ($this->nonConstructorMemberUsesNewKeyword($member)) {
// что-то делаем
}

private function nonConstructorMemberUsesNewKeyword(Member $member): bool
{
return $member->hidesBaseClassMember && ($member->nodeType != NodeType::INSTANCE_INITIALIZER);
}

9. Требования по написанию комментариев

9.1. Документирование кода

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

<?php
/**
* Класс Program
* основной класс программы
* выводящий текст "Hello, World!"
*/
class Program
{
/**
* Метод main() является
* входной точкой работы программы
* @param array $args Аргумент метода main()
*/
public static function main(array $args): void
{
echo "Hello, World!\n";
}
}
?>

Такие комментарии отображаются в подсказках при написании кода.

9.2. Комментирование публичных методов и свойств

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

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

9.3. Отслеживание работы

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

9.4. Закомментированный код

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

10. Структура проекта и организация файлов

10.1. Стандартная структура

project/
├── app/
│ ├── Controllers/
│ ├── Models/
│ ├── Services/
│ ├── Repositories/
│ └── Middleware/
├── config/
├── database/
│ ├── migrations/
│ └── seeds/
├── public/
│ └── index.php
├── resources/
│ ├── views/
│ └── assets/
├── routes/
├── tests/
├── vendor/
├── .env
├── .env.example
├── composer.json
└── README.md

10.2. Пространства имён

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

namespace App\Controllers;
namespace App\Models;
namespace App\Services;

10.3. Автозагрузка

Используйте Composer для автозагрузки классов. Настройте composer.json для соответствия структуре проекта:

{
"autoload": {
"psr-4": {
"App\\": "app/"
}
}
}

11. Работа с базами данных

11.1. Использование ORM

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

11.2. Параметризованные запросы

Если используете прямые SQL-запросы, всегда применяйте параметризованные запросы для защиты от SQL-инъекций:

$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);
$user = $stmt->fetch();

11.3. Валидация данных

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

12. Безопасность

12.1. Валидация входных данных

Добавляйте валидацию для всех значений, которые поступают извне — интеграционное получение данных, ввод пользователя.

12.2. Экранирование вывода

Всегда экранируйте вывод данных для защиты от XSS-атак:

echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');

12.3. Защита от CSRF

Используйте токены для защиты от CSRF-атак в формах:

<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">

13. Тестирование

13.1. Модульные тесты

Критически важные компоненты должны быть покрыты модульными и интеграционными тестами. Используйте подходы TDD/BDD по уместности.

13.2. Тестирование исключений

Исключения — для исключительных ситуаций, а не для управления потоком выполнения. Не используйте try-catch вместо if. Не стоит оборачивать каждый блок кода в try-catch — это снижает читаемость, скрывает реальные проблемы и усложняет отладку.

13.3. Избегание предотвратимых исключений

Избегайте исключений, которые можно предотвратить проверкой:

// Вместо этого:
try {
$value = $array[$index];
} catch (OutOfBoundsException $e) {
$value = null;
}

// Делайте так:
$value = $index < count($array) ? $array[$index] : null;