Автоформатирование кода: PHP CodeSniffer и PHP CS Fixer

Автоформатирование кода: PHP CodeSniffer и PHP CS Fixer

Отсутствие форматирования - первый признак того, что перед вами код, очень сомнительного качества. А уровень квалификации людей, писавших такой код, далёк даже до джуниора. Однако реальная жизнь вносит свои коррективы, и если уж Вам "прилетел" проект с таким чудо-кодом, в первую очередь надо привести форматирование в порядок, а также позаботиться о том, чтобы подобное больше не попадало в репозиторий. Поэтому тема разговора - PHP CodeSniffer, PHP CS Fixer и git pre-commit хук.

 

На laracasts есть замечательное видео по этой теме в контексте PhpStorm, здесь же пойдёт речь о том, чего не рассказывал в том видео Джефри (ну а если хотите настроить фиксер под Sublime, смотрите эту статью). 

PHP CodeSniffer

Давайте начнём с PHP CodeSniffer. Этот инструмент поможет выявить нарушения форматирования, при надлежащей настройке PhpStorm будет вас об этом информировать путём выделения проблемных частей кода. Кроме всего прочего мы можем воспользоваться консолью для вывода информации об ошибках. 

Полезные ссылки:

Установка

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

composer require "squizlabs/php_codesniffer=*"  --dev

 

Используемый стандарт

Прежде чем двигаться дальше - обязательно согласуйте стандарт с командой:

  • если решение зависит от Вас - объясните всем, какого стандарта следует придерживаться, и есть ли какие-либо дополнительные правила (например, обязательная пустая строка перед оператором return)
  • если Вы не тим-лид - согласуйте с ним настройки (вообще-то это его работа, но не всегда всё идеально)
  • если Вы работаете в интернациональной команде - опять-таки, первое, что нужно сделать - согласовать стандарт с девелоперами "на том конце провода"

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

Можно каждый раз в консоли руками пописывать какие-то дополнительные правила или условия, указывать стандарт и т.д. Но, мы же понимаем, что это не оптимальный вариант. Поэтому, в корне проекта создадим файл phpcs.xml и добавим туда следующее (в качестве примера использую код из текущего проекта):

