Wednesday, 23 September 2026

Can a PHP Application Be Designed for 10–20 Years of Change?

Can a PHP Application Be Designed for 10–20 Years of Change?

Most applications are not designed with a ten-year lifespan in mind.

They are built to solve the immediate problem.

Ship the product.

Get users.

Add features.

Then, somewhere along the way, the application becomes important enough that replacing it is no longer simple.

Ten years later, the business may still depend on it.

But the framework has changed.

The infrastructure has changed.

The team has changed.

The database has grown.

The original developers may be gone.

And suddenly the question becomes:

Can this system keep evolving without needing a complete rewrite?

I think that is one of the most important architectural questions we can ask.

Long-lived software has a different problem

When an application is expected to live for years, technical decisions behave differently.

A dependency that looks convenient today may become a migration problem later.

A framework-specific API used everywhere may make future upgrades much harder.

A database table shared across ten unrelated features may become impossible to separate.

A global helper that saves time today may become invisible coupling tomorrow.

The problem is not that any of these decisions are automatically wrong.

The problem is accumulation.

Small shortcuts become structural assumptions.

And structural assumptions are expensive to change.

Stability does not mean avoiding change

Designing for longevity does not mean freezing the architecture.

It means making change less dangerous.

A ten-year-old application should not look exactly like it did in year one.

It should evolve.

That might mean:

PHP version changes
Framework upgrades
Database changes
Infrastructure changes
New deployment models
New security requirements
New integrations
New business capabilities

The goal is not to predict all of those changes.

You cannot.

The goal is to avoid making today's decisions impossible to undo.

Boundaries matter more than predictions

Suppose an application contains:

Customers
Orders
Billing
Reporting
Notifications

If all five areas share the same internal state, database logic and framework services directly, future change becomes expensive.

But if each capability has clearer responsibilities and explicit interfaces, the system has more room to evolve.

Billing can change without Reporting knowing every internal detail.

Notifications can move to a queue without rewriting Orders.

Reporting can eventually become a separate service if that becomes useful.

You do not need to know today which of those things will happen.

You only need enough separation to make them possible later.

That is what good boundaries buy you.

Framework coupling deserves attention too

Every framework gives you useful abstractions.

Routing.

Dependency injection.

ORMs.

Queues.

Events.

Authentication.

Caching.

Those are valuable.

But if your business logic depends directly on framework internals everywhere, the framework effectively becomes part of every domain decision.

That makes future replacement harder.

I am not arguing for hiding the framework behind abstractions everywhere.

That can become pointless architecture.

But business rules that matter for ten years should not be impossible to understand outside a specific controller, ORM model or helper function.

The more important the business capability, the more useful it is to keep its core rules explicit.

Data usually outlives code

Code gets rewritten surprisingly often.

Data does not.

A ten-year-old application may contain:

millions of records
historical transactions
customer documents
audit history
integration identifiers
business corrections
legacy states

That data becomes part of the company's history.

So long-lived architecture needs to think seriously about ownership.

Which capability owns which data?

Who is allowed to write it?

What happens when schemas evolve?

How do older records remain understandable?

How do migrations roll back?

A framework upgrade may take weeks.

A bad data migration can create problems that last years.

Tests become institutional memory

One of the biggest risks in old systems is losing the reason behind behavior.

You might see code like:

if ($customer->createdBefore('2019-04-01')) {
    // special calculation
}

Ten years later, nobody remembers why.

Maybe it is obsolete.

Maybe removing it breaks a contractual rule for thousands of customers.

Tests can preserve some of that knowledge.

Not just unit tests.

Behavioral tests around important business outcomes.

Those tests become executable documentation for future developers.

They help answer:

“What must still work after we change this?”

For long-lived software, that is extremely valuable.

Operational knowledge also matters

Architecture is not only source code.

A system that is easy to understand but difficult to deploy is still difficult to change.

Long-lived applications benefit from:

repeatable deployments
observable failures
documented dependencies
health checks
clear rollback procedures
automated quality gates
known runtime requirements

The fewer things that exist only in one engineer's memory, the safer the system becomes over time.

Avoid designing for imaginary futures

There is a trap here.

Trying to design for twenty years can easily turn into overengineering.

You do not need ten abstraction layers because something might change in 2034.

You do not need microservices because the company might become large.

You do not need adapters around every standard library call.

Designing for change should not mean designing for every possible future.

It means being careful with decisions that are difficult to reverse.

That is a much smaller and more practical goal.

This is one of the ideas behind EvolvePHP

When I think about EvolvePHP, one of the questions I keep returning to is:

What would make an application easier to evolve five or ten years from now?

That is why the framework direction emphasizes:

  • modular boundaries,

  • explicit contracts,

  • controlled service lifetimes,

  • observable execution,

  • incremental modernization,

  • interoperability,

  • selective extraction.

I do not expect a framework to make an application future-proof.

Nothing can.

But a framework can either make future change easier or make itself another obstacle.

I want EvolvePHP to lean toward the first.

The real measure of architecture

Good architecture is often demonstrated on the day something is built.

I think the harder test comes years later.

Can another developer understand the boundaries?

Can one capability change without destabilizing everything?

Can dependencies be upgraded?

Can infrastructure evolve?

Can part of the system be replaced without replacing all of it?

Can the application survive developers, technologies and business models changing around it?

If the answer is yes, then the architecture has done something valuable.

Because the best long-term architecture is not one that predicts the future.

It is one that leaves enough room for the future to be different.

When Should a Modular Monolith Become Microservices?


A modular monolith can take you surprisingly far.

You can have clear business boundaries, independent modules, explicit contracts, good tests, background workers, queues, caching, horizontal scaling and strong deployment automation without splitting the application into separate services.

So at what point should one of those modules actually become a microservice?

I don't think the answer is:

“When the application gets big.”

And it definitely isn't:

“When we reach enough users.”

The better question is:

What problem would independent deployment solve that the modular monolith can no longer solve well?

That distinction matters.

Start with a real boundary

Suppose an application contains:

Customers
Orders
Billing
Reporting
Notifications

Inside a modular monolith, these can already be separate architectural units.

They may have their own services, domain rules, tests and public contracts while still running inside one application.

That means:

Module boundary ≠ Service boundary

A module says:

“This capability owns this responsibility.”

A service adds another decision:

“This capability should also run and deploy independently.”

That second decision comes with significant cost.

So there should be evidence for it.

Signal 1: Independent scaling

Imagine Reporting becomes dramatically more expensive than the rest of the application.

A few large customers begin generating millions of records and running heavy analytics.

Now you might have:

Customers    normal load
Orders       normal load
Billing      normal load
Reporting    20x workload

Scaling the entire application just to handle Reporting may become wasteful.

If Reporting already has a clean boundary, moving it into a separately scalable service starts to make sense.

This is a good extraction signal because there is a measurable problem.

Signal 2: Independent deployment becomes valuable

Suppose Billing must change frequently because payment providers, tax rules or compliance requirements keep changing.

But every Billing deployment currently requires releasing the entire application.

That may eventually create unnecessary coordination.

If Billing has:

  • a clear API,

  • clear data ownership,

  • good tests,

  • independent operational requirements,

then independent deployment may become valuable.

Again, the reason is not “microservices are better.”

The reason is that deployment coupling has become expensive.

Signal 3: Team ownership changes

Architecture often follows organizations.

