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.

Thursday, 3 September 2026

Secure by Default Is Not Enough — Frameworks Need Security Evidence

Secure by Default Is Not Enough — Frameworks Need Security Evidence

“Secure by default” sounds reassuring.

It is also something almost every serious framework wants to say.

Use parameterized queries. Set sensible cookie defaults. Escape output. Protect forms from CSRF. Make unsafe configuration harder.

All of that matters.

But while designing EvolvePHP 2, I have become increasingly uncomfortable with stopping there.

Because there is another question behind every security claim:

How do we know the protection actually holds?

That is where I think frameworks need to go further.

Not just secure defaults.

Security evidence.

A security claim is not the same as a security guarantee

Imagine a framework says:

“Request state is isolated between persistent-worker executions.”

That sounds good.

But what actually supports the claim?

Documentation?

A code comment?

A few unit tests?

Or have we repeatedly tried to make one execution leak into another?

The distinction matters.

For EvolvePHP, one of the architectural rules is that execution-specific state should not survive into the next execution.

That may include:

current user
current tenant
transaction context
authorization state
trace context
temporary listeners

If I claim those things are isolated, I want more than confidence in the implementation.

I want tests designed specifically to prove me wrong.

Tests should attack the assumptions

A normal test might do this:

Execution A starts
Execution A completes
Execution B starts
Execution B completes

Everything passes.

Great.

But an assurance test should be more hostile.

Execution A
User: Alice
Tenant: Company A
Handler: succeeds

Execution B
Anonymous
No tenant
Handler: throws

Execution C
User: Bob
Tenant: Company B
Cleanup: partially fails

Then ask:

  • Can Bob see Alice's state?

  • Does an anonymous execution inherit authentication?

  • Is a transaction left open?

  • Does trace context survive?

  • Is the worker reused after cleanup becomes uncertain?

That is a different mindset.

The goal isn't only to show that the framework works.

It is to discover the conditions under which it stops being safe.

Some rules should be architectural invariants

There are certain things I would rather make difficult or impossible by design.

For example:

Application-scoped service
        ↓
Execution-scoped CurrentUser

That is a dangerous lifetime relationship in a long-running process.

Instead of documenting:

“Please don't do this.”

the container should reject it when possible.

Likewise, if cleanup fails, EvolvePHP should not quietly assume the worker is safe.

The rule should be:

Cleanup successful
→ reuse may continue

Cleanup failed or uncertain
→ quarantine

Then the test suite should continuously verify that no path accidentally turns cleanup failure into safe reuse.

These are the kinds of claims that can become measurable.

Unit tests are only the beginning

Traditional tests are necessary, but they are not enough for the kinds of guarantees I want EvolvePHP to eventually make.

Different problems need different kinds of pressure.

Property-based tests can generate many combinations of lifecycle state rather than testing only examples we thought of manually.

Fuzzing can feed unexpected input into parsers, routing, protocol boundaries, and other exposed surfaces.

Mutation testing can deliberately alter security-sensitive code and show whether our tests actually notice.

Fault injection can simulate database failures, reset failures, telemetry failures, timeouts, and partial cleanup.

Soak testing can run persistent workers through hundreds of thousands of executions while alternating users, tenants, successes, failures, and memory pressure.

The idea is not complexity for its own sake.

It is proportional evidence for important claims.

Security is bigger than the runtime

Execution isolation is only one part of security.

A PHP framework also has to think about familiar application risks:

  • SQL injection

  • XSS

  • CSRF

  • SSRF

  • broken authorization

  • insecure sessions

  • unsafe redirects

  • secrets exposure

  • file and path handling

  • dependency vulnerabilities

  • insecure defaults

I don't want EvolvePHP to invent its own definition of application security.

That is why the direction includes alignment with established security references such as OWASP ASVS and secure-development practices such as NIST SSDF.

Not so I can put a badge on the website.

But so there is a recognized baseline asking:

What security requirements apply here?

and:

What evidence do we have that we followed them?

Framework security and application security are different

This distinction is important.

Even a well-designed framework cannot guarantee that every application built with it is secure.

A developer can still make an authorization mistake.

A company can expose a secret.

A deployment can be misconfigured.

A custom plugin can introduce a vulnerability.

So I do not want EvolvePHP to eventually claim:

“Applications built with EvolvePHP are secure.”

That would be irresponsible.

A better goal is:

EvolvePHP should provide safe defaults, enforce important boundaries where possible, expose insecure conditions, and make security verification easier for application teams.

That is a much more defensible promise.

“Secure by default” should be the starting point

I still want secure defaults.

Developers should not need to be security specialists just to get sensible behavior.

But defaults alone are not enough for software that may eventually run financial systems, SaaS platforms, government applications, or other long-lived business systems.

The stronger question is:

Can we show why we believe this behavior is safe?

That is the direction I want EvolvePHP to move toward.

Not:

“Trust us. We designed it securely.”

But:

These are our security and safety claims. These are the boundaries. And here is the evidence continuously trying to prove those claims wrong.

For me, that is a much stronger foundation for trust.

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.