Saturday, 29 August 2026

Dependency Injection Is Easy Until the Process Lives Forever

Dependency Injection Is Easy Until the Process Lives Forever

Dependency injection feels simple when the application process is short-lived.

A request comes in.

The framework resolves a few services.

The request finishes.

Then everything disappears.

Under that model, a lot of lifetime mistakes are easy to miss.

But persistent PHP workers change the rules.

Once the same application process handles multiple requests, jobs, or messages, dependency injection stops being only about convenience.

It becomes part of runtime safety.

That is one of the reasons EvolvePHP 2 treats service lifetimes as an architectural concern rather than a container feature.

The easy version of dependency injection

In a typical application, we might have something like:

final class BillingService
{
    public function __construct(
        private PaymentGateway $gateway
    ) {}
}

That is straightforward.

The container creates BillingService, injects PaymentGateway, and the application uses it.

Most developers understand this part.

The harder question is:

How long should each of those objects live?

That question becomes much more important when the PHP process itself stays alive.

Imagine the application boots once

A persistent worker might look like this:

Application boots
      ↓
Request A
      ↓
Request B
      ↓
Request C
      ↓
Request D

Now imagine we have a service created during application boot:

BillingService
Lifetime: Application

And BillingService depends on:

CurrentUser
Lifetime: Request

Conceptually:

Application-scoped BillingService
            ↓
Execution-scoped CurrentUser

That relationship is dangerous.

Why?

Because BillingService may live for hours.

CurrentUser should live for one request.

If the long-lived service captures the short-lived object, the user from Request A can remain reachable during Request B.

This is called a captive dependency

The general problem is often known as a captive dependency.

A longer-lived service captures a dependency with a shorter lifetime.

For example:

Application
    ↓
Execution

or:

Singleton
    ↓
Request-specific state

In a disposable request model, the process may end before this becomes visible.

In a persistent worker, it can turn into:

Request A
User: Alice
     ↓
BillingService retains Alice
     ↓
Request B
User: Bob
     ↓
BillingService still references Alice

That can become more than a bug.

If the state involves users, tenants, authorization, transactions, or sensitive data, it becomes a security problem.

This is why lifetimes need names

For EvolvePHP 2, I am working around three basic lifetimes:

Application
Execution
Transient

Application

Lives as long as the booted application.

Good candidates might include:

  • immutable configuration,

  • route metadata,

  • shared infrastructure,

  • reusable stateless services.

Execution

Lives for one unit of work.

That could be:

  • one HTTP request,

  • one queue message,

  • one scheduled job,

  • one CLI operation,

  • one worker task.

Typical execution state might include:

  • current user,

  • current tenant,

  • request context,

  • transaction context,

  • trace context,

  • authorization state.

Transient

Created when needed and not automatically shared.

This gives the container more information than:

“Can I build this object?”

It can also ask:

“Is this dependency relationship safe?”

Some dependencies should be rejected

Suppose the container sees:

ApplicationService
      ↓
CurrentTenant

and CurrentTenant is execution-scoped.

Instead of allowing it and hoping the developer resets everything correctly, I would rather the framework reject the relationship.

Something like:

Invalid lifetime dependency:

Application-scoped service
cannot directly depend on
Execution-scoped service.

That is much better than discovering the mistake through a cross-tenant production incident.

A framework should make unsafe architecture difficult.

The opposite direction is usually fine

This relationship makes much more sense:

Execution Service
       ↓
Application Service

For example:

CheckoutHandler
      ↓
PaymentGateway

The execution-scoped handler can safely depend on a long-lived, stateless payment gateway abstraction.

When the execution finishes, the handler disappears.

The shared service continues.

The important rule is that the shared service does not retain the execution-specific object.

Service locators can hide the problem

There is another pattern that makes lifetime issues harder to see:

$container->get(CurrentUser::class);

from anywhere in the application.

This can feel convenient.

But now dependencies become invisible.

A class may appear to have no dependency on current user state while secretly resolving it from a global container.

That makes architecture harder to reason about.

It also makes testing and persistent-worker safety harder.

I prefer explicit dependencies where practical.

A constructor tells us:

This service needs these things.

That gives both developers and tooling something concrete to inspect.

Factories do not automatically solve it

A common workaround is:

“Just inject a factory.”

Sometimes that is correct.

For example, an application-scoped service might receive a factory that creates an execution-specific object only during the active execution.

But the important part is still ownership.

The long-lived service must not keep the result beyond the execution.

Otherwise we have only hidden the captive dependency behind another abstraction.

Cleanup still matters

Correct lifetimes reduce risk, but some infrastructure still needs reset behavior.

A connection may retain transaction state.

A logger may carry execution context.

A telemetry system may hold baggage.

An event dispatcher may temporarily register execution-specific listeners.

So persistent execution still needs:

Execute
   ↓
Cleanup
   ↓
Reset
   ↓
Verify
   ↓
Reuse or Quarantine

Dependency lifetimes and cleanup work together.

Lifetimes prevent unsafe ownership.

Cleanup removes temporary state.

Both matter.

DI becomes architecture, not just plumbing

This is the bigger point.

In short-lived applications, dependency injection is often discussed as:

  • easier testing,

  • cleaner constructors,

  • replacing implementations,

  • avoiding manual object creation.

Those are useful benefits.

But in long-running PHP, DI also answers:

  • who owns state,

  • how long it survives,

  • what may depend on what,

  • whether process reuse is safe.

That changes its importance.

This influences EvolvePHP deeply

I do not want persistent-worker support to be something added after the framework is already designed.

If the container allows unsafe lifetime relationships from the beginning, adding RoadRunner or FrankenPHP later means auditing everything after the fact.

I would rather start with the safer model.

Ask every service:

What lifetime does this belong to?

Then enforce the rules around it.

Because persistent PHP does not just make applications faster by keeping them warm.

It also makes old assumptions about object lifetime visible.

And once the process lives forever, dependency injection is no longer just about how objects are created.

It is about making sure the wrong state does not live forever with them.

Why “Just Rewrite It” Is Usually Bad Modernization Advice

Why “Just Rewrite It” Is Usually Bad Modernization Advice

There is a sentence developers say very easily when they are looking at an old codebase:

“We should just rewrite it.”

I understand the temptation.

You open a project and find an old PHP version, a framework that is no longer supported, classes with thousands of lines, duplicated business logic, direct SQL everywhere, globals, static state, and dependencies nobody wants to touch.

Starting again sounds cleaner.

Sometimes it is the right decision.

But in my experience, “just rewrite it” is often advice that underestimates what the existing system actually contains.

Old code contains more than code

