Разработка на Laravel становится максимально эффективной, если использовать автоматизацию на каждом этапе: от настройки окружения до проверки кода и тестирования. В этой статье я покажу, как построить процесс разработки, который снижает количество ручной работы, повышает качество кода и ускоряет выпуск функционала.
Материал рассчитан на разработчиков с опытом работы с Laravel, которые хотят внедрить автоматические проверки, статический анализ, единый стиль кода и готовую сборку Docker Compose для быстрого старта проектов. Я поделюсь своим опытом, конкретными инструментами и практическими примерами.
Настройка Docker compose
Любой проект на Laravel у меня начинается с тщательной настройки Docker Compose. Это позволяет сразу получить полностью рабочее и изолированное окружение для разработки, тестирования и мониторинга. Такой подход минимизирует конфликты зависимостей, ускоряет старт проекта и обеспечивает стабильную работу всех сервисов.
В моей сборке обычно используются следующие сервисы:
| Сервис | Назначение |
|---|---|
| php-fpm | Обработка PHP-запросов приложения |
| PostgreSQL | Реляционная база данных |
| Grafana | Визуализация метрик и логов |
| Loki | Централизованное логирование |
| pgAdmin | Веб-интерфейс для управления PostgreSQL |
| Redis | Кэш и очереди Laravel |
| RedisInsight | Веб-мониторинг Redis |
| Queue | Обработка фоновых задач через php artisan queue:work |
Каждый сервис разворачивается в отдельном контейнере, что позволяет:
- гибко настраивать окружение;
- управлять зависимостями независимо друг от друга;
- обновлять компоненты без простоя остальных сервисов.
Такой подход делает разработку, поддержку и масштабирование проекта удобными и безопасными.
| Совет: для быстрого старта можно использовать готовый docker-compose.yml, который сразу поднимает все сервисы в изолированном окружении и настраивает базовые параметры для Laravel. |
services:
app:
build: ../..
container_name: pet
user: root
depends_on:
- pgdb
- redis
- loki
env_file:
- .env
working_dir: /var/www/
volumes:
- .:/var/www
networks:
- pet
dns:
- 8.8.8.8
- 1.1.1.1
pgdb:
container_name: pgdb
image: postgres
tty: true
restart: always
environment:
- POSTGRES_DB=${DB_DATABASE}
- POSTGRES_USER=${DB_USERNAME}
- POSTGRES_PASSWORD=${DB_PASSWORD}
ports:
- ${PGDB_PORT}
volumes:
- ./docker/postgres:/var/lib/postgresql/data
networks:
- pet
nginx:
image: nginx:latest
container_name: nginx
restart: unless-stopped
ports:
- ${NGINX_PORT}
- "443:443"
volumes:
- .:/var/www
- ./docker/nginx:/etc/nginx/conf.d
- /etc/letsencrypt:/etc/letsencrypt:ro
environment:
- TZ=${SYSTEM_TIMEZONE}
depends_on:
- pgdb
- app
- pgadmin
networks:
- pet
pgadmin:
image: dpage/pgadmin4:latest
restart: always
depends_on:
- pgdb
environment:
- PGADMIN_DEFAULT_EMAIL=${PGADMIN_EMAIL}
- PGADMIN_DEFAULT_PASSWORD=${PGADMIN_PASSWORD}
ports:
- ${PGADMIN_PORT}
networks:
- pet
redis:
image: redis:latest
container_name: redis
restart: always
ports:
- ${REDIS_PORT}
environment:
- REDIS_PASSWORD=${REDIS_PASSWORD}
command: [ "redis-server", "--requirepass", "${REDIS_PASSWORD}" ]
networks:
- pet
redisinsight:
image: redislabs/redisinsight:latest
container_name: redisinsight
ports:
- ${REDISINSIGHT_PORT}
volumes:
- ./docker/redisinsight:/db
restart: always
networks:
- pet
grafana:
image: grafana/grafana:latest
container_name: grafana
user: "472"
ports:
- ${GRAFANA_PORT}
environment:
- GF_SECURITY_ADMIN_USER=${GRAFANA_USER}
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
volumes:
- ./docker/grafana:/var/lib/grafana
depends_on:
- loki
networks:
- pet
queue:
build: ../..
image: docker_template:latest
container_name: laravel_queue
restart: always
depends_on:
- app
- redis
env_file:
- .env
working_dir: /var/www
volumes:
- .:/var/www
command: php artisan queue:work --sleep=3 --tries=3 --timeout=90
networks:
- pet
dns:
- 8.8.8.8
- 1.1.1.1
loki:
image: grafana/loki:latest
container_name: loki
ports:
- ${LOKI_PORT}
networks:
- pet
volumes:
pgdata:
networks:
pet:
driver: bridge
Под каждый сервис я разворачиваю отдельный контейнер, что позволяет гибко настраивать окружение, управлять зависимостями и обновлять компоненты независимо друг от друга. Такой подход делает разработку и поддержку проекта более удобной и масштабируемой.
Поддержка Code Style с Laravel Pint
Для соблюдения PSR-12 стандартов в проектах на Laravel я использую пакет laravel/pint. Этот инструмент выполняет статический анализ кода и автоматически форматирует файлы PHP в соответствии с установленными правилами.
Pint интегрируется в процесс разработки и позволяет:
- запускать проверки при коммитах;
- автоматически исправлять ошибки форматирования;
- ускорять приведение кода к единому стилю.
Такой подход снижает вероятность появления разрозненного или неаккуратного кода и экономит время на ручные исправления.
Пример конфигурации pint.json для проекта:
{
"preset": "psr12",
"exclude": [
"vendor",
"storage",
"node_modules",
"bootstrap/cache"
],
"rules": {
"array_syntax": {
"syntax": "short"
},
"binary_operator_spaces": {
"default": "single_space"
},
"braces": true,
"class_attributes_separation": {
"elements": {
"const": "one",
"method": "one",
"property": "one"
}
},
"no_unused_imports": true,
"ordered_imports": true,
"phpdoc_separation": true,
"phpdoc_align": true,
"single_quote": true,
"ternary_to_null_coalescing": true,
"trailing_comma_in_multiline": {
"after_heredoc": true
},
"types_spaces": {
"space": "none"
},
"phpdoc_no_empty_return": false,
"no_superfluous_phpdoc_tags": false,
"concat_space": {
"spacing": "one"
}
}
}
Эта конфигурация позволяет:
- поддерживать чистоту и единый стиль кода;
- стандартизировать форматирование массивов, операторов, скобок, импортов и PHPDoc;
- исключать лишние импорты и автоматически выравнивать документацию.
| Совет: запуск Pint перед коммитом помогает всегда поддерживать код в правильном стиле, не тратя время на исправления после ревью. |
Статический анализ кода с PHPStan и Larastan
Для выявления ошибок и потенциальных багов в проектах на Laravel я использую комбинацию phpstan/phpstan и nunomaduro/larastan.
Эти инструменты выполняют статический анализ кода и помогают обнаруживать:
- неправильное использование типов;
- недостающие проверки;
- потенциальные ошибки на ранней стадии разработки.
Пример конфигурации phpstan.neon для проекта:
parameters:
level: 6
paths:
- app
- routes
excludePaths:
- vendor
- storage
- bootstrap
errorFormat: table
checkMissingVarTagTypehint: false
inferPrivatePropertyTypeFromConstructor: true
ignoreErrors:
- identifier: missingType.iterableValue
- identifier: missingType.generics
- '#referenced with incorrect case#'
includes:
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
Основные преимущества использования PHPStan + Larastan:
- ошибки выявляются ещё до запуска приложения;
- повышается стабильность и качество кода;
- интеграция в процесс разработки минимизирует риск появления багов в продакшене.
Автоматические проверки с Git Hooks и Shell-скриптами
Для поддержания качества кода я использую Git Hooks, которые автоматически проверяют код перед коммитом и пушем. Все проверки вынесены в отдельные shell-скрипты, что позволяет гибко настраивать их для разных проектов.
Основные подходы:
- Pre-commit: проверка изменённых файлов
- Проверяются только новые или изменённые файлы, что ускоряет процесс;
- Скрипты запускают Pint и PHPStan, автоматически исправляют стиль и выявляют ошибки;
- Если проблем нет, коммит продолжается без задержек.
- Постепенное исправление старых ошибок
- Для старых проектов скрипты проверяют, что количество ошибок в файле уменьшилось хотя бы на 1–2 по сравнению с предыдущим коммитом;
- Такой подход позволяет внедрять проверки без блокировки разработки.
- Проверка наличия тестов для классов
- Проверка работы Docker-сборки
| Совет: интегрируйте эти скрипты с самого начала проекта, чтобы автоматизация стала частью привычного рабочего процесса. |
Все актуальные скрипты и примеры можно посмотреть в репозитории:
https://github.com/prog-time/git-hooks
Shell скрипт для работы с PHPStan
Пример работы PHPStan
Более актуальная версия здесь.
#!/bin/bash
# ------------------------------------------------------------------------------
# Runs PHPStan analysis on PHP files with progressive error reduction.
# Accepts strictness mode ("strict" or default) and list of files as arguments.
# Tracks error counts per file in .phpstan-error-count.json baseline.
# In default mode: allows commit if errors decreased by at least 1.
# In strict mode: requires zero errors. Fails if error threshold exceeded.
# ------------------------------------------------------------------------------
# -----------------------------
# PARAMETERS
# -----------------------------
STRICTNESS="$1"
shift 1
FILES=("$@")
BASELINE_FILE=".phpstan-error-count.json"
BLOCK_COMMIT=0
# Initialize baseline if missing
if [ ! -f "$BASELINE_FILE" ]; then
echo "{}" > "$BASELINE_FILE"
fi
# -----------------------------
# CHECK IF FILES EXIST
# -----------------------------
if [ ${#FILES[@]} -eq 0 ]; then
echo "[PHPStan] No PHP files to check."
exit 0
fi
echo "[PHPStan] Checking ${#FILES[@]} files (strictness=$STRICTNESS)"
# -----------------------------
# LOOP THROUGH FILES
# -----------------------------
for FILE in "${FILES[@]}"; do
# Skip if file does not exist (safety check)
if [ ! -f "$FILE" ]; then
echo "File not found, skipping: $FILE"
continue
fi
echo "Checking: $FILE"
# Count current errors
ERR_NEW=$(vendor/bin/phpstan analyse --error-format=raw --no-progress "$FILE" 2>/dev/null | grep -c '^')
ERR_OLD=$(jq -r --arg file "$FILE" '.[$file] // empty' "$BASELINE_FILE")
if [ -z "$ERR_OLD" ]; then
echo "File not checked before. It has $ERR_NEW errors."
ERR_OLD=$ERR_NEW
fi
# Determine target errors
if [ "$STRICTNESS" = "strict" ]; then
TARGET=0
else
TARGET=$((ERR_OLD - 1))
[ "$TARGET" -lt 0 ] && TARGET=0
fi
# Compare and report
if [ "$ERR_NEW" -le "$TARGET" ]; then
echo "OK: was $ERR_OLD, now $ERR_NEW"
# Update baseline
jq --arg file "$FILE" --argjson errors "$ERR_NEW" '.[$file] = $errors' "$BASELINE_FILE" \
> "$BASELINE_FILE.tmp" && mv "$BASELINE_FILE.tmp" "$BASELINE_FILE"
else
echo "Too many errors: $ERR_NEW (must be <= $TARGET)"
vendor/bin/phpstan analyse --no-progress --error-format=table "$FILE"
# Keep old baseline
jq --arg file "$FILE" --argjson errors "$ERR_OLD" '.[$file] = $errors' "$BASELINE_FILE" \
> "$BASELINE_FILE.tmp" && mv "$BASELINE_FILE.tmp" "$BASELINE_FILE"
BLOCK_COMMIT=1
fi
echo "------------------"
done
# -----------------------------
# BLOCK COMMIT IF NEEDED
# -----------------------------
if [ "$BLOCK_COMMIT" -eq 1 ]; then
echo "Commit blocked. Reduce the number of errors according to strictness rules."
exit 1
fi
echo "[PHPStan] Check completed successfully."
exit 0
Shell скрипт для работы с Pint
Пример работы Pint
Более актуальная версия здесь.
#!/bin/bash
# ------------------------------------------------------------------------------
# Runs Laravel Pint code style checker on PHP files.
# Accepts list of file paths as arguments, filters only .php files.
# Auto-fixes style issues and stages corrected files for commit.
# Always exits with 0 (non-blocking hook).
# ------------------------------------------------------------------------------
if [ $# -eq 0 ]; then
echo "[Pint] No PHP files to check."
exit 0
fi
FILES=()
for f in "$@"; do
[[ "$f" == *.php ]] && FILES+=("$f")
done
if [ ${#FILES[@]} -eq 0 ]; then
echo "[Pint] No PHP files to check."
exit 0
fi
# -----------------------------
# Run Pint in test mode
# -----------------------------
vendor/bin/pint --test "${FILES[@]}"
RESULT=$?
if [ $RESULT -ne 0 ]; then
echo "Pint found code style issues. Auto-fixing..."
vendor/bin/pint "${FILES[@]}"
git add "${FILES[@]}"
echo "[Pint] Code style fixed automatically."
else
echo "[Pint] All files pass code style."
fi
exit 0
Проверка наличия тестов для классов
Для достижения этой цели я использую скрипт, который проверяет наличие тестов для каждого PHP-класса, добавленного или изменённого в коммите.
Скрипт получает список изменённых и добавленных PHP-файлов и ищет соответствующий тестовый файл в директории tests.
Например, если в проекте есть класс app/Services/UserService.php, скрипт потребует создать файл теста tests/Unit/Services/UserServiceTest.php. Таким образом, любой новый или изменённый класс обязательно должен иметь соответствующий тест, что помогает поддерживать качество и надёжность кода.
Более актуальная версия здесь.
#!/bin/bash
# ------------------------------------------------------------------------------
# Finds and validates unit test coverage for PHP classes.
# Checks if each modified PHP file has a corresponding unit test.
# Excludes common non-testable patterns (Controllers, Models, DTOs, etc.).
# Fails if any class is missing its expected test.
# ------------------------------------------------------------------------------
set -e
# -----------------------------
# CONFIG
# -----------------------------
EXCLUDE_PATTERNS=(
"*Test" "*Search" "*Controller*" "*Console*" "*Jobs*"
"*Models*" "*Resources*" "*Requests*" "*DTO*" "*Dtos*"
"*Kernel*" "*Middleware*" "*config*" "*ValueObject*"
"*Enum*" "*Exception*" "*Migration*" "*Seeder*"
"*MockDto*" "*api*" "*Providers*" "*Abstract*"
"*Rules*"
)
# -----------------------------
# Find project root
# -----------------------------
find_project_root() {
local current_dir="$PWD"
while [[ "$current_dir" != "/" ]]; do
if [[ -f "$current_dir/composer.json" ]]; then
echo "$current_dir"
return 0
fi
current_dir=$(dirname "$current_dir")
done
echo -e 'Laravel project root not found (composer.json missing)\n'
exit 1
}
# -----------------------------
# Determine if class should be tested
# -----------------------------
should_be_tested() {
local classname="$1"
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
# shellcheck disable=SC2053 # Glob matching is intentional
if [[ "$classname" == $pattern ]]; then
return 1
fi
done
return 0
}
# -----------------------------
# Extract class name with namespace from a PHP file
# -----------------------------
extract_classname_from_file() {
local file="$1"
if [[ ! -f "$file" && -n "$PROJECT_ROOT" ]]; then
file="$PROJECT_ROOT/$file"
fi
if [[ ! -f "$file" ]]; then
return 1
fi
local namespace
namespace=$(grep -m1 "^namespace " "$file" | sed 's/namespace \(.*\);/\1/' | tr -d ' ')
local classname
classname=$(grep -m1 "^class " "$file" | sed 's/class \([a-zA-Z0-9_]*\).*/\1/')
if [[ -n "$classname" ]]; then
if [[ -n "$namespace" ]]; then
echo -e "$namespace\\$classname"
else
echo -e "$classname"
fi
fi
}
# -----------------------------
# Find all test classes
# -----------------------------
find_test_classes() {
local project_root="$1"
find "$project_root/tests" -type f -name "*Test.php" 2>/dev/null |
while IFS= read -r file; do
extract_classname_from_file "$file"
done | sort -u
}
# -----------------------------
# Analyze coverage for a single class
# -----------------------------
analyze_coverage() {
local classname="$1"
shift
local test_classes=("$@")
[[ ! $(should_be_tested "$classname"; echo $?) -eq 0 ]] && return 0
local expected_test="Tests\\Unit\\${classname}Test"
local found=0
for test_class in "${test_classes[@]}"; do
test_class="$(echo "$test_class" | tr -d '\r\n')"
if [[ "$test_class" == "$expected_test" ]]; then
found=1
break
fi
done
if [[ $found -eq 0 ]]; then
echo -e "No found $expected_test"
return 1
fi
return 0
}
# -----------------------------
# Main
# -----------------------------
main() {
if [[ "$#" -eq 0 ]]; then
echo 'No PHP files changed — skipping'
exit 0
fi
PROJECT_ROOT=$(find_project_root)
TEST_CLASSES=()
while IFS= read -r line; do
[[ -n "$line" ]] && TEST_CLASSES+=("$line")
done < <(find_test_classes "$PROJECT_ROOT")
HAS_MISSING_TESTS=0
for file in "$@"; do
if [[ -z "$file" ]]; then
continue
fi
if [[ "${file##*.}" != "php" ]]; then
continue
fi
classname=$(extract_classname_from_file "$file")
if [[ -z "$classname" ]]; then
continue
fi
analyze_coverage "$classname" "${TEST_CLASSES[@]}" || HAS_MISSING_TESTS=1
done
if [[ $HAS_MISSING_TESTS -eq 1 ]]; then
echo -e "Some classes are missing tests! Failing CI."
exit 1
fi
exit 0
}
main "$@"
Запуск автотестов
Для своих проектов я использую Docker Compose сборки, поэтому и скрипт для запуска рассчитан на запуск внутри контейнера.
Более актуальная версия здесь.
#!/bin/bash
# ------------------------------------------------------------------------------
# Runs PHPUnit tests inside Docker container for modified PHP files.
# Accepts list of PHP file paths as arguments.
# Maps each file to its corresponding Unit test class and executes via artisan.
# Excludes non-testable patterns (Controllers, Models, DTOs, etc.).
# Fails if any test fails or required test class is missing.
# ------------------------------------------------------------------------------
set -e
COMPOSE_FILE="/home/project/docker-compose.yml"
SERVICE_NAME="app"
PROJECT_PATH="/var/www"
# -----------------------------
# CONFIG — list of exclusion patterns
# -----------------------------
EXCLUDE_PATTERNS=(
"*Test" "*Search" "*Controller*" "*Console*" "*Jobs*"
"*Models*" "*Resources*" "*Requests*" "*DTO*" "*Dtos*"
"*Kernel*" "*Middleware*" "*config*" "*ValueObject*"
"*Enum*" "*Exception*" "*Migration*" "*Seeder*"
"*MockDto*" "*api*" "*Providers*" "*Abstract*"
"*Rules*"
)
# -----------------------------
# Helpers
# -----------------------------
find_project_root() {
local current_dir="$PWD"
while [[ "$current_dir" != "/" ]]; do
if [[ -f "$current_dir/composer.json" ]]; then
echo "$current_dir"
return 0
fi
current_dir=$(dirname "$current_dir")
done
echo "Laravel project root not found (composer.json missing)"
exit 1
}
path_to_classname() {
local path="$1"
path="${path%.php}"
path="${path#app/}"
echo "${path//\//\\}"
}
should_be_tested() {
classname="$1"
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
# shellcheck disable=SC2053 # Glob matching is intentional
if [[ "$classname" == $pattern ]]; then
return 1
fi
done
return 0
}
get_expected_test_classname() {
local classname="$1"
echo "Tests\\Unit\\${classname}Test"
}
find_test_class_path() {
local test_classname="$1"
local project_root="$2"
local test_path="${test_classname//\\//}.php"
local full_path="$project_root/tests/${test_path#Tests/}" # убираем префикс "Tests"
if [[ -f "$full_path" ]]; then
echo "$full_path"
return 0
fi
return 1
}
run_test_for_class() {
local test_classname="$1"
local project_root="$2"
local test_file
test_file=$(find_test_class_path "$test_classname" "$project_root")
if [[ -z "$test_file" ]]; then
echo "Test file not found for: $test_classname"
return 1
fi
local classname
classname=$(basename "$test_file" .php)
echo "Running test: $test_classname"
echo "File: $classname"
if docker compose -f "$COMPOSE_FILE" exec -T "$SERVICE_NAME" sh -c "cd $PROJECT_PATH && php artisan test --filter='$classname'"; then
echo "Test passed: $test_classname"
return 0
else
echo "Test failed: $test_classname"
return 1
fi
}
analyze_and_run_tests() {
local app_file="$1"
local project_root="$2"
# Convert file path to class name
local normalized_classname
normalized_classname=$(path_to_classname "$app_file")
# Skip if class matches exclusion patterns
if ! should_be_tested "$normalized_classname"; then
echo "Class does not require testing: $normalized_classname"
echo "---"
return 0
fi
# Get expected test class
local expected_test
expected_test=$(get_expected_test_classname "$normalized_classname")
# Run the test
if run_test_for_class "$expected_test" "$project_root"; then
echo "---"
return 0
else
echo "---"
return 1
fi
}
# -----------------------------
# Main function — принимает список файлов
# -----------------------------
main() {
local files=("$@")
if [[ ${#files[@]} -eq 0 ]]; then
echo "[RunTests] No PHP files to test!"
exit 0
fi
local project_root
project_root=$(find_project_root)
local has_failures=0
for app_file in "${files[@]}"; do
[[ -z "$app_file" ]] && continue
! analyze_and_run_tests "$app_file" "$project_root" && has_failures=1
done
if [ "$has_failures" -eq 1 ]; then
echo "❗ One or more tests failed or are missing."
exit 1
else
echo "🎉 All tests for modified classes passed successfully!"
exit 0
fi
}
main "$@"
Итог
Автоматизация процесса разработки на Laravel позволяет значительно повысить эффективность команды и качество проекта.
Ключевые моменты, которые делают процесс максимально продуктивным:
- Настройка окружения через Docker Compose — быстрое и стабильное поднятие всех сервисов для разработки, тестирования и мониторинга.
- Автоматические проверки стиля кода (Pint) — поддержка единого PSR-12 стандарта без ручной работы.
- Статический анализ кода (PHPStan + Larastan) — выявление ошибок и потенциальных багов на ранних этапах разработки.
- Git Hooks и shell-скрипты — автоматическая проверка изменённых файлов, контроль наличия тестов и проверка работы Docker-сборки.
- Покрытие тестами — обязательное тестирование новых и изменённых классов повышает надёжность приложения.
Следуя этим практикам, вы сможете:
- сократить время на исправление ошибок;
- поддерживать единый стиль кода;
- обеспечить стабильность и предсказуемость работы приложения;
- ускорить внедрение новых функций.
В итоге автоматизация превращает рутинные задачи в прозрачный процесс, позволяя разработчикам сосредоточиться на создании полезного функционала и развитии проекта.