Laravel Automation: How to Make Your Development Process Fast and Reliable
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.ymlthat 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.