A legacy application may look ugly technically and still carry years of business knowledge.

Imagine a system that has been running for ten years.

During that time:

  • customers reported edge cases,

  • accountants changed reporting rules,

  • payment providers behaved unexpectedly,

  • regulators introduced new requirements,

  • staff developed unusual workflows,

  • developers fixed hundreds of production bugs.

Not all of that knowledge made it into documentation.

A lot of it ended up here:

if this customer type...
unless this account was created before...
except when this payment provider...
but only after this approval...

Those conditions may look terrible when you read them.

Some probably are.

But some represent real business rules that nobody remembers anymore.

When you rewrite the application, you are not just rewriting PHP.

You are trying to rediscover the business.

The new system starts behind

This is one of the biggest rewrite problems.

Suppose the existing application already has:

Authentication
Customers
Orders
Billing
Reporting
Notifications
Administration
Integrations

The rewrite starts with none of them.

So while the old system is running the business, the new team begins rebuilding features that already exist.

Meanwhile, the business does not stop.

A new payment integration is needed.

Management requests another report.

A security issue needs fixing.

A customer discovers an edge case.

Now the old system continues changing while the new system is trying to catch it.

The finish line moves.

That is how a six-month rewrite quietly becomes an eighteen-month project.

Feature parity is harder than it sounds

Teams often create a checklist:

Login ✓
Orders ✓
Payments ✓
Reports ✓

Then everyone feels close to completion.

But “Orders works” can hide hundreds of behaviors.

What happens when an order is partially cancelled?

What happens when the customer's account was migrated from an older version?

What happens when an external API times out after accepting the request?

What happens to historical reports?

What permissions does a regional administrator have?

The old application may already know the answers because production forced someone to solve them years ago.

The rewrite team may discover them again one bug at a time.

Rewrites create two systems to maintain

During a large rewrite, you temporarily create a strange architecture:

Legacy Application
        +
New Application

Both matter.

Both require developers.

Both may need infrastructure.

Both need security fixes.

Both may contain versions of the same business logic.

And developers eventually start asking:

Where should I implement this new feature?

Sometimes the answer becomes:

both.

That is not modernization.

It is duplicated risk.

I prefer asking a smaller question

Instead of starting with:

“How do we replace this system?”

I think teams should first ask:

“What part of this system is actually hurting us?”

Maybe it is Billing.

Maybe the reporting subsystem is impossible to change.

Maybe authentication is insecure.

Maybe an old framework prevents the PHP version from being upgraded.

Maybe deployment takes two hours.

Maybe nothing is fundamentally wrong except one badly coupled module.

Those are very different problems.

They should not automatically receive the same solution.

Modernization does not have to mean replacement

There are several ways an old application can evolve.

You can upgrade dependencies.

You can introduce tests around risky areas.

You can replace infrastructure adapters.

You can establish module boundaries.

You can extract one capability.

You can put a modern API in front of legacy logic.

You can move some workloads to workers.

You can replace one subsystem and leave five others alone.

Conceptually:

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

might become:

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

        |
        v

Modern Reporting Module

Nothing dramatic happened.

Customers still log in.

Orders still work.

The business keeps operating.

Only the part that needed change moved.

I think that is often a healthier modernization strategy.

Sometimes the old code should stay

This is another thing developers do not say enough.

Imagine a legacy component that:

  • has worked reliably for eight years,

  • rarely changes,

  • has no known serious security problem,

  • is well understood,

  • costs almost nothing to maintain.

Why rewrite it?

Because the code style looks old?

Because another framework is more fashionable?

Modernization should reduce risk or cost.

Otherwise we may simply be exchanging known problems for new ones.

A good modernization assessment should sometimes conclude:

Leave this part alone.

Data makes rewrites even harder

Code is usually easier to move than data.

A mature application might have millions of records and years of history.

Then questions appear:

Which system owns new records during migration?

Do we synchronize databases?

How do we verify migrated data?

Can we roll back?

What happens if the new system writes data that the old one cannot understand?

This is where supposedly clean rewrites become complicated very quickly.

The database remembers the history of the business even when the developers do not.

This thinking influenced Evolve Bridge

One reason I am building EvolvePHP around incremental modernization is that I don't want adoption to require replacing an entire application.

The direction is closer to:

Audit
  ↓
Understand the system
  ↓
Identify boundaries
  ↓
Bridge
  ↓
Modernize one capability
  ↓
Validate
  ↓
Repeat only if useful

Evolve Bridge is intended to let an existing Laravel, Symfony, legacy PHP, or custom system coexist with newer EvolvePHP capabilities.

Not convert the entire application magically.

Not hide difficult migration decisions.

Just create controlled boundaries where change can happen gradually.

Rewrites still have their place

There are cases where starting again makes sense.

The existing system may be tiny.

Its behavior may be well documented.

The technology may be completely incompatible with future requirements.

The business may be willing to freeze the old product while the new one is built.

Or maintaining the current architecture may genuinely cost more than replacement.

In those cases, rewrite.

But make that decision because the evidence supports it.

Not because new code feels better than old code.

Modernization should preserve what still works

The goal should not be to produce the cleanest possible repository.

The goal should be to improve the system without unnecessarily putting the business at risk.

Sometimes that means replacing a lot.

Sometimes it means replacing almost nothing.

Most of the time, I suspect the best answer sits somewhere between those extremes.

So when I hear:

“Just rewrite it.”

my next question is usually:

Which part, exactly—and what problem are we solving by rewriting it?

That question often leads to a much better modernization plan.

Thursday, 27 August 2026

The Modular Monolith Is Still Underrated

The Modular Monolith Is Still Underrated

For a while, microservices became almost synonymous with “serious architecture.”

If an application was expected to grow, the assumption seemed to be that it should eventually become a collection of independently deployed services.

I understand why.

Microservices can give teams autonomy. Different parts of a system can scale independently. Deployments can be isolated. Technology choices can vary between services.

Those are real advantages.

But I think we sometimes skip an important question:

Does the application actually need to be distributed yet?

More often than not, I think the better starting point is still a modular monolith.

That idea is central to how I’m designing EvolvePHP 2.

A monolith is not automatically a mess

When developers hear "monolith," they sometimes imagine this:

Controllers
Models
Helpers
Services
Utils
MoreHelpers
RandomStuff

Everything can access everything.

Business rules are spread across the application.

Changing Billing somehow breaks Orders.

Nobody knows who owns a particular database table.

Eventually every new feature becomes harder to implement.

That is a badly structured monolith.

But it is not the only kind of monolith.

A modular monolith might instead look like:

Application

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

It still runs as one application.

