| name | legacy-modernization |
| description | Modernize legacy Laravel/PHP code. Strangler fig pattern, incremental upgrades, backward compatibility. Use when upgrading old Laravel versions or modernizing legacy patterns. |
Legacy Modernization
Safely modernize legacy code without breaking existing functionality.
When to Use
- Upgrading Laravel versions (5.x โ 11.x)
- Migrating from old PHP patterns
- Replacing deprecated code
- Modernizing architecture
- Adding tests to legacy code
Core Principle
Never break what's working. Use incremental changes with backward compatibility.
1. The Strangler Fig Pattern
Replace legacy code gradually, not all at once.
Strategy
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Your Application โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โ
โ โ Legacy โ โโ โ New Component โ โ
โ โ Code โ โ (Facade/API) โ โ
โ โโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โ
โ โ โ โ
โ (Gradually (New code โ
โ deprecated) takes over) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Implementation
class PaymentFacade
{
public function __construct(
private LegacyPaymentProcessor $legacy,
) {}
public function process(Order $order): PaymentResult
{
return $this->legacy->doPayment($order->toArray());
}
}
class ModernPaymentService implements PaymentServiceInterface
{
public function process(Order $order): PaymentResult
{
}
}
class PaymentFacade
{
public function process(Order $order): PaymentResult
{
if (Feature::active('modern-payments')) {
return $this->modern->process($order);
}
return $this->legacy->doPayment($order->toArray());
}
}
2. Laravel Version Upgrades
Pre-Upgrade Checklist
Upgrade Process
php artisan cache:clear
php artisan config:clear
php artisan route:clear
php artisan view:clear
composer clear-cache
composer update
php artisan vendor:publish --tag=laravel-assets --force
php artisan migrate
php artisan test
Common Breaking Changes
| From | To | Change |
|---|
| 5.x โ 8.x | Middleware | handle($request, Closure $next) โ add Response return type |
| 8.x โ 9.x | PHP 8.0 | Constructor property promotion available |
| 9.x โ 10.x | PHP 8.1 | Enums, readonly properties |
| 10.x โ 11.x | PHP 8.2 | Minimal skeleton structure |
3. Modernizing PHP Patterns
Old: Arrays for DTOs
$user = [
'name' => 'John',
'email' => 'john@example.com',
];
class CreateUserDTO
{
public function __construct(
public readonly string $name,
public readonly string $email,
) {}
}
Old: No Type Hints
function calculateTotal($items, $tax) {
}
function calculateTotal(Collection $items, float $taxRate): Money
{
}
Old: Static Helpers
class Helper
{
public static function formatMoney($amount) { ... }
}
class MoneyFormatter
{
public function __construct(
private LocaleService $locale,
) {}
public function format(Money $amount): string { ... }
}
Old: Global State
function getApiUrl() {
global $config;
return $config['api_url'];
}
class ApiClient
{
public function __construct(
private string $baseUrl,
) {}
}
$this->app->bind(ApiClient::class, fn () => new ApiClient(
config('services.api.url')
));
4. Adding Tests to Legacy Code
Characterization Tests
Capture current behavior (even if buggy) before refactoring.
class LegacyOrderService
{
public function calculateTotal($order)
{
}
}
test('legacy order total calculation', function () {
$service = new LegacyOrderService();
$result = $service->calculateTotal([
'items' => [
['price' => 100, 'qty' => 2],
['price' => 50, 'qty' => 1],
],
'discount' => 10,
]);
expect($result)->toBe(240.0);
});
Approval Testing
test('legacy report generation', function () {
$report = (new LegacyReportService())->generate();
expect($report)->toMatchSnapshot();
});
5. Feature Flags for Safe Rollout
Implementation
return [
'modern_checkout' => env('FEATURE_MODERN_CHECKOUT', false),
'new_search' => env('FEATURE_NEW_SEARCH', false),
];
class Feature
{
public static function active(string $feature): bool
{
return config("features.{$feature}", false);
}
}
if (Feature::active('modern_checkout')) {
return $this->modernCheckout->process($order);
}
return $this->legacyCheckout->process($order);
Gradual Rollout
class Feature
{
public static function active(string $feature, ?User $user = null): bool
{
$config = config("features.{$feature}");
if (is_bool($config)) {
return $config;
}
if (is_int($config) && $user) {
return ($user->id % 100) < $config;
}
return false;
}
}
'modern_checkout' => 25,
6. Database Migrations for Legacy
Add Without Breaking
Schema::table('users', function (Blueprint $table) {
$table->string('full_name')->nullable();
});
User::query()->lazyById()->each(function ($user) {
$user->update(['full_name' => $user->name]);
});
Dual-Write Pattern
class User extends Model
{
public function setNameAttribute($value)
{
$this->attributes['name'] = $value;
$this->attributes['full_name'] = $value;
}
public function getNameAttribute()
{
return $this->full_name ?? $this->attributes['name'];
}
}
7. Rollback Plan
Always Have Escape Route
git tag pre-modernization-v1
Database Rollback
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('full_name');
});
}
8. Modernization Checklist
Phase 1: Prepare
Phase 2: Facade
Phase 3: Implement
Phase 4: Migrate
Phase 5: Cleanup
Remember: Legacy code is code that works. Modernize to make it better, not to prove you can.