When five developers work on the same application, a single deployment model may be perfectly manageable.

When several teams independently own Customers, Billing, Reporting and Notifications, coordination pressure increases.

At some point, this:

Team A ─┐
Team B ─┼── one deployment
Team C ─┘

may become a bottleneck.

A service boundary can allow a team to own its capability more independently.

But I would still resist splitting purely because teams exist.

The business boundary should already be healthy before distribution makes it stronger.

Otherwise you simply turn internal coupling into network coupling.

Signal 4: Security or compliance needs stronger isolation

Sometimes a capability genuinely needs a stronger boundary.

Payments may need different access controls.

Sensitive documents may require stricter infrastructure.

A regulated workload may require separate audit, deployment or operational policies.

Now process or network isolation may provide something the in-process module cannot.

This is one of the stronger reasons to extract because the boundary is driven by an actual security or compliance requirement.

Signal 5: The runtime requirements are genuinely different

Maybe most of your platform works perfectly well in PHP.

But one capability eventually needs something very different.

Perhaps a worker needs extreme concurrency.

Maybe image processing is CPU-heavy.

Perhaps another ecosystem provides a substantially better tool for a specific workload.

With a good modular architecture, you might eventually have:

PHP Application
├── Customers
├── Orders
├── Billing
└── Reporting

          ↓

Specialized Processing Service
          Go / Rust / PHP

The important part is that the extraction is local.

You don't rewrite the application because one workload changed.

What is not a strong reason?

I would be cautious about extracting because:

"The codebase is getting large."
"We might scale one day."
"Big companies use microservices."
"We want Kubernetes."
"Services look cleaner."

Those may describe future possibilities.

They do not necessarily describe current problems.

Microservices introduce their own architecture:

  • network failures,

  • retries,

  • idempotency,

  • authentication between services,

  • distributed observability,

  • deployment coordination,

  • message/version compatibility,

  • data consistency,

  • operational overhead.

Before extraction, most of those problems may not exist.

After extraction, they become your responsibility.

Data ownership is the hardest test

Before I extract a module, I would ask one particularly uncomfortable question:

Can this capability genuinely own its data?

Suppose Billing is extracted but still directly reads and writes twenty tables owned by Orders and Customers.

You haven't really created an independent service.

You have created a distributed application sharing a database.

Sometimes that is an acceptable intermediate step.

But it should be recognized as one.

A stronger boundary looks more like:

Orders owns order state
Billing owns payment state

Orders → Billing contract
Billing → Orders contract

Ownership becomes explicit.

Extraction should be earned

The progression I prefer is:

Monolith
   ↓
Modular Monolith
   ↓
Observe real pressure
   ↓
Identify one boundary
   ↓
Prove independent ownership
   ↓
Extract if the benefit exceeds the cost

That gives the architecture time to tell you where distribution is actually useful.

It also means some modules may never become services.

That is completely fine.

A successful modular monolith does not have to “graduate”

This is probably the biggest misconception.

A modular monolith is not necessarily an intermediate architecture waiting to become microservices.

It may be the correct long-term architecture.

And if one capability eventually deserves independent deployment, you should be able to extract that capability, not redesign the entire system.

That is the architectural direction I care about with EvolvePHP as well:

start modular, preserve boundaries, measure pressure, and extract selectively.

The question should never be:

“Are we big enough for microservices?”

It should be:

“What concrete problem becomes easier if this particular boundary becomes independently deployable?”

If there isn't a strong answer yet, the module can probably stay exactly where it is.


Tuesday, 15 September 2026

EvolvePHP 2 Reaches Its First Alpha — The Framework Is Becoming Real

EvolvePHP 2 Reaches Its First Alpha — The Framework Is Becoming Real

This is a milestone I have been looking forward to for a while.

EvolvePHP 2.0.0-alpha.1 is officially out.

Until now, most of what I have written about EvolvePHP has focused on architecture: modular applications, execution isolation, long-running PHP, modernization, Bridge, service lifetimes and designing software for change.

With this release, those ideas are no longer only plans.

There is now a tagged, testable EvolvePHP 2 Alpha.

It is still experimental. APIs can change. It is not production-ready.

But it is real.

The first Alpha was published on September 13, 2026, targeting the 2.x line.

What has actually been built?

Quite a lot of the foundation is now implemented.

The Alpha includes:

  • a runtime-neutral Core with configuration, service registration, Application/Execution/Transient service lifetimes, execution scopes, deterministic reset and execution orchestration;

  • a PSR-based HTTP foundation with middleware, routing, dispatch, health handling and explicit response-resolution/emission boundaries;

  • module and plugin foundations with dependency/capability graphs, lifecycle orchestration, restricted service registration and Composer-based plugin discovery;

  • Evolve Doctor diagnostics and development tooling;

  • read-only Evolve Audit foundations and adoption-planning declarations;

  • embedded Bridge foundations for PSR, Laravel and Symfony;

  • remote HTTP/JSON Bridge support, including an isolated PHP 7.4-compatible legacy client;

  • an application skeleton with explicit CLI, route configuration and module/plugin generators.

One area I am particularly pleased with is the execution model.

EvolvePHP already distinguishes between:

Application
Execution
Transient

An HTTP request, CLI command or other execution can receive its own execution scope. Cleanup/reset results remain separate from the business operation result, which gives future runtimes enough information to decide whether a process is still safe to reuse.

That foundation is important for where EvolvePHP is going.

Modernization is also present in the Alpha

The modernization direction is no longer only an RFC.

Evolve Audit can inspect existing PHP source and Composer evidence without executing the target application.

Adoption planning can record ownership, compatibility requirements, migration evidence and rollback evidence.

Bridge can then support a capability that needs to coexist with an existing application.

For compatible environments, embedded integration can use PSR, Laravel or Symfony adapters.

Where PHP versions or dependency graphs cannot safely coexist, Remote Bridge provides a process boundary instead. The legacy remote client has a deliberately isolated PHP 7.4 compatibility boundary while EvolvePHP 2 itself continues to require PHP 8.4.

The principle remains:

Understand
   ↓
Plan ownership
   ↓
Integrate
   ↓
Move one capability
   ↓
Validate

Not:

Rewrite everything

How can you try Alpha 1?

There is an important limitation here.

The first-party packages are not yet independently published, and public composer create-project installation is not available yet.

So I do not want to pretend this is already the normal end-user installation experience.

For now, Alpha 1 is a source preview.

Developers who want to inspect or contribute to the framework can clone the repository:

git clone https://github.com/josiahking/evolvephp.git
cd evolvephp
composer install
composer quality

The repository also contains the accepted application skeleton and dedicated Alpha documentation. The skeleton already owns explicit route and CLI configuration, with doctor and route:list as its baseline commands. Development tooling also provides module:new and plugin:new when DevTools is available.

For example, the generators are designed around explicit application-owned components:

module:new Billing
    ↓
src/Modules/Billing/

plugin:new Cache
    ↓
src/Plugins/Cache/

Generated components are deliberately not auto-enabled.

Public package installation comes later.

Now the road toward Beta begins

Development has already moved beyond the Alpha tag.

Phase 8 — Evolve Insight is now in progress. The current roadmap shows the Insight storage projection and in-memory store foundation completed, with the SQLite diagnostic store next. Insight is intended to become EvolvePHP's local diagnostic system: collectors, safe diagnostic storage, retention and eventually a Telescope-style dashboard with Evolve-specific architecture and leak diagnostics.