It might still use one database.

It may still be deployed as one unit.

But internally, each capability has a clear boundary.

That distinction matters.

Folders do not create modules

It is easy to create this:

Modules/
├── Billing/
├── Orders/
└── Customers/

and call the architecture modular.

But if Billing can freely import internal classes from Orders, modify Customer tables directly, and reach into Reporting whenever convenient, the boundaries are mostly cosmetic.

A module should have something it owns.

It should expose deliberate contracts.

For example:

Orders
   |
   | public contract
   v
Billing

instead of:

Billing
   |
   v
Orders/Internal/WhateverWasConvenient.php

That means architecture needs rules.

Some relationships should be allowed.

Others should fail tests or validation.

This is one of the things I want EvolvePHP to enforce rather than merely recommend.

Why not start with microservices?

Because distribution creates a completely different class of problems.

Inside one process, this may be straightforward:

$order = $orders->create($data);

$billing->charge($order);

Split those capabilities across the network and suddenly we have more questions.

What if Billing is unavailable?

What if the request times out?

What if Billing completed the charge but the response never arrived?

Should Orders retry?

Could the customer be charged twice?

Do we need idempotency?

How do we trace the operation across services?

How do we handle version compatibility?

What if one service deploys before another?

None of these problems are impossible.

But they are real costs.

I don't think teams should pay those costs before there is a reason.

Scaling does not automatically mean microservices

Another argument I often hear is:

"We'll need microservices when we scale."

Maybe.

But scale has several meanings.

More users?

More requests?

More developers?

More data?

More deployments?

One application can serve a lot of traffic by simply running multiple copies:

            Load Balancer
                 |
       +---------+---------+
       |         |         |
      App       App       App

Add caching.

Queues.

Database replicas.

Object storage.

Workers.

There is a lot of scaling available before the application needs to be split into dozens of services.

Sometimes the first scaling problem isn't the architecture at all.

It is one slow query.

The real value is optionality

This is where modularity becomes important.

I don't want EvolvePHP applications to begin with the assumption:

Everything will always remain a monolith.

But I also don't want:

Everything will eventually become a microservice.

Both are predictions about a future we don't know yet.

A better architecture preserves options.

Start here:

Application

├── Identity
├── Orders
├── Billing
└── Reporting

Then imagine Billing eventually has very different requirements.

Maybe it needs stricter security controls.

Maybe a dedicated team now owns it.

Maybe it processes much heavier workloads.

Maybe it needs to be deployed independently.

If the boundary was designed properly, the architecture can evolve:

Application
├── Identity
├── Orders
└── Reporting

       |
       v

Billing Service

That is very different from designing Billing as a distributed service on day one because somebody thinks the company might become large someday.

Extraction should be earned

I like this rule:

Extract a service because you have evidence that the boundary benefits from independent deployment—not because microservices look more advanced on an architecture diagram.

Good reasons might include:

  • independent scaling requirements,

  • separate team ownership,

  • security or compliance isolation,

  • different availability requirements,

  • independent release cadence,

  • genuinely distinct operational characteristics.

"Netflix uses microservices" is not one of them.

Your application probably does not have Netflix's problems.

That is fine.

A modular monolith also helps teams

The benefit isn't only deployment simplicity.

Clear modules make ownership easier.

A developer working on Billing should be able to understand:

what Billing owns
what Billing exposes
what Billing depends on
what depends on Billing

That is valuable whether Billing runs inside the same process or on another continent.

In fact, if a team cannot maintain good boundaries inside one codebase, moving those boundaries across HTTP does not automatically improve the architecture.

Sometimes it just turns bad dependencies into network calls.

This matters for modernization too

The same idea applies to older applications.

Imagine a legacy PHP application where everything is tightly coupled.

Before thinking about microservices, perhaps the first modernization step is simply identifying capabilities:

Legacy Application
        ↓
Customers
Orders
Billing
Reporting

Then establish boundaries gradually.

Once those boundaries exist, you can decide what actually needs to move.

Maybe Billing becomes a separate service.

Maybe Reporting moves to a new EvolvePHP module.

Maybe Customers stays exactly where it is.

That is the philosophy behind Evolve Bridge as well:

move what matters; keep what still works.

The goal is not a monolith

This distinction is important.

I'm not arguing that monoliths are always better.

I'm arguing that distribution should be a consequence of requirements, not an architectural starting ideology.

The goal is not:

Build a monolith.

And it is not:

Build microservices.

The goal is:

Build clear boundaries, then let deployment architecture evolve when reality gives you a reason.

That is what I mean by architectural optionality.

For EvolvePHP 2, the direction is straightforward:

Start modular.

Integrate through explicit contracts.

Keep deployment simple while you can.

Extract selectively when the evidence says you should.

Because a modular monolith is not the architecture you settle for before becoming sophisticated.

Done properly, it may be the architecture that gives you the freedom to become sophisticated later—without paying for complexity before you need it.

Part 2: What EvolvePHP Can Learn From Spring Without Becoming Spring for PHP

Part 2: What EvolvePHP Can Learn From Spring Without Becoming Spring for PHP

In the first part, I looked at why Java and Spring became trusted in enterprise environments.

The answer was not simply performance.

It was predictability.

Architecture.

Operations.

Security.

Lifecycle discipline.

Long-term support.

So what should a modern PHP framework actually learn from that?

Not everything.

And certainly not by trying to rebuild Spring in PHP.

Start with modularity

One lesson is that large systems need clear boundaries.

A business application might begin as:

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

That can remain one deployment.

There is no reason to start with microservices.

But the boundaries should be real enough that one capability can eventually move.

For example:

Application
├── Identity
├── Customers
├── Orders
└── Reporting

        |
        v

   Billing Service

Billing becomes a service only when there is a real reason.

Not because microservices are fashionable.

This is the kind of evolutionary architecture I want EvolvePHP to support.

Lifecycle discipline matters

A framework should understand the difference between:

application state
execution state
temporary state

And it should prevent unsafe lifetime relationships.

A long-lived application service should not accidentally retain a user, tenant or transaction that belongs to one execution.

When an execution completes, cleanup should be deterministic.

If cleanup fails or becomes uncertain:

quarantine

Do not silently reuse the worker.

That is conservative by design.

For serious systems, conservatism is often a feature.

Observability should not be added after production breaks

Enterprise systems need to explain what they are doing.

When something fails, teams need to know:

  • which request failed,
  • which dependency slowed down,
  • which deployment introduced the problem,
  • what happened before the exception,
  • whether memory or connections are growing.

That is why EvolvePHP separates:

Evolve Insight

for local developer diagnostics,

from:

