Supercharging Laravel Development: The Power of Git Pre-commit Hooks!
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