Then comes Phase 9 — Evolve Observe and OpenTelemetry.

This takes a different role from Insight. Observe is intended for production observability: standard traces, metrics and structured logs, including context propagation across HTTP and background work.

Phase 10 — Infrastructure Contracts and Adapters moves EvolvePHP closer to practical application development by introducing replaceable contracts and initial adapters around databases, cache, sessions, queues, storage, locks, secrets and external clients.

A new Phase 10.5 — Views & Templating has also been added before reusable modules. The direction is a native PHP view engine with safe rendering, layouts, partials, shared data and module-aware view resolution, while keeping Twig or Blade optional rather than making either a framework-wide dependency.

One clarification to my earlier roadmap wording: Phase 11 is currently targeted at 2.0 Stable rather than Beta.

Phase 11 will prove the module architecture with real first-party reusable capabilities: Audit Log, Webhooks and API Keys.

So the progression is becoming much clearer:

Alpha
Core + HTTP + Modules + Doctor + Audit + Bridge
        ↓
Beta
Insight + Observe + Infrastructure + Views
        ↓
Stable direction
Reusable modules + further production hardening

A milestone, not the finish line

There is still a lot to build.

The Alpha does not yet provide the complete concrete production web runtime. Public package installation is not ready. Production deployment and several runtime integrations remain ahead.

But Alpha 1 matters because EvolvePHP has crossed an important line.

It is no longer only an architecture I am describing.

There is now code behind the execution model, module system, modernization workflow, Bridge, diagnostics and developer experience.

And from here, the work becomes increasingly visible.

EvolvePHP 2.0.0-alpha.1 is the first checkpoint.

Now we build toward Beta.

Microservices Are Not the Goal — Architectural Optionality Is

Microservices Are Not the Goal — Architectural Optionality Is

Microservices are often treated like the destination of a successful application.

Start with a monolith.

Grow.

Split into services.

Become “modern.”

I think that framing is backwards.

Microservices are not the goal.

The goal is to build a system that can change without forcing the business into a rewrite every time the architecture needs to evolve.

That is what I mean by architectural optionality.

A distributed system is not automatically a better system.

There are good reasons to use microservices:

  • independent scaling,

  • independent deployment,

  • separate security boundaries,

  • different team ownership,

  • different runtime needs,

  • different release cycles.

But distribution also introduces new problems:

network failures
timeouts
retries
idempotency
message delivery
service discovery
versioning
distributed tracing
data ownership
eventual consistency
operational complexity

Those costs may absolutely be worth it.

But they should be accepted because they solve a real problem, not because the system reached a certain size.

Scaling does not automatically mean microservices

A common assumption is:

More traffic
    ↓
Microservices

But traffic alone is not enough.

A well-designed monolith can handle a lot.

You can scale application instances horizontally.

You can introduce queues.

You can move files to object storage.

You can add caching.

You can optimize expensive operations.

You can improve database design.

None of those require turning every business capability into a separate service.

A better question is:

Which part of the system actually benefits from independent deployment?

That is much more useful.

Start with boundaries

Suppose an application has:

Customers
Orders
Billing
Reporting
Notifications

If all of those areas share everything freely, extracting Billing later will be painful.

But if they already have clear boundaries and explicit dependencies, you have options.

Billing can remain inside the monolith.

Or later it can move behind HTTP, a queue, RPC, or events.

That is the difference between:

“We need microservices.”

and:

“We can extract this capability if the evidence says we should.”

The modular monolith preserves choice

This is one reason I think the modular monolith is still underrated.

A good modular monolith gives you many of the architectural benefits people want from microservices:

  • clear ownership,

  • explicit dependencies,

  • isolated business capabilities,

  • testable boundaries,

  • easier reasoning,

  • replaceable implementations.

But you still deploy one application.

You still have local calls.

You still have one operational surface.

That keeps the system simpler while preserving the option to distribute later.

Extraction should happen because something changed

Imagine Billing suddenly needs to process ten times more work than the rest of the application.

Or a separate payments team takes ownership.

Or regulatory requirements demand stronger isolation.

Or Billing needs a deployment schedule that cannot be tied to the rest of the platform.

Now extraction has a reason.

Modular Monolith

Customers
Orders
Billing
Reporting
Notifications

        ↓

Customers
Orders       ──────> Billing Service
Reporting
Notifications

The architectural boundary already existed.

The deployment boundary changed.

That is healthier than starting with five services because you think the company might grow.

Optionality also applies to language choice

Suppose most of the system is PHP.

Later, one capability develops requirements that strongly favor Go or Rust.

Maybe it is CPU intensive.

Maybe it has a very different concurrency profile.

Maybe another ecosystem has better tooling for that workload.

If the architecture has good boundaries, you should be able to make that choice locally.

PHP Application
      |
      +---- Orders
      +---- Customers
      +---- Reporting
      |
      +---- High-throughput Worker → Go

That does not mean PHP failed.

It means the architecture allowed the team to choose the right tool without rewriting everything else.

Reversibility matters

One thing I increasingly value in architecture is reversibility.

How expensive is it to change your mind?

If choosing a framework, database, deployment model, or integration pattern creates a ten-year commitment, that decision deserves scrutiny.

Sometimes that commitment is necessary.

But where possible, I would rather preserve choices.

That does not mean abstracting everything.

Over-abstraction creates its own problems.

It means putting boundaries where the business already has meaningful boundaries and avoiding unnecessary coupling between them.

This influences EvolvePHP

A major design goal behind EvolvePHP is not:

“Make building microservices easy.”

It is closer to:

Build modular applications now, and make selective extraction possible later.

That is why explicit contracts, component boundaries, execution isolation, interoperability, and modernization matter so much.

A new EvolvePHP application should not need to begin as a distributed system.

The preferred path is:

Modular Monolith
       ↓
Observe actual pressure
       ↓
Identify the affected capability
       ↓
Extract only when justified

The same idea applies to existing systems.

Modernization should create options rather than simply replacing one form of lock-in with another.

The real goal

Microservices are useful.

So are monoliths.

So are queues, workers, serverless functions, and separately deployed services.

None of them should become an ideology.

The better question is:

Can our architecture support the deployment model we need when we actually need it?

That is architectural optionality.

And for long-lived software, I think that is more valuable than choosing the architecture that looks most advanced today.

Microservices are one possible destination.

The real goal is preserving the ability to choose.

Thursday, 10 September 2026

Long-Running PHP with EvolvePHP: Designing for Safe Reuse

Long-Running PHP with EvolvePHP: Designing for Safe Reuse

Long-running PHP is usually introduced as a performance story.

Boot the application once.

Keep the process alive.

Handle many requests or jobs.

Avoid repeating expensive initialization.

That can absolutely improve performance.

But while working on EvolvePHP 2, I have become more interested in a different problem:

How do you know the process is still safe to reuse?

That question changes how I think about persistent PHP.

Because keeping a process alive is easy.

Keeping it clean between executions is harder.

Traditional PHP gives you an automatic reset

With the classic request-per-process model, application state has a natural ending.

A request arrives.

PHP runs.

A response is produced.

The request ends.

Conceptually:

Start
  ↓
Boot
  ↓
Handle Request
  ↓
Response
  ↓
Process ends

Anything accidentally left in memory disappears with the process.

That is surprisingly useful.