Evolve Observe

for production telemetry such as traces, metrics and structured logs.

A framework should help operators understand it while it is running.

Security should come with evidence

I also do not want EvolvePHP to eventually say:

EvolvePHP is secure.

That statement is too vague.

Instead, I want security claims to be testable.

If EvolvePHP claims:

Execution B cannot observe Execution A's state

then the framework should have tests repeatedly trying to break that guarantee.

If it claims:

Cleanup failure can never result in safe worker reuse

that should also be enforced.

This is where Evolve Assurance fits.

The direction includes things such as:

  • architecture tests,
  • property-based testing,
  • fault injection,
  • fuzzing,
  • mutation testing,
  • persistent-worker soak testing,
  • supply-chain checks,
  • independent security review.

The idea is simple:

Trust should come from evidence, not confidence.

Upgradeability is part of enterprise architecture

Most frameworks focus heavily on:

create project

But long-lived applications spend far more time doing:

maintain
debug
upgrade
modernize

That changes how a framework should be designed.

A system expected to survive fifteen years should help answer:

  • Which APIs are deprecated?
  • Which dependencies conflict?
  • Which modules are incompatible?
  • What will break in this upgrade?
  • Can one part modernize without replacing everything?

This is where Evolve Audit, Doctor, Bridge and Upgrade Confidence fit into the wider EvolvePHP direction.

The framework should not only help applications begin.

It should help them survive change.

Spring has one advantage architecture cannot manufacture

Time.

Spring has decades of production history.

It has survived incidents, security vulnerabilities, major upgrades and organizational change.

Companies trust it partly because they know what failure looks like.

EvolvePHP does not have that yet.

Strong architecture is not the same as production maturity.

So the most accurate description today is:

EvolvePHP is enterprise-oriented, not enterprise-proven.

The second part has to be earned.

That will require:

stable contracts
production deployments
security reviews
benchmark evidence
upgrade history
multiple maintainers
reference applications
independent adoption
long-term support

No architecture diagram can replace those things.

Can PHP become a stronger enterprise platform?

I think yes.

But the path is not about proving PHP can serve HTTP requests quickly.

PHP already does that.

The harder work is improving:

  • modularity,
  • runtime safety,
  • security,
  • observability,
  • transactions,
  • upgradeability,
  • dependency governance,
  • long-term maintenance.

And especially:

predictability.

Enterprises often choose “boring” technology because boring systems are easier to operate, hire for, budget for and explain when something fails.

That is the lesson I want EvolvePHP to take from Spring.

Not:

How do I make PHP imitate Java?

But:

What did mature enterprise platforms learn over decades, and how can PHP benefit from those lessons without losing its simplicity?

That is a much more useful goal.

Part 1: Can PHP Be an Enterprise Platform? What Java and Spring Got Right

Part 1: Can PHP Be an Enterprise Platform? What Java and Spring Got Right

PHP has powered a huge part of the web for years.

It is easy to deploy, widely understood, relatively inexpensive to run, and supported by mature frameworks such as Laravel and Symfony.

But once the conversation moves into banking, insurance, government systems, large enterprise platforms, or software expected to survive for ten or twenty years, another stack appears very quickly:

Java and Spring.

That raises a more useful question than “Is Java better than PHP?”

What did Java and Spring get right that made enterprises trust them for long-lived systems?

That question matters to me while building EvolvePHP 2.

Because if PHP wants to compete more seriously in enterprise environments, we should understand why those environments became comfortable with Java in the first place.

PHP can already handle serious traffic

One misconception is that enterprises choose Java because PHP cannot scale.

That is too simplistic.

Modern PHP applications can run behind load balancers, scale horizontally, consume queues, use Redis, Kafka, PostgreSQL, object storage, containers and OpenTelemetry.

PHP can process very large workloads.

So the question is not simply:

Can PHP handle enough requests?

The harder question is:

Can an organization confidently operate, maintain and evolve the system for many years?

That is where Java has built a major advantage.

Enterprises optimize for organizational risk

A bank or large company does not only ask:

Which framework lets us build this feature fastest?

It also asks:

  • Can we hire engineers for this stack?
  • Will the ecosystem still exist in ten years?
  • Can different teams work safely in the same system?
  • Are there mature monitoring and security tools?
  • Can we upgrade without rewriting everything?
  • What happens when the original developers leave?
  • Can vendors and consultants support us?

Java has decades of answers to those questions.

Spring built on top of that history.

Spring is more than a web framework

It is tempting to compare Spring Boot directly with Laravel.

But Spring sits inside a much larger ecosystem.

There are tools and projects around:

  • dependency injection,
  • authentication and authorization,
  • persistence,
  • distributed systems,
  • batch processing,
  • messaging,
  • modularity,
  • observability,
  • integration,
  • transactions.

The important thing is not that every Spring application uses all of them.

It is that companies know the ecosystem is there when complexity increases.

That reduces risk.

Architecture can be enforced

One of the biggest lessons I take from enterprise Java is that architecture should not exist only in diagrams.

Imagine an application with:

Customers
Orders
Billing
Reporting
Notifications

Initially the boundaries look clean.

Years later, everything begins calling everything else.

The application still runs, but changing one feature becomes dangerous.

A folder called Modules/Billing is not enough.

A real boundary should say:

Billing → Contracts     allowed
Billing → Orders/Internal     forbidden

And ideally, the framework or architecture tests should enforce that.

That is something I want EvolvePHP to take very seriously.

Dependency injection is really about lifecycle

Dependency injection is often discussed as a convenience.

But in long-lived applications it becomes much more important.

It helps answer:

  • Who owns this dependency?
  • How long does it live?
  • Can it be replaced?
  • Does it contain request-specific state?
  • Can a long-lived service safely hold it?

This matters even more with persistent PHP workers.

Suppose an application-level service captures the current user.

Under traditional PHP-FPM, the mistake may disappear with the request.

Under a persistent worker, that state may survive.

Now dependency injection becomes part of runtime safety.

Java had to think about long-running processes early

Java servers have always forced developers to think about:

memory
connections
state
threads
resource cleanup

Traditional PHP gave us a simpler model.

A request starts.

The request ends.

Most state disappears.

But modern PHP runtimes are changing that assumption.

With FrankenPHP, RoadRunner and queue workers, a PHP process may remain alive for many executions.

That means frameworks need clearer concepts such as:

Application lifetime
Execution lifetime
Transient lifetime

This is why EvolvePHP treats execution isolation as foundational rather than something to add later.

The lesson is not “become Java”

I do not think PHP needs to imitate Java.

PHP has strengths Java does not have.

It is simple to deploy.

