Supercharging Laravel Development: The Power of Git Pre-commit Hooks!

Abdullah AlHabal
6 min readOct 3, 2024

As developers at TechPundits and TeknoClass, we understand that code quality isn’t just about writing features — it’s about building maintainable, scalable applications. In this guide, I’ll share how we’ve revolutionized our development workflow using Git pre-commit hooks in Laravel projects.

Why Pre-commit Hooks Matter 🤔

Before diving into implementation, let’s understand why pre-commit hooks are crucial in a modern Laravel development workflow:

  • Early Detection: Catch issues before they enter your codebase
  • Consistency: Ensure all team members follow the same standards
  • Automation: Eliminate manual code quality checks
  • Efficiency: Reduce code review friction
  • Architecture: Enforce architectural decisions automatically

The Complete Implementation Guide 🛠️

Let’s build a comprehensive pre-commit hook system that will transform your Laravel development workflow. We’ll break this down into manageable steps.

1. Setting Up the Basic Infrastructure

First, create your pre-commit hook file:

code .git/hooks/pre-commit

2. Essential Quality Checks

Our pre-commit hook performs several crucial checks:

Code Style and Syntax

  • PHP syntax validation
  • Laravel Pint formatting
  • PHP CS Fixer
  • Strict types declaration check

Static Analysis

  • PHPStan/Larastan analysis
  • Unused imports detection
  • Code coupling analysis

JavaScript Quality

  • ESLint validation
  • Prettier formatting

Environment and Configuration

  • .env file protection
  • Migration status verification
  • .gitignore validation

3. Directory Structure Validation

One of the most important aspects of maintaining a Laravel project is ensuring proper directory structure. Our hook validates:

required_directories=(
["app/Models"]="Model files"
["app/Services"]="Service files"
["app/Http/Controllers"]="Controller files"
["app/Http/Middleware"]="Middleware files"
["app/Http/Requests"]="Form requests"
["app/Http/Resources"]="API resources"
["app/Providers"]="Providers"
["app/Exceptions"]="Exceptions"
["database/migrations"]="Migrations"
["database/seeders"]="Seeders"
["database/factories"]="Factories"
["tests"]="Test files"
)

4. Enforcing Architectural Decisions

The hook ensures adherence to architectural patterns:

Service Layer Validation

  • Services must have corresponding interfaces
  • Proper interface implementation
  • Constructor dependency injection

File Location Rules

  • Models must be in app/Models
  • Services must be in app/Services
  • Proper migration naming format

5. Naming Convention Enforcement

Consistent naming is crucial for maintainable code:

  • Models: PascalCase (e.g., User.php)
  • Services: PascalCase with ‘Service’ suffix (e.g., UserService.php)
  • Interfaces: Service name with ‘Interface’ suffix (e.g., UserServiceInterface.php)

Installation and Setup

Required Dependencies

# PHP development tools
composer require --dev laravel/pint
composer require --dev phpstan/phpstan
composer require --dev friendsofphp/php-cs-fixer
composer require --dev nunomaduro/larastan
composer require --dev barryvdh/laravel-ide-helper
composer require --dev squizlabs/php_codesniffer
composer require --dev phpmd/phpmd

# JavaScript tools
npm install --save-dev eslint prettier

Team Integration

Create a setup script (setup-hooks.sh):

#!/bin/bash
cp hooks/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

Pro Tips for Maximum Efficiency 💡

1. Performance Optimization

  • Only check staged files
  • Use parallel processing where possible
  • Implement caching for repeated checks

2. Customization for Your Team

  • Add project-specific naming conventions
  • Implement team-specific code style rules
  • Create custom validation patterns

3. Handling Exceptions

Sometimes you need to bypass checks:

git commit -m "Emergency fix" --no-verify

Common Issues and Solutions 🔧

1. Permission Issues

chmod +x .git/hooks/pre-commit

. Hook Not Running

  • Verify the file is executable
  • Check the path is correct
  • Ensure bash is available

3. False Positives

  • Adjust rule severity levels
  • Add specific exclusions where necessary
  • Document exceptions in your team’s coding standards

Conclusion 🎯