Now imagine the process stays alive:

Boot
 ↓
Request A
 ↓
Request B
 ↓
Request C
 ↓
Request D
 ↓
...

Suddenly, state from Request A can potentially affect Request B.

The runtime has stopped giving us a clean slate automatically.

We have to create one ourselves.

The dangerous state is often ordinary state

Consider something simple:

final class TenantContext
{
    public static ?string $tenantId = null;
}

Request A sets:

tenantId = company-a

The request finishes.

Request B belongs to:

company-b

But if that static value was not reset correctly, we have a much more serious problem than a memory leak.

We have an isolation failure.

The same concern applies to:

  • authenticated users,

  • tenant context,

  • database transactions,

  • request caches,

  • listeners,

  • authorization state,

  • telemetry context,

  • temporary resources.

Long-running PHP turns lifecycle management into part of correctness.

EvolvePHP uses explicit execution boundaries

This is why EvolvePHP 2 does not model everything around an HTTP request.

The lower-level concept is an execution.

An execution might eventually represent:

HTTP request
Queue message
Scheduled job
CLI command
Worker task

The important part is that each unit of work gets a clear lifetime.

The model I am using is broadly:

Application
    │
    ├── Execution A
    │
    ├── Execution B
    │
    └── Execution C

The application may survive.

The execution must not.

That distinction is fundamental.

Service lifetimes need to match reality

EvolvePHP currently distinguishes three service lifetimes:

Application
Execution
Transient

Application services may survive across many executions.

Execution services belong to one unit of work.

Transient services are created when requested and are not cached.

This allows the container to reason about lifetime relationships instead of treating every dependency as equivalent.

For example:

Application-scoped service
        ↓
Execution-scoped CurrentUser

is dangerous.

The application service could capture a user belonging to one execution and retain it into another.

That relationship should not quietly succeed.

This is one of the areas where dependency injection becomes more than convenience.

It becomes a safety boundary.

Cleanup needs to be deterministic

At the end of an execution, EvolvePHP closes the execution scope.

Services that explicitly participate in reset can be cleaned up in a deterministic order.

Conceptually:

Execution starts
      ↓
Services created
      ↓
Operation runs
      ↓
Reset participants
      ↓
Scope closes

The important word here is explicitly.

I do not think a framework should pretend it can magically identify every piece of state that needs resetting.

Services that own reusable state need a clear cleanup contract.

And cleanup needs to happen even when the operation itself fails.

What if cleanup fails?

This was one of the questions that influenced the runtime architecture heavily.

Suppose the business operation succeeds:

Payment processed successfully

but during cleanup:

TenantContext reset fails

What is the result?

The payment still succeeded.

We should not rewrite history and pretend that it failed.

But can we safely reuse the PHP process?

Probably not.

That is why EvolvePHP separates two questions:

Did the operation succeed?

Is the process safe to reuse?

They are not the same question.

An execution outcome can preserve the original result or exception separately from cleanup failure.

Then the runtime can make an explicit reuse decision.

Conceptually:

Handler succeeds
Cleanup succeeds
        ↓
      REUSE

but:

Handler succeeds
Cleanup fails
        ↓
    QUARANTINE

The process is treated as uncertain.

The safe response is not:

“Hopefully the next request is fine.”

It is:

Do not give this process more work.

Quarantine is deliberately fail-closed

I like this model because it avoids pretending we know more than we do.

If cleanup failed, the framework may not be able to prove exactly what state remains.

So rather than attempting clever recovery inside Core, the result says that process reuse is unsafe.

A runtime adapter can later decide whether that means:

stop accepting work
finish current response
restart worker
replace process

That operational policy belongs to the runtime.

The framework's job is to expose the truth.

This also changes error handling

A long-running runtime can have four interesting outcomes:

Operation success + cleanup success
Operation failure + cleanup success
Operation success + cleanup failure
Operation failure + cleanup failure

The first two may still leave the process reusable.

The last two should not.

That is quite different from saying:

exception = bad
no exception = good

Runtime safety requires more information than that.

EvolvePHP is not claiming complete persistent-runtime support yet

There is an important limitation here.

EvolvePHP 2 already has the execution-scope, cleanup and reuse/quarantine foundations.

But the broader persistent-runtime work is still ahead.

Concrete integration with runtimes such as FrankenPHP and later RoadRunner, stronger persistent-worker validation, concurrency abstractions and production runtime adapters belong to later development.

So I would not currently describe EvolvePHP as a finished persistent PHP platform.

The architecture is being prepared for that future.

That distinction matters.

Long-running PHP is not just about speed

I think this is the biggest lesson.

Persistent execution can make PHP faster.

But once the process survives the request, performance becomes only one part of the engineering problem.

You also need to think about:

Lifetime
Isolation
Cleanup
Ownership
Failure
Observability
Reuse

Because a process that handles 10,000 requests quickly is not impressive if request 9,427 sees state belonging to request 9,426.

The real goal is not:

Keep PHP alive as long as possible.

It is:

Keep PHP alive only while we still have evidence that the process is safe to reuse.

That is the direction I want EvolvePHP's long-running runtime model to take.

Wednesday, 9 September 2026

Evolving PHP: The Language Is Changing, and So Is the Way We Build With It

 

Evolving PHP: The Language Is Changing, and So Is the Way We Build With It  For a long time, PHP had a very clear operating model.  A request comes in.  PHP starts.  The application runs.  A response goes out.  The process ends.  That model shaped a lot of how PHP applications were designed.  State could live in convenient places because the process was short-lived.  Global variables were less dangerous than they would be in a long-running process.  Memory leaks were often masked by process termination.  Cleanup was less visible because the runtime effectively cleaned everything up for you.  That model worked very well.  But PHP is evolving.  And the way we design PHP applications needs to evolve with it.  PHP is no longer only a request-per-process language  The traditional PHP-FPM model is still valid and useful.  But it is no longer the only serious way to run PHP.  Today, PHP can also run in long-lived workers, application servers, queue consumers, schedulers, event-driven processes, and persistent runtimes.  That changes something fundamental.  The application may now live for:  1 request 100 requests 10,000 requests or hours of continuous work  That means old assumptions become more important.  What happens to request-specific state after the request finishes?  What happens to the current user?  The current tenant?  The database transaction?  Listeners?  Caches?  Telemetry context?  Temporary services?  A process that does not terminate forces us to answer questions that the traditional runtime often answered for us.  Performance is only part of the story  Persistent PHP is often discussed as a performance topic.  And performance does matter.  Booting the application once and reusing it can reduce repeated initialization work.  But I think the more interesting question is not:  “How much faster can PHP become?”  It is:  “What architectural assumptions change when the process survives?”  That is a much bigger question.

For a long time, PHP had a very clear operating model.

A request comes in.

PHP starts.

The application runs.

A response goes out.

The process ends.

That model shaped a lot of how PHP applications were designed.

State could live in convenient places because the process was short-lived.

Global variables were less dangerous than they would be in a long-running process.

Memory leaks were often masked by process termination.

Cleanup was less visible because the runtime effectively cleaned everything up for you.

That model worked very well.

But PHP is evolving.

And the way we design PHP applications needs to evolve with it.

PHP is no longer only a request-per-process language

The traditional PHP-FPM model is still valid and useful.

But it is no longer the only serious way to run PHP.

Today, PHP can also run in long-lived workers, application servers, queue consumers, schedulers, event-driven processes, and persistent runtimes.