Developer feedback loops are fast.

Composer is mature.

Modern PHP has a much stronger type system than earlier versions.

The better question is:

Which enterprise lessons can PHP adopt without losing what makes PHP productive?

For me, those lessons include:

  • enforceable architecture,
  • lifecycle discipline,
  • modularity,
  • operational visibility,
  • stronger security practices,
  • predictable upgrades.

That is where the conversation becomes interesting.

Because PHP does not need to become Java.

But it can learn from what enterprise ecosystems have already discovered the hard way.

Friday, 21 August 2026

Why Persistent PHP Workers Need Execution Isolation

 


For most of PHP's history, developers have had a very useful safety net.

A request comes in.

PHP handles it.

The response is returned.

Then the request-specific state effectively disappears with the process lifecycle.

Conceptually:

Request starts
    ↓
Application runs
    ↓
Response returned
    ↓
Request state disappears

That model has shaped the way PHP applications are written for years.

It is simple.

It is predictable.

And, whether we realize it or not, it protects us from a lot of mistakes.

But persistent PHP workers change that assumption.

When the same PHP process handles more than one request, message or task, state from the previous execution can survive into the next one.

That is where execution isolation becomes important.

And it is one of the reasons EvolvePHP 2 treats an execution as a first-class architectural concept.

Persistent workers change the rules

Traditional PHP-FPM gives applications a relatively disposable execution model.

A request might look like this:

Request A
    ↓
PHP process
    ↓
Response A

Later another request arrives.

From the application's point of view, it is generally working with a clean request lifecycle.

Persistent runtimes work differently.

The process may stay alive:

Application boots once
        ↓
Request A
        ↓
Request B
        ↓
Request C
        ↓
Request D

That can be extremely useful.

The framework does not need to bootstrap everything from scratch every time.

Services can remain warm.

Configuration may already be loaded.

Expensive initialization can be reused.

This can improve performance significantly.

But there is a cost.

The process now has memory.

And that memory can become dangerous.

Imagine a very simple mistake

Suppose somebody writes something like this:

final class CurrentUser
{
    public static ?User $user = null;
}

During Request A:

CurrentUser = Alice

The request completes.

Under a disposable process model, the problem may never become obvious.

The process goes away.

Then Request B starts somewhere else.

But under a persistent worker, Request B may enter the exact same process.

If cleanup did not happen correctly:

Request A
CurrentUser = Alice

        ↓

Request B
CurrentUser = Alice   ← still there

Now Bob's request could observe Alice's state.

That is no longer just an implementation bug.

It is potentially a security vulnerability.

The same problem applies to much more than users

The current authenticated user is only the easiest example.

Execution-specific state can include:

  • tenant information,

  • locale,

  • timezone,

  • authorization decisions,

  • database transactions,

  • ORM state,

  • request metadata,

  • trace context,

  • logging context,

  • event listeners,

  • temporary caches,

  • feature flags,

  • impersonation state,

  • connection state.

Imagine a multi-tenant SaaS application.

Request A belongs to:

Tenant A

Then the worker handles another request for:

Tenant B

If some service accidentally retains the previous tenant:

Tenant A state
      ↓
worker reused
      ↓
Tenant B request

the consequences can be serious.

Cross-tenant leakage is one of the worst classes of bugs a SaaS system can have.

This is why "request scope" is not enough for me

When I started thinking about EvolvePHP 2, I did not want the framework architecture to assume that all application work is an HTTP request.

Modern applications do much more.

They process:

HTTP requests
queue messages
scheduled jobs
CLI commands
worker tasks

A queue worker has the same fundamental problem as a web worker.

It handles one piece of work.

Then another.

Then another.

So I prefer a broader term:

Execution.

An execution is one isolated unit of application work.

That gives us a model like:

Application
    |
    +-- Execution A
    |
    +-- Execution B
    |
    +-- Execution C

Each execution gets its own state.

And when it ends, that state must not become visible to the next execution.

EvolvePHP uses three foundational lifetimes

The current EvolvePHP 2 architecture is built around three basic service lifetimes:

Application
Execution
Transient

Application lifetime

These services may exist for as long as the booted application exists.

Examples might include:

configuration
router definitions
immutable metadata
connection pools
shared infrastructure

They must not accidentally hold state that belongs to one execution.

Execution lifetime

These services belong to exactly one unit of work.

Examples could include:

current user
current tenant
request context
authorization context
transaction context
execution logger context

When that execution ends, they end with it.

Transient lifetime

These are created when needed and are not automatically shared.

This sounds simple.

But the dependency rules are what make it useful.

A long-lived service must not capture short-lived state

Consider this:

Application Service
       ↓
Current User

If the application service lives for the entire worker lifetime, but Current User belongs to one execution, we now have a lifetime mismatch.

The long-lived service can accidentally retain the shorter-lived object.

EvolvePHP treats that as an architectural problem.

Conceptually, this direction is allowed:

Execution-scoped service
        ↓
Application-scoped service

But this is dangerous:

Application-scoped service
        ↓
Execution-scoped service

The longer-lived service should not capture the shorter-lived state.

That is one of the things a container or architecture checker can help prevent.

It is much better to reject that relationship early than discover it after production traffic starts leaking state.

Cleanup is not an optional courtesy

Even with correct lifetimes, some reusable infrastructure may hold mutable state.

That means the end of an execution needs a deliberate cleanup phase.

Conceptually:

Execution begins
    ↓
Work runs
    ↓
Primary result captured
    ↓
Cleanup
    ↓
Reset reusable state
    ↓
Can this process safely run again?

That final question matters.

A framework should not assume cleanup succeeded simply because it tried to perform cleanup.

What happens when cleanup fails?

This is where I think persistent-worker safety becomes more interesting.

Suppose the application work succeeds:

Order created successfully

Then cleanup fails.

Maybe a reset participant throws an exception.

Maybe a transaction state cannot be confirmed.

Maybe telemetry context cannot be safely detached.

Maybe some execution-specific resource cannot be closed.

The application result and the cleanup result are two different things.

EvolvePHP's direction is to preserve that distinction.

Conceptually:

Primary outcome: SUCCESS
Cleanup outcome: FAILURE
Process reuse:   UNSAFE

The successful business operation should not magically become a failed business operation simply because cleanup failed afterward.

But the process should also not quietly accept another execution.

That is where quarantine comes in.

Quarantine means "do not trust this process anymore"

The idea is simple:

Cleanup successful
       ↓
worker may be reused

but:

Cleanup failed or uncertain
       ↓
worker quarantined
       ↓
no new execution

Quarantine is not the same as killing the process immediately.

