Skip to main content

Command Palette

Search for a command to run...

Laravel Automation: How to Make Your Development Process Fast and Reliable

Updated
9 min read

Laravel development reaches its peak efficiency when automation is applied at every stage — from environment setup to code quality checks and testing. In this article, I'll show you how to build a development workflow that reduces manual work, improves code quality, and speeds up feature delivery.

This material is aimed at developers with Laravel experience who want to introduce automated checks, static analysis, unified code style, and a ready-made Docker Compose setup for quick project starts.


Docker Compose Setup

Every Laravel project I work on starts with a carefully configured Docker Compose setup. This immediately gives you a fully functional and isolated environment for development, testing, and monitoring. This approach minimizes dependency conflicts, speeds up project bootstrapping, and ensures stable operation of all services.

My typical setup includes the following services:

Service Purpose
php-fpm Handles PHP application requests
PostgreSQL Relational database
Grafana Metrics and log visualization
Loki Centralized logging
pgAdmin Web interface for managing PostgreSQL
Redis Laravel cache and queues
RedisInsight Redis web monitoring
Queue Background task processing via php artisan queue:work

Each service runs in its own container, which allows you to:

  • flexibly configure the environment

  • manage dependencies independently

  • update components without downtime for other services

Tip: For a quick start, you can use a ready-made docker-compose.yml that brings up all services in an isolated environment and configures the basic Laravel parameters out of the box.

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 with Laravel Pint

To enforce PSR-12 standards in Laravel projects, I use the laravel/pint package. This tool performs static code analysis and automatically formats PHP files according to configured rules.

Pint integrates into the development workflow and allows you to:

  • run checks on commits

  • automatically fix formatting issues

  • quickly bring code in line with a unified style

Tip: Running Pint before each commit ensures your code is always properly styled without wasting time fixing things after a review.

Example pint.json configuration:

{
    "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"
        }
    }
}

This configuration standardizes formatting of arrays, operators, braces, imports, and PHPDoc blocks, removes unused imports, and automatically aligns documentation.


Static Analysis with PHPStan and Larastan

To catch errors and potential bugs early, I combine phpstan/phpstan and nunomaduro/larastan. These tools perform static analysis and help detect:

  • incorrect type usage

  • missing checks

  • potential bugs at an early stage of development

Errors are caught before the application even runs, code stability improves, and integrating this into the workflow minimizes the risk of bugs reaching production.

Example phpstan.neon configuration:

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

Automated Checks with Git Hooks and Shell Scripts

To maintain code quality, I use Git Hooks that automatically check code before commits and pushes. All checks are extracted into separate shell scripts, making them easy to adapt for different projects.

Tip: Integrate these scripts from the very beginning of the project so that automation becomes a natural part of the workflow.

All scripts and examples are available in the repository: https://github.com/prog-time/git-hooks

1. Pre-commit: checking changed files

Only new or modified files are checked, which speeds things up. The scripts run Pint and PHPStan, automatically fix style issues, and catch errors. If everything is clean, the commit proceeds without delay.

2. Gradual cleanup of existing errors

For legacy projects, the scripts verify that the error count in a file has decreased by at least 1–2 compared to the previous commit. This lets you introduce checks without blocking ongoing development.

3. Checking for test coverage of classes

4. Validating the Docker build


Shell Script for PHPStan

#!/bin/bash
 
COMMAND="$1"  # commit or push
 