That changes something fundamental.

The application may now live for:

1 request
100 requests
10,000 requests
or hours of continuous work

That means old assumptions become more important.

What happens to request-specific state after the request finishes?

What happens to the current user?

The current tenant?

The database transaction?

Listeners?

Caches?

Telemetry context?

Temporary services?

A process that does not terminate forces us to answer questions that the traditional runtime often answered for us.

Performance is only part of the story

Persistent PHP is often discussed as a performance topic.

And performance does matter.

Booting the application once and reusing it can reduce repeated initialization work.

But I think the more interesting question is not:

“How much faster can PHP become?”

It is:

“What architectural assumptions change when the process survives?”

That is a much bigger question.

A persistent process can expose bugs that were previously hidden.

For example:

class UserContext
{
    public static ?int $currentUserId = null;
}

In a traditional short-lived request, this may appear harmless.

In a reused process, the next request may inherit stale state if cleanup is incomplete.

Now the problem is not performance.

It is isolation.

Dependency injection becomes more serious

Dependency injection is usually introduced as a way to improve testability and reduce coupling.

That is true.

But in long-running applications, it also becomes a lifetime problem.

Suppose an application-scoped service depends on a request-scoped service.

Conceptually:

Application Service
        ↓
Current User

If the application service lives for hours while the current user should live for one request, the lifetime relationship is wrong.

The container should not simply allow that because the types happen to match.

This is where service lifetimes start to matter much more:

Application
Execution
Transient

Application services can live across many executions.

Execution services belong to one request, job, or command.

Transient services are created when needed.

The relationship between them becomes part of application safety.

Cleanup becomes part of correctness

In short-lived PHP, process termination often acts as cleanup.

In a persistent runtime, cleanup needs to become explicit.

Imagine this flow:

Request starts
    ↓
User state created
    ↓
Transaction opened
    ↓
Telemetry context created
    ↓
Handler runs
    ↓
Cleanup begins

If cleanup succeeds, the process may be safe to reuse.

But what if cleanup fails?

That question does not get enough attention.

A framework should not simply assume:

“The request is over, continue.”

If the state of the process is uncertain, reusing that process can be dangerous.

Sometimes the correct answer is:

Do not reuse it.

That is a runtime safety decision, not just an error-handling decision.

Observability becomes more valuable too

Long-running systems are harder to understand when something goes wrong.

You may need to know:

  • which execution created the state,

  • which service failed to reset,

  • whether cleanup completed,

  • whether the process was reused,

  • what happened before a failure,

  • whether tenant or user context leaked.

Logs alone may not always be enough.

This is one reason I think observability needs to be designed into modern application architecture rather than added after problems appear.

Tracing, metrics, execution context, structured events, and failure evidence become more useful as runtime complexity increases.

PHP is also becoming more attractive for different workloads

The ecosystem is gradually making PHP viable for workloads that were traditionally pushed immediately toward other languages.

That does not mean PHP should replace everything.

Go, Rust, Java, and other languages remain excellent choices depending on the problem.

But PHP applications no longer need to assume:

“If this runs continuously, it must be rewritten in another language.”

Sometimes the correct answer may still be another language.

But it should be an architectural decision, not an automatic one.

If a PHP application can safely handle a workload with acceptable performance and operational behavior, there is value in keeping the system simpler.

Framework design needs to catch up

This is the part that interests me most.

A framework designed only around short-lived requests can still run in a persistent environment.

But that does not mean it was designed for one.

I think modern PHP frameworks increasingly need to think about:

  • explicit service lifetimes,

  • execution isolation,

  • deterministic cleanup,

  • stale-state prevention,

  • runtime reuse decisions,

  • observability boundaries,

  • worker safety,

  • concurrency assumptions,

  • graceful failure.

These concerns should not be hidden inside runtime adapters.

They should influence the framework architecture itself.

This is influencing EvolvePHP

A lot of the EvolvePHP 2 architecture has been shaped by this idea.

The runtime model is not limited to HTTP requests.

An execution might be:

HTTP request
Queue message
Scheduled job
CLI command
Worker task

Each execution gets its own scope.

Execution-specific state should not silently become application-global state.

Cleanup is explicit.

Cleanup failures remain visible.

And if execution cleanup leaves the process in an uncertain condition, the runtime can decide that the process should not be reused.

My goal is not to make PHP look like Java or Go.

I acknowledge that PHP itself is changing.

And frameworks should evolve with it.

PHP’s future is not just faster PHP

When people talk about the future of PHP, the conversation often focuses on syntax, JIT, benchmarks, or framework performance.

Those things matter.

But I think the deeper evolution is architectural.

PHP is moving into environments where processes live longer, workloads are broader, infrastructure is more distributed, and operational expectations are higher.

That means application design has to become more deliberate.

The old request-per-process model hid a lot of complexity.

Persistent runtimes expose it.

And that is not necessarily a bad thing.

It gives us a chance to build systems with clearer boundaries, stronger isolation, better observability, and more predictable behavior.

PHP is evolving.

The interesting question now is whether our application architecture evolves with it.

Tuesday, 8 September 2026

Migrating Legacy PHP Applications to Modern Frameworks: What Actually Changes?

Migrating Legacy PHP Applications to Modern Frameworks: What Actually Changes?

Migrating a legacy PHP application to a modern framework can look deceptively simple from the outside.

You have an old application.

You choose Laravel, Symfony, or another modern framework.

You move the routes, controllers, models, and views.

Done.

Except that is rarely what really happens.

The difficult part of framework migration is not translating syntax.

It is deciding which old assumptions should survive and which ones should not.

That distinction matters because it is entirely possible to move a legacy application into a modern framework and still end up with a legacy system.

A newer framework does not automatically create a newer architecture

Imagine an old application with this structure:

index.php
includes/
functions.php
database.php
users.php
orders.php
payments.php
reports.php

Everything talks directly to everything else.

Business rules are mixed with SQL.

Authentication depends on session globals.

A migration begins.

Months later, the application looks like:

Controllers/
Models/
Services/
Repositories/
Middleware/

That certainly looks more modern.

But suppose the new OrderService still directly updates customer, inventory, payment, and reporting tables.

The folders changed.

The coupling did not.

This is one of the biggest risks in framework migration:

We can modernize the shape of the code without modernizing the architecture.

Start by understanding why you are migrating

Before choosing a destination framework, I think teams should be able to answer:

What problem are we actually trying to solve?

Maybe the current framework no longer supports modern PHP.

Maybe dependencies are abandoned.

Maybe security updates have stopped.

Maybe it is difficult to hire developers who understand the stack.

Maybe the codebase has become hard to test.

Maybe deployments are fragile.

Those are good reasons to modernize.

But each one may lead to a different migration plan.

If the main problem is PHP compatibility, a framework upgrade may be enough.

If the main problem is architectural coupling, changing frameworks without changing boundaries may achieve very little.

Laravel may be the right answer

For many PHP applications, Laravel is a sensible destination.

It has a large ecosystem, good developer experience, strong community support, familiar conventions, and plenty of available developers.

If the goal is to move an application from an unsupported custom framework into a widely understood PHP stack, Laravel can reduce a lot of organizational risk.

The same is true for Symfony in environments that value stronger componentization, explicit architecture, and long-term enterprise use.

There is nothing wrong with choosing a mature framework.