The framework's responsibility is to make the safety decision:

This process is no longer proven safe for reuse.

A runtime adapter or worker supervisor can then decide how to recycle or terminate it.

That separation is intentional.

Core framework code should not need to know whether the application is running under FrankenPHP, RoadRunner, a queue worker or some future runtime.

It only needs to know:

Reusable
or
Quarantined

Failure should not automatically poison the worker

There is another side to this.

Suppose application code throws an exception.

That does not necessarily mean the PHP process is corrupted.

For example:

handler failure
cleanup success

could still result in:

Primary outcome: FAILURE
Cleanup outcome: SUCCESS
Process reuse:   SAFE

That distinction is important.

An application-level exception and a runtime-integrity failure are not the same thing.

The worker should not necessarily be discarded just because one request returned an error.

What matters is whether execution state has been safely isolated and cleaned up.

The full model looks more like this

                Handler
                  |
          +-------+-------+
          |               |
       success          failure
          |               |
          +-------+-------+
                  |
               cleanup
                  |
          +-------+-------+
          |               |
       success          failure
          |               |
        reuse          quarantine

More specifically:

handler success + cleanup success
→ success + reusable

handler failure + cleanup success
→ failure + reusable

handler success + cleanup failure
→ success preserved + quarantine

handler failure + cleanup failure
→ original failure preserved + quarantine

I like this model because it separates business outcome from process safety.

The original failure should remain the original failure

Imagine application code fails because:

PaymentAuthorizationException

Then cleanup also fails.

A framework could easily replace the original exception with:

CleanupException

Now the most important information is lost.

The application's real failure has been hidden by lifecycle cleanup.

EvolvePHP's design direction is to preserve both:

Primary failure
    +
Cleanup failure

The primary failure stays identifiable.

Cleanup failure becomes additional lifecycle information.

And because cleanup failed:

reuse = false

This gives operations and observability systems a much clearer picture of what actually happened.

Execution identifiers help us reason about this

Every execution should have a unique identifier.

For example:

execution: 01K...

That identifier exists before application code runs.

It remains stable for that execution.

And it can be included safely in:

logs
diagnostics
telemetry
error reports

Then you can trace something like:

Execution ABC
kind: HTTP
handler: failed
cleanup: failed
reuse: quarantined

That becomes especially useful when the same application process handles thousands of units of work.

The execution ID is not necessarily the same thing as an HTTP request ID or OpenTelemetry trace ID.

It has a simpler job:

identify one unit of framework work.

Global "current execution" state is tempting

There is a very convenient design pattern that looks like this:

Execution::current()

or:

CurrentContext::get()

available globally from anywhere.

That can make APIs feel simple.

But it also creates ambient mutable state.

And ambient mutable state is exactly what becomes dangerous in long-running workers.

If the framework keeps:

global current user
global current tenant
global current execution

then everything depends on the framework resetting those globals perfectly.

I would rather make execution state explicit wherever practical.

That may require slightly more discipline from developers.

But explicit dependencies are easier to understand, test and isolate.

Sequential execution should be the baseline

Persistent workers lead naturally to another question:

Why not process multiple executions concurrently inside the same process?

Maybe eventually.

But concurrency changes the safety model dramatically.

If two executions run simultaneously:

Execution A ────────┐
                    ├── same process
Execution B ────────┘

then any mutable application-level state becomes much more dangerous.

Now isolation cannot depend merely on "reset before the next request."

There may be no "next" request.

The two executions overlap.

So the conservative approach is:

Execution A
    ↓
complete + cleanup
    ↓
Execution B

until concurrency isolation has explicit contracts and evidence behind it.

I think frameworks should earn concurrency support rather than assume it.

Performance should not come before isolation

Persistent workers are attractive because of performance.

Applications can avoid repeated bootstrapping.

Containers remain warm.

Route metadata may already be compiled.

Expensive setup can be reused.

That is good.

But the optimization is not worth introducing cross-request state leakage.

I would rather have:

slightly slower
correctly isolated

than:

very fast
occasionally leaks tenant state

Performance work should happen after the lifecycle model is safe.

And EvolvePHP's design should make the cost of isolation measurable so we can optimize it without weakening it.

This is especially important in enterprise software

The risk becomes more serious as applications become more valuable.

Imagine persistent workers handling:

banking requests
health records
government workflows
SaaS tenant data
payment processing
enterprise approvals

An execution leak does not just produce a strange UI bug.

It could expose confidential information or apply one customer's context to another customer's operation.

That is why execution isolation belongs in the framework architecture rather than in a deployment guide titled:

"Things to remember when using long-running workers."

The runtime model should make unsafe behavior harder.

Testing execution isolation needs to be aggressive

A few happy-path unit tests are not enough.

If EvolvePHP eventually claims production support for a persistent runtime, I want that claim backed by repeated-execution evidence.

For example:

Execution 1
authenticated user A
tenant A

Execution 2
anonymous
no tenant

Execution 3
authenticated user B
tenant B

Execution 4
handler throws

Execution 5
cleanup fails

Execution 6
must not run in quarantined worker

Repeat variations of that thousands or hundreds of thousands of times.

Then verify:

no previous user remains
no previous tenant remains
no stale transaction remains
no stale execution service remains
no stale telemetry context remains
memory does not grow without bound
cleanup failure always produces quarantine

This is part of the broader Evolve Assurance direction.

The point is not to claim that bugs are impossible.

The point is to make safety claims measurable and continuously test them.

Persistent PHP is an opportunity

I don't see persistent runtimes as a problem PHP should avoid.

They are an opportunity.

PHP applications can benefit from:

warm boot state
long-lived workers
lower initialization overhead
queue processing
modern application servers

But adopting that runtime model responsibly means acknowledging that one of PHP's historical safety nets is disappearing.

The process does not necessarily forget everything anymore.

So the framework has to help developers create that isolation deliberately.

This is why execution isolation is foundational in EvolvePHP

I could have treated persistent-worker support as something to add later.

Build the framework first.

Then create a RoadRunner integration.

Then add FrankenPHP.

Then fix whatever state leaks appear.

I don't want to do that.

By then, framework services may already have been designed around unsafe assumptions.

Instead, EvolvePHP starts with:

Application lifetime
Execution lifetime
Transient lifetime

and then asks every feature:

Which lifetime owns this state?

That question affects:

  • dependency injection,

  • authentication,

  • authorization,

  • transactions,

  • logging,

  • telemetry,

  • modules,

  • queues,

  • HTTP,

  • workers.

It is much easier to preserve isolation when the lifecycle is part of the original architecture.

The principle is simple

