A PHP upgrade is successful only if the application still does the work its users depend on. A green dependency installation does not prove that search finds the right records, a translated page keeps its address, or an uploaded image survives the next release. Modernization therefore begins with observable behaviour and operational constraints, not with replacing the framework.
DigiSpace provides a concrete example: a Laravel application with a Blade public site, database-backed localized content and a Filament administration panel. The repository also retains Inertia and Vue dependencies. That does not mean every admin screen uses the same stack. An accurate assessment traces the routes and views actually used instead of treating the dependency list as a complete architecture map.
The intended business result is a sequence of changes that can be checked independently. A search regression test protects catalogue behaviour; a content update preserves editorial fields it does not own; and a deployment check verifies the resulting page and asset. These controls make a change reviewable, but do not prove that every release is reversible.
Establish the version and integration baseline
The inspected DigiSpace manifest requires PHP ^8.3, Laravel ^13.0 and PHPUnit ^11.0. Those are dependency constraints, not a statement that every environment is running an identical patch version. Before an upgrade, record the installed packages, PHP runtime, extensions, database engine and worker configuration on the actual target.
Inventory the application's external boundaries too. DigiSpace includes storage, monitoring, CRM and reCAPTCHA dependencies. A runtime change may leave ordinary pages working while an outbound API request, uploaded object or scheduled command fails. The baseline should identify who owns each integration and how it can be exercised without sending real customer data or creating duplicate business records.
The official PHP 8.2-to-8.3 migration guide separates new features from incompatible and deprecated behaviour. It covers that particular version step. An application starting on an earlier PHP release needs the intervening guides as well. PHP 8.3 is the project's minimum constraint here, not a universal recommendation for a new system.
Compare three modernization approaches
An in-place upgrade keeps the current architecture and adjusts incompatible dependencies and code. It has the smallest product change when the structure still fits the business. Its limitation is that it may preserve coupling that makes subsequent features expensive. Version support and architecture quality are related concerns, but not the same task.
Incremental replacement introduces a boundary around one workflow and changes that workflow while the rest remains in use. Martin Fowler's Strangler Fig explanation describes this approach and its transitional cost. Temporary adapters and routing rules are real work; they need ownership and eventual removal rather than becoming another permanent layer.
A full rewrite may make sense when the product or data model is fundamentally changing. It also creates a large acceptance and migration problem: undocumented behaviour must be discovered, old and new systems may need parallel support, and data must cross a clearly defined cutover. None of those obligations disappear because the replacement uses a newer framework.
Start with a real user-visible boundary
DigiSpace's service search is a useful bounded example. The application stores both categorized catalogue services and internal feature rows associated with pricing packages. Search should return the former when they match. It should not advertise a feature row that has no public service category.
HeaderSearchTest captures that distinction. The following is the actual test method, formatted across lines for readability. It belongs to the repository's test class, which imports the two models, uses RefreshDatabase and SeedsPublicSite, and calls seedPublicSite() during setup. It is not a standalone PHP script:
public function test_service_search_lists_matching_categorised_services(): void
{
$category = ServiceCategory::create(['name' => 'Web', 'slug' => 'web']);
Service::create([
'title' => 'Laravel development',
'slug' => 'laravel-development',
'status' => 'active',
'service_category_id' => $category->id,
'description' => 'Backend'
]);
Service::create([
'title' => 'Orphaned laravel service',
'slug' => 'orphan',
'service_category_id' => null,
'description' => 'no category'
]);
$this->get('/uk/service-search?search=laravel')
->assertOk()
->assertSee('Laravel development')
->assertDontSee('Orphaned laravel service');
}
The route is /uk/service-search, with the query parameter search. It is not /en/services/search. The records use Service::create(); this example does not invent a factory that the model does not provide. These details determine whether a reader can reproduce the behaviour in this repository.
The controller applies both an active-status condition and whereHas('serviceCategory'). This test verifies a particular rendered outcome under its fixtures. To prove category filtering independently of every other condition, an additional fixture should explicitly mark the orphaned row active. Separate tests should cover an empty query, no matches and localized content. A single passing example must not be described as complete search coverage.
Keep test isolation separate from application data
RefreshDatabase can reset schema in the resolved test database. Do not run the example against a populated local application or production database. Check the effective environment, connection, hostname and database name first; a cached Laravel configuration can undermine assumptions about which environment variables are in use.
DigiSpace uses MySQL-specific behaviour elsewhere in its public-site queries, so replacing MySQL with SQLite merely to simplify a run can hide compatibility issues. Use a dedicated MySQL test database for the repository's database-backed suite, with fake external integrations where needed. Any reset or cleanup should be scoped to that verified test target.
Once those prerequisites are satisfied, the focused suite target is tests/Feature/HeaderSearchTest.php. The expected outcome is not just HTTP 200: the matching catalogue title appears and the orphaned title does not. A failed assertion should lead back to routing, fixtures or query conditions rather than immediately weakening the test.
Separate code changes from editorial data
A repository can contain seed content without owning every current database value. DigiSpace's JSON-backed fresh-install seeders insert rows, whereas StructureTranslationsSeeder matches existing records and fills missing translations. Existing non-empty editorial translations win. Neither mechanism is a general-purpose publisher for a revised article.
This distinction explains why deploying an updated JSON file may leave a live page unchanged. Conversely, replaying fresh-install seeders into a populated database can fail or affect unrelated state. A targeted content change should identify records by stable slug, restrict the fields it changes, preserve authors and images unless explicitly in scope, and take a recoverable snapshot beforehand.
For modernization, the same rule applies to migrations: know which data a change owns. Avoid combining a runtime upgrade, a schema redesign and broad content replacement in one release. When a result is wrong, smaller changes make it possible to identify which operation introduced the difference.
Check deployment results, not just build results
DigiSpace's deployment hook runs migrations, publishes Livewire assets, rebuilds configuration and view caches, and generates the sitemap. That is concrete repository behaviour. It is not proof that every migration can be undone, or that restoring a previous code directory restores database content as well.
Define code rollback and data recovery separately. A previous release may no longer understand a changed schema. A database restore may discard edits made after the backup. For schema evolution, prefer compatible staged changes when feasible; for content, preserve a field-level snapshot and check for intervening edits before restoring it.
Public assets need an independent check. A correct og:image tag can still point to a missing file. Verify the HTTP result, content type, image dimensions and visible image rather than treating a metadata assertion as a complete social-preview test. For localized pages, inspect canonical links and actual navigation destinations as well as translated headings.
What this proves, and when not to copy it
The repository gives evidence of a targeted search test, explicit public query constraints and separate deployment and translation mechanisms. It does not provide a measured before-and-after reduction in outages or development cost. The defensible benefit is narrower: important behaviours can be named and checked instead of being rediscovered manually after each change.
Do not turn incremental modernization into endless patching when the underlying product is being replaced. Do not introduce a service layer around every short function without a concrete boundary to protect. And do not begin a broad upgrade when backups, runtime visibility and an isolated verification environment are still missing.
For a bounded engagement, see PHP development and modernization. Start with one workflow, its expected result and its failure conditions. That creates a useful first release and a better basis for deciding whether the next step is maintenance, extraction or replacement.