The mistake is assuming the framework will make every architectural decision for you.

It will not.

Migration can happen gradually

A framework migration does not always need a single cutover date.

Suppose an application contains:

Customers
Orders
Billing
Reporting
Notifications

Maybe Reporting is reasonably independent.

Instead of rebuilding all five capabilities before launch, you could move Reporting first.

Conceptually:

Legacy Application
├── Customers
├── Orders
├── Billing
└── Notifications

        |
        v

Modern Reporting

Then observe it.

Run it in production.

Learn from the migration.

Only then decide what should move next.

This is often less exciting than a full rewrite.

It is also usually easier to control.

Data ownership matters more than controllers

One of the hardest parts of any framework migration is deciding who owns the data during the transition.

If both the legacy application and the new application can modify the same business state, things get complicated quickly.

Imagine both systems can create or update invoices.

Now you need to answer:

  • Which system is authoritative?

  • What happens if one succeeds and the other fails?

  • How do you reconcile differences?

  • Can one system understand records created by the other?

  • What happens during rollback?

A cleaner migration usually has explicit ownership.

For example:

Legacy owns Orders
New system owns Reporting

Later that ownership can change.

But it should change deliberately.

Authentication can become another hidden trap

Legacy applications often have years of authentication and authorization behavior embedded in them.

Moving login forms is easy.

Preserving the actual security model is harder.

You need to understand:

  • how identities are stored,

  • how sessions work,

  • how passwords are handled,

  • how permissions are calculated,

  • whether other systems depend on the same authentication state.

A migration should not accidentally weaken authorization simply because the new framework provides a nicer authentication API.

Identity and authorization deserve their own migration plan.

Tests make migration much safer

If the existing application has poor test coverage, I would not necessarily stop everything and attempt complete coverage first.

Instead, protect the behavior you are about to move.

Suppose you are migrating Billing.

Capture the important Billing behavior:

normal payment
failed payment
refund
duplicate request
discount
tax calculation
historical customer case

Then migrate against that behavior.

The tests become a record of what the old system actually does rather than what everyone thinks it does.

That is incredibly valuable in legacy modernization.

Where EvolvePHP fits into my thinking

While building EvolvePHP 2, I have been thinking about framework migration slightly differently.

I do not want adopting EvolvePHP to require the assumption:

“Eventually everything must become EvolvePHP.”

My direction is closer to:

Modernize the capabilities that benefit from moving, and allow the rest of the system to keep working.

That means coexistence matters.

Clear boundaries matter.

Data ownership matters.

Rollback matters.

And interoperability with existing PHP applications matters.

But it is influencing the architecture from the beginning.

Because I think frameworks should help applications evolve—not force businesses into another all-or-nothing rewrite.

Choosing the framework is only one decision

When somebody asks:

“Should we migrate this legacy application to Laravel or Symfony?”

my answer would usually begin with more questions.

What is wrong with the existing system?

Which capabilities change frequently?

Where is the data ownership?

What can be migrated independently?

What must remain available throughout the migration?

How will you roll back?

Only after those questions does framework choice become really useful.

Because my goal is not simply:

Move old PHP into a new framework.

My goal is:

Make the application safer, easier to understand, easier to operate, and easier to change for the next several years.

A modern framework can help enormously with that.

But the real modernization happens in the decisions you make while moving there, and not in the framework name at the end.

Monday, 7 September 2026

Modernizing Legacy PHP Applications Using EvolvePHP

Modernizing Legacy PHP Applications Using EvolvePHP

Modernizing an old PHP application sounds straightforward until you actually open the codebase.

You may find an old framework, unsupported PHP version, direct SQL everywhere, global state, fragile authentication, outdated packages, and years of business logic spread across controllers, cron jobs, helpers, templates and database procedures.

At that point, the usual instinct is:

“We need to move this to something modern.”

But that still leaves the hardest question unanswered:

How do we modernize without breaking the business that already depends on it?

That is one of the problems I want EvolvePHP to help solve.

Not by converting an entire application automatically.

Not by pretending migration is easy.

But by helping teams understand the system, create boundaries, and move one capability at a time.

Modernization should begin with diagnosis

Before changing architecture, I think the first task should be understanding what actually exists.

A legacy application may have problems such as:

  • unsupported PHP versions,

  • abandoned Composer packages,

  • hidden global state,

  • direct access to superglobals,

  • static service state,

  • weak test coverage,

  • tightly coupled database access,

  • unsafe persistent-worker assumptions,

  • unclear module boundaries.

You cannot make a good modernization plan if you do not know where those risks are.

That is the role I see for Evolve Audit.

Conceptually:

Legacy PHP Application
        ↓
    Evolve Audit
        ↓
Architecture and modernization report

Audit should help answer questions like:

  • What framework is this application using?

  • Which dependencies are obsolete?

  • Where is state being shared?

  • What parts of the application are highly coupled?

  • Which capabilities look like natural module boundaries?

  • Which areas are likely to be dangerous to migrate first?

The goal is not to produce a score saying:

“Your application is bad.”

The goal is to give the team enough information to make better decisions.

Then check whether the environment is ready

Architecture is only part of the problem.

Modernization can also fail because the runtime itself is not ready.

Maybe required PHP extensions are missing.

Maybe filesystem permissions are wrong.

Maybe a package combination cannot coexist.

Maybe the application holds execution-specific state in static properties.

That is where Evolve Doctor fits.

Think of it as a runtime and operational readiness check.

Audit
  ↓
Doctor
  ↓
Can we safely introduce this modernization step?

Audit tells you what you have.

Doctor tells you whether the environment and runtime assumptions are safe enough for the next step.

The next question is ownership

Suppose Audit reveals that Reporting is relatively isolated while Billing is deeply connected to Orders, Customers and several payment providers.

It may be tempting to modernize Billing because it causes the most pain.

But that does not necessarily make it the best first candidate.

A safer first move may be Reporting.

For example:

Legacy Application
├── Customers
├── Orders
├── Billing
├── Reporting
└── Notifications

could gradually become:

Legacy Application
├── Customers
├── Orders
├── Billing
└── Notifications

        |
        v

Evolve Reporting Module

The legacy application continues running.

Only one capability changes ownership.

That matters.

During modernization, there should always be a clear answer to:

Which system owns this operation right now?

Ambiguous ownership is where incremental migration becomes dangerous.

This is where Evolve Bridge comes in

Evolve Bridge is not intended to be a code converter.

It is an integration boundary.

The old system might be Laravel, Symfony, CodeIgniter, CakePHP, Yii or something completely custom.

The new capability might run inside the same process when dependencies are compatible.

Or it might run remotely when stronger isolation is needed.

Conceptually:

Legacy PHP
    |
    | delegated operation
    v
Evolve Bridge
    |
    v
Evolve Module

or:

Legacy PHP
    |
    | HTTP / Queue / Event
    v
Evolve Service

The integration mechanism can vary.

The principle stays the same:

Modernize one capability without requiring the rest of the application to move.

The database usually becomes the hardest part

Code can be reorganized relatively easily.

Data ownership is more difficult.

If the new Reporting module only reads legacy data, the transition may be straightforward.

If Billing is being migrated, things become more serious.

Who writes invoices?

Who owns payment state?

Can both systems modify the same tables?

What happens if the new system succeeds but the old system times out?

A modernization plan needs explicit data ownership.

I would rather begin with:

Legacy owns data
Evolve reads through adapter

and move deliberately from there than allow two systems to write the same business state without clear rules.

Rollback should be part of the plan

A migration is not complete simply because traffic can move forward.

You also need to know what happens if it fails.

Before cutover, I would want to know:

  • Can traffic return to the legacy capability?

  • Has the new system changed data the old system cannot understand?

  • Can we identify which version processed an operation?

  • Can we compare behavior between both implementations?

  • What evidence tells us the migration is safe?

That is one reason I think modernization should happen in small steps.

Small steps are easier to observe and easier to reverse.

Not everything needs to become EvolvePHP

This is important.

If Customers works well, keep it.

If an old Reporting module is reliable and rarely changes, perhaps it stays too.

The goal should not be:

“How much of this application can we move to EvolvePHP?”

The better question is:

“Which parts benefit enough from modernization to justify moving them?”

Sometimes the answer will eventually be most of the application.

Sometimes it may only be one or two capabilities.

Both outcomes can be successful.

The wider direction

The modernization flow I am building toward looks roughly like this:

Audit
  ↓
Doctor
  ↓
Adoption Plan
  ↓
Bridge
  ↓
Modernize
  ↓
Validate
  ↓
Upgrade Confidence

Each stage reduces a different kind of uncertainty.

That is the part of EvolvePHP I find increasingly important.

Frameworks are usually very good at helping developers start new applications.

But there is an enormous amount of PHP software already running businesses today.

Those systems need somewhere to go too.

I do not think the answer should always be:

“Rewrite it in a new framework.”

Sometimes the better answer is:

Understand what you have. Create a safe boundary. Move one capability. Prove it works. Then decide whether anything else needs to move.

That is the modernization path I want EvolvePHP to make practical.

Friday, 4 September 2026

Modernizing Legacy PHP Applications: Pros and Cons

Modernizing Legacy PHP Applications: Pros and Cons

Modernizing a legacy PHP application usually sounds like an obvious win.

Newer PHP version.

Supported dependencies.

Better architecture.

Cleaner code.

Improved security.

Easier deployment.

All of that sounds good.

But modernization is not free.

Every improvement comes with cost, risk, and disruption.

That is why I think the useful question is not:

“Should we modernize?”

It is:

“What do we gain, what do we risk, and what is worth changing first?”

That balance matters.

The obvious benefit: supported technology

One of the strongest reasons to modernize is simply getting back onto supported software.

A legacy system might be running:

PHP 7.x
Old framework
Abandoned packages
Unsupported libraries

That creates risk.

Security fixes stop.

New infrastructure becomes harder to adopt.

Developers spend more time working around compatibility problems.

Moving to supported versions can immediately improve the situation.

This is often modernization at its most practical.

No dramatic architecture change required.

Just reducing unnecessary exposure.

Modernization can improve security

Old applications often carry years of assumptions that no longer fit modern security expectations.

You may find:

  • outdated password handling,

  • unsafe session settings,

  • old authentication flows,

  • weak dependency hygiene,

  • direct SQL construction,

  • unvalidated redirects,

  • inconsistent authorization,

  • insecure file handling.

Modernization creates an opportunity to review those areas properly.

But this needs an important caveat.

New code is not automatically secure code.

A rewrite can introduce fresh vulnerabilities just as easily as old code can retain them.

Security improves when modernization includes deliberate review, testing, safer defaults, and better architecture.

Not simply because the framework version changed.

Maintainability can improve dramatically

One of the biggest wins is making the application easier to understand.

A legacy system might have business rules scattered across:

controllers
helpers
models
templates
cron scripts
random utility classes

Modernization can introduce clearer boundaries.

For example:

Customers
Orders
Billing
Reporting
Notifications

Now developers can understand where functionality belongs.

That can reduce the fear around changing the codebase.

And when developers are less afraid of the system, delivery speed usually improves.

Testing gets easier

Legacy applications often have weak test coverage because the architecture was not designed for testing.

Everything may depend on:

globals
static methods
database state
framework internals
shared mutable state

Refactoring toward explicit dependencies and better boundaries can make testing much easier.

That matters because modernization itself is risky.

Tests help answer:

Did we preserve the behavior that already mattered?

Sometimes the most valuable thing modernization gives you is not cleaner code.

It is confidence.

Modern infrastructure becomes possible

Older applications can become trapped by their runtime assumptions.

They may expect:

  • one server,

  • local filesystem storage,

  • manually configured cron jobs,

  • state stored in memory,

  • no structured telemetry,

  • deployment through file uploads.

Modernizing can open the door to:

containers
horizontal scaling
object storage
queues
workers
centralized logs
OpenTelemetry
automated deployment

But not every application needs all of those.

Modern infrastructure should solve real operational problems.

Otherwise we are replacing one form of complexity with another.


The other side: modernization has real costs

This is the part that gets less attention.

You can break behavior nobody documented

Legacy systems accumulate hidden business knowledge.

You may discover code that looks completely unnecessary:

if customer_created_before_2019 ...

The natural instinct is to remove it.

Then three weeks later, a long-standing customer can no longer complete a transaction.

That strange condition may have existed for a reason.

Modernization often reveals how much of the business is encoded only in software.

That makes every “cleanup” decision potentially important.

The project can become much larger than expected

A modernization effort may begin with:

“Let’s upgrade PHP.”

Then you discover:

Framework cannot run on new PHP
        ↓
Framework upgrade required
        ↓
Old dependencies incompatible
        ↓
Authentication package abandoned
        ↓
Database layer changed
        ↓
Tests missing

Suddenly a runtime upgrade becomes an architecture project.

This is why I prefer auditing the system before setting timelines.

The visible problem is rarely the whole problem.

Dual systems create complexity

Incremental modernization reduces big-bang risk, but it introduces another challenge.

For some period of time you may have:

Legacy system
      +
Modernized capability

Now you need clear answers around:

  • routing,

  • ownership,

  • authentication,

  • data,

  • deployment,

  • observability,

  • rollback.

If both systems think they own the same business operation, things can get messy quickly.

Incremental modernization is safer only when boundaries are explicit.

Data migration can be more difficult than code migration

Developers naturally focus on source code.

But data is usually where modernization becomes serious.

Which system owns a record?

Can both systems write?

How do we migrate history?

What happens if migration fails halfway?

Can old and new schemas coexist?

Can we roll back after new writes have already happened?

The code may be replaceable.

The business history stored in the database is not.

There is also an opportunity cost

A team modernizing the platform is not building other things.

For six months, developers may be spending time on:

dependency upgrades
refactoring
migration scripts
test coverage
deployment changes

while competitors are shipping customer-facing features.

That does not mean modernization is wrong.

It means the business case needs to be real.

Technical discomfort alone may not justify a major project.

Developers also have to learn the new system

A newer architecture may be technically better and still slow the team down initially.

New concepts.

New tooling.

New deployment model.

New framework.

New conventions.

Modernization creates learning cost.

If the team is not prepared for that, productivity can temporarily fall before it improves.


So when is modernization worth it?

For me, the strongest case exists when the current system is actively limiting the business.

For example:

  • security support is ending,

  • upgrades are blocked,

  • important features take too long,

  • outages are becoming more common,

  • deployment is risky,

  • hiring is difficult,

  • infrastructure cannot scale,

  • critical knowledge is trapped in a few people.

Those are measurable problems.

