Code Monkey home page Code Monkey logo

php-conventions's Introduction

Цель данных рекомендаций — снижение сложности восприятия, поддержки, тестирования и расширения кода, написанного разными авторами; она достигается путём рассмотрения серии правил и ожиданий относительно написания PHP-кода.

Слова «НЕОБХОДИМО» / «ДОЛЖНО» («MUST»), «НЕДОПУСТИМО» («MUST NOT»), «ТРЕБУЕТСЯ» («REQUIRED»), «НУЖНО» («SHALL»), «НЕ ПОЗВОЛЯЕТСЯ» («SHALL NOT»), «СЛЕДУЕТ» («SHOULD»), «НЕ СЛЕДУЕТ» («SHOULD NOT»), «РЕКОМЕНДУЕТСЯ» («RECOMMENDED»), «МОЖЕТ» / «ВОЗМОЖНО» («MAY») и «НЕОБЯЗАТЕЛЬНО» («OPTIONAL») в этом документе следует понимать так, как это описано в RFC 2119 (и его переводе).

1. Оформление

1.1. Базовый стандарт оформления кода

Код ДОЛЖЕН быть оформлен согласно всем правилам, указанным в стандарте PSR-12.

1.2. Строки

Жесткое ограничение строки ДОЛЖНО составлять 120 символов. В случае превышения этого ограничения автоматические системы проверки стиля ДОЛЖНЫ считать это ошибочной ситуацией, для таких ситуаций НЕОБХОДИМО явно отключать проверку стиля с помощью аннотаций:

  • // @codingStandardsIgnoreStart
  • // @codingStandardsIgnoreStop

Это требование дополняет PSR-12 2.3. Lines.

// @codingStandardsIgnoreStart
use VendorWithVerlyLongName\ProjectrWithVerlyLongName\ServicesWithVerlyLongName\ServiceFolderWithVerlyLongName\ClassWithVerlyLongName;
// @codingStandardsIgnoreStop

1.3. Выравнивание присвоений переменных и элементов массива

Блок присвоений ДОЛЖЕН быть выровнен по самому длинному присвоению в блоке. Если операция присвоения превышает максимальную длину строки НЕОБХОДИМО:

  • или начать новый блок с помощью пустой строки;
  • или выражение должно быть перенесено на новую строку с отступом в 4 пробела, при этом оператор присвоения ДОЛЖЕН остаться на той же строке, что и переменная.
// Правильно
$varName      = 'varName';
$variableName = 'variableName';
// Неправильно
$varName = 'varName';
$variableName = 'variableName';
// Правильно
$varName                            = 'varName';
$secondVariableWithVeryLongNameHere =
    '123456790123456790123456790123456790123456790123456790123456790123456790123456790';
// Неправильно
$varName                            = 'varName';
$secondVariableWithVeryLongNameHere
    = '123456790123456790123456790123456790123456790123456790123456790123456790123456790';
// Правильно
$firstVariableWithVeryLongNameHere = 'varName';

$variableName = '123456790123456790123456790123456790123456790123456790123456790123456790123456790';
// Правильно
[
    'elementName'     => 'elementName',
    'longNameElement' => 'longNameElement',
]
// Неправильно
[
    'elementName' => 'elementName',
    'longNameElement' => 'longNameElement',
]
// Правильно
[
    'elementName'                       => 'elementName',
    'secondElementWithVeryLongNameHere' =>
        '123456790123456790123456790123456790123456790123456790123456790123456790123456790',
]
// Неправильно
[
    'elementName'                       => 'elementName',
    'secondElementWithVeryLongNameHere'
        => '123456790123456790123456790123456790123456790123456790123456790123456790123456790',
]
// Правильно
[
    'firstElementWithVeryLongNameHere' => 'elementName',

    'elementName' => '123456790123456790123456790123456790123456790123456790123456790123456790123456790',
]

1.4. Массивы

При объявлении многострочного массива в конце последнего объявления ДОЛЖНА ставиться запятая, для однострочного массива запятую ставить НЕДОПУСТИМО.

// Правильно
[
    'firstElement'  => 'firstElement',
    'secondElement' => 'secondElement',
]
// Неправильно
[
    'firstElement'  => 'firstElement',
    'secondElement' => 'secondElement'
]
// Правильно
['firstElement' => 'firstElement', 'secondElement' => 'secondElement']
// Неправильно
['firstElement' => 'firstElement', 'secondElement' => 'secondElement',]

1.5. Последовательность вызовов (Chaining)

Каждый элемент вызова для последовательностей, состоящих из трех и более элементов ДОЛЖЕН находиться на новой строке. В случае превышения максимальной длины строки каждый элемент последовательности вызовов ДОЛЖЕН находиться на новой строке.

// Правильно
$this->firstMethod();
// Неправильно
$this
    ->firstMethod();
// Правильно
$this
    ->firstMethod()
    ->secondMethod();
// Неправильно
$this->firstMethod()->secondMethod();
// Неправильно
$this
    ->firstMethod()->secondMethod();
// Правильно
$this
    ->firstMethod()
    ->thirdMethod(
        $firstArgument,
        $secondArgument,
        $thirdArgument,
        $fourthArgument,
        $fifthArgument,
        $sixArgument
    );
// Неправильно
$this->firstMethod()->thirdMethod(
    $firstArgument,
    $secondArgument,
    $thirdArgument,
    $fourthArgument,
    $fifthArgument,
    $sixArgument
);

1.6. Выделение управляющих инструкций

Управляющие инструкции: if, for, foreach, while, do-while, switch, break, continue, return ДОЛЖНЫ отделяться от кода того же уровня вложенности одной пустой строкой.

// Правильно
$count = 5;

if ($count === 5) {
// ...
}

// ...
// Неправильно
$count = 5; // Отсутствует перевод строки
if ($count === 5) {
// ...
}
$length = 12; // Отсутствует перевод строки
// ...
// Правильно
{
    if ($count === 5) {
    // ...
    }
}
// Неправильно
{

    // Лишняя пустая строка
    if ($count === 5) {
    // ...
    }
    // Лишняя пустая строка
}

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

2.1. Базовый стандарт для оформления документации в коде

Код ДОЛЖЕН быть оформлен согласно правилам, указанным в стандарте PSR-19.

2.2. Дублирование типов в docblock

Указание типов аргументов с помощью @param и @return, дублирующее сигнатуру метода НЕДОПУСТИМО, кроме случаев:

  • Наличия комментария к параметру, или результату.
  • Аннотации используются сторонними средствами: psalm, phan, phpstan, и т.д.
