CI — это автоматический ответ на вопрос: «Сохранило ли изменение ожидаемое поведение проекта?» Чтобы научиться им пользоваться, недостаточно вставить workflow со строкой composer test, если такой команды в проекте нет. Поэтому начнём с маленького PHP-проекта, напишем реальный тест и проведём изменение через красный и зелёный запуск GitHub Actions.
После урока: у вас будут Composer-проект с автозагрузкой, два теста, workflow и воспроизводимая процедура диагностики. Локально нужны PHP 8.4, расширения DOM, XML, mbstring, Composer 2 и Git. Пример использует PHPUnit 12, которому требуется PHP 8.3+. Аккаунт GitHub нужен для удалённого запуска, а тесты можно выполнить и без него.
1. Подготовьте проект и зависимости
Создайте каталог php-ci-lab с подпапками src, tests и .github/workflows. В корне сохраните composer.json:
{
"name": "devops-lab/release-label",
"description": "A minimal release label and CI demonstration",
"license": "MIT",
"type": "project",
"require": {"php": "^8.4"},
"require-dev": {"phpunit/phpunit": "^12.0"},
"autoload": {"psr-4": {"App\\": "src/"}},
"scripts": {"test": "phpunit tests"}
}
Выполните composer install. Первый запуск создаст composer.lock; включите его в Git. Последующие установки будут использовать записанные версии. composer update нужен для осознанного обновления зависимостей, а не для каждой CI-проверки. Каталог vendor в репозиторий не добавляйте.
Для сайтов, ботов и pet-проектов можно использовать Beget: виртуальный хостинг от 420 ₽/мес., VPS от 330 ₽/мес. и тестовый период.
2. Напишите код с проверяемым контрактом
Файл src/ReleaseLabel.php:
<?php
declare(strict_types=1);
namespace App;
use InvalidArgumentException;
final readonly class ReleaseLabel
{
public function __construct(private string $value)
{
if (trim($value) === '') {
throw new InvalidArgumentException('Release label is empty');
}
}
public function toString(): string
{
return $this->value;
}
}
У объекта одна ответственность: представлять непустое обозначение релиза. Он не читает окружение и не делает HTTP-запросов. Это позволяет проверить поведение без базы, сети и запуска веб-сервера.
Файл tests/ReleaseLabelTest.php:
<?php
declare(strict_types=1);
use App\ReleaseLabel;
use PHPUnit\Framework\TestCase;
require_once __DIR__ . '/../vendor/autoload.php';
final class ReleaseLabelTest extends TestCase
{
public function testPreservesValidLabel(): void
{
self::assertSame('v0.1.0', (new ReleaseLabel('v0.1.0'))->toString());
}
public function testRejectsEmptyLabel(): void
{
$this->expectException(InvalidArgumentException::class);
new ReleaseLabel(' ');
}
}
Запустите composer test. Ожидайте успешное завершение и две пройденные проверки. Формат строки summary зависит от версии PHPUnit. Важно, что число тестов равно двум, а код завершения — ноль. На Linux его можно посмотреть отдельной командой echo $? сразу после запуска.
3. Добавьте полноценный workflow
Файл .github/workflows/tests.yml:
name: PHP tests
on:
pull_request:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
jobs:
tests:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: dom, xml, mbstring, xmlwriter
coverage: none
tools: composer:v2
- name: Validate dependency definition
run: composer validate --strict
- name: Install locked dependencies
run: composer install --no-interaction --prefer-dist --no-progress
- name: Check syntax
run: php -l src/ReleaseLabel.php
- name: Run behavior tests
run: composer test
Сначала загружаются исходники, затем готовятся PHP и Composer, устанавливаются зависимости и запускаются проверки. В permissions нет права записи: тестовому заданию оно не требуется. persist-credentials: false не оставляет токен checkout в Git-конфигурации для последующих команд. Major-теги Actions удобны для учебного примера; для чувствительного проекта зафиксируйте действия по проверенным полным commit SHA.
timeout-minutes ограничивает продолжительность задания. Он не исправляет зависание, но помогает не тратить ресурсы бесконечно. workflow_dispatch позволяет вручную повторить workflow на выбранной ветке после добавления файла в основную ветку.
4. Передайте проект в GitHub и докажите, что CI ловит ошибку
Добавьте vendor/ в .gitignore. Зафиксируйте исходники, тесты, workflow, composer.json и composer.lock. Отправьте их в свой репозиторий. Откройте вкладку Actions и проверьте первый запуск.
Создайте ветку experiment/broken-label. Измените toString(), чтобы метод возвращал 'broken', и отправьте ветку в pull request. Теперь тест сохранения версии обязан упасть: ожидалось v0.1.0, получено broken. Верните правильный код новым коммитом и дождитесь зелёного результата.
Это важнее значка CI в README: вы проверили, что конкретная регрессия обнаруживается. В настройках защиты main назначьте этот check обязательным, если выбранный план и тип репозитория поддерживают правило. Сам workflow без правила слияния не запрещает человеку нажать merge.
5. Адаптируйте подготовку к Laravel
В существующем Laravel-проекте не заменяйте composer.json учебным. Используйте его lock-файл и штатные тесты. Для проекта, который поддерживает SQLite и выполняет тестовые миграции через RefreshDatabase, добавьте расширение pdo_sqlite и после установки зависимостей выполните:
- name: Prepare disposable Laravel environment
env:
APP_ENV: testing
DB_CONNECTION: sqlite
DB_DATABASE: ':memory:'
SESSION_DRIVER: array
CACHE_STORE: array
QUEUE_CONNECTION: sync
run: |
cp .env.example .env.testing
php artisan key:generate --env=testing
php artisan test --env=testing
Этот шаг заменяет composer test в Laravel workflow либо вызывается отдельным тестовым заданием. Проверьте phpunit.xml: принудительные значения из него могут изменить ожидаемое окружение. Имена параметров кэша зависят от поколения приложения; используйте настройки своей версии Laravel.
Не запускайте миграции в отдельном PHP-процессе для SQLite :memory: в надежде сохранить таблицы до следующей команды: память принадлежит соединению. Тестовый framework должен подготовить схему в процессе тестов. Если проект требует MySQL-специфичные возможности, добавьте одноразовый MySQL service; подмена его SQLite не доказывает совместимость SQL.
6. Диагностируйте первый неуспешный шаг
- Class App\ReleaseLabel not found: сравните регистр имени файла, namespace и PSR-4-префикс. Выполните
composer dump-autoloadлокально; Linux учитывает регистр путей. - Missing ext-dom или ext-mbstring: добавьте нужное расширение, а не отключайте platform requirements.
- Lock file is not up to date: осознанно обновите lock локально и закоммитьте его вместе с composer.json.
- Тест зелёный при испорченном коде: проверьте путь tests, число найденных тестов и отсутствие
continue-on-errorили|| true.
Самостоятельная работа: добавьте запрет перевода строки в метке релиза, сначала напишите падающий тест, затем исправьте класс. Успех — красный запуск до исправления и зелёный после него. Не добавляйте тестовому workflow доступ к серверу: поставка приложения — отдельная задача с отдельными правами.
Официальные материалы: требования PHPUnit 12, синтаксис workflow, тестирование Laravel.
Подборка онлайн-курсов для разработчиков — Python, Java, веб и DevOps.
Разберём задачу и предложим понятный состав работ, срок и стоимость.
Статья может содержать партнёрские ссылки. Это не влияет на стоимость для вас, но помогает развивать блог.

