Автоформатування коду: 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 CodеSniffer також включає і 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 $?

 

Взагалі-то хуки git-а повинні розміщуватися в директорії 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 за вас його не напише. Пам'ятайте про це.

На цьому на сьогодні все. Успіхів!