<?xml version="1.0"?>
<ruleset name="SIDev">
    <description>The Extended PSR2 coding standard.</description>
    <rule ref="PEAR">
        <exclude name="PEAR.Commenting.FileComment.Missing" />
        <exclude name="PEAR.Commenting.ClassComment.Missing" />
        <exclude name="PEAR.Commenting.ClassComment.MissingAuthorTag" />
        <exclude name="PEAR.Commenting.ClassComment.MissingCategoryTag" />
        <exclude name="PEAR.Commenting.ClassComment.MissingLicenseTag" />
        <exclude name="PEAR.Commenting.ClassComment.MissingLinkTag" />
        <exclude name="PEAR.Commenting.ClassComment.MissingPackageTag" />
        <exclude name="PEAR.Commenting.FileComment.MissingVersion" />
        <exclude name="PEAR.Commenting.FileComment.MissingCategoryTag" />
        <exclude name="PEAR.Commenting.FileComment.MissingPackageTag" />
        <exclude name="PEAR.Commenting.FileComment.MissingAuthorTag" />
        <exclude name="PEAR.Commenting.FileComment.MissingLicenseTag" />
        <exclude name="PEAR.Commenting.FileComment.MissingLinkTag" />
        <exclude name="PEAR.Commenting.FileComment.WrongStyle" />
        <exclude name="PEAR.Commenting.FunctionComment.MissingParamComment" />
        <exclude name="PEAR.Commenting.FunctionComment.SpacingAfterParamType" />
        <exclude name="PEAR.Commenting.FunctionCallSignature.CloseBracketLine" />
        <exclude name="PEAR.Files.IncludingFile.UseInclude" />
        <exclude name="PEAR.Functions.FunctionCallSignature.ContentAfterOpenBracket" />
        <exclude name="PEAR.Functions.FunctionCallSignature.CloseBracketLine" />
        <exclude name="PEAR.Functions.FunctionCallSignature.Indent" />
        <exclude name="PEAR.NamingConventions.ValidVariableName.PrivateNoUnderscore" />
        <exclude name="PEAR.WhiteSpace.ObjectOperatorIndent.Incorrect" />
        <exclude name="PEAR.WhiteSpace.ScopeIndent.IncorrectExact" />
    </rule>
    <rule ref="PSR2">
        <exclude name="PSR2.Methods.FunctionCallSignature.CloseBracketLine" />
        <exclude name="PSR2.Methods.FunctionCallSignature.ContentAfterOpenBracket" />
        <exclude name="PSR2.Methods.FunctionCallSignature.Indent" />
    </rule>
    <rule ref="PSR2.Namespaces.NamespaceDeclaration.BlankLineAfter" />
    <rule ref="Generic.Files.LineLength">
        <properties>
            <property name="lineLimit" value="90"/>
            <property name="absoluteLineLimit" value="130"/>
        </properties>
    </rule>
    <rule ref="Generic.Commenting.DocComment.MissingShort">
        <severity>0</severity>
    </rule>
    <rule ref="Generic.Commenting.DocComment.TagValueIndent">
        <severity>0</severity>
    </rule>
    <rule ref="Generic.Commenting.DocComment.ContentAfterOpen">
        <severity>0</severity>
    </rule>
    <rule ref="Generic.Commenting.DocComment.ContentBeforeClose">
        <severity>0</severity>
    </rule>
    <rule ref="Generic.Arrays.DisallowLongArraySyntax" />
    <rule ref="PSR1.Classes.ClassDeclaration.MissingNamespace">
        <exclude-pattern>database/*</exclude-pattern>
    </rule>
    <rule ref="PEAR.Commenting.FunctionComment.Missing">
        <exclude-pattern>tests/*</exclude-pattern>
    </rule>
    <file>app/</file>
    <file>database/</file>
    <file>routes/</file>
    <file>tests/</file>
</ruleset>

 

Пояснения:

  • как видите мы также подключаем правила форматирования PEAR, но поскольку нужные не все, мы указываем какие из них следует исключить.
  • также мы исключаем некоторые из правил PSR1 и Generic (которые в себя включает PSR2). 
  • никогда не следует проверять директории node_modules и vendor, поскольку нет гарантии, что разработчики пакетов, придерживаются того же стандарта, что и Вы, даже если речь идёт о конкретном стандарте без всяких дополнительных правил и исключений. Кроме того, не проверяйте директории, где будут размещаться кэшированные файлы. Поскольку данный проект написан на Laravel, я поступил проще и не исключил что-либо, а указал, что нужно проверять только те директории, в которых будет "жить" наш исходный код, а именно: app, database, routes, tests

Следующий шаг - объяснить снифферу, где смотреть правила. Откроем composer.json и в разделе scripts пропишем, что по умолчанию нужно использовать кастомный стандарт, и дабы вывод ошибок сделать более наглядным, следует использовать цвета. Помимо этого, также следует выполнять скрипт после установки и обновления зависимостей - если кто-то/что-то изменит используемый по умолчанию стандарт, я хочу его снова установить:

"scripts": {
    "code-sniffer": [
        "./vendor/bin/phpcs --config-set default_standard phpcs.xml",
        "./vendor/bin/phpcs --config-set colors 1"
    ],
    ...
    "post-install-cmd": [
        "@code-sniffer"
    ],
    "post-update-cmd": [
        "@code-sniffer"
    ]
}

 

Основные команды

Пояснение: ниже я намеренно пишу vendor/bin/phpcs, чтобы акцентировать внимание на том, что пакет установлен в проекте. Если же CodeSniffer установлен глобально, используйте просто phpcs 

Вывести список установленных стандартов:

vendor/bin/phpcs -i

 

Вывести список снифов, используемых в конкретном стандарте:

vendor/bin/phpcs --standart=PSR2 -e

 

Проверить форматирование файла file.php:

vendor/bin/phpcs /path/to/file.php

 

Проверить весь проект :

vendor/bin/phpcs

 

Проверить весь проект с выводом информации о том, где конткретно были встречены ошибки:

vendor/bin/phpcs -s

 

Вывести код-репорт, т.е. отчёт со сниппетами кода, где были обнаружены ошибки:

vendor/bin/phpcs --report=code

 

Есть ещё и другие команды, но, по моему мнению, вполне хватает перечисленных. Справедливости ради стоит отметить, что PHP CoedSniffer также включает и code beautifier fixer, т.е. можно исправить ошибки (читай отформатировать) командой:

vendor/bin/phpcbf

 

но я всё же предпочитаю php-cs-fixer

PHP CS Fixer

Установка

Опять-таки подумайте устанвливать интсрумент глобально, или для каждого проекта. Второй вариант:

composer require friendsofphp/php-cs-fixer --dev

 

Глобальная установка:

composer global require friendsofphp/php-cs-fixer

 

Если выбираете глобальную установку, убедитесь, что в переменную PATH добавлен путь к бинарным файлам composer-a. Проверьте это командой:

echo $PATH

 

Если путь отсутствует - добавьте. Эти действия зависят от платформы. В моём случае (Ubuntu 18.04) необходимо было открыть файл .bashrc находящийся домашнем каталоге /home/<your_username> и дописать в конце файла:

export PATH="$PATH:$HOME/.composer/vendor/bin"

 

Пользовательские правила

Предполагаю, что стандарт уже согласован. В корне проекта создаём файл .php_cs.dist и добавляем следующий код (опять-таки беру код из моего текущего проекта):

<?php

$finder = PhpCsFixer\Finder::create()
    ->exclude(['bootstrap', 'database', 'node_modules', 'public', 'storage', 'tests', 'vendor'])
    ->notPath('*')
    ->in(__DIR__);

return PhpCsFixer\Config::create()
    ->setRules([
        '@PSR2' => true,
        'array_syntax' => ['syntax' => 'short'],
        'blank_line_after_namespace' => true,
        'blank_line_before_return' => true,
        'linebreak_after_opening_tag' => true,
        'multiline_whitespace_before_semicolons' => ['strategy' => 'no_multi_line'],
        'no_extra_blank_lines' => true,
        'no_trailing_whitespace' => true,
        'no_unused_imports' => true,
        'no_useless_else' => true,
        'no_useless_return' => true,
        'phpdoc_order' => true,
        'phpdoc_separation' => true,
        'phpdoc_single_line_var_spacing' => true,
        'phpdoc_trim' => true,
        'single_blank_line_at_eof' => true,
        'single_blank_line_before_namespace' => true,
    ])
    ->setFinder($finder);

 

Обратите внимание, что помимо стандарта PSR2 также указаны некие дополнительные правила, которые будет применены во время форматирования. Например, удаление бесполезных импортов, бесполезных операторов else и return, сортировка phpdoc блоков и разделение их на группы, удаление лишних пустых строк и т.д. Все доступные инструкции описаны в документации - очень советую посвятить одни выходные изучению возможностей фиксера и просто поэкспериментировать.

Автоформат

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

vendor/bin/php-cs-fixer fix

// или
php-cs-fixer fix

 

Если не указано иного, php-cs-fixer будет создавать файл с кэшем .php_cs.cache - сразу добавьте название этого файла в .gitignore

Основные команды

Вывести список команд:

php-cs-fixer list

 

Вывести описание правил входящих в конкретный стандарт:

php-cs-fixer describe @PSR2

 

И, как уже был сказано, отформатировать (для разнообразия укажем стандарт Symfony):

php-cs-fixer fix --rules=@Symfony /path/to/file.php

 

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

Git pre-commit hook

Всё это замечательно, но не будет иметь никакого смысла, если вся команда не будет следовать установленным правилам. Решение - не пропускать коммиты с не отформатированными кодом. Для этого поспользуемся перехватчиками Git. В корне проекта создадим файл pre-commit.sh и скопируем в него следующее:

#!/bin/sh

PASS=true

echo "\nValidating PHPCS:\n"

which ./vendor/bin/phpcs &> /dev/null
if [ $? -eq 1 ]; then
  echo "\033[41mPlease install PHPCS\033[0m"
  exit 1
fi

./vendor/bin/phpcs -s

if [ $? -eq 0 ]; then
  echo "\033[32mPHPCS Passed! \033[0m"
else
  echo "\033[41mPHPCS Failed! \033[0m"
  PASS=false
fi

echo "\nPHPCS validation completed!\n"

if ! $PASS; then
  echo "\033[41mCOMMIT FAILED:\033[0m Your commit contains files that should pass PHPCS but do not. \n"
  echo "\033[41mCOMMIT FAILED:\033[0m Please fix the PHPCS errors and try again.\n"
  exit 1
else
  echo "\033[42mCOMMIT SUCCEEDED\033[0m\n"
fi

exit $?

 

Вообще-то хуки гит-а должны размещаться в директории projects/.git/hooks, но дело в том, что директории .git нет в удалённом репозитории - она созадётся на локальной машине только после инициализации гита или клонирования проекта. Т.е. каким-то образом надо положить этот хук на локальных машинах других разработчиков и изменить права, позволив файлу выполняться. И опять на помощь приходит composer.json. Создадим соответствующий скрипт, и попросим выполнять его после каждой инсталяции или апдейта. Вместе с предыдущим кодом раздел scripts файла composer.json будет выглядеть так:

"scripts": {
    "code-sniffer": [
        "./vendor/bin/phpcs --config-set default_standard phpcs.xml",
        "./vendor/bin/phpcs --config-set colors 1"
    ],
    "pre-commit-check": [
        "cp pre-commit.sh .git/hooks/pre-commit",
        "chmod +x .git/hooks/pre-commit"
    ],
    ...
    "post-install-cmd": [
        "@code-sniffer",
        "@pre-commit-check"
    ],
    "post-update-cmd": [
        "@code-sniffer",
        "@pre-commit-check"
    ]
}

 

Подведём итоги. Мы используем:

  • PHP CodeSniffer для вывода информации об ошибках форматирования
  • PHP CS Fixer для автоформатирования и исправления ошибок
  • Git pre-commit хук для предотвращения попадания неотформатированного кода в репозиторий путём отклонения коммитов

Внимание: php-cs-fixer исправит только те ошибки, которые он может исправить, не более. Например, если у Вас не написан phpdoc, fixer за вас его не напишет. Помните об этом.

На этом на сегодня всё. Удачи!