Persistent workers are not unsafe because they are persistent.

They are unsafe when applications behave as though the process is disposable even though it is not.

The solution is not to avoid reuse.

The solution is to make reuse conditional on isolation.

So the EvolvePHP rule is intentionally conservative:

One execution owns its state.

Cleanup must happen deterministically.

The next execution must not see the previous one.

If cleanup cannot prove the process is safe, do not reuse it.

That is the foundation.

Because the most dangerous persistent-worker bug is often not the request that fails.

It is the next request that succeeds using state it was never supposed to have.

And that is exactly why persistent PHP workers need execution isolation.

Modernize PHP Without Rewriting Everything — Evolve Bridge

 


There is a kind of advice developers give very easily when they are not responsible for the consequences.

“Just rewrite it.”

I have heard versions of that advice many times.

An application is old.

The framework is outdated.

The codebase has grown messy.

Dependencies are difficult to upgrade.

Nobody fully understands some parts of the system anymore.

So the clean technical answer seems obvious:

Start again.

Sometimes that is the right answer.

But in real businesses, it is often the most dangerous one.

That problem is one of the reasons I am building Evolve Bridge.

Old software is not automatically bad software

A business application can be badly structured and still be extremely valuable.

It may have been running for ten years.

It may process customer registrations every day.

It may generate invoices.

It may connect to payment providers.

It may manage stock, claims, applications, subscriptions or government records.

It may contain thousands of small business rules that were added gradually because real users discovered real edge cases.

Those rules may not be documented anywhere else.

They exist in the code.

So when somebody says:

“We can rebuild this in six months.”

what they often mean is:

“We can rebuild the parts of the system we currently understand in six months.”

Those are not the same thing.

Rewrites hide risk

Suppose a business has this application:

Legacy PHP Application
├── Authentication
├── Customers
├── Billing
├── Orders
├── Reporting
├── Notifications
├── Admin
└── Third-party integrations

Maybe it was written using an older version of Laravel.

Maybe CodeIgniter.

Maybe Symfony.

Maybe CakePHP or Yii.

Maybe there is no framework at all.

After years of development, the team decides the architecture is becoming difficult to maintain.

A rewrite begins.

Now the company has two systems:

Existing Application
        +
New Application

The old one is still serving customers.

The new one is trying to catch up.

But the business does not stop changing while the rewrite happens.

A new tax rule arrives.

A payment provider changes its API.

Management wants a new approval workflow.

A customer discovers an important bug.

I have experience this in my former employment while they were adding new features to their existing Laravel application and rewrite the existing application on Node.js.

A regulator asks for another report.

Where do those changes go?

Usually both systems begin changing at the same time.

That is when the rewrite becomes more difficult.

The finish line keeps moving.

I think modernization should be smaller than that

This is the problem Evolve Bridge is intended to address.

Instead of beginning with:

“How do we replace this application?”

start with:

“What capability actually needs to change?”

That creates a very different modernization strategy.

Imagine the legacy application's reporting system has become painful.

Maybe generating reports takes too long.

Maybe reporting code is tightly coupled to everything else.

Maybe the business wants a new analytics team to work on it independently.

Instead of rebuilding the whole application, we could create a boundary:

Existing PHP Application
├── Authentication
├── Customers
├── Billing
├── Orders
├── Notifications
└── Admin

           |
           |
      Evolve Bridge
           |
           v

     Reporting Module

The old application continues working.

Only Reporting moves.

If the migration succeeds, perhaps another capability moves later.

If it does not make sense to move anything else, we stop.

That is perfectly acceptable.

Modernization should not become a religion.

Evolve Bridge is not a code converter

This distinction is important.

The goal is not:

Laravel code
    ↓
magic converter
    ↓
EvolvePHP code

I don't think serious modernization works that way.

Frameworks have different lifecycle assumptions.

Applications make different architectural decisions.

Business logic becomes mixed with framework behavior.

Automated conversion may help with mechanical work, but it cannot safely understand every architectural decision in a mature application.

So Evolve Bridge is being designed as an integration boundary, not a rewrite engine.

The job of the Bridge is to help two systems coexist deliberately.

For example:

Laravel
   |
   | delegated operation
   v
Evolve Bridge
   |
   v
Evolve Module

or:

Legacy PHP
     |
     | HTTP
     v
Evolve Service

or eventually:

Symfony Application
       |
       | Queue/Event
       v
  Evolve Capability

The integration mechanism can differ.

The principle stays the same.

One capability at a time.

Sometimes the systems can live in the same process

For compatible modern PHP applications, one possible Bridge model is embedded integration.

Conceptually:

Laravel / Symfony
       |
       v
  Evolve Bridge
       |
       v
  Evolve Module

Both systems run inside the same PHP process.

The host framework still owns the main application.

It may continue owning:

  • routing,

  • sessions,

  • existing authentication,

  • the top-level error lifecycle,

  • existing controllers,

  • existing business capabilities.

EvolvePHP only owns the part delegated to it.

That matters because I do not want integrating EvolvePHP to mean secretly placing another framework in control of the entire application.

Ownership has to be clear.

Other applications need process separation

Same-process integration is not always safe or possible.

Imagine an application running:

PHP 7.x
old framework
old Composer dependencies

while EvolvePHP 2 requires a modern PHP runtime and modern dependencies.

Trying to force those dependency trees into the same process would be a bad idea.

In that case the better architecture may be:

Legacy Application
       |
       | HTTP / Queue / Events
       v
 EvolvePHP Service

Now each side can have its own:

  • PHP version,

  • dependencies,

  • release cycle,

  • process lifecycle,

  • deployment strategy.

That is a stronger isolation boundary.

It also introduces distributed-system concerns such as latency, timeouts and retries.

Those concerns should not be hidden.

Remote integration is more expensive than an in-process call.

But sometimes that cost is exactly what allows an old system to keep running while new capabilities move forward.

One capability needs one owner

There is another rule I consider important.

During migration, it is easy to create situations where both systems think they own the same thing.

That becomes dangerous.

For example:

Legacy Billing
      +
New Billing

Which one is responsible for the invoice?

Which one owns the database record?

Which one applies refunds?

What happens if one succeeds and the other fails?

Modernization can accidentally create more complexity than it removes if ownership is unclear.

So the Bridge direction assumes something much simpler:

At any point in the migration, a capability or business aggregate should have one authoritative owner.

That owner can change during migration.

But the transition should be explicit.

Database ownership is one of the hardest parts

Moving controllers is easy.

Moving ownership of data is much harder.

Imagine an Orders module that currently writes to twenty legacy tables.

There are several modernization approaches.