# CHECK NEW FILES
if [ "$COMMAND" = "commit" ]; then
    NEW_FILES=\((git diff --cached --name-only --diff-filter=A | grep '\.php\)')
 
    if [ -z "$NEW_FILES" ]; then
        echo "✅ No new PHP files. Skipping PHPStan check for new files."
    else
        echo "🔍 Running PHPStan on new files only..."
        ./vendor/bin/phpstan analyse --no-progress --error-format=table $NEW_FILES
        if [ $? -ne 0 ]; then
          echo "❌ NEW FILES! PHPStan found type errors (REQUIRED FIX)"
          exit 1
        fi
    fi
fi
 
# CHECK MODIFIED FILES
 
BASELINE_FILE=".phpstan-error-count.json"
BLOCK_COMMIT=0
 
if [ "$COMMAND" = "commit" ]; then
    ALL_FILES=\((git diff --cached --name-only --diff-filter=ACM | grep '\.php\)' || true)
elif [ "$COMMAND" = "push" ]; then
    BRANCH=$(git rev-parse --abbrev-ref HEAD)
    ALL_FILES=\((git diff --name-only origin/\)BRANCH --diff-filter=ACM | grep '\.php$' || true)
else
    echo "Unknown command: $COMMAND"
    exit 1
fi
 
if [ ! -f "$BASELINE_FILE" ]; then
    echo "{}" > "$BASELINE_FILE"
fi
 
if [ -z "$ALL_FILES" ]; then
  echo "✅ [PHPStan] No PHP files to check."
  exit 0
fi
 
echo "🔍 [PHPStan] Checking files..."
 
for FILE in $ALL_FILES; do
    echo "📄 Checking: $FILE"
 
    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 previously checked. It has $ERR_NEW errors."
        ERR_OLD=$ERR_NEW
    fi
 
    TARGET=$((ERR_OLD - 1))
    [ "$TARGET" -lt 0 ] && TARGET=0
 
    if [ "\(ERR_NEW" -le "\)TARGET" ]; then
        echo "✅ Improved: was \(ERR_OLD, now \)ERR_NEW"
        jq --arg file "\(FILE" --argjson errors "\)ERR_NEW" '.[\(file] = \)errors' "\(BASELINE_FILE" > "\)BASELINE_FILE.tmp" && mv "\(BASELINE_FILE.tmp" "\)BASELINE_FILE"
    else
        echo "❌ Errors: \(ERR_NEW (required ≤ \)TARGET)"
        vendor/bin/phpstan analyse --no-progress --error-format=table "$FILE"
        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
 
if [ "$BLOCK_COMMIT" -eq 1 ]; then
    echo "⛔ Commit blocked. Reduce the number of errors compared to the previous version."
    exit 1
fi
 
echo "✅ [PHPStan] Check completed successfully."
 
exit 0

Shell Script for Pint

#!/bin/bash
 
COMMAND="$1"  # commit or push
 
if [ "$COMMAND" = "commit" ]; then
    ALL_FILES=\((git diff --cached --name-only --diff-filter=ACM | grep '\.php\)' || true)
elif [ "$COMMAND" = "push" ]; then
    BRANCH=$(git rev-parse --abbrev-ref HEAD)
    ALL_FILES=\((git diff --name-only origin/\)BRANCH --diff-filter=ACM | grep '\.php$' || true)
else
    echo "Unknown command: $COMMAND"
    exit 1
fi
 
if [ -z "$ALL_FILES" ]; then
  echo "✅ [Pint] No PHP files to check."
  exit 0
fi
 
echo "🔍 [Pint] Checking code style..."
 
vendor/bin/pint --test $ALL_FILES
 
RESULT=$?
 
if [ $RESULT -ne 0 ]; then
  echo "❌ Pint found issues. Auto-fixing..."
  vendor/bin/pint $ALL_FILES
  echo "$ALL_FILES" | xargs git add
  echo "✅ [Pint] Code style fixed. Please re-run your commit."
  exit 1
fi
 
echo "✅ [Pint] All clean."
exit 0

Checking for Test Coverage

To enforce this, I use a script that checks whether a test exists for every PHP class added or modified in a commit. The script gets the list of changed files and looks for a corresponding test file in the tests directory.

For example, if the project contains app/Services/UserService.php, the script will require a test file at tests/Unit/Services/UserServiceTest.php. Every new or modified class must have a corresponding test — this helps maintain code quality and reliability.

The latest version of this script is available at: https://github.com/prog-time/git-hooks


Validating the Docker Build

It's equally important to regularly verify that the Docker build works. A dedicated shell script restarts all containers and checks that they started successfully, ensuring that configuration or code changes haven't broken any services.

#!/bin/bash
 
echo "=== Stopping all containers ==="
docker-compose down
 
echo "=== Building containers ==="
docker-compose build
 
echo "=== Starting containers in background ==="
docker-compose up -d
 
echo "=== Waiting 5 seconds for services to start ==="
sleep 5
 
echo "=== Checking container status ==="
STATUS=$(docker-compose ps --services --filter "status=running")
 
if [ -z "$STATUS" ]; then
  echo "Error: no containers are running!"
  exit 1
else
  echo "Running containers:"
  docker-compose ps
fi
 
echo "=== Checking HEALTH status ==="
docker ps --filter "health=unhealthy" --format "table {{.Names}}\t{{.Status}}"
 
echo "=== Script complete ==="
 
exit 0

Summary

Automating the Laravel development process significantly improves team efficiency and project quality. The key practices:

  • Docker Compose environment setup — fast and stable launch of all services for development, testing, and monitoring.

  • Automated code style checks (Pint) — enforcing a unified PSR-12 standard without manual effort.

  • Static code analysis (PHPStan + Larastan) — catching errors and potential bugs early in development.

  • Git Hooks and shell scripts — automatic checking of changed files, verifying test coverage, and validating the Docker build.

  • Test coverage — mandatory testing of new and modified classes improves application reliability.

Following these practices lets you reduce time spent fixing bugs, maintain a consistent code style, ensure application stability, and speed up feature delivery. Automation turns routine tasks into a transparent process — freeing developers to focus on building useful functionality and growing the project.