Migrating Legacy PHP 7 Apps to Laravel 10: A Performance & Security Deep Dive with Dockerized Deployments
Understanding the Migration Landscape: PHP 7 to Laravel 10
Migrating a legacy PHP 7 application to Laravel 10 is not merely an upgrade; it’s a strategic architectural shift. PHP 7, while robust, lacks the modern features, performance optimizations, and security paradigms inherent in Laravel 10 and its underlying PHP 8.x runtime. Key areas of focus during this transition include dependency management, framework-specific components, database interactions, and the overall application architecture. This deep dive will address performance bottlenecks, security vulnerabilities, and provide a practical, Docker-centric deployment strategy.
Pre-Migration Assessment: Identifying Pain Points
Before touching any code, a thorough audit of the existing PHP 7 application is paramount. This involves:
- Performance Profiling: Utilize tools like Xdebug’s profiler, Blackfire.io, or Tideways to pinpoint CPU-intensive functions, slow database queries, and excessive memory consumption.
- Dependency Audit: Document all Composer dependencies. Identify outdated packages that may not be compatible with PHP 8.x or Laravel 10, or those with known security vulnerabilities.
- Codebase Analysis: Scan for deprecated PHP functions, insecure coding practices (e.g., direct SQL injection vulnerabilities, improper input sanitization), and architectural anti-patterns. Tools like PHPStan or Psalm can automate much of this.
- Database Schema Review: Analyze query patterns and identify potential indexing issues or inefficient joins that might be exacerbated by framework changes.
- Infrastructure Assessment: Understand the current deployment environment. This will inform the Dockerization strategy.
Strategic Refactoring: Core Components and Dependencies
Laravel 10 leverages PHP 8.1+, introducing significant performance and feature enhancements. The migration strategy should prioritize adopting these where possible.
Dependency Management with Composer
The first step is updating your `composer.json` to target PHP 8.1+ and the latest Laravel versions. This often requires significant package updates or replacements.
Example `composer.json` (Targeting Laravel 10):
{
"name": "my-company/legacy-app-migration",
"description": "Migrated legacy application.",
"type": "project",
"require": {
"php": "^8.1|^8.2|^8.3",
"laravel/framework": "^10.0",
"laravel/sanctum": "^3.2",
"laravel/tinker": "^2.8",
"doctrine/dbal": "^3.6", // Example: If using Doctrine for DB abstraction
"league/flysystem-aws-s3-v3": "^3.13", // Example: S3 filesystem
"spatie/laravel-permission": "^5.8", // Example: Authorization
"ext-redis": "*", // Ensure Redis extension is available
"ext-json": "*", // Ensure JSON extension is available
"ext-openssl": "*", // Ensure OpenSSL extension is available
"ext-pdo": "*", // Ensure PDO extension is available
"ext-pdo_mysql": "*" // Ensure MySQL PDO driver is available
},
"require-dev": {
"faker/faker": "^1.21",
"laravel/breeze": "^1.19", // Or Jetstream for more features
"laravel/sail": "^1.21",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^7.0",
"phpunit/phpunit": "^10.1",
"phpstan/phpstan": "^1.10"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi"
],
"test": "@php vendor/bin/phpunit",
"phpstan": "@php vendor/bin/phpstan analyse --ansi"
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true
}
},
"minimum-stability": "stable",
"prefer-stable": true
}
Run composer update. Address any conflicts or deprecation warnings meticulously. If a direct upgrade isn’t possible for a critical package, investigate alternative packages or consider a phased rewrite of that specific functionality.
PHP 7 Deprecations and Laravel 10 Compatibility
PHP 8.x has removed many functions and features deprecated in PHP 7.x. Common issues include:
- `create_function()`: Replace with anonymous functions (closures).
- `each()`: Replace with
foreachloops. - `__autoload()`: Replace with Composer’s autoloader.
- String to number comparisons: PHP 8.x has stricter type juggling rules. Ensure explicit type casting where necessary.
- Error handling: Many warnings and notices are now thrown as exceptions. Update `try-catch` blocks accordingly.
Example: Handling Deprecated Error Reporting (if applicable):
// Legacy PHP 7 style (prone to warnings)
// set_error_handler(function ($errno, $errstr) {
// // Handle error
// });
// Modern PHP 8+ approach with Exceptions
try {
// Code that might throw exceptions
$result = some_operation();
} catch (Throwable $e) {
// Log or handle the exception
report($e);
// Optionally re-throw or return an error response
throw $e;
}
Performance Enhancements in Laravel 10 and PHP 8.x
Laravel 10, combined with PHP 8.x, offers substantial performance gains. Leveraging these requires conscious effort during migration.
Leveraging PHP 8.x JIT Compiler
The Just-In-Time (JIT) compiler in PHP 8.x can significantly speed up CPU-bound operations. While Laravel applications are often I/O bound, JIT can still provide benefits, especially in complex computations or long-running tasks. Ensure your `php.ini` is configured to enable it if your deployment environment supports it (e.g., custom Docker images, server configurations). For typical web requests, the impact might be marginal, but for background jobs or CLI tasks, it can be substantial.
; php.ini settings for JIT opcache.enable=1 opcache.enable_cli=1 opcache.jit=tracing ; or function for more aggressive optimization opcache.jit_buffer_size=128M
Database Query Optimization
Laravel’s Eloquent ORM is powerful but can be a source of performance issues if not used correctly. PHP 8.x’s improved type handling and Laravel 10’s optimizations contribute, but fundamental query optimization remains key.
1. Eager Loading: Always use eager loading (`with()`) to avoid the N+1 query problem.
// Inefficient: N+1 query problem
$users = User::all();
foreach ($users as $user) {
echo $user->posts->count(); // Executes a query for each user's posts
}
// Efficient: Eager loading
$users = User::with('posts')->get();
foreach ($users as $user) {
echo $user->posts->count(); // Only one query for users, one for all posts
}
2. Select Specific Columns: Avoid `SELECT *` when you only need a few columns.
// Inefficient
$users = User::all();
// Efficient
$users = User::select('id', 'name', 'email')->get();
3. Database Indexing: Ensure your database tables have appropriate indexes for columns used in `WHERE`, `JOIN`, and `ORDER BY` clauses. Use Laravel’s migrations for managing schema changes.
// Example migration for indexing
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->index('user_id');
$table->index('published_at');
});
}
public function down(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->dropIndex('posts_user_id_index');
$table->dropIndex('posts_published_at_index');
});
}
};
4. Query Builder vs. Eloquent: For highly complex or performance-critical queries, the Query Builder can sometimes be more performant as it bypasses some Eloquent overhead. However, Eloquent’s features often outweigh this for typical use cases.
Caching Strategies
Implement robust caching for frequently accessed, rarely changing data. Laravel provides excellent support for various cache backends (Redis, Memcached, file).
// Example: Caching configuration in config/cache.php
'default' => env('CACHE_DRIVER', 'redis'),
// Example: Using cache in application
use Illuminate\Support\Facades\Cache;
$users = Cache::remember('all_active_users', now()->addMinutes(60), function () {
return User::where('is_active', true)->get();
});
Security Hardening for Modern Applications
Security is a continuous process. Laravel 10 and PHP 8.x offer built-in protections, but vigilant development practices are essential.
Input Validation and Sanitization
Never trust user input. Laravel’s built-in validation is powerful and should be used extensively.
use Illuminate\Http\Request;
public function store(Request $request)
{
$validatedData = $request->validate([
'title' => 'required|string|max:255',
'body' => 'required|string',
'publish_date' => 'nullable|date',
'user_id' => 'required|exists:users,id',
]);
// $validatedData contains only the validated and cleaned data
Post::create($validatedData);
return redirect('/posts')->with('success', 'Post created successfully!');
}
For outputting data, always use Blade’s double curly braces `{{ $variable }}` which automatically escape HTML, or use the `e()` helper function.
// Blade template
<p>{!! $unsafeHtml !!}</p> // DANGEROUS: Renders raw HTML
<p>{{ $safeHtml }}</p> // SAFE: Escapes HTML
// In PHP code
echo e($potentiallyUnsafeString);
Authentication and Authorization
Laravel’s built-in authentication scaffolding (Breeze, Jetstream) provides a secure foundation. Ensure you’re using the latest versions and understand their security implications. For authorization, consider packages like `spatie/laravel-permission` for role-based access control (RBAC).
// Example using spatie/laravel-permission
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
// Create roles and permissions
$adminRole = Role::create(['name' => 'admin']);
$editPostsPermission = Permission::create(['name' => 'edit posts']);
$adminRole->givePermissionTo($editPostsPermission);
// Assign role to user
$user = User::find(1);
$user->assignRole('admin');
// Check permissions in controller or Blade
if ($user->can('edit posts')) {
// User can edit posts
}
@can('edit posts')
<a href="...">Edit Post</a>
@endcan
Dependency Security Scanning
Regularly scan your Composer dependencies for known vulnerabilities using tools like:
composer audit(built into Composer 2.x)- Snyk
- Dependabot (GitHub integration)
composer audit
Address any reported vulnerabilities promptly, prioritizing critical and high-severity issues.
Dockerized Deployment Strategy
Docker provides an isolated, consistent environment for development, testing, and production, simplifying the migration and deployment process.
`docker-compose.yml` for Development
A typical development setup includes PHP-FPM, Nginx, and a database (e.g., MySQL or PostgreSQL).
version: '3.8'
services:
nginx:
image: nginx:stable-alpine
container_name: legacy_app_nginx
ports:
- "8080:80"
volumes:
- ./docker/nginx/conf.d:/etc/nginx/conf.d
- .:/var/www/html # Mount application code
depends_on:
- php
networks:
- app-network
php:
build:
context: .
dockerfile: Dockerfile
args:
PHP_VERSION: 8.2 # Or your desired PHP version
container_name: legacy_app_php
volumes:
- .:/var/www/html # Mount application code
networks:
- app-network
mysql:
image: mysql:8.0
container_name: legacy_app_mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: legacy_db
MYSQL_USER: user
MYSQL_PASSWORD: password
ports:
- "3306:3306" # Optional: For direct DB access from host
volumes:
- db_data:/var/lib/mysql
networks:
- app-network
volumes:
db_data:
networks:
app-network:
driver: bridge
`Dockerfile` for PHP Service
This Dockerfile installs PHP and necessary extensions. Use an official PHP image as a base.
# Use a specific PHP version
ARG PHP_VERSION=8.2
FROM php:${PHP_VERSION}-fpm-alpine
LABEL maintainer="Antigravity <[email protected]>"
# Install system dependencies
RUN apk update && apk add --no-cache \
git \
zip \
unzip \
icu-dev \
libzip-dev \
libpng-dev \
libjpeg-turbo-dev \
freetype-dev \
libxml2-dev \
oniguruma-dev \
postgresql-dev \
libpq-dev \
supervisor \
acl \
libmcrypt-dev \
libzip \
libpng \
libjpeg-turbo \
freetype \
libxml2 \
oniguruma \
libmcrypt \
&& rm -rf /var/cache/apk/*
# Install PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd \
iconv \
intl \
zip \
pcntl \
sockets \
pdo \
pdo_mysql \
redis \
exif \
mbstring \
xml \
opcache
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
# Configure PHP.ini settings
COPY docker/php/php.ini /usr/local/etc/php/conf.d/custom.ini
COPY docker/php/php-fpm.conf /usr/local/etc/php-fpm.d/zz-custom.conf
# Install Supervisor for background processes (optional)
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
RUN mkdir -p /var/log/supervisor
# Set working directory
WORKDIR /var/www/html
# Expose port
EXPOSE 9000
# Default command
CMD ["/usr/bin/supervisord", "-n"]
Nginx Configuration (`docker/nginx/conf.d/default.conf`)
This configuration directs traffic to the PHP-FPM service.
server {
listen 80;
index index.php index.html;
server_name localhost;
root /var/www/html/public; # Laravel public directory
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_include fastcgi_params;
fastcgi_pass php:9000; # Service name and port from docker-compose.yml
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PHP_VALUE "upload_max_filesize = 100M \n post_max_size = 100M";
}
location ~ /\.ht {
deny all;
}
# Add headers for security
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "strict-origin-when-cross-origin";
# add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;"; # Example CSP, requires careful tuning
}
Production Deployment Considerations
For production, you’ll want a more robust setup:
- Separate Environments: Use different Docker Compose files or Kubernetes manifests for staging and production.
- CI/CD Pipeline: Automate builds, tests, and deployments.
- Managed Database: Use a managed database service (AWS RDS, Google Cloud SQL) instead of running MySQL in Docker.
- Load Balancing: Employ Nginx, HAProxy, or cloud provider load balancers.
- Container Orchestration: For scalability and resilience, use Kubernetes or AWS ECS.
- Secrets Management: Use tools like HashiCorp Vault, AWS Secrets Manager, or Kubernetes Secrets for sensitive information.
- Logging and Monitoring: Centralize logs (ELK stack, Grafana Loki) and monitor application performance (Prometheus, Datadog).
Post-Migration Testing and Monitoring
Thorough testing is non-negotiable. This includes:
- Unit Tests: Ensure existing unit tests are updated and new ones are written for refactored code.
- Integration Tests: Verify interactions between different components.
- End-to-End Tests: Use tools like Cypress or Selenium to simulate user interactions.
- Performance Testing: Use tools like ApacheBench (`ab`), k6, or JMeter to load test the application and compare performance against the PHP 7 baseline.
- Security Audits: Conduct penetration testing and vulnerability scans.
Implement comprehensive monitoring using tools like:
- Application Performance Monitoring (APM): New Relic, Datadog, Dynatrace.
- Error Tracking: Sentry, Bugsnag.
- Server Metrics: Prometheus, Grafana.
Continuously monitor logs and performance metrics to identify and address any regressions or new issues that arise post-migration.