За то время, что я работаю PHP-разработчиком, способ написания и доставки кода сильно изменился. В ранних приложениях Symfony и Zend Framework группы PHP-FIG не существовало, а стандарты кодирования определялись на усмотрение того, кто их писал. На протяжении тех лет, когда мы наблюдали широкое распространение стандартов PSR, надежные инструменты статического анализа были несколько разрозненными. Так было до настоящего времени, пока не вышла версия 1.0 PHPStan. Давайте отметим это событие, ознакомившись с некоторыми ее возможностями!
Одно из больших преимуществ использования скомпилированного языка, такого как Java или C#, заключается в том, что процесс компиляции завершится неудачей, если ваш код не является типобезопасным, обеспечивая соблюдение стандартов. Поскольку PHP является интерпретируемым языком, у нас нет такой роскоши.
Благодаря огромному количеству инструментов DevOps, доступных в современной веб-разработке и статическом анализе, нам в действительности предоставляются те же самые механизмы, но с помощью различных средств. Поскольку это так, то и я не смогу сильно настаивать на использовании чего-то похожего на ту среду, которую буду описывать. Итак, зачем вам нужен этот инструментарий? Давайте рассмотрим пример.
Как правило, когда речь заходит о подобных инструментах, стараются выбрать тему, которая была бы интересной или применимой к тому, о чем вы пишете. Но для этой статьи я собираюсь представить вам сценарий, с которым я лично неоднократно сталкивался в агентской среде:
"Помогите! Моё PHP-приложение создано кем-то другим, и нужно, чтобы кто-либо помог мне спасти его и взял на себя обслуживание, потому что необходимо создать функции X/Y/Z, а функционал A/B/C даже не работает нормально!".
Взятие на себя чужой кодовой базы/проекта — это всегда сплошная лотерея. Если вы беретесь за осуществление проекта, потому что ему нужны новые функции, а он уже погряз в технических долгах, знайте, что прежде чем приступать к чему-то еще, сначала придется разобраться с этим. Хуже того, многие из этих проектов (по моему опыту) обычно поступают с полным отсутствием тестов для самодокументирования кода. Рассмотрим классический пример, который встречается то и дело:
$someData = \MyNamespace\MyORM\MyRepository::findAllBySomething(SOMETHING);
foreach ($someData as $myEntity) {
$myEntity->doTheThing();
}
Вы не писали этот класс сущности или метод репозитория. У них нет никаких тайпхинтов, потому что изначально это было написано на PHP5.3, или разработчик их не использовал. Хорошо, если ваш ORM возвращает массив тех же сущностей, но одна ошибка, один нулевой результат в возвращаемом значении findAllBySomething()
и doTheThing()
выбросит фатальную ошибку.
Пришло время применить анализатор PHPStan.
Легко сказать "используйте PHPStan", однако если вы используете унаследованное приложение или приложение с большими техническими долгами, то вам нужна стратегия, а не выбрасывать все подряд, чтобы посмотреть, что получится. Во-первых, вы должны познакомиться с Уровнями Правил.
PHPStan структурирован так, чтобы запускаться с заданными уровнями правил, пронумерованными от 0 до 9:
базовые проверки, неизвестные классы, неизвестные функции, неизвестные методы, вызываемые на $this
, неправильное количество аргументов, передаваемых этим методам и функциям, всегда неопределенные переменные.
возможно неопределенные переменные, неизвестные магические методы и свойства на классах с __call
и __get
.
неизвестные методы проверяются на всех выражениях (не только на $this
), валидация PHPDocs.
типы возвращаемых значений, типы, присваиваемые свойствам.
базовая проверка мертвого кода — всегда ложные instanceof
и другие проверки типов, мертвые ветви else
, недостижимый код после возврата; и т.д.
проверка типов аргументов, передаваемых методам и функциям.
сообщать об отсутствующих тайпхинтах.
сообщать о частично неправильных типах объединения — если вы вызываете метод, который существует только для некоторых типов в объединении, уровень 7 начинает сообщать об этом; другие возможные неправильные ситуации.
сообщать о вызове методов и доступе к свойствам на nullable типах.
будьте строги к mixed типу — единственная разрешенная операция, которую вы можете сделать с ним, это передать его другому mixed.
Вот почему ваша стратегия очень важна. Если у вас есть унаследованный проект, написанный кем-то другим, и вы запускаете программу выполнения задач PHPStan на уровне 9, то можете быть ошеломлены результатами, которые она выдает. Все сломано! Для рефакторинга я бы предложил следующее:
Установите себе контрольные точки для каждого определенного уровня и начните с малого.
В конечном итоге долгосрочные инвестиции окупятся (мы скоро перейдем к пайплайнам), но установите верхний уровень, на который вы готовы перейти при классификации "исправления технического долга" в соответствии с вашим собственным "определением выполненного проекта".
Де-факто хорошей целью для унаследованного проекта является прохождение Правил Уровня 6. Именно на этом этапе ваша кодовая база может перейти из состояния "опасно" в состояние "правильно". Это сделает Правило Уровня 6 вашей базовой линией).
Это очень важно: обязательно выделите время (спринты, тикеты на Jira для мазохистов) для исправления того, что PHPStan помечает на каждом уровне правил. Во многих случаях исправить технический долг непросто, и вы понятия не имеете, какие логические ошибки бизнес-домена могут быть в вашем приложении.
Устанавливая инкрементные цели для Уровня Правил, убедитесь, что вы настроили свой пайплайн до фиксации изменений, чтобы не внести новые запахи кода во время рефакторинга. Настройка пайплайна потребует от вас определения базовой линии (к этому мы еще вернемся).
В мире DevOps существует слишком большое количество вариантов инструментария для решения ваших проблем. Для данного примера я предлагаю только один подход, но он является наименее сложным по сравнению с другими доступными вариантами. После того, как вы определились со стратегией, пришло время настроить свой пайплайн так, чтобы не коммитить новый код, который сначала не прошел через PHPStan.
Я люблю внедрять инструментарий, чтобы исключить любую возможность единичных точек отказа, и в результате этого циничного подхода настоятельно рекомендую запускать статический анализ как на локальных машинах разработчиков, так и на серверных CI-проверках в вашем репозитории.
Composer + PHPStan
Прежде всего, вы захотите инсталлировать PHPStan в свой проект. Для этого будем использовать composer, исходя из предположения, что ваш унаследованный код, надеюсь, использует управление пакетами. Если нет, вы можете установить composer и использовать composer init для создания нового проекта.
Чтобы установить PHPStan, выполните следующее:
$composer require --dev phpstan/phpstan
Мы добавляем --dev
, поскольку в продакшне он нам не нужен (теоретически!).
Конфигурация: создание базовой линии.
Это довольно интересная функция PHPStan. Ваша базовая линия устанавливает "нулевой уровень" вашего приложения, так что все текущие ошибки, существующие в рамках выбранного вами Уровня Правил, игнорируются до тех пор, пока вы не решите их устранить, но в то же время может быть установлен уровень правил для любых новых изменений. Разумный подход, описанный в стратегии, заключается в том, чтобы установить базовую линию на Уровне Правила 6:
Весь новый код, вносимый в проект, должен соответствовать Уровню Правила 6.
Затем вы можете установить целевые показатели технического долга для более низких уровней, как указано в ваших стратегических целях.
Чтобы создать базовую линию, выполните следующие действия:
vendor/bin/phpstan analyse --level 6 \ --configuration phpstan.neon \ src/ tests/ --generate-baseline
Сейчас у вас есть конфигурация базовой линии, установленная в указанном файле (phpstan.neon), которая сохраняет подробный обзор ошибок по каждому файлу.
Теперь вы хотите, чтобы PHPStan предотвращал коммиты в вашем репозитории до того, как они будут выгружены в ваш источник. Для этого мы используем Git hooks.
Git hooks
Мне потребовались годы, чтобы понять, что git на самом деле устанавливает хуки в стандартной комплектации в новый git-репозиторий при git init. Вы можете прочитать подробнее о git hooks здесь. Мы собираемся отредактировать pre-commit хук. Если вы не применяли никаких хуков ранее в вашем проекте, то можете активировать хук pre-commit, переименовав его — запустите его из корня вашего проекта:
mv ./.git/hooks/pre-commit.sample ./.git/hooks/pre-commit
Теперь откройте файл, удалите его содержимое и скопируйте следующее:
#!/bin/sh
#
exec < /dev/tty
if git rev-parse --verify HEAD >/dev/null 2>&1
then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi
gitDiffFiles=$(git diff --name-only --diff-filter=d $against | grep \.php)
if [ "$gitDiffFiles" != "" ]; then
analysisResult=$(vendor/bin/phpstan analyse $gitDiffFiles)
if [ "$analysisResult" = "" ]; then
echo 'PHPStan pass'
else
echo "$analysisResult"
exit 1;
fi
fi
# Redirect output to stderr.
exec 1>&2
# If there are whitespace errors, print the offending file names and fail.
exec git diff-index --check --cached $against --
Теперь, когда вы включили pre-commit, PHPStan будет запускаться перед каждым коммитом и анализировать базовую линию на предмет новых файлов, которые были изменены в git-коммите. Больше никакого дурно пахнущего закоммиченного кода!
Возможно, вы захотите настроить триггер командной строки при переходе на более высокий уровень, поэтому, когда он должен измениться (или вы захотите включить другие функции PHPStan), измените аргументы строки analysisResult=$(vendor/bin/phpstan analyse $gitDiffFiles)
.
Чем больше защиты вы сможете организовать для своего кода, тем лучше. Запуск PHPStan на стороне сервера после перехода к вашему коду в рамках Непрерывной Интеграции является обязательным условием. Для этого примера мы будем использовать Github Actions, но имейте в виду, что можно настроить этот процесс с тем же уровнем функциональности в CircleCI, Bitbucket Pipelines, Gitlab CI/CD или Jenkins. Вот пример рабочего процесса actions, настроенного на Github, при сборке кода с помощью контейнера Ubuntu:
---
name: build
on: [ push, pull_request ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
name: Build example
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.0
extensions: json, mbstring
coverage: pcov
env:
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run PHPStan
run: vendor/bin/phpstan analyse .
Команда в разделе “Выполнить PHPStan” может быть настроена в соответствии с вашими требованиями таким же образом, как вы можете настроить команду при локальном запуске PHPStan. Я написал данный рабочий процесс для запуска PHPStan на уровне по умолчанию для всех файлов в проекте (этот рабочий процесс еще не запустил composer, так что у него не будет ненужного и неэффективного шага по его запуску в папке vendor), поэтому здесь я бы рекомендовал иметь для извлечения конфигурацию, которая устанавливает Уровень Правил для всего проекта.
Теперь у вашего унаследованного проекта есть стратегия очистки кода и пайплайны для предотвращения появления новых ошибок в коммитах при проведении анализа относительно базовой линии для всего существующего кода. Такая настройка может дать вам гораздо больше уверенности в отношении принятия решения по реализации проекта, а также поможет понять, где находятся вероятные области, которые нуждаются в рефакторинге для устранения технического долга.
Заявляю об этом громко, особенно для тех, кто на задней парте: PHPStan и любой другой инструмент статического анализа не является заменой тестам! Я бы сказал так: набор тестов и PHPStan дополняют друг друга в оценке качества вашего кода.
Ошибочно полагать, будто набор тестов вам практически не нужен. Самое существенное здесь то, что статический анализ не может проверить вашу доменную логику. Хотя такое утверждение может показаться очевидным, однако это может сбить с толку, поскольку PHPStan способен устранить необходимость в определенных тестах. Примером может служить тест instanceOf, который утверждает, что создаваемый класс является конечным результатом процесса. PHPStan исключает данное требование, поскольку он обеспечивает анализ, необходимый для устранения этой потенциальной ошибки, но он не знает заранее о логике вашего домена — это то, что вам действительно нужно протестировать.
И помните, что существуют альтернативы!
на главную сниппетов