Автоматизация Laravel: как сделать процесс разработки быстрым и надежным

Разработка на 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-скрипты, что позволяет гибко настраивать их для разных проектов.

Основные подходы:

  1. Pre-commit: проверка изменённых файлов
    • Проверяются только новые или изменённые файлы, что ускоряет процесс;
    • Скрипты запускают Pint и PHPStan, автоматически исправляют стиль и выявляют ошибки;
    • Если проблем нет, коммит продолжается без задержек.
  2. Постепенное исправление старых ошибок
    • Для старых проектов скрипты проверяют, что количество ошибок в файле уменьшилось хотя бы на 1–2 по сравнению с предыдущим коммитом;
    • Такой подход позволяет внедрять проверки без блокировки разработки.
  3. Проверка наличия тестов для классов
  4. Проверка работы 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-сборки.
  • Покрытие тестами — обязательное тестирование новых и изменённых классов повышает надёжность приложения.

Следуя этим практикам, вы сможете:

  • сократить время на исправление ошибок;
  • поддерживать единый стиль кода;
  • обеспечить стабильность и предсказуемость работы приложения;
  • ускорить внедрение новых функций.

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