- 🇺🇸 English (work is in progress, pull requests are welcome)
- Введение
- Ценности
- Принципы
- Общие правила
- Правила разделения бизнес-логики
- Работа с файлами
- Работа с переменными
- Логические переменные и методы
- Работа с массивами
- Работа со строками
- Работа с датами
- Работа с пространствами имён
- Работа с методами
- Возврат результата работы метода
- Работа с классами
- Работа с объектами
- Комментирование кода
- Работа с исключениями
- Работа с внешним хранилищем данных
- Особенности Pull Request (PR)
- Работа с шаблонами
- Работа с литералами
- Работа с условиями
- Работа с тернарными операторами
- Про тесты
- Использование chain-объектов
- Работа со скриптами
- Авторы
Этот документ содержит правила написания кода (Code Conventions) в компании Roistat. У нас накопился большой опыт разработки сложных проектов, которым мы решили поделиться с остальными. Вы можете взять этот документ как есть или использовать его как основу для вашего собственного Code Conv.
Здесь всегда находится актуальная версия нашего Code Conv, так как мы ссылаемся на него при проведении наших Code Review.
О нашем опыте использования Code Conv вы можете прочитать в статье на Хабре.
Code Conv — это правила, которые нужно соблюдать при написании любого кода. Мы различаем Code Style и Code Conv. Для нас Code Style — это внешний вид кода. То есть расстановка отступов, запятых, скобок и прочего. А Code Conv — это смысловое содержание кода. Правильные алгоритмы действий, правильные по смыслу названия переменных и методов, правильная композиция кода. Соблюдение Code Style легко проверяется автоматикой. А вот проверить соблюдение Code Conv в большинстве случаев может только человек.
Обратите внимание: Code Style в примерах может отличаться от Code Style вашего проекта. Придерживаться надо тому Code Style, который принят у вас. Code Conv не об этом.
Главная цель Code Conv — сохранение низкой стоимости разработки и поддержки кода на длинной дистанции.
Основные ценности, помогающие достичь этой цели:
Читаемость
Код должен легко читаться, а не легко записываться. Это значит, что такие вещи как синтаксический сахар (если он направлен на ускорение записи, а не дальнейшего чтения кода) вредны. Обратите внимание, что быстродействие кода не является ценностью, поэтому не самый оптимальный цикл, но удобный для понимания, будет лучше, чем быстрый, но сложный. Преждевременная оптимизация зачастую вредит читаемости, но не даёт никакой пользы. Оптимизацией нужно заниматься тогда, когда это реально стало востребованно. Не нужно без объективной необходимости экономить переменные, буквы для их названий, оперативную память, несколько итераций цикла и так далее.
Вандалоустойчивость
Код надо писать так, чтобы у разработчика, который с ним будет работать, было как можно меньше возможности внести ошибку. Например, покрывайте тестами не только краевые условия, но и кейсы, которые могут появиться в результате доработок кода и рефакторинга.
Поддержание наименьшей энтропии
Энтропия — это количество информации, из которой состоит проект (информационная емкость проекта). Код проекта должен выполнять продуктовые требования с сохранением наименьшей возможной энтропии.
Принципы — это способы соблюдения описанных выше ценностей. Они чуть более детальны, содержат основные методологии разработки и подходы, которыми мы руководствуемся.
Код должен быть:
- Понятным, явным. Явное лучше, чем неявное. Например, не должны использоваться магические методы. Также нельзя использовать
exitи любые другие операторы, которые могут завершить или изменить работу процесса. - Удобным для использования сейчас
- Удобным для использования в будущем
- Должен стремиться к соблюдению принципов KISS, SOLID, DRY, GRASP
- Код должен обладать слабым зацеплением и высокой связностью (подробно это описано в GRASP). Любая часть системы должна иметь изолированную логику и при надобности внешний интерфейс, который позволяет с этой логикой работать. Любая внутренняя часть должна иметь возможность быть измененной без какого-либо ущерба внешним системам
- Код должен быть таким, чтобы его можно было автоматически отрефакторить в IDE (например, Find usages и Rename в PHPStorm). То есть должен быть слинкован типизацией и PHPDoc'ами
- В БД не должны храниться части кода (даже названия классов, переменных и констант), так как это делает невозможным автоматический рефакторинг
- Последовательным. Код должен читаться сверху вниз. Читающий не должен держать что-то в уме, возвращаться назад и интерпретировать код иначе. Например, надо избегать обратных циклов
do {} while (); - Должен иметь минимальную цикломатическую сложность
Если код можно убрать, и работа системы от этого не изменится, его быть не должно.
Плохо:
if (false) {
legacyMethodCall();
}
// ...$legacyCondition = true;
if ($legacyCondition) {
finalizeData($data);
}Хорошо:
// ...finalizeData($data);Это упростит миграцию кода на новую версию языка. Часто в новой версии языка удаляются какие-либо функции или изменяется их работа. Чем меньше идет завязки на язык и его версию, тем лучше.
Специфичные функции всегда лучше использовать через функции-обёртки внутри проекта. Тогда в случае миграции придется исправлять одно место, а не тысячу.
Как понять, можно ли использовать встроенную в PHP функцию или нет?
Если эта функция уже используется повсеместно в проекте, значит, её можете использовать и вы. Например, это может быть
explode/implode. Если эти функции будут изменены в новой версии PHP, то в любом случае придется переделать много кода и делать это будет автоматика.Если эта функция не используется или используется только через обёртку в специализированном сервисе, то и вы использовать её можете только через обёртку (добавляется при необходимости).
Плохо:
if (ctype_digit($number)) {
// ...
}
$distance = levenshtein($text1, $text2);
$urlParts = parse_url($url);Хорошо:
if ($number >= 0) {
// ...
}
$distance = $textService->levenshtein($text1, $text2);
$urlParts = $urlService->parseUrl($url);0 и пустую строку нельзя использовать в качестве показателя отсутствия значения.
functionsendEmail(string$title, ?string$message = null, ?string$date = null): void {
// ...
}
// сообщение не было передано$object->sendEmail('Title', null, '2017-01-01');
// было передано пустое сообщение$object->sendEmail('Title', '', '2017-01-01');Однако, это правило не относится к массивам.
Плохо:
functiondeleteUsersByIds(array$ids = [], bool$someOption = false) {
// ...
}
deleteUsersByIds(null, true);Хорошо:
deleteUsersByIds([], true);Итого: использование пустой строки почти всегда является ошибкой.
Сервис – это класс без состояния, содержащий бизнес-логику. Данные для обработки сервис получает либо в виде параметров публичных методов, либо других сервисов.
Сервис не может использовать в качестве источника данных глобальные переменные или окружение:
Плохо:
class User {
publicfunctionloadUsers() {
$path = getenv('DATA_PATH'); // так нельзя!// ...
}
}Хорошо:
class Env {
publicfunctiongetDataPath(): string {
returngetenv('DATA_PATH');
}
}
class User {
/** * @var Env */private$_env;
publicfunction__construct(Env$env) {
$this->_env = $env;
}
publicfunctionloadUsers() {
$path = $this->_env->getDataPath();
// ...
}
}Однако, это правило не работает, если получение данных из внешних источников — единственная бизнес-логика сервиса.
Для работы с хранилищем мы используем репозиторий. Это частный случай сервиса, но он получает данные из БД через адаптеры.
Контроллер принимает и обрабатывает запросы. Он получает параметры на вход, запрашивает данные из сервисов и возвращает представление.
Модель — простой объект со свойствами, не содержащий никакой другой бизнес-логики, кроме геттеров и сеттеров. Геттер — метод, позволяющий получить какую-то информацию из внутреннего состояния объекта. Это не обязательно поле, как оно есть. Он может брать значения нескольких полей и делать простые манипуляции с ними (не запрашивая внешней продуктовой бизнес-логики). Сеттер — аналогично может изменять внутреннее состояние одного или нескольких полей без запросов «наружу».
Условный упрощенный пример:
class Bill {
public$id;
public$sum;
public$isPaid;
public$paidDate// ...
public functionmarkAsPaid(\DateTime$paymentDate) {
$this->isPaid = true;
$this->paidDate = $paymentDate;
}
}Желательно делать модели неизменяемыми, см. Работа с объектами. Хотите больше гибкости — можно использовать chain-объекты.
Представлением в зависимости от требуемого ответа сервера может быть HTML-шаблон, API-объект или что-то иное. Обратите внимание, API-объект и модель данных это разные сущности, даже если у них совпадает название и все поля. Нельзя просто вернуть в JSON-ответе сервера модель из хранилища:
Плохо:
publicfunctionactionUsers(): Response {
$users = $repository->loadUsers();
returnnewResponse(['data' => $users]);
}Свойства модели хранилища могут поменяться из-за новых технических требований, но объект API это продукт, вы должны изменять его явно:
Хорошо:
// src/Entity/Api/User.phpnamespaceEntity\Api;
class User {
public$id;
public$name;
}
// src/Controller/Api/User.phppublicfunctionactionUsers(): Response {
$users = $repository->loadUsers();
$apiUsers = array_map(function ($user) {
return$this->_convertUserToApiObject($user);
}, $users);
returnnewResponse(['data' => $apiUsers);
}
privatefunction_convertUserToApiObject(Entity\Mapper\User$user): Entity\Api\User {
// ...
}Кроме случаев, когда внутри файла содержится класс. В таком случае файл должен повторять названия класса, то есть должен быть написан в стиле UpperCamelCase. Аналогично обычные директории и пространства имён.
Нельзя писать короткие названия, например $c. Нельзя назвать переменную $day и хранить в ней массив статистических данных за этот день.
Плохо:
$customer = newUser();
$client = newUser();
$object = newUser();Хорошо:
$user = newUser();Если это отфильтрованный по какому-то признаку проект, то признак добавляется к названию. Например, $unpaidProject.
Плохо:
$project = newProject();
$name = $project->name;
$project = $project->name;Хорошо:
$project = newProject();
$projectName = $project->name;Плохо:
$usersStored = [];Хорошо:
$storedUsers = [];Исключение: сгруппированные по некому признаку поля или константы. В этом случае можно использовать префикс.
class ProjectInfo {
constSTATUS_READY = 1;
constSTATUS_BLOCKED = 2;
public$billingIsPaid;
public$billingPaidDate;
public$billingSum;
}Амперсанды могут использоваться только как логические или битовые операторы.
Плохо:
functionremovePrefix(string &$name) {
// ...
}Хорошо:
functionremovePrefix(string$name): string {
// ...return$result;
}📖 Переменные и свойства объекта должны являться существительными и называться так, чтобы они правильно читались при использовании, а не при инициализации.
Плохо:
$object->expire_at$object->setExpireAt($date);
$object->getExpireAt();Хорошо:
$object->expiration_date;
$object->setExpirationDate($date);
$object->getExpirationDate();Нельзя писать $projectsArray, надо писать просто $projects. Это же касается и форматов (JSON, XML и т.п.), и любой другой не относящейся к предметной области информации.
Плохо:
$projectsList = $repository->loadProjects();
$projectsListIds = $utils->extractField('id', $projectsList);Хорошо:
$projects = $repository->loadProjects();
$projectsIds = $utils->extractField('id', $projects);Исключение — если эта переменная объект.
Плохо:
functionparseText(string$text) {
$text = trim($text);
// ...
}Хорошо:
functionparseText(string$text) {
$trimmedText = trim($text);
// ...
}Плохо:
$foo = false; $bar = true;Хорошо:
$foo = false;
$bar = true;Плохо:
functionloadUsers(array$ids) {
$usersIds = $ids;
// ...
}Также можно использовать clone, если без него код серьёзно усложнится, а с ним станет понятным и очевидным. Простой пример — клонирование объектов DateTime. Или использование клонирования для сравнения двух версий объекта: старой и новой.
Плохо:
functionloadAnalyticsData(\DateTime$intervalStart) {
$intervalEnd = new \DateTime($intervalStart->format('Y-m-d H:i:s'));
$intervalEnd->modify('+1 day');
}
functionupdateUser(User$user) {
$oldUser = newUser();
$oldUser->id = $user->id;
$oldUser->name = $user->name;
// ...logObjectDiff($user, $oldUser);
}Хорошо:
functionloadAnalyticsData(\DateTime$intervalStart) {
$intervalEnd = clone$intervalStart;
$intervalEnd->modify('+1 day');
}
functionupdateUser(User$user) {
$oldUser = clone$user;
// ...logObjectDiff($user, $oldUser);
}Плохо:
$foo = $bar = strlen($someVar);Хорошо:
$bar = strlen($someVar);
$foo = $bar;Плохо:
$this->_callSomeFunc($bar = strlen($foo));Хорошо:
$bar = strlen($foo);
$this->_callSomeFunc($bar);Плохо:
if (strlen($foo = json_encode($bar)) > 100) {
// ...
}Хорошо:
$foo = json_encode($bar);
if (strlen($foo) > 100) {
// ...
}Плохо:
publicfunctiongetProjectDir(): string {
$prefix = 'ACME_';
$name = $prefix . 'PROJECT_DIR';
returnconstant($name);
}
publicfunctiongetProjectDir(): string {
returnconstant('ACME_PROJECT_DIR');
}Хорошо:
publicfunctiongetProjectDir(): string {
returnACME_PROJECT_DIR;
}Переменные правильно называть, описывая их содержимое, а методы — задавая вопрос. Если переменная содержит свойство объекта, следуем правилу признак объекта добавляется к названию.
Плохо:
$isUserValid = $user->valid();
$isProjectAnalytics = $accessManager->getProjectAccess($project, 'analytics');Хорошо:
$userIsValid = $user->isValid();
$projectCanAccessAnalytics = $accessManager->canProjectAccess($project, 'analytics');Геттеры именуются аналогично переменным:
class User {
private$_billingIsPaid;
private$_isEnabled;
publicfunctionisEnabled() {
return$this->_isEnabled;
}
publicfunctionbillingIsPaid() {
return$this->_billingIsPaid;
}
}Такое именование позволяет легче читать условия:
// if user is valid, then do somethingif ($userIsValid) {
// do something
}Плохо:
if ($project->isInvalid()) {
// ...
}
if ($project->isNotValid()) {
// ...
}
if ($accessManager->isAccessDenied()) {
// ...
}Хорошо:
if (!$project->isValid()) {
// ...
}
if (!$accessManager->isAccessAllowed()) {
// ...
}
if ($accessManager->canAccess()) {
// ...
}Флаг в качестве параметра это признак того, что функция делает больше одной вещи, нарушая принцип единственной ответственности (Single Responsibility Principle или SRP). Избавляйтесь от них, выделяя код внутри логических блоков в отдельные ветви выполнения.
Свойства объекта при этом могут иметь логический тип. Сам объект, являясь лишь контейнером для данных, не выполняет логики, а значит о нарушении принципа единственной ответственности речь не идет. Это означает, что аргумент конструктора такого объекта тоже имеет право быть логического типа.
Плохо:
functionsomeMethod() {
// ...$projectNotificationIsEnabled = $notificationManager->isProjectNotificationEnabled($project);
storeUser($user, $projectNotificationIsEnabled);
}
functionstoreUser(User$user, bool$isNotificationEnabled) {
// ...if ($isNotificationEnabled) {
notify('new user');
}
}Хорошо:
functionsomeMethod() {
// ...storeUser($user);
if ($notificationManager->isProjectNotificationEnabled($project)) {
notify('new user');
}
}
functionstoreUser(User$user) {
// ...
}
// Использование флага в конструкторе для инициализации свойства логического типаclass SchrodingerCat {
private$_isAlive;
publicfunction__construct(bool$isAlive) {
$this->_isAlive = $isAlive;
}
}Обратите внимание, что array_merge все числовые ключи приводит к int, даже если они записаны строкой.
Плохо:
return$initialData + $loadedData;Хорошо:
namespaceService;
class ArrayUtils {
publicfunctionmergeArrays(array$array1, array$array2): array {
returnarray_merge($array1, $array2);
}
}
publicfunctionsomeMethod() {
return$this->_arrayUtils->mergeArrays($initialData, $loadedData);
}isset проверяет не ключ на его наличие, а значение этого ключа, если он есть. Это разные методы с разным поведением и назначением. Если вы хотите проверить значение ключа, то делайте это явно. Сначала явно проверьте наличие ключа через array_key_exists и обработайте ситуацию его отсутствия, затем приступайте к работе со значением.
Плохо:
functionprocessRequestData(array$requestData) {
$data = [];
if (isset($requestData['project_key'])) {
// ...
}
return$data;
}Хорошо:
functionprocessRequestData(array$requestData) {
$data = [];
if (array_key_exists('project_key', $requestData)) {
// ...
}
return$data;
}Допустимо использовать сокращенный вариант ??, с явным указанием дефолтного значения.
functiongetProjectKey(array$requestData) {
return$requestData['project_key'] ?? null;
}То есть не применяем разные встроенные в PHP инструменты. Приведем несколько очевидных примеров (однако, правило ими не исчерпывается):
Плохо:
$arr = [
'project_key' => 'foo',
'key' => 'bar',
'user_id' => 300,
];
uasort($arr);Плохо:
$arr = [
'project_key' => 'foo',
'key' => 'bar',
'user_id' => 300,
1 => 'value1',
2 => 'value2',
];
$arr[3] = 'value3';📖 Для проверки наличия значения по индексу в обычных (не ассоциативных) массивах используем count($array) > N
Плохо:
if (array_key_exists(1, $users)) {
// ...
}
if (isset($users[1])) {
// ...
}Хорошо:
if (count($users) > 1) {
// ...
}Двойные кавычки используются только, если:
- Внутри строки должны быть одинарные кавычки
- Внутри строки используется подстановка переменных
- Внутри строки используются спец. символы
\n,\r,\tи т.д.
Плохо:
$string = "Some string";
$string = 'Some \'string\'';
$string = "\t".'Some string'."\n";Хорошо:
$string = 'Some string';
$string = "Some 'string'";
$string = "\tSome string\n";📖 Вместо лишней конкатенации используем подстановку переменных в двойных кавычках с помощью фигурных скобок
Плохо:
$string = 'Object with type "' . $object->type() . '" has been removed';Хорошо:
$string = "Object with type \"{$object->type()}\" has been removed";Плохо:
$date = $request->get('date');
$interval = 86400*30;
loadSomeData($date, $interval);Хорошо:
$date = $this->_dateService->instance($request->get('date'));
$interval = new \DateInterval('P30D');
loadSomeData($date, $interval);В проекте для этого должен быть фабричный метод в сервисе для работы с датами.
Плохо:
$date = new \DateTime();Хорошо:
$date = $this->_dateService->instance();- строка с датой и временем должна быть везде в одинаковом формате
- формат не должен включать временную зону, если для этого нет особых требований
- при прочих равных в дате без часового пояса всегда подразумевается UTC0
- если строку по какой-то причине невозможно использовать, используем
int
Плохо:
class User {
public$creation_time;
}
$user->creation_time = time();Хорошо:
class User {
/** * @type string */public$creation_date;
}
$user->creation_date = '2018-01-18 12:54:11';В зависимости от текущей даты месяц и год могут принимать разные временные промежутки (високосный и обычный год, разное количество дней в месяце). Вместо этого в качестве указания интервала используем дни, часы, минуты, секунды.
Плохо:
$dateTime = new \DateTime('-2 month');
$dateInterval = new \DateInterval('P2M');Хорошо:
$dateTime = new$this->_dateTime->instance('-60 days');
$dateInterval = new \DateInterval('P60D');Месяц или год необходимо использовать, если это напрямую указано в требованиях задачи как календарный месяц или календарный год.
📖 Все пространства имён должны быть подключены через use в начале файла. В самом коде не должно быть обратного слеша перед названием пространства имён
Плохо:
$object = new \Some\Object();Хорошо:
useSome;
$object = newSome\Object();Плохо:
useTimeZone;
$date = newTimeZone('Europe\Moscow');Хорошо:
$date = new \TimeZone('Europe\Moscow');Плохо:
useEntity\User;
useEntity\Project;
$user = newUser();
$project = newProject();Хорошо:
useEntity;
$user = newEntity\User();
$project = newEntity\Project();Они запутывают код и его понимание. Если у вас совпадают названия пространств имён, то, скорее всего, вы делаете что-то не так. Допустимо использовать псевдоним, если другое решение будет слишком сложным.
Плохо:
useComponent\User;
useEntity\UserasUserEntity;
$user = newUserEntity();Хорошо:
useComponent\User;
useEntity;
$user = newEntity\User();📖 Должна быть использована максимально возможная типизация для вашей версии PHP. Все параметры и их типы должны быть описаны в объявлении метода либо в PHPDoc. Возвращаемое значение тоже.
Плохо:
/** * @param $id * @param $name * @param $tags * @return mixed */functionstoreUser($id, $name, $tags = []) {
// ...
}Хорошо:
// для PHP 7.1functionmakeCoffee(string$type, int$volume): Coffee {
// ...
}
// в PHP 7.1 тип элементов массива в объявлении метода указать нельзя, поэтому добавляем PHPDoc/** * @param int $id * @param string $name * @param string[] $tags * @return User|null */functionstoreUser(int$id, string$name, array$tags = []): ?User {
// ...
}
// если метод возвращает смешанный тип данных, то необходимо явно это указать/** * @param callable $callback * @return mixed */functionexecute(callable$callback) {
// ...
}
// для PHP 5.6// без строгой типизации возвращаемых типов любой метод// может вернуть null, так что можно его не указывать в PHPDoc/** * @param int $id * @param string $name * @param string[] $tags * @return User */functionstoreUser($id, $name, array$tags = []) {
// ...
}Наибольшую пользу это приносит при работе с массивами:
Плохо:
/** * @param array $users * @param mixed $project * @param int $timestamp * @return mixed */publicfunctionsomeMethod($users, $project, $timestmap) {
foreach ($usersas$user) {
// IDE не сможет определить тип $user
}
// ...
}Хорошо:
/** * @param Users[] $users * @param Project $project * @param int $timestamp * @return Foo */publicfunctionsomeMethod(array$users, Project$project, int$timestmap): Foo {
foreach ($usersas$user) {
// подсказки IDE и рефакторинг работают корректно
}
// ...
}Плохо:
publicfunctionitems() {
// ...
}
publicfunctionconvertedDataObject(array$data) {
// ...
}Хорошо:
publicfunctionloadItems() {
// ...
}
publicfunctionconvertDataToObject(array$data) {
// ...
}Например, вместо getDate() следует писать date(). Геттер — метод, работающий только с полями своего объекта.
Плохо:
class User {
private$_date;
private$_customFields;
publicfunctiongetDate() {
return$this->_date;
}
publicfunctiongetCustomFields() {
returnjson_decode($this->_customFields);
}
}Хорошо:
class User {
private$_date;
private$_customFields;
publicfunctiondate() {
return$this->_date;
}
publicfunctiondecodedCustomFields() {
returnjson_decode($this->_customFields);
}
}📖 Методы названия, которых начинаются c check и validate, должны выбрасывать исключения и не возвращать значения
Плохо:
publicfunctionvalidateRequestData(array$requestData): bool {
if (!array_key_exists('key', $requestData)) {
returnfalse;
}
// ...returntrue;
}Хорошо:
publicfunctionvalidateRequestData(array$requestData): void {
if (!array_key_exists('key', $requestData)) {
thrownewValidationError('Field "key" not found');
}
// ...
}Если метод используется наследниками класса, то он объявляется protected. Если используется сторонними классами, тогда public.
Если код без рекурсии будет очень сложен для написания и понимания и при этом рекурсия гарантированно не выйдет за ограничения стека вызовов.
Для кеширование в памяти используем свойство объекта.
Плохо:
publicfunctionloadData() {
static$_cachedData;
if ($_cachedData === null) {
$_cachedData = [];
}
return$_cachedData;
}Хорошо:
private$_cachedData = [];
publicfunctionloadData() {
if ($this->_cachedData === null) {
$this->_cachedData = [];
}
return$this->_cachedData;
}📖 Параметры в методах должны следовать в следующем порядке: обязательные → часто используемые → редко используемые
Нужно соблюдать читаемость при написании вызова.
Плохо:
publicfunction method($required, $practicallyUnused = 5, $often = [], $lessOften = null)
public function filter($value, $name, $operator) // ...$service->filter(15, "id", "=")Хорошо:
publicfunction method($required, $often = [], $lessOften = null, $practicallyUnused = 5)
public function filter($name, $operator, $value) // ...$service->filter("id", "=", 15)Плохо:
functionf(int$number = null) {}Хорошо:
functionf(?int$number = null) {}
functionf(?int$number) {}Метод не может в разных ситуациях возвращать разные типы данных.
Плохо:
functionloadUser() {
if ($someCondition) {
return ['id' => 1];
}
returnnewUser();
}Хорошо:
functionloadUser(): User {
if ($someCondition) {
$user = newUser();
$user->id = 1;
return$user;
}
returnnewUser();
}📖 Если метод возвращает один объект (или скалярный тип), то в случае, если объект не найден, возвращается null
Если же метод возвращает список объектов, то в случае, когда список пуст, возвращает пустой массив. Нельзя возвращать вместо пустого списка null.
Плохо:
functionloadUsers() {
if ($someCondition) {
returnnull;
}
return [newUser()];
}Хорошо:
/** * @return User[] */functionloadUsers(): array {
if ($someCondition) {
return [];
}
return [newUser()];
}Однако, бывают ситуации, когда надо явно указать, что данные отсутствуют, а не содержат пустой список.
Пример: значения полей объекта задаются пользователем. Возможны две ситуации:
- пользователь не знает, каким категориям принадлежит объект —
null - пользователь знает, что объект не принадлежит ни одной категории — пустой массив (
[])
Тогда для получения категорий объекта будет правильным такой код:
/** * для PHP 5.6 * @return array|null */functiongetObjectCategories($object) {
if ($object->categories === null) {
returnnull;
}
returnparseCategories($object->categories);
}
// для PHP 7.1functiongetObjectCategories($object): ?array {
if ($object->categories === null) {
returnnull;
}
returnparseCategories($object->categories);
}Если у вас большой метод (больше 15 строк), возвращаемая переменная должна называться $result, если с ней могут
происходить изменения в середине работы метода.
В любом месте в методе должно быть понятно, где вы оперируете результатом, а где локальными переменными.
Плохо:
functionloadUsers(): array {
$users = [];
// ... много кода, изменяющего переменную $usersreturn$users;
}Хорошо:
functionloadUsers(): array {
$result = [];
// ... много кода, изменяющего переменную $resultreturn$result;
}Если никакой ошибки не произошло, но отсутствует результат, то это null (или пустой массив), однако если все же произошла исключительная ситуация, которая не заложена системой, то должно кидаться исключение.
Плохо:
functionloadUsers(): array {
if ($connectionError !== null) {
return []; // потеряли ошибку, никто не узнает о проблемах с подключением
}
// ...if (count($data) === 0) {
return [];
}
// ...return$result;
}Хорошо:
functionloadUsers(): array {
if ($connectionError !== null) {
thrownewException\ConnectionError();
}
// ...if (count($data) === 0) {
return [];
}
// ...return$result;
}📖 Метод должен придерживаться следующей структуры: Проверка параметров → Получение данных → Работа → Результат
Во время проверки параметров и получения необходимых данных метод должен возвращать соответствующее пустое значение или кидать исключение. После того как метод получил все необходимые данные и приступил к работе выход из метода крайне нежелателен. Возможны редкие исключения, облегчающие понимание и читаемость кода.
Плохо:
publicfunctionsomeMethod(): int {
$isValid = $this->_someCheck();
if ($isValid) {
$tmp = 0;
$someValue = $this->_getSomeValue();
if ($someValue > 0) {
$tmp = $someValue;
}
$anotherValue = $this->_getAnotherValue();
if ($anotherValue > 0) {
return$tmp + $anotherValue;
} else {
return$someValue;
}
} else {
thrownew \Exception('Invalid condition');
}
}Хорошо:
/** * @throws \Exception */publicfunctionsomeMethod(): int {
$result = 0;
$isValid = $this->_someCheck();
if (!$isValid) {
thrownew \Exception('Invalid condition');
}
$someValue = $this->_getSomeValue();
if ($someValue > 0) {
$result += $someValue;
}
$anotherValue = $this->_getAnotherValue();
if ($anotherValue > 0) {
$result += $anotherValue;
}
return$result;
}Хорошо:
trait AjaxResponseTrait {
// ...
}Хорошо:
interface ApplicationInterface {
// ...
}Хорошо:
abstractclass AbstractApplication {
// ...
}Если свойство используется наследниками класса, то оно объявляется protected. Если используется сторонними классами, тогда public.
Плохо:
abstractclass Loader {
public$data = [];
publicfunctiongetData() {
return$this->data;
}
publicfunctioninit() {
$this->data = $this->load();
}
abstractpublicfunctionload();
}Хорошо:
abstractclass Loader {
/** * @type array */private$_cachedData = [];
publicfunctiongetData(): array {
return$this->_cachedData;
}
publicfunctioninit(): void {
$this->_cachedData = $this->_load();
}
abstractprotectedfunction_load(): array;
}📖 Методы и свойства в классе должны быть отсортированы по уровням видимости и по порядку использования сверху вниз
Уровни видимости: public -> protected -> private.
Плохо:
class SomeClass {
private$_privPropA; public$pubPropA;
protected$_protPropA;
protectedfunction_protA() {
}
publicfunctionpubB() {
}
privatefunction_privA() {
return$this->_protA();
}
publicfunctionpubA() {
$this->_privA();
return$this->pubB();
}
}Хорошо:
class SomeClass {
public$pubPropA;
protected$_protPropA;
private$_privPropA;
publicfunctionpubA() {
$this->_privA();
return$this->pubB();
}
publicfunctionpubB() {
}
protectedfunction_protA() {
}
privatefunction_privA() {
return$this->_protA();
}
}Плохо:
class SomeObject {
/** * @var int */public$id;
}Хорошо:
class SomeObject {
/** * @var int */private$_id;
publicfunction__construct(int$id) {
$this->_id = $id;
}
publicfunctionid(): int {
return$this->_id;
}
}📖 Статические вызовы можно делать только у самого класса. У экземпляра можно обращаться только к его свойствам и методам
Плохо:
$type = $user::TYPE;Хорошо:
$type = User::TYPE;Желание добавить комментарий — признак плохо читаемого кода. Любой участок кода, который вы хотели бы выделить или прокомментировать, надо выносить в отдельный метод.
Фразу, которую вы хотели написать в комментарии, надо привести в простой вид и сделать ее названием метода.
Плохо:
publicfunctiondeleteApprovedUsers() {
// load users filter them by approval$users = $repository->loadUsers();
array_filter($users, function($user) {
return$user->is_approved;
});
foreach ($usersas$user) {
// ...
}
}Хорошо:
publicfunctiondeleteApprovedUsers() {
$users = $this->loadApprovedUsers();
foreach ($usersas$user) {
// ...
}
}
publicfunctionloadApprovedUsers(): array {
$users = $repository->loadUsers();
array_filter($users, function($user) {
return$user->is_approved;
});
}Лучше соблюдать одинаковый формат в рамках проекта
Хорошо:
functionloadUsers(): array {
$result = $repository->loadUsers();
// hack: status field was removed from storage foreach ($resultas$user) {
$user->status = 'active';
}
// hack endreturn$result;
}Хорошо:
/** * https://en.wikipedia.org/wiki/Quicksort */functionquickSort(array$arr): array {
// ...
}
/** * https://habrahabr.ru/post/320140/ */functiongenerateRandomMaze() {
// ...
}Хорошо:
functionloadUsers(): array {
$result = $repository->loadUsers();
// @todo: delete the hack when field will be restored// hack: status field was removed from storageforeach ($resultas$user) {
$user->status = 'active';
}
// hack endreturn$result;
}📖 На каждом уровне бизнес-логики (проект, компонент, библиотека) должно быть абстрактное базовое исключение
Далее либо обработаны, либо на их основании должно бросаться свое исключение. Новое исключение должно содержать предыдущее.
Хорошо:
namespaceService\Facebook;
useException;
useFacebookAds;
publicfunctionrequestData() {
// ...try {
$objects = $facebookAds->requestData($params);
} catch (FacebookAds\Exception\Exception$e) {
thrownewException\ExternalServiceError("Facebook error", 0, $e);
}
//..
}Они предназначены для логирования и отладки. Текст исключения можно показать пользователю, если оно явно для этого предназначено: например, реализует интерфейс HumanReadableInterface.
interface HumanReadableInterface {
publicfunctiongetUserMessage(): string;
}
publicfunctionhandleException(\Throwable$exception): void {
if ($exceptioninstanceof HumanReadableInterface) {
echo$exception->getUserMessage();
return;
}
// ...
}Плохо:
$users = loadUsers();
foreach ($usersas$user) {
$userProjects = loadUserProjects($user);
// ...
}Хорошо:
$users = loadUsers();
$projects = loadProjects();
$indexedProjects = [];
foreach ($projectsas$project) {
if (!array_key_exists($project->user_id, $indexedProjects)) {
$indexedProjects[$project->user_id] = [];
}
$indexedProjects[$project->user_id][] = $project;
}
foreach ($usersas$user) {
if (!array_key_exists($user->id, $indexedProjects)) {
continue;
}
$userProjects = $indexedProjects[$user->id];
}То есть должна быть колонка date/creation_date. Или должен быть зависимый объект (связь 1 к 1), у которого есть такая колонка. Редактируемые записи должны иметь и дату редактирования: update_date или modification_date.
Любая атомарная часть кода должна выделяться в отдельную подзадачу и отдельный PR.
Перенос методов в другие классы и места должны быть выделены в отдельный PR. Последующий рефакторинг после переноса тоже должен быть в отдельном PR.
📖 В случае большого PR — ответственность за долгий просмотр несет сам разработчик, сделавший такой PR
Нормальный объем кода — 1-300 строк в зависимости от его сложности. PR заглушек и архитектуры может содержать много формального кода, который легко быстро проверить. PR же конкретного метода может содержать много сложностей даже в 10 строчках.
Все что можно смержить в master без последствий (даже если это еще не готовый результат, а только заглушки или часть, но они скрыты от юзеров и никому не мешают), должен мержиться в master и PR должен создаваться в master.
Также не должно быть забытых комментариев, бессмысленных переносов строк и прочего "строительного мусора". Каждое изменение, которое вы предлагаете сделать в master-ветке, должно так или иначе относиться к решению поставленной вам задачи.
Все необходимые данные должны быть загружены до рендера и переданы в виде параметров шаблона.
Они должны быть или вынесены в переменную или константу, или сравниваться с переменной, или передаваться на вход методу с понятной сигнатурой. В коде должен присутствовать в явном виде ответ: за что отвечает это число и почему оно именно такое?
Плохо:
$isOnlyDeleted = 1;
if ($object->is_deleted === $isOnlyDeleted) {
// ...
}
for ($i = 0; $i < 5; $i++) {
// ...
}Хорошо:
if ($object->is_deleted === 1) {
// ...
}
$apiMaxRetryLimit = 5;
for ($i = 0; $i < $apiMaxRetryLimit; $i++) {
// ...
} Плохо:
if (count($userProjects)) {
// ...
}
if ($project) {
// ...
}Хорошо:
if ($isResponseError) { // $isResponseError = true// ...
}
if ($response->isError()) { // isError method returns boolean// ...
}
if (count($userProjects) > 0) {
// ...
}📖 В сравнении не boolean переменных используется строгое сравнение с приведением типа (===), автоматическое приведение и нестрогое сравнение не используются
Плохо:
if ($project) {
// ...
}
if ($request->postData('sum') == 100) {
// ...
}
if (!$request->postData('sum')) {
// ...
}
if (!$bill->comment) {
// ...
}Хорошо:
if ($project === null) { // $project is an object// ...
}
if ((int)$request->postData('sum') === 100) {
// ...
}
if ($bill->comment === '') {
// ...
}📖 Автоматическое приведение типов разрешено только, когда один из операндов — литерал с фиксированным типом
При сравнении двух переменных с неизвестными типами для читающего код человека не очевидно, к чему они будут приведены интерпретатором. Если же тип одного из операндов известен, то всё становится очевидно и ручное приведение типов не требуется.
Если вы хотите проверить значение boolean пришедшее извне, то делается это так:
Плохо:
if ((int)$request->get('is_something') > 0) {
// ...
}
if ((int)$request->get('is_something') === 1) {
// ...
}
if ((int)$user->is_registered === 0) {
// ...
}Хорошо:
if ($request->get('is_something') > 0) {
// ...
}
if ($user->is_registered) {
// ...
}
if (!$user->is_registered) {
// ...
}Это нарушает запрет на бесполезный код.
Плохо:
if ($bill->isPaid() == true) {
// ...
}
if ($bill->isPaid() !== false) {
// ...
}
if (!$bill->isPaid() === true) {
// ...
}
if (!(!$bill->isPaid() === true)) {
// ...
}
if ((bool)$phone->is_external === true) {
// ...
}Хорошо:
if ($bill->isPaid()) {
// ...
}Если вам нужна строка, то проверять надо на то, что переменная является строкой. Не надо проверять на то, что она не является числом или чем-то еще. Перечислять все возможные варианты, чем переменная не должна быть, значит повышать риск ошибки и усложнять поддержку кода.
Плохо:
if (!is_numeric($value) && !is_object($value)) {
// ...
}Хорошо:
if (is_string($value) && $value !== '') {
// ...
}📖 Если вы используете встроенную функцию PHP, которая возвращает 0, 1 и, возможно, false, то при возможности результат ее работы используем в условии как bool без дополнительных сравнений
Это не касается случая, когда вам нужно отделить два разных результата между собой, например отдельно отработать 0 и false.
Плохо:
if (preg_match($pattern, $subject) === 1) {
// ...
}
if (!strpos($search, $text)) {
// ...
}Хорошо:
if (preg_match($pattern, $subject)) { // handle success
}
if (!preg_match($pattern, $subject)) {
// handle not success
}
if (preg_match($pattern, $subject) === false) {
// handle error
}
if (strpos($search, $text) === false) {
// handle not success
}📖 При использовании в условном выражении одновременно операторов И и ИЛИ обязательно выделять приоритет скобками
Обратите внимание на различие в значении двух вариантов правильного использования
Плохо:
if ($isMobile || $isSizeTooBig && $isAllowedToShrink) {
// ...
}Хорошо:
if (($isMobile || $isSizeTooBig) && $isAllowedToShrink) {
// ...
}
if ($isMobile || ($isSizeTooBig && $isAllowedToShrink)) {
// ...
}📖 Тернарный оператор следует использовать, если обе ветви условия предназначены для установки одной переменной одним языковым выражением
При наличии логики в ветках условия следует рассмотреть возможность вынести ее в отдельный метод.
Плохо:
if ($isExternal) {
$bill = $this->loadExternalBill();
} else {
$bill = $this->loadInternalBill();
}Хорошо:
$bill = $isExternal ? $this->loadExternalBill() : $this->loadInternalBill();📖 Использовать цепочки из тернарных операторов ?: допустимо только при указании значения по умолчанию
Плохо:
$contact = $this->loadContactByPhone() ?: $this->loadContactByEmail() ?: $this->loadContactByName();Хорошо:
$lead = $this->loadLeadFromCache() ?: $this->loadLeadFromDB();
$contact = $this->loadContactByPhone() ?: $this->loadContactByEmail() ?: $this->loadContactByName() ?: null;Они должны быть написаны с соблюдением соглашений, описанных в этом документе.
📖 В дата провайдерах для тестов надо писать комментарий или ассоциативный массив к структуре отдаваемого массива значений
Плохо:
publicfunctionisEmailAddressData(): array {
return [
['test@test.ru', true ],
// ...
]
}Хорошо:
publicfunctionisEmailAddressData(): array {
return [
// email isValid
['test@test.ru', true],
['@test.ru', false],
['invalidEmail', false],
// ...
]
}
// Или:publicfunctionisEmailAddressData(): array {
return [
'valid' => ['email' => 'test@test.ru', 'isValid' => true],
'invalid with @' => ['email' => '@test.ru', 'isValid' => false],
'invalid' => ['email' => 'invalidEmail', 'isValid' => false],
// ...
]
}Метод с большим количеством необязательных параметров (А) может быть заменен chain-объектом. В объекте конструктор принимает все обязательные параметры, а все необязательные реализуются сеттерами без глагола set (только существительное), возвращающими текущий объект (chaining методов). Метод-глагол у объекта один без параметров, он завершает использование объекта и выполняет действие, которое должен был выполнить метод А.
Был метод:
functionsend($method, $url, $body = null, $headers = null, $retries = 1, $timeout = 300) {}Должен замениться на chain-объект:
publicfunction__construct($method, $url) {
// ...
}
publicfunctionbody($body) {
return$this;
}
// остальные методы с необязательными параметрамиpublicfunctionsend();Новый объект используется так:
new$sender($method, $url)->body($body)->retries(10)->timeout(25)->send();📖 Любой скрипт, который изменяет данные, должен иметь подтверждение перед выполнением действий с данными и debug по результатам работы
Плохо:
// cli/delete_items.php$repository->deleteItems();Исправим, чтобы случайный запуск не удалил элементы:
Хорошо:
// cli/delete_items.php$totalItems = $repository->countItems();
if (!confirm("Do you want to delete {$totalItems} item(s)?")) {
echo'Delete canceled, exit';
exit(1);
}
$repository->deleteItems();
functionconfirm(string$question): bool {
returnreadline("{$question} [y/n]: ") === 'y'
}