You might initially allow the new module to read existing data through a carefully defined adapter.

Later you might introduce a new model.

Eventually the source of truth might move.

The important thing is that this transition should be planned rather than accidental.

Conceptually:

Stage 1
Legacy system owns data
Evolve reads through adapter

then perhaps:

Stage 2
Legacy owns old records
New operations delegated to Evolve

and eventually:

Stage 3
Evolve owns capability and data
Legacy becomes consumer

There is no single migration strategy that fits every application.

Evolve Bridge should provide boundaries and tooling.

It should not pretend difficult data migration questions disappear.

Authentication has the same problem

A legacy application may already know who the user is.

EvolvePHP should not necessarily require the user to authenticate twice simply because one capability has moved.

Instead, the host may translate the identity into a trusted integration contract.

But there is an important difference between:

"This host says the user is Josiah"

and:

"Josiah is allowed to perform this operation"

The first is identity.

The second is authorization.

EvolvePHP should still authorize operations it owns.

That means Bridge integration needs explicit trust boundaries.

Authentication data should not simply be copied between frameworks and assumed to be safe.

Remote writes are particularly dangerous

One area where I do not want Evolve Bridge to hide complexity is state-changing remote operations.

Imagine:

Legacy Application
      |
      | POST /payment
      v
Evolve Service

The request times out.

Did the payment happen?

A timeout does not mean:

“Nothing happened.”

The remote side may have completed the operation and the response was simply lost.

If the legacy application retries blindly, it could perform the operation twice.

This is why remote modernization eventually needs patterns such as:

  • idempotency keys,

  • operation identifiers,

  • reconciliation,

  • explicit failure states,

  • safe retry policies.

This kind of behavior is especially important in financial and enterprise systems.

Framework integration should not give developers false confidence around distributed failure.

Rollback matters as much as migration

Developers naturally think about the happy direction:

Legacy
   ↓
Modern

But real migrations sometimes fail.

A new capability may have performance problems.

The integration may behave differently under production traffic.

An important edge case may be missing.

The company may need to temporarily return traffic to the old implementation.

So the modernization process should also ask:

How do we go back safely?

That could involve:

route ownership
feature flags
deployment version
data compatibility
migration checkpoint
rollback validation

A migration without a rollback story is not a controlled migration.

It is a bet.

Bridge should be useful before the migration begins

The more I have thought about modernization, the more I have realized that the actual integration layer is only one part of the problem.

Before moving anything, teams need to understand what they have.

That is why the broader EvolvePHP modernization direction now includes tools such as:

Evolve Audit
     ↓
Evolve Doctor
     ↓
Adoption Plan
     ↓
Evolve Bridge
     ↓
Modernize

Evolve Audit is intended to help answer questions like:

  • What PHP version is this system using?

  • Which dependencies are abandoned?

  • Where is global or static state being used?

  • Where are the strongest coupling points?

  • Which areas look like natural capability boundaries?

  • What could prevent persistent-runtime adoption?

  • Which parts of the application are the best modernization candidates?

Doctor has a more operational role.

It should answer things like:

  • Is the environment correctly configured?

  • Are required extensions available?

  • Is this application safe for the runtime being proposed?

  • Are there configuration or lifecycle problems that would make the migration unsafe?

I think modernization starts with diagnosis.

Not with writing new code.

This also changes how EvolvePHP competes

EvolvePHP does not need to convince every framework developer to stop using their preferred framework.

If EvolvePHP only provides value after you move the whole system to EvolvePHP, adoption becomes extremely expensive.

I want the opposite.

The framework should be able to earn trust gradually.

This is important for enterprise systems

Enterprise applications rarely have the luxury of being replaced overnight.

A government platform may have hundreds of workflows.

A bank may have dozens of surrounding systems connected to its core platform.

An ERP may contain years of company-specific behavior.

A marketplace may have sellers, buyers, billing, messaging, search and logistics integrations.

The safest modernization strategy is often not:

old system → new system

It is closer to:

old system
    |
    +---- capability A
    |
    +---- capability B
    |
    +---- capability C

and then gradually:

old system
    |
    +---- capability A
    |
    +---- Evolve capability B
    |
    +---- capability C

and later perhaps:

old system
    |
    +---- Evolve capability A
    |
    +---- Evolve capability B
    |
    +---- capability C

There may never be a dramatic rewrite day.

The architecture simply evolves.

I think that is healthier.

Sometimes the correct decision is not to migrate

This is also something modernization tooling should be able to tell you.

Suppose Audit examines an old capability and you discover:

  • it works reliably,

  • it rarely changes,

  • it has no serious security problems,

  • there are good tests,

  • maintenance cost is low.

Why rewrite it?

Modernization should solve business and engineering problems.

Not satisfy architectural fashion.

A good modernization platform should sometimes say:

Leave this alone.

That is a feature, not a failure.

Evolve Bridge is really about reducing the cost of change

The idea behind EvolvePHP 2 has increasingly become less about:

“How do I build another PHP application?”

and more about:

“How do I make PHP applications easier to change over their lifetime?”

For greenfield systems, that means starting modular.

For existing systems, it means creating controlled paths to evolve.

Evolve Bridge sits between those two worlds.

It should allow something built ten years ago to cooperate with something being built today.

Not because the old architecture is perfect.

Not because the new architecture is automatically better.

But because replacing a working business system all at once is often unnecessary risk.

What success would look like

If Evolve Bridge works the way I want, a modernization conversation might eventually look like this:

We have a twelve-year-old PHP application.

Fine.

It cannot be rewritten right now.

Fine.

We want to move Billing first.

Let's analyze the boundary.

Authentication must remain in the old application.

Fine.

The old application is running an incompatible PHP version.

Use a remote boundary.

We need rollback.

Define ownership and rollback before cutover.

After Billing works, we may move Reporting.

Good.

We may never move Customer Management.

Also good.

That is very different from telling the company:

“Come back when you're ready to rebuild everything.”

Modernize without rewriting everything

I don't think the future of PHP depends only on helping developers create more new applications.

There are already enormous amounts of PHP software running businesses today.

A lot of it will still be running five or ten years from now.

The interesting question is what happens to those systems as technology changes around them.

New PHP versions.

New deployment environments.

Persistent workers.

Cloud infrastructure.

New security requirements.

New observability expectations.

New business requirements.

Those applications need somewhere to go.

Evolve Bridge is my attempt to create one possible path.

Not:

Rewrite everything.

But:

Understand what you have. Create a boundary. Move what needs to move. Keep what still works. Repeat only when it makes sense.

That is the modernization philosophy behind EvolvePHP.

Modernize PHP without rewriting everything.