They give modernization a purpose.

The best approach is rarely “modernize everything”

I prefer something more deliberate:

Understand
   ↓
Prioritize
   ↓
Protect behavior with tests
   ↓
Modernize the highest-value area
   ↓
Measure the result
   ↓
Continue only when justified

That keeps modernization connected to outcomes.

Not fashion.

Not architecture diagrams.

Not a desire to make every file look new.

The goal
Modernizing Legacy PHP Applications: Pros and Cons

is not newer code

This is probably the most important point.

Modernization succeeds when the application becomes:

  • safer,

  • easier to change,

  • easier to operate,

  • easier to understand,

  • cheaper to maintain.

If the team spends a year rebuilding everything and ends up with the same delivery problems, the modernization failed even if the code looks beautiful.

Old software can absolutely need modernization.

But modernization itself also needs discipline.

The right question is not:

“How much of this legacy application can we replace?”

It is:

“Which changes give us enough long-term value to justify the risk of making them?”

That is where a good modernization strategy begins.

Modernizing Legacy Applications in PHP: Considerations and Approaches

 

Modernizing Legacy Applications in PHP: Considerations and Approaches

Legacy PHP applications are everywhere.

Some are ten or fifteen years old. Some are running on unsupported PHP versions. Some use frameworks that are no longer maintained. Others were built without a framework at all.

And many of them are still doing real work every day.

That creates an uncomfortable situation.

The application needs to change, but changing it may be risky.

The temptation is usually to jump straight to a technical solution:

Upgrade PHP.
Move to Laravel.
Rewrite it.
Break it into microservices.

I think modernization should start somewhere else.

With understanding.

First, what does “legacy” actually mean?

Old software is not automatically bad software.

A PHP application written eight years ago may still be stable, understandable and inexpensive to maintain.

Meanwhile, a three-year-old application can already feel impossible to change.

For me, a system becomes a modernization problem when things like these start happening:

  • supported PHP versions cannot be adopted;

  • important dependencies are abandoned;

  • security fixes become difficult;

  • deployments are fragile;

  • developers are afraid to change certain areas;

  • business logic is tightly coupled;

  • testing is weak or nonexistent;

  • infrastructure cannot evolve;

  • new features take increasingly longer to deliver.

Age is only one signal.

The real problem is the cost and risk of change.

Before touching the code, understand what you have

A mature application contains more than PHP files.

It contains business behavior.

It may integrate with:

Payment providers
Banks
Email/SMS services
Government APIs
CRMs
Accounting software
Legacy databases
File storage
Third-party services

Some integrations may not even be documented properly anymore.

Before modernization, I would want answers to questions such as:

  • Which PHP and framework versions are running?

  • Which dependencies are unsupported?

  • Where does authentication happen?

  • Which modules own important business data?

  • Which external systems depend on this application?

  • What are the most frequently changed areas?

  • Which parts are considered dangerous to touch?

  • What tests already exist?

  • What must never break?

Modernization without that understanding can easily turn into archaeology during production incidents.

There is more than one modernization strategy

This is where I think teams sometimes make modernization harder than necessary.

There is no single correct approach.

1. Upgrade in place

Sometimes the best modernization project is simply:

PHP 7.x
   ↓
PHP 8.x

Old dependencies
   ↓
Supported dependencies

The application's architecture remains mostly unchanged.

That can still deliver major benefits:

  • security support,

  • performance improvements,

  • modern tooling,

  • easier hiring,

  • access to maintained libraries.

Not every modernization project needs a new framework.

2. Refactor gradually

Another approach is to improve the existing application while it continues running.

For example:

Direct SQL
   ↓
Repository / persistence boundary

Global state
   ↓
Explicit dependencies

Large controller
   ↓
Application services

This can be slow, but it keeps changes small.

It also allows the team to learn about the application before making larger architectural decisions.

3. Establish module boundaries

A tightly coupled application may benefit from becoming a modular monolith before anything is extracted.

Perhaps the system currently looks like:

Application
   ↓
Everything depends on everything

The modernization target could be:

Application

├── Customers
├── Orders
├── Billing
├── Reporting
└── Notifications

Still one application.

Still one deployment.

But now responsibilities and dependencies are clearer.

That alone can make future modernization much easier.

4. Replace one capability

Sometimes one part of the application is causing most of the pain.

Maybe Reporting cannot scale.

Maybe Billing has become too risky to change.

Maybe a legacy authentication system needs replacing.

Instead of rebuilding everything:

Legacy Application
├── Customers
├── Orders
├── Billing
├── Reporting
└── Notifications

you might gradually reach:

Legacy Application
├── Customers
├── Orders
├── Billing
└── Notifications

        |
        v

Modern Reporting Capability

The business keeps operating while one boundary evolves.

This is often much easier to control than a full replacement.

5. Rewrite

Yes, rewrites still belong on the list.

There are systems where replacement is genuinely the right choice.

For example:

  • the application is relatively small;

  • requirements are well understood;

  • data migration is straightforward;

  • the current technology cannot support future needs;

  • maintaining the old code costs more than replacing it.

But a rewrite should be an evaluated option.

Not the default reaction to code we dislike.

Framework migration deserves the same caution

Moving from an old PHP framework to Laravel, Symfony or another modern framework can be valuable.

But framework migration does not automatically solve architecture problems.

If tightly coupled business logic is moved unchanged into a newer framework, you may end up with:

a modern framework containing a legacy architecture.

The syntax changed.

The underlying problem did not.

I think modernization should first identify business boundaries and ownership.

Framework choice comes after that.

Data usually determines how difficult the migration really is

Applications can often tolerate temporary duplication of code.

Data is much less forgiving.

Questions quickly appear:

  • Which system is the source of truth?

  • Can both old and new systems write?

  • How are historical records handled?

  • How do we validate migrated data?

  • Can the migration be reversed?

  • What happens during partial failure?

This is why I strongly prefer explicit ownership during incremental modernization.

At any point, somebody should be able to answer:

Which system owns this data and operation right now?

Ambiguous ownership creates very expensive bugs.

Testing changes the risk completely

Modernizing an application without tests is possible.

But every change carries much more uncertainty.

One practical strategy is not to attempt perfect test coverage first.

Start around the behavior you intend to change.

Capture what the legacy system currently does.

Then modernize against that evidence.

Tests become a safety net for discovering undocumented behavior.

Sometimes a test that exposes a strange legacy rule is more valuable than immediately “cleaning up” that rule.

Modernization should have a rollback story

Every migration plan naturally explains how to move forward.

Fewer explain how to move backward.

Before replacing a capability, I want to know:

How do we cut over?
How do we observe it?
How do we know it is working?
How do we return safely if it isn't?

If rollback is impossible, that risk should at least be explicit.

Modernization should reduce uncertainty, not hide it.

Start with the problem, not the destination

This is probably the principle I come back to most.

Don't begin with:

“How do we migrate this system to Framework X?”

Begin with:

“What prevents this application from changing safely?”

Maybe the answer is the framework.

Maybe it is PHP version compatibility.

Maybe it is database coupling.

Maybe it is missing tests.

Maybe it is deployment.

Maybe it is one badly designed subsystem.

Once that is clear, the modernization strategy becomes much easier to reason about.

Because modernization is not really about making old PHP look new.

It is about making a valuable application safer and cheaper to change.

And sometimes the best modernization decision is surprisingly small.