// Правильно
public function incrementProductPriceByName(string $productName, float $price): bool
{
// ...
// Неправильно
/**
 * @param string $productName
 * @param float  $price
 * @return bool
 */
public function incrementProductPriceByName(string $productName, float $price): bool
{
// ...
// Правильно
/** 
 * @param array $parsedUrl
 * @phan-param array{scheme:string,host:string,path:string} $parsedUrl
 * @psalm-param array{scheme:string,host:string,path:string} $parsedUrl  
*/
public function showUrl(string $label, array $parsedUrl, string $host): string
{

Указание типов свойств с помощью @var, дублирующее тип свойства НЕДОПУСТИМО, кроме случаев наличия комментария к свойству (php 7.4+).

// Правильно (< php 7.4)
/** @var string */
private $productName;
// Неправильно (< php 7.4)
private $productName;
// Правильно (php 7.4+)
private ?string $productName;
// Правильно (php 7.4+)
/** @var string|null Contains product name */
private ?string $productName;
// Неправильно (php 7.4+)
/** @var string|null */
private ?string $productName;

2.3. Массивы в docblock

Типы элементов массивов РЕКОМЕНДУЕТСЯ уточнять в docblock.

// Правильно
/**
 * @param string[] $productNames
 */
public function incrementProductPricesByNames(array $productNames, float $price): bool
{
// ...
// Неправильно
/**
 * @param array $productNames
 */
public function incrementProductPricesByNames(array $productNames, float $price): bool
{
// ...

2.4. Неопределенные типы аргументов и возвращаемых результатов

В случае, если аргумент (или возвращаемый результат) метода может быть разных типов НЕОБХОДИМО перечислить все допустимые типы в docblock.

// Правильно
/**
 * @param string|int $stringOrIntArgument
 * @return float|string|object
 */
public function mixedMethod($stringOrIntArgument)
{
// ...
// Неправильно
/**
 * @param mixed $stringOrIntArgument
 * @return mixed
 */
public function mixedMethod($stringOrIntArgument)
{
// ...

2.5. Тип переменных

Если ожидаемый тип переменной явно не определен НЕОБХОДИМО определить его с помощью однострочного docblock комментария. Так же необходимо указывать ожидаемый тип, если IDE не может его определить, или определяет не корректно.

$rows = [
    [
        'id'        => 1,
        'createdAt' => new \DateTimeImmutable(),
    ],
    [
        'id'        => 2,
        'createdAt' => new \DateTimeImmutable(),
    ],
    // ...
];

foreach ($rows as $row) {
    /** @var int $id */
    $id = $row['id'];
    /** @var \DateTimeImmutable $createdAt */
    $createdAt = $row['createdAt'];
    // ...
}

2.6. Свойства

Свойства класса ДОЛЖНЫ содержать либо декларацию типа, либо docblock для всех возможных типов значений, которое они могут содержать. ДОПУСТИМО указывать декларацию типа и docblock только в случаях, когда dockblock уточняет декларацию, либо при наличии комментария. Если docblock МОЖЕТ быть описан одной строкой — ДОЛЖЕН использоваться однострочный docblock.

/** @var string[]|null */
private $names;

/** @var int|null */
private $count;
/** @var string[]|null */
private ?array $names;

private ?int $count;

2.7. Методы и функции

Docblock для методов и функций ДОЛЖНЫ быть многострочными.

// Неправильно
/** @param string[] $userNames */
public function saveUserNames(array $userNames): void
// Правильно
/**
 * @param string[] $userNames
 */
public function saveUserNames(array $userNames): void

3. Объявление констант, свойств и методов

3.1. Последовательность объявлений констант, свойств и методов

В классе ДОЛЖНА соблюдаться последовательность объявлений элементов согласно следующему списку:

  1. Публичные константы.
  2. Защищенные константы.
  3. Приватные константы.
  4. Публичные свойства.
  5. Защищенные свойства.
  6. Приватные свойства.
  7. __construct.
  8. __destruct.
  9. __clone.
  10. __invoke.
  11. __toString.
  12. Публичные методы.
  13. Защищенные методы.
  14. Приватные методы.

3.2. Именование свойств

Названия свойств ДОЛЖНЫ описывать предназначение данных, которые они хранят.

// Правильно
/** @var string[] */
private $userNames;
// Неправильно
/** @var string[] */
private $data;

3.3. Разделение свойств

Каждое свойство ДОЛЖНО отделяться от других свойств, констант и методов одной пустой строкой. Если свойство объявляется, как первый элемент класса — пустая строка перед ним НЕДОПУСТИМА. Если свойство объявляется, как последний элемент класса — пустая строка после него НЕДОПУСТИМА.

// Правильно
/** @var string[] */
private $userNames;

/** @var int[] */
private $userIds;
// Неправильно
/** @var string[] */
private $userNames;
/** @var int[] */
private $userIds;
// Правильно
{
    /** @var string[] */
    private $userNames;
// Неправильно
{

    /** @var string[] */
    private $userNames;
// Правильно
    /** @var string[] */
    private $userNames;
}
// Неправильно
    /** @var string[] */
    private $userNames;

}

3.4. Модификаторы доступа для свойств

Для модификаторов доступа к свойствам ДОЛЖНЫ выполняться следующие правила:

  • private - СЛЕДУЕТ использовать по умолчанию;
  • protected - СЛЕДУЕТ использоваться только для случаев доступа из дочерних классов;
  • public - использование НЕДОПУСТИМО;
  • static - использование НЕДОПУСТИМО.

3.5 Типы свойств

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

3.6. Именование методов

Названия методов ДОЛЖНЫ описывать предназначение их использования внешним кодом, а не детали реализации.

// Правильно
public function findUserById(int $id): ?User
// Неправильно
public function find(int $id): ?User

3.7. Разделение методов

Каждый метод ДОЛЖЕН отделяться от других методов, свойств и констант одной пустой строкой. Если метод объявляется, как первый элемент класса — пустая строка перед ним НЕДОПУСТИМА. Если метод объявляется, как последний элемент класса — пустая строка после него НЕДОПУСТИМА.

// Правильно
public function findUserById(int $id): ?User
// ...
}

public function findUserByName(string $name): ?User
// ...
}
// Неправильно
public function findUserById(int $id): ?User
// ...
}
public function findUserByName(string $name): ?User
// ...
}
// Правильно
{
    public function findUserById(int $id): ?User
// Неправильно
{

    public function findUserById(int $id): ?User
// Правильно
    public function findUserById(int $id): ?User
}
// Неправильно
    public function findUserById(int $id): ?User

}

3.8. Модификаторы доступа для методов

Для модификаторов доступа к свойствам ДОЛЖНЫ выполняться следующие правила:

  • private - СЛЕДУЕТ использовать для методов, предназначенных для использования внутри класса;
  • protected - СЛЕДУЕТ использоваться только для случаев доступа из дочерних классов;
  • public - СЛЕДУЕТ использовать для методов, которые предназначены для использования из вне;
  • static - использование НЕДОПУСТИМО.

3.9. Порядок аргументов в методе

Аргументы метода ДОЛЖНЫ объявляться в следующей последовательности:

  1. Типизированные аргументы.
  2. Nullable-аргументы.
  3. Опциональные аргументы.
  4. Аргумент с .... (php 7.4+)
public function firstExample(string $first, ?int $second, bool $third = false, float ...$fourth): string
// ...
public function secondExample(?int $second, bool $third = false, float ...$fourth): string
// ...
public function thirdExample(bool $third = false, float ...$fourth): string

3.10. Массив в виде аргумента

Для методов, содержащих один и более аргументов с типом массив РЕКОМЕНДУЕТСЯ указывать хотя бы один такой аргумент с помощью оператора ....

// Правильно
public function concatStrings(string ...$parts): string
// Неправильно
/**
 * @param string $parts
 */
public function concatStrings(array $parts): string

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

4.1. Неявные приведения типов

Неявное приведение типов НЕДОПУСТИМО.

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

echo 5 + '5abc5';
// 10

4.2. Сравнения с преобразованием типов

Сравнения с преобразованием типов == и != НЕДОПУСТИМЫ. Вместо этого НЕОБХОДИМО использовать тождественные сравнения: === и !==.

Проблемы тут те же, что и при неявном приведении типов.

if ('abc' == 0) {
    echo 'wat';
}

// wat

4.3. Инструкция switch

Использовать инструкцию switch НЕОБХОДИМО с гарантией корректности типов каждого проверяемого выражения.

Инструкция switch при выполнении проверок case использует сравнения с приведением типов. Это может привести к тем же проблемам, что и неявное приведение типов.

switch ('abc') {
    case 0:
        echo 'wat';

        break;
}

// wat

4.4. Присвоения в условных операциях

Присвоение в условиях инструкций if, while, do-while НЕДОПУСТИМО.

В большом количестве учебной литературы используются конструкции вида while ($row = ...) или if ($row = ...). Выражения в скобках неявно приводятся к bool, что может привести к неожиданным последствиям.

$rows = [0, null, ''];

while ($row = next($rows)) {
    printf("\$row = %s\n", var_dump($row, true));
}

// Ничего не выведет

4.5. Ошибки

Создание ошибок с помощью trigger_error НЕДОПУСТИМО, вместо этого ДОЛЖНЫ использоваться исключения.

Ошибки могут быть перехвачены только глобально, с помощью set_error_handler. Это значит, что контекст выполнения будет потерян. Так же ошибки не содержат stack trace, в отличие от исключений.

4.6. Оператор управления ошибками @

Оператор @ ДОЛЖЕН быть использован для выражений, которые могут бросить ошибку, для остальных ситуаций его использование НЕДОПУСТИМО. В случае подавления ошибки ДОЛЖНО быть брошено исключение с описанием причин возникновения ошибки.

// Без @ будет ошибка:
// Warning: fopen(path/to/not/exists/file): failed to open stream: No such file or directory
$file = @fopen('path/to/not/exists/file', 'r');

if ($file === false) {
    throw new \RuntimeException('Could not open file: "path/to/not/exists/file"');
}

4.7. goto

Использование инструкции goto НЕДОПУСТИМО.

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

4.8. eval

Использование инструкции eval НЕДОПУСТИМО.

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

4.9. Глобальные переменные и global

Использование инструкции global НЕДОПУСТИМО.

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

4.10. Статические свойства

Использование статических свойств НЕДОПУСТИМО.

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

4.11. Суперглобальные переменные

Использование суперглобальных переменных ДОЛЖНО быть сведено к минимуму. Данные из суперглобальных переменных РЕКОМЕНДУЕТСЯ получать на этапе инициализации.

4.12. Динамическая подстановка имен

Динамическая подстановка имен переменных, свойств, функций и методов НЕДОПУСТИМА.

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

// Неправильно
$this->{$methodName}($argument);

4.13. Магические методы

Использование следующих магических методов НЕДОПУСТИМО:

  • __call
  • __callStatic
  • __get
  • __set

Данные методы усложняют чтение и понимание кода, как следствие его поддержку.

4.14. Валидация аргументов

Каждый аргумент публичного метода, защищенного метода, или функции ДОЛЖЕН быть проверен на корректность типа и граничные значения. Каждый аргумент приватного метода ДОЛЖЕН быть проверен на корректность типа, проверять граничные значения РЕКОМЕНДУЕТСЯ. Если аргумент не валиден — штатное выполнение метода (функции) невозможно, по этой причине ДОЛЖНО быть брошено исключение.

4.15. \DateTime

Вместо \DateTime НЕОБХОДИМО использовать \DateTimeImmutable.

Так как в PHP объекты передаются по ссылке изменение объекта \DateTime в одной части кода влечет за собой изменение по всему рантайму, что может привести к непредсказуемым последствиям. Что бы исключить целый класс ошибок, связанных с не явным изменением даты/времени из внешнего кода НЕОБХОДИМО использовать \DateTimeImmutable.

$externalServiceGenerateExpiredAt = function (\DateTime $createdAt): \DateTime {
    return $createdAt->modify('3 days');
};

$createdAt = new \DateTime('2019-01-01');
$expiredAt = $externalServiceGenerateExpiredAt($createdAt);

printf("createdAt: %s, expiredAt: %s", $createdAt->format('Y-m-d'), $expiredAt->format('Y-m-d'));

// createdAt: 2019-01-04, expiredAt: 2019-01-04

4.16. Обработка часовых поясов

Для задач хранения и обработки времени НЕОБХОДИМО использовать часовой пояс UTC. Для задач связанных с выводом МОЖНО использовать произвольный часовой пояс.

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

4.17. SQL

Для подстановки параметров в SQL запросы НЕОБХОДИМО использовать псевдопеременные.

Подстановка параметров с помощью конкатенации ведет к целому классу проблем безопасности: sql-инъекции.

5. Принципы программирования

5.1. Здравый Смысл

Принцип Здравого Смысла разрешает отмену любого правила данных рекомендаций, в случае, когда правило приводит к чрезмерному усложнению поддержки кода. Этот принцип МОЖНО использовать, но очень осторожно.

5.2. YAGNI

РЕКОМЕНДУЕТСЯ следовать принципу YAGNI.

5.3. SOLID

Код ДОЛЖЕН следовать принципам SOLID.

5.4. DRY

Принципу DRY СЛЕДУЕТ придерживаться только в случае, когда он не противоречит SOLID и Здравому смыслу.

Примеры, когда не стоит следовать принципу DRY:

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

  • В тестах DRY может привести к ложно позитивным и ложно негативным ошибкам.

5.5. KISS

Принципу KISS СЛЕДУЕТ придерживаться только в случае, когда он не противоречит другим правилам данных рекомендаций.

Примеры, когда не стоит следовать принципу KISS:

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

  • Методы должны быть "не большого размера". Здесь проблемы те же, что и у классов. Разделяя метод на множество маленьких вы расширяете интерфейс класса, что в будущем может привести к излишней связанности, как с данным классом, так и с его наследниками.

5.6. TMTOWTDI

Принцип TMTOWTDI НЕ РЕКОМЕНДУЕТСЯ использовать.

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

5.7. GRASP

РЕКОМЕНДУЕТСЯ следовать принципам GRASP.

6. Антишаблоны проектирования

6.1. ActiveRecord

ActiveRecord РЕКОМЕНДУЕТСЯ считать антишаблоном и не использовать его. Вместо этого РЕКОМЕНДУЕТСЯ использовать шаблон Repository.

ActiveRecord объединяет сущности, представляющие домен вместе с инфраструктурой, в виде логики работы с базой данных. Такое поведение нарушает принципы SRP и ISP из SOLID, и приводит к следующим последствиям.

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

6.2. Singleton

Singleton РЕКОМЕНДУЕТСЯ считать антишаблоном и не использовать его. Вместо этого РЕКОМЕНДУЕТСЯ использовать Dependency Injection.

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

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

7.1. Покрытие кода

Каждый метод (функция) ДОЛЖНЫ быть покрыты тестами для всех возможных вариантов выполнения метода (функции).

// Для данного метода ДОЛЖНО быть 3 теста.
// 1. Число $number кратно $divider, что бы проверить корректность преобразование типа.
// Например `divide(4, 2);`.
// 2. Число $number не кратно $divider. Например `divide(1, 2);`.
// 3. Число $divider равно 0. Например `divide(3, 0);`.
public function divide(int $number, int $divider): float.
{
    if ($divider === 0) {
        throw new \InvalidArgumentException('Argument "$divider" must be not zero');
    }

    return (float) $number / $divider;
}

// Для данного метода ДОЛЖНО быть 4 теста.
// 1. Название команды соответствует `self::FIRST_COMMAND`.
// 2. Название команды соответствует `self::SECOND_COMMAND`.
// 3. Название команды - пустая строка.
// 4. Команда не найдена.
public function execute(string $commandName): void
{
    if (empty($commandName)) {
        throw new \InvalidArgumentException('Argument "$commandName" must be not empty');
    }

    switch ($commandName) {
        case self::FIRST_COMMAND:
            $this->firstCommand();

            break;
        case self::SECOND_COMMAND:
            $this->secondCommand();

            break;
        default:
            throw new \DomainException(sprintf('Unknown command: "%s"', $commandName));
    }
}

7.2. Стратегия тестирования

Код ТРЕБУЕТСЯ покрывать, согласно "белому ящику". В случае чрезмерной сложности использования "белого ящика" СЛЕДУЕТ использовать стратегию "черного ящика".

7.3. Разделение тест методов

Каждый тест метод ДОЛЖЕН иметь полностью независимое состояние, относительно других тест методов. Каждый тест метод ДОЛЖЕН проверять конкретное поведение тестируемого метода (функции), тест методы, которые проверяют несколько аспектов поведения НЕДОПУСТИМЫ.

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

7.4. Тестируемый объект

Тестируемый объект ТРЕБУЕТСЯ создавать с помощью оператора new. В случае невозможности, или чрезмерной сложности теста ДОПУСКАЕТСЯ использовать mock от тестируемого класса. Название для объекта СЛЕДУЕТ использовать то же, что и название тестируемого класса, в lowerCamelCase.

7.5. Проверка результатов

Проверку результатов НЕОБХОДИМО выполнять на тождество, т.е. и на тип и на значение. Числа с плавающей точкой ДОЛЖНЫ проверяться с учетом погрешности. Значения времени, зависящие от текущего времени ДОЛЖНЫ проверяться с учетом погрешности.

8. PHPUnit

<?php
// Service/UserRegistrator.php 

declare(strict_types = 1);

namespace Vendor\Project\Service;

use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\User\UserInterface;

class UserRegistrator
{
    /** @var PasswordEncoderInterface */
    private $passwordEncoder;

    /** @var LoggerInterface */
    private $logger;

    /** @var string */
    private $salt;

    public function __construct(PasswordEncoderInterface $passwordEncoder, LoggerInterface $logger, string $salt)
    {
        if (empty($salt)) {
            throw new \InvalidArgumentException('Variable "$salt" must be not empty');
        }

        $this->passwordEncoder = $passwordEncoder;
        $this->logger          = $logger;
        $this->salt            = $salt;
    }

    public function register(EntityManagerInterface $entityManager, string $userName, string $password): UserInterface
    {
        if (preg_match('/^[a-z][a-z0-9_]{5,254}$/i', $userName)) {
            throw new \InvalidArgumentException(
                sprintf('Variable "$userName" is invalid, actual value: "%s"', $userName)
            );
        } elseif (empty($password)) {
            throw new \InvalidArgumentException('Variable "$password" must be not empty');
        }

        $this->logger->info(sprintf('Register user: "%s"', $userName));

        try {
            $encodedPassword = $this->passwordEncoder->encodePassword($password, $this->salt);
            $user            = new User($userName, $encodedPassword);

            $entityManager->persist($user);
            $entityManager->flush();

            return $user;
        } catch (\Throwable $exception) {
            $this->logger->error($exception->getMessage(), ['exception' => $exception]);

            throw $exception;
        }
    }
}
<?php
// Tests/Service/UserRegistratorTest.php 

declare(strict_types = 1);

namespace Vendor\Project\Tests\Service;

use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Vendor\Project\Service\UserRegistrator;

class UserRegistratorTest extends TestCase
{
    public function testConstructorWithEmptySalt(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('Variable "$salt" must be not empty');

        $salt = '';

        /** @var PasswordEncoderInterface|MockObject $passwordEncoder */
        $passwordEncoder = $this->createMock(PasswordEncoderInterface::class);
        /** @var LoggerInterface|MockObject $logger */
        $logger = $this->createMock(LoggerInterface::class);

        new UserRegistrator($passwordEncoder, $logger, $salt);
    }

    public function testRegister(): void
    {
        $salt            = 'salt';
        $userName        = 'userName';
        $password        = 'password';
        $encodedPassword = 'encodedPassword';

        /** @var PasswordEncoderInterface|MockObject $passwordEncoder */
        $passwordEncoder = $this->createMock(PasswordEncoderInterface::class);
        /** @var LoggerInterface|MockObject $logger */
        $logger = $this->createMock(LoggerInterface::class);

        /** @var EntityManagerInterface|MockObject $entityManager */
        $entityManager = $this->createMock(EntityManagerInterface::class);

        $userRegistrator = new UserRegistrator($passwordEncoder, $logger, $salt);

        $entityManagerIncrement = 0;

        $logger
            ->expects($this->once())
            ->method('info')
            ->with(sprintf('Register user: "%s"', $userName));

        $passwordEncoder
            ->expects($this->once())
            ->method('encodePassword')
            ->with($password, $salt)
            ->willReturn($encodedPassword);

        $entityManager
            ->expects($this->at($entityManagerIncrement++))
            ->method('persist')
            ->with(
                $this->callback(
                    function (UserInterface $user) use ($encodedPassword, $userName): bool {
                        $this->assertSame($userName, $user->getUsername());
                        $this->assertSame($encodedPassword, $user->getPassword());

                        return true;
                    }
                )
            );

        /** @noinspection PhpUnusedLocalVariableInspection */
        $entityManager
            ->expects($this->at($entityManagerIncrement++))
            ->method('flush');

        $logger
            ->expects($this->never())
            ->method('error');

        $user = $userRegistrator->register($entityManager, $userName, $password);

        $this->assertSame($userName, $user->getUsername());
        $this->assertSame($encodedPassword, $user->getPassword());
    }

    public function testRegisterWithInvalidUserName(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('Variable "$userName" is invalid, actual value: "*"');

        $salt     = 'salt';
        $userName = '*';
        $password = 'password';

        /** @var PasswordEncoderInterface|MockObject $passwordEncoder */
        $passwordEncoder = $this->createMock(PasswordEncoderInterface::class);
        /** @var LoggerInterface|MockObject $logger */
        $logger = $this->createMock(LoggerInterface::class);

        /** @var EntityManagerInterface|MockObject $entityManager */
        $entityManager = $this->createMock(EntityManagerInterface::class);

        $userRegistrator = new UserRegistrator($passwordEncoder, $logger, $salt);

        $logger
            ->expects($this->never())
            ->method('info');

        $passwordEncoder
            ->expects($this->never())
            ->method('encodePassword');

        $entityManager
            ->expects($this->never())
            ->method('persist');

        $entityManager
            ->expects($this->never())
            ->method('flush');

        $logger
            ->expects($this->never())
            ->method('error');

        $userRegistrator->register($entityManager, $userName, $password);
    }

    public function testRegisterWithInvalidPassword(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('Variable "$password" must be not empty');

        $salt     = 'salt';
        $userName = 'userName';
        $password = '';

        /** @var PasswordEncoderInterface|MockObject $passwordEncoder */
        $passwordEncoder = $this->createMock(PasswordEncoderInterface::class);
        /** @var LoggerInterface|MockObject $logger */
        $logger = $this->createMock(LoggerInterface::class);

        /** @var EntityManagerInterface|MockObject $entityManager */
        $entityManager = $this->createMock(EntityManagerInterface::class);

        $userRegistrator = new UserRegistrator($passwordEncoder, $logger, $salt);

        $logger
            ->expects($this->never())
            ->method('info');

        $passwordEncoder
            ->expects($this->never())
            ->method('encodePassword');

        $entityManager
            ->expects($this->never())
            ->method('persist');

        $entityManager
            ->expects($this->never())
            ->method('flush');

        $logger
            ->expects($this->never())
            ->method('error');

        $userRegistrator->register($entityManager, $userName, $password);
    }

    public function testRegisterWithUnexpectedDbException(): void
    {
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage('UnexpectedException');

        $salt            = 'salt';
        $userName        = 'userName';
        $password        = 'password';
        $encodedPassword = 'encodedPassword';
        $exception       = new \Exception('UnexpectedException');
        $logContext      = ['exception' => $exception];

        /** @var PasswordEncoderInterface|MockObject $passwordEncoder */
        $passwordEncoder = $this->createMock(PasswordEncoderInterface::class);
        /** @var LoggerInterface|MockObject $logger */
        $logger = $this->createMock(LoggerInterface::class);

        /** @var EntityManagerInterface|MockObject $entityManager */
        $entityManager = $this->createMock(EntityManagerInterface::class);

        $userRegistrator = new UserRegistrator($passwordEncoder, $logger, $salt);

        $entityManagerIncrement = 0;

        $logger
            ->expects($this->once())
            ->method('info')
            ->with(sprintf('Register user: "%s"', $userName));

        $passwordEncoder
            ->expects($this->once())
            ->method('encodePassword')
            ->with($password, $salt)
            ->willReturn($encodedPassword);

        $entityManager
            ->expects($this->at($entityManagerIncrement++))
            ->method('persist')
            ->with(
                $this->callback(
                    function (UserInterface $user) use ($encodedPassword, $userName): bool {
                        $this->assertSame($userName, $user->getUsername());
                        $this->assertSame($encodedPassword, $user->getPassword());

                        return true;
                    }
                )
            );

        /** @noinspection PhpUnusedLocalVariableInspection */
        $entityManager
            ->expects($this->at($entityManagerIncrement++))
            ->method('flush')
            ->willThrowException($exception);

        $logger
            ->expects($this->once())
            ->method('error')
            ->with($exception->getMessage(), $logContext);

        $userRegistrator->register($entityManager, $userName, $password);
    }
}

8.1. Именование тестовых классов

Тестовый класс ДОЛЖЕН состоять из префикса Test и названия тестируемого класса.

8.1. Именование тест методов

Тестовый метод ДОЛЖЕН начинаться с префикса test, далее указывается название тестируемого метода в UpperCamelCase. Тест без дополнительных аспектов в названии ДОЛЖЕН считаться позитивным. Для описания дополнительных аспектов в названии тестового метода СЛЕДУЕТ использовать префиксы With и Without. Множество дополнительных аспектов разделяется с помощью строки And.

public function testLogMessage(): void
// ...
public function testLogMessageWithEmptyMessage(): void
// ...
public function testLogMessageWithEmptyMessageAndEmtyContext(): void
// ...
public function testLogMessageWithInvalidContext(): void

8.2. Структура теста

Каждый тест ТРЕБУЕТСЯ оформлять согласно структуре, описанной ниже (каждый блок отделяется от остальных пустой строкой).

  1. Ожидаемый тип исключения.
  2. Ожидаемое сообщение исключения.
  3. Переменные, используемые в тесте.
  4. Mock-объекты аргументов конструктора тестируемого класса.
  5. Mock-объекты аргументов тестируемого метода.
  6. Создание тестируемого объекта.
  7. Инкременты вызовов mock-объектов.
  8. Поведение методов mock-объектов согласно порядку их вызова.
  9. Вызов тестируемого метода.
  10. Проверка результатов.

ДОПУСКАЕТСЯ отклонение от данной структуры, в случае, если полное следование ей невозможно. Например, когда значение переменной [3] определяется только после вызова конструктора тестируемого класса [6].

8.3. Переменные, используемые в тесте

Переменные, используемые в тесте ДОЛЖНЫ следовать следующим правилам.

  1. НЕДОПУСТИМО использовать свойства тест классов.
  2. Значения, которые будут использоваться отдельно ДОЛЖНЫ быть вынесены в отдельные переменные.
  3. Значения для переменных ТРЕБУЕТСЯ указывать явно.
  4. Значения для переменных НЕ ДОЛЖНЫ зависеть от времени, кроме ситуаций, когда тестируемый метод зависит от текущего времени.

8.4. Mock-объекты

Mock-объект ДОЛЖЕН быть объявлен, согласно следующим правилам.

  1. Переменную, содержащая mock-объект СЛЕДУЕТ называть согласно названию аргумента метода (функции), где будет использоваться данный объект.
  2. Mock-объект ДОЛЖЕН быть помечен строчным docblock, содержащими класс объекта, для которого создается mock и mock-интерфейс.
  3. Mock-объект ДОЛЖЕН присваиваться переменной тестового метода.
/** @var PasswordEncoderInterface|MockObject $passwordEncoder */
$passwordEncoder = $this->createMock(PasswordEncoderInterface::class);
/** @var LoggerInterface|MockObject $logger */
$logger = $this->createMock(LoggerInterface::class);

8.5. Инкременты вызовов mock-объектов

Инкременты вызовов mock-объектов - это переменные типа int, со значением 0. Названия для этих переменных ДОЛЖНЫ повторять названия mock-объектов, которым они соответствуют с суффиксом Increment. Инкременты ДОЛЖНЫ использоваться, когда необходимо описать последовательность вызовов методов mock-объекта.

8.6. Поведение методов mock-объектов

Поведение методов mock-объектов ДОЛЖНО описываться согласно одной из следующих структур.

  1. Метод methodName mock-объекта $mockObject НЕ ДОЛЖЕН быть вызван.
$mockObject
    ->expects($this->never())
    ->method('methodName');
  1. Метод methodName mock-объекта $mockObject ДОЛЖЕН быть вызван с аргументами $methodArguments и вернуть результат.
$mockObject
    ->expects($this->once())    // Или `->expects($this->at($mockObjectIncrement++))`
    ->method('methodName')
    ->with(...$methodArguments)
    ->willReturn($methodResult) // Или `->willReturnSelf();`
  1. Метод methodName mock-объекта $mockObject ДОЛЖЕН быть вызван один раз без аргументов и вернуть результат.
$mockObject
    ->expects($this->once())    // Или `->expects($this->at($mockObjectIncrement++))`
    ->method('methodName')
    ->willReturn($methodResult) // Или `->willReturnSelf();`
  1. Метод methodName mock-объекта $mockObject ДОЛЖЕН быть вызван с аргументами $methodArguments и бросить исключение $exception.
$mockObject
    ->expects($this->once())    // Или `->expects($this->at($mockObjectIncrement++))`
    ->method('methodName')
    ->with(...$methodArguments)
    ->willThrowException($exception);
  1. Метод methodName mock-объекта $mockObject ДОЛЖЕН быть вызван один раз без аргументов и бросить исключение $exception.
$mockObject
    ->expects($this->once()) // Или `->expects($this->at($mockObjectIncrement++))`
    ->method('methodName')
    ->willThrowException($exception);

8.7 Порядок утверждений для одного значения

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

// Правильно
/** @var JsonResponse|Response $response */
$response = $action->run(/* ... */);

$this->assertSame($expectedContent, $response->getContent());
$this->assertSame(Response::HTTP_OK, $response->getStatusCode());
$this->assertInstanceOf(JsonResponse::class, $response);
// Неправильно
// В случае возникновения ошибки не будет ясно, что же за ошибка произошла, вместо этого получим только несоответствие
// типа $response, или статус кода.
/** @var JsonResponse|Response $response */
$response = $action->run(/* ... */);

$this->assertInstanceOf(JsonResponse::class, $response);
$this->assertSame(Response::HTTP_OK, $response->getStatusCode());
$this->assertSame($expectedContent, $response->getContent());
// Правильно
$exceptionContext = $object->method(/* ... */);

$this->assertIsArray($exceptionContext);
$this->assertNotEmpty($exceptionContext);
$this->assertArrayHasKey('exception', $exceptionContext);
$this->assertInstanceOf(\Exception::class, $exceptionContext['exception']);
$this->assertSame($expectedExceptionMessage, $exceptionContext['exception']->getMessage());
// Неправильно
$exceptionContext = $object->method(/* ... */);

$this->assertSame($expectedExceptionMessage, $exceptionContext['exception']->getMessage());

8.8. Проверка утверждений на основании результатов собственных проверок

Проверка утверждений на основании результатов собственных проверок ДОПУСКАЕТСЯ только в случае, когда отсутствует assert* метод, включающий эту проверку. Во всех остальных случая НЕОБХОДИМО использовать assert* метод.

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

// Правильно
$this->assertSame($expectedString, $actualString);

// Неправильно
$this->assertSame(true, $expectedString === $actualString);

// Неправильно
$this->assertTrue($expectedString === $actualString);
// Правильно
$this->assertStringStartsWith($expectedPrefix, $actualString);

// Неправильно
$this->assertSame(0, strpos($actualString, $expectedPrefix));
// Правильно
$this->assertTrue($actualBool);

// Неправильно
$this->assertTrue($actualBool === true);

// Неправильно
$this->assertSame(true, $actualBool);

8.9. Проверки значений на основании TestCase::callback

Для проверок значений на основании TestCase::callback НЕОБХОДИМО использовать утверждения, а не возвращать false.

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

// Правильно
$userRepository
    ->expects($this->once())
    ->method('findByGroup')
    ->with(
        $this->callback(
            function (Group $group) use ($groupId, $groupName): bool {
                $this->assertSame($groupId, $group->getId());
                $this->assertSame($groupName, $group->getName());
                
                return true;
            }
        )
    )
    ->willReturn($users);
// Неправильно
$userRepository
    ->expects($this->once())
    ->method('findByGroup')
    ->with(
        $this->callback(
            function (Group $group) use ($groupId, $groupName): bool {
                // Если следующая проверка не выполнится, в лог будет добавлено сообщение о том, что аргумент не
                // прошел проверку. Информация о том, какая из частей этой проверки дала ложный результат, и какие
                // в принципе значения сравнивались, будет потеряна.
                return $group->getId() === $groupId && $group->getName() === $groupName;
            }
        )
    )
    ->willReturn($users);

8.10. Проверка утверждений для числовых значений

Для проверок числовых значений без учета погрешности ДОЛЖЕН использоваться метод assertSame.

// Правильно
$expected = 5;
$actual   = 5;

$this->assertSame($expected, $actual);
// Неправильно
$expected = 5;
$actual   = 5;

$this->assertEqual($expected, $actual);

Для проверок числовых значений с учетом погрешности ДОЛЖЕН использоваться метод assertEqual с обязательным указанием погрешности.

// Правильно
$expected = 5;
$actual   = 4.5;

$this->assertEqual($expected, $actual, '', 1);

8.11. Проверка утверждений для \DateTimeImmutable

Проверки объектов \DateTimeImmutable ДОЛЖНЫ осуществляться на основании timestamp, а не сравнения объектов. Для проверок \DateTimeImmutable, не зависящих от текущего времени ДОЛЖЕН использоваться метод assertSame.

$expected = new \DateTimeImmutable('2019-01-01 10:20:30');
$actual   = new \DateTimeImmutable('2019-01-01 10:20:30');

$this->assertSame($expected->getTimestamp(), $actual->getTimestamp());

Для проверок \DateTimeImmutable зависящих от текущего времени ДОЛЖЕН использоваться метод assertEqual с обязательным указанием погрешности.

$expected = new \DateTimeImmutable();
$actual   = new \DateTimeImmutable();

$this->assertEqual($expected->getTimestamp(), $actual->getTimestamp(), '', 2);

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

9. IDE

Для разработки php проектов РЕКОМЕНДУЕТСЯ использовать PhpStorm.

9.1. Inspections и Code Style

Настройки инспекций РЕКОМЕНДУЕТСЯ импортировать из phpstorm/php-conventions-inspections.xml. Данные инспекции используют PHP_CodeSniffer.

Настройки код стайла РЕКОМЕНДУЕТСЯ импортировать из phpstorm/php-conventions-code-style.xml.

ВАЖНО Inspections и Code Style содержат настройки только для PHP, по этой причине СТОИТ выполнять импорт как расширение схемы, выбрав опцию Current scheme.

php-conventions's People

Contributors

evgeniir avatar github-actions[bot] avatar index0h avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

php-conventions's Issues

1.3. Выравнивание присвоений переменных и элементов массива

1.3. Выравнивание присвоений переменных и элементов массива

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

$inside_left_floating_width   = 0;
$inside_right_floating_width  = 0;
$outside_left_floating_width  = 0;
$outside_right_floating_width = 0;

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

$varName                            = 'varName';
$secondVariableWithVeryLongNameHere =
    '123456790123456790123456790123456790123456790123456790123456790123456790123456790';

Другой утрированный пример более наглядно демонстрирующий проблему.

$user_access_token    = $user_access_token_storage->get();
$i                    = 1;
$has_promote_articles = false;
$result               = [];
$ids                  = [];
$categories           = [];
foreach ($article_rep->slice($start, self::PER_PAGE) as $article) {
   // ...

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

$choice_translation_domain = 'article';
$total                     = $article_rep->countOf(
    new PublishedArticles(new \DateTimeImmutable()),
    new Cache(self::CACHE_TTL)
);

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

                            $descendant_delimeter = strrpos($query, '::');
                            $isChild              = substr($query, $descendant_delimeter - 5, 5) === 'child';
                            $el                   = substr($query, $descendant_delimeter + 2);
                            $query                = substr($query, 0, strrpos($query, '/')).
                                ($isChild ? '/' : '//').$el;
                            // потребовалось сделать перенос так как уперлись в 120 символов

                            $p   = $i + 1;
                            $nth = trim(mb_substr($selector, $p, strpos($selector, ')', $i) - $p));
                            // другой логический блок переменных который имеет свои отступы и
                            // вступает в диссонанс с предыдущим блоком из-за разницы длинны в отступах

Так диссонанс будет нагляднее

$descendant_delimeter = strrpos($query, '::');
$isChild              = substr($query, $descendant_delimeter - 5, 5) === 'child';
$el                   = substr($query, $descendant_delimeter + 2);
$query                = substr($query, 0, strrpos($query, '/')).($isChild ? '/' : '//').$el;

$p   = $i + 1;
$nth = trim(mb_substr($selector, $p, strpos($selector, ')', $i) - $p));

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

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

1.6. Выделение управляющих инструкций

1.6. Выделение управляющих инструкций

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

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

$articles = $article_rep->match(new PublishedArticles());
$map = [];

foreach ($articles as $article) {
    $map[$article->getId()] = $article;
}

Но даже из описания понятно, что у нас есть 2 логических блока которые надо визуально разделять и объявление переменной $map относится к блоку формирования карты, а не к блоку получения данных. То есть, правильней писать так:

$articles = $article_rep->match(new PublishedArticles());

$map = [];
foreach ($articles as $article) {
    $map[$article->getId()] = $article;
}

Добавить шаблон соглашений, для конкретного проекта

Некоторые из пунктов основного списка очень спорные, например выравнивание, или dto+public свойства.

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

PhpStorm codestyle and inspections

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

Dependency Ingection

в разделе 6.2. Singleton орфографическая ошибка Dependency Ingection

psalm vs phpstan vs phan vs (???)

@EvgeniiR @peter-gribanov @alexgivi @fox-hellraiser @WoZ @gotterdemarung
Господа, прошу вас о конструктивной критике и доводам по следующим статическим анализаторм psalm vs phpstan vs phan vs (ваш вариант). Хочу помимо idea инспекций / кодстайла добавить более нейтивные валидаторы кода, для запуска например через CI.
Для проверки стиля я предполагаю использовать PHP_CodeSniffer, но для остального немного теряюсь, так как экспертизы в этих анализаторах у меня мало.

Добавить введение

Необходимо описать общую философию и для каких проектов эти требования подходят

Add recommendations for php unit assertions

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

Analyze roistat php-code-conventions

https://github.com/roistat/php-code-conventions

  • Правила разделения бизнес-логики
  • Часто упоминаемые объекты именуются одинаково во всем проекте
  • Признак объекта добавляется к названию
  • Переменные, отражающие свойства объекта, должны включать название объекта
  • Переменные по возможности должны называться на корректном английском
  • К переменной нельзя обращаться по ссылке (через &)
  • Нельзя изменять переменные, которые передаются в метод на вход
  • Нельзя нескольким переменным присваивать одно и то же значение
  • Нельзя использовать константы через метод constant
  • Названия boolean методов и переменных должны содержать глагол is, has или can
  • Запрещены отрицательные логические названия
  • Не используйте boolean переменные (флаги) как параметры функции
  • Для конкатенации массивов запрещено использовать оператор +.
  • Для проверки наличия ключа в ассоциативном массиве используем array_key_exists, а не isset
  • Нельзя смешивать в массиве строковые и числовые ключи
  • Для проверки наличия значения по индексу в обычных (не ассоциативных) массивах используем count($array) > N
  • Строки обрамляются одинарными кавычками
  • Вместо лишней конкатенации используем подстановку переменных в двойных кавычках
  • Название метода должно начинаться с глагола и соответствовать правилам именования переменных.
  • Методы названия, которых начинаются c check и validate, должны выбрасывать исключения и не возвращать значения
  • Использование рекурсий допускается только в исключительном случае
  • Запрещается кешировать данные в статических переменных метода
  • Nullable параметры должны быть помечены ?, даже если указано значение по умолчанию.
  • Метод всегда должен возвращать только одну структуру данных (или null) или ничего не возвращать
  • Если метод возвращает один объект (или скалярный тип), то в случае, если объект не найден, возвращается null
  • В больших методах возвращаемая переменная должна называться $result
  • Метод должен явно отличать нормальные ситуации от исключительных
  • Метод должен придерживаться следующей структуры: Проверка параметров → Получение данных → Работа → Результат
  • Трейты имеют постфикс Trait
  • Интерфейсы имеют постфикс Interface
  • Абстрактные классы имеют префикс Abstract
  • Все объекты должны быть неизменяемыми (immutable), если от них не требуется обратного
  • Статические вызовы можно делать только у самого класса. У экземпляра можно обращаться только к его свойствам и методам
  • В общем случае комментарии запрещены
  • Вынужденные хаки должны быть помечены комментариями
  • Готовые алгоритмы, взятые из внешнего источника, должны быть помечены ссылкой на источник
  • При разработке прототипа допустимо помечать участки кода @todo
  • По умолчанию тексты исключений не должны показываться пользователю
  • Назначение всех числовых литералов должно быть понятным из контекста
  • В условном операторе должно проверяться исключительно boolean значение
  • В сравнении не boolean переменных используется строгое сравнение с приведением типа (===), автоматическое приведение и нестрогое сравнение не используются
  • Не надо сравнивать boolean с true/false
  • Проверять переменные надо на наличие позитивного вхождения, а не отсутствие негативного
  • Тернарный оператор следует использовать, если обе ветви условия предназначены для установки одной переменной одним языковым выражением

PhpStorm плагины

было бы неплохо добавить список полезных плагинов для PhpStorm, например в секцию 9.2. я могу посоветовать - Php Inspections (EA Extended), SonarLint. возможно вы сможете дополнить этот список еще какими-то полезными плагинами.

Не правильно

Наречие “неправильно” в значении “ошибочно” пишется слитно – неправильно.

Наречие “не правильно”, если рядом есть местоимение либо наречие с отрицанием, а также слова “отнюдь”, “вовсе”, “далеко” пишется раздельно – не правильно.

https://obrazovaka.ru/kak-pishetsya/nepravilno.html

Basic php conventions

Написать основу соглашений по оформлению кода

Добавить оглавление

Для каждого пункта потребуется заменить заголовок с формата:

### 3.2. Именование свойств

на

<a name="property-nameing"><h2>3.2. Именование свойств</h2></a>

Для полученных якорей сделать оглавление.

Как вариант - поискать и задействовать внешние генераторы

4.4. Присвоения в условных операциях

4.4. Присвоения в условных операциях

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

if (
    ($token = $this->token_storage->getToken()) &&
    ($user = $token->getUser()) &&
    $user instanceof User
) {
    // do something
}

VS

$token = $this->token_storage->getToken();
if ($token instanceof TokenInterface) {
    $user = $token->getUser();
    if ($user instanceof User) {
        // do something
    } else {
        // мы должны писать return или else, что бы не менялось поведение программы
    }
}

Пример посложнее

if (
    ($request = $this->request_stack->getMasterRequest()) &&
    ($session = $request->getSession()) &&
    ($token = $this->token_storage->getToken()) &&
    ($user = $token->getUser()) &&
    $user instanceof User &&
    ($confirm_token = $session->get('confirm_token')) &&
    $user->getConfirmToken() === $confirm_token
) {
    // do something
}

VS

$request = $this->request_stack->getMasterRequest();
$token = $this->token_storage->getToken()
if ($request instanceof Request && $token instanceof TokenInterface) {
    $session = $request->getSession();
    $user = $token->getUser();
    if (
        $session instanceof SessionInterface &&
        $user instanceof User &&
        $session->has('confirm_token')
    ) {
        $confirm_token = $session->get('confirm_token');
        if ($confirm_token && $user->getConfirmToken() === $confirm_token) {
            // do something
        } else {
            // обрабатываем исключительную ситуацию
        }
    } else {
        // обрабатываем исключительную ситуацию
    }
}

Проблемы AR.

Проблема ActiveRecord заключается в том, что помимо данных модель содержит подключение к базе данных, как следствие код, использующий модель получает неявную зависимость от БД, что МОЖЕТ привести к проблемам безопасности. Что бы покрыть unit тестами код, создающий ActiveRecord с большой вероятностью придется менять поведение драйвера подключения к БД, что приведет к крайне высокой сложности.

Не совсем так. Сущности(модели) это часть системы которая содержит самую важную логику(бизнес-правила) и не должна зависеть от модулей которые реализую правила приложения и уж тем более от инфраструктуры(подключение к БД).

Проблема AR в том что сложно покрыть юнитами класс который содержит в себе логику и пишет/читает из БД

Трактовка сущностей как моделей данных это ещё один антипаттерн - https://martinfowler.com/bliki/AnemicDomainModel.html . Он, правда, весьма популярен сейчас. Увы.

3.7. Модификаторы доступа для методов

рекомендации по архитектуре

нашел хорошую статью
https://habr.com/ru/post/482154/

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

  1. все классы могут содержать либо final public методы, либо abstract protected (для наследования), либо private.
  2. если класс рассчитан на наследование - делать его абстрактным. если нет - делать его финальным.
  3. не использовать наследование, кроме тех случаев, когда это надо. использовать композицию и паттерн "декоратор".

конечно, следовать этим принципам по всей строгости весьма трудно, но принципы хорошие.

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

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

2.2. Дублирование типов в docblock

2.2. Дублирование типов в docblock

Очень популярное решение, особенно в связи с вводом type hinting в PHP для базовых типов. В Symfony уже давно практикуют этот подход и недавно включили правило для этого кейса в PHP CS Fixer.

Я безусловно согласен с тем, что дублирование типов в аннотациях бессмысленно и логично описывать только те типы которые нельзя задекларировать средствами языка. Однако, на мой взгляд, это затрудняет восприятие и чтение кода и на то есть 2 причины:

  1. Блок комментариев перед методом визуально отделяет конец одного метода и начало второго. По широкой зелёной полосе очень хорошо ориентироваться в границах методов при беглом просмотре класса. По блоку аннотаций легко найти начало метода. Код класса вообще без комментариев превращается в лапшу и читать его субъективно тяжелее.
  2. В случае описания полного списка принимаемых и возвращаемых значений в DocBlock, у нас есть 2 места где мы декларируем типы, это в аннотациях и в коде метода. В аннотациях описание более полное поэтому мы читаем только аннотации. Если в аннотациях описание неполное, то мы вынуждены читать как аннотации так и декларацию в коде. То есть, читать нужно одновременно в 2 местах и переключатся между разными формами декларации типов. Особенно это неудобно если в аннотациях кастомизирован тип одного или нескольких аргументов метода из середины большого списка аргументов. Выигрыш мы получаем только если у нас вообще нет кастомизации типов в аннотациях.
/**
 * @param Articles[] $third
 */
public function example(string $first, ?int $second, array $third, bool $fourth = false, float ...$fifth): string

Еще больше усугубляется проблема если мы начинаем использовать свои PHPDoc-теги статических анализаторов типа Psalm и Phan.

/** 
 * @param array $parsed_url
 * @phan-param array{scheme:string,host:string,path:string} $parsed_url
 * @psalm-param array{scheme:string,host:string,path:string} $parsed_url  
*/
public function showUrl(string $label, array $parsed_url, string $host): string

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

/**
 * @param string $controller The controller name (a string like Bundle\BlogBundle\Controller\PostController::indexAction)
 */
protected function forward(string $controller, array $path = [], array $query = []): Response
{
    $request = $this->container->get('request_stack')->getCurrentRequest();
    $path['_controller'] = $controller;
    $subRequest = $request->duplicate($query, null, $path);

    return $this->container->get('http_kernel')->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
}

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

3.5. Именование методов

3.5. Именование методов

Названия методов ДОЛЖНЫ описывать предназначение их использования внешним кодом, а не детали реализации.

// Не правильно
public function findUserById(int $id): ?User
// Правильно
public function find(int $id): ?User

Первый вариант описывает реализацию, в отличии от второго. И название метода дублирует аргумент и возвращаемое значение. Масло масленное.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.