Git pre-commit hooks are more than just a development tool — they’re a fundamental part of maintaining code quality in Laravel projects. By automating these checks, we free up developers to focus on what matters: building great features.

What’s Next?

Consider extending your pre-commit hooks to include:

  • Security vulnerability scanning
  • Performance impact analysis
  • Documentation coverage checks
  • Test coverage verification

Remember: The goal isn’t to create barriers to committing code — it’s to ensure that when code is committed, it meets your team’s standards for quality and maintainability.

Let’s keep coding! 😉👨🏻‍💻

If you found this article helpful, follow me for more Laravel and PHP development tips. Check out my other articles on clean architecture and Laravel best practices!

#Laravel #PHP #GitHooks #CleanCode #WebDevelopment #CodeQuality #SoftwareEngineering

#!/bin/bash

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# Exit when any command fails
set -e

echo -e "${YELLOW}Running pre-commit hooks...${NC}"

# Store the staged files
STAGED_PHP_FILES=$(git diff --cached --name-only --diff-filter=d | grep ".php$" | tr '\n' ' ')
STAGED_JS_FILES=$(git diff --cached --name-only --diff-filter=d | grep ".js$" | tr '\n' ' ')

# Function to display error
display_error() {
echo -e "${RED}❌ $1${NC}"
exit 1
}

# Function to check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}

# Directory Structure Validation
validate_directory_structure() {
echo "📁 Validating project structure..."

declare -A required_directories=(
["app/Models"]="Model files (*.php)"
["app/Services"]="Service files (*Service.php)"
["app/Http/Controllers"]="Controller files (*Controller.php)"
["app/Http/Middleware"]="Middleware files (*.php)"
["app/Http/Requests"]="Form request files (*Request.php)"
["app/Http/Resources"]="API resource files (*Resource.php)"
["app/Providers"]="Provider files (*Provider.php)"
["app/Exceptions"]="Exception files (*.php)"
["database/migrations"]="Migration files"
["database/seeders"]="Seeder files (*Seeder.php)"
["database/factories"]="Factory files (*Factory.php)"
["tests"]="Test files (*Test.php)"
)

for dir in "${!required_directories[@]}"; do
if [ ! -d "$dir" ]; then
display_error "Missing required directory: $dir (${required_directories[$dir]})"
fi
done
}

# File Location Validation
validate_file_locations() {
echo "📍 Validating file locations..."

# Check Models location
find app/ -name "*.php" -not -path "app/Models/*" | while read file; do
if grep -q "^class.*extends.*Model" "$file"; then
display_error "Found model outside app/Models directory: $file"
fi
done

# Check Services location
find app/ -name "*Service.php" -not -path "app/Services/*" | while read file; do
display_error "Found service outside app/Services directory: $file"
done

# Check Migration naming
find database/ -name "*.php" -path "database/migrations/*" | while read file; do
if ! [[ $(basename "$file") =~ ^[0-9]{4}_[0-9]{2}_[0-9]{2}_[0-9]{6}_.*\.php$ ]]; then
display_error "Invalid migration filename format: $file"
fi
done
}

# Naming Convention Validation
validate_naming_conventions() {
echo "✍️ Validating naming conventions..."

# Service naming convention
find app/Services -type f -name "*.php" | while read file; do
if [[ $(basename "$file") =~ \. ]]; then
display_error "Invalid service class filename (contains dots): $file"
fi

if ! [[ $(basename "$file") =~ ^[A-Z][a-zA-Z]+Service\.php$ ]]; then
display_error "Invalid service class naming convention: $file (Should be PascalCase ending with 'Service.php')"
fi
done

# Model naming convention
find app/Models -type f -name "*.php" | while read file; do
if ! [[ $(basename "$file") =~ ^[A-Z][a-zA-Z]+\.php$ ]]; then
display_error "Invalid model naming convention: $file (Should be PascalCase)"
fi
done
}

# Check for unnecessary files
check_unnecessary_files() {
echo "🧹 Checking for unnecessary files..."

unnecessary_patterns=(
"*.log"
"*.swap"
"*.swo"
"*.swp"
"*~"
".DS_Store"
"Thumbs.db"
".idea/"
".vscode/"
"*.tmp"
"*.temp"
"*.cache"
"npm-debug.log"
"yarn-debug.log"
"yarn-error.log"
)

for pattern in "${unnecessary_patterns[@]}"; do
found_files=$(find . -name "$pattern" -not -path "./vendor/*" -not -path "./node_modules/*")
if [ ! -z "$found_files" ]; then
echo "Found unnecessary files:"
echo "$found_files"
display_error "Please remove unnecessary files"
fi
done
}

# Service Layer Architecture Validation
validate_service_architecture() {
echo "🏛️ Validating service architecture..."

find app/Services -type f -name "*Service.php" | while read file; do
# Check for interface
service_name=$(basename "$file" .php)
interface_path="app/Contracts/Services/${service_name}Interface.php"
if [ ! -f "$interface_path" ]; then
display_error "Missing interface for service: $service_name (Expected at: $interface_path)"
fi

# Check interface implementation
if ! grep -q "implements.*${service_name}Interface" "$file"; then
display_error "Service $service_name doesn't implement its interface"
fi

# Check dependency injection
if ! grep -q "__construct" "$file"; then
echo -e "${YELLOW}⚠️ Warning: Service $service_name might be missing dependency injection${NC}"
fi
done
}

# Run base checks
if [ ! -f "artisan" ]; then
display_error "Not a Laravel project root directory!"
fi

# Run PHP syntax check
echo "🔍 Checking PHP syntax..."
for FILE in $STAGED_PHP_FILES; do
php -l "$FILE" >/dev/null 2>&1 || display_error "PHP syntax error in $FILE"
done

# Check strict types declaration
echo "🔍 Checking strict types declaration..."
for FILE in $STAGED_PHP_FILES; do
if ! grep -q "declare(strict_types=1);" "$FILE"; then
display_error "Strict types declaration missing in $FILE"
fi
done

# Run Laravel Pint
if command_exists "./vendor/bin/pint"; then
echo "🎨 Running Laravel Pint..."
./vendor/bin/pint --test || display_error "Laravel Pint found issues"
fi

# Run PHP CS Fixer
if command_exists "./vendor/bin/php-cs-fixer"; then
echo "🧹 Running PHP CS Fixer..."
./vendor/bin/php-cs-fixer fix --dry-run --diff || display_error "PHP CS Fixer found issues"
fi

# Run PHPStan/Larastan
if command_exists "./vendor/bin/phpstan"; then
echo "🔍 Running PHPStan..."
./vendor/bin/phpstan analyse --level=max || display_error "PHPStan found issues"
fi

# Run ESLint for JavaScript
if [ ! -z "$STAGED_JS_FILES" ] && command_exists "eslint"; then
echo "🔍 Running ESLint..."
./node_modules/.bin/eslint $STAGED_JS_FILES || display_error "ESLint found issues"
fi

# Run Prettier for JavaScript
if [ ! -z "$STAGED_JS_FILES" ] && command_exists "prettier"; then
echo "🎨 Running Prettier..."
./node_modules/.bin/prettier --check $STAGED_JS_FILES || display_error "Prettier found formatting issues"
fi

# Check .env file protection
if git diff --cached --name-only | grep -q ".env$"; then
display_error ".env file cannot be committed"
fi

# Check migration status
echo "📊 Checking migration status..."
php artisan migrate:status || display_error "Migration status check failed"

# Generate IDE Helper files
if command_exists "./vendor/bin/ide-helper"; then
echo "🔧 Generating IDE Helper files..."
php artisan ide-helper:generate
php artisan ide-helper:meta
php artisan ide-helper:models -N
fi

# Run all structural validations
validate_directory_structure
validate_file_locations
validate_naming_conventions
check_unnecessary_files
validate_service_architecture

# Add modified files back to staging
if [ -n "$STAGED_PHP_FILES" ]; then
git add $STAGED_PHP_FILES
fi
if [ -n "$STAGED_JS_FILES" ]; then
git add $STAGED_JS_FILES
fi

echo -e "${GREEN}✅ All pre-commit hooks passed!${NC}"
exit 0

--

--

Abdullah AlHabal

Junior Backend Software Engineer | Laravel, PHP, NestJS | API Design | MySQL, PostgreSQL | Docker | Web Technologies Enthusiast