DevelopmentAll levels

PHP

PHP is the language that runs a large share of the web, and the one people judge from a version they last saw in 2012. Modern PHP has types, enums and a strict mode; what has not changed is the execution model, and that model explains nearly everything else. This page covers the request lifecycle, what the language actually looks like now, Laravel against Symfony, and where PHP projects usually go wrong.

Model
One request per worker
State
Outside the process
Standards
PSR, via PHP-FIG

What actually happens when a request arrives

PHP is almost always run as PHP-FPM: a pool of worker processes sitting behind a web server. A request is handed to a free worker, that worker builds the whole application from nothing, produces a response, and then discards every object, variable and connection it created. The next request gets a worker in the same empty state. This is called the shared-nothing model, and it is the single most important thing to understand about the language.

A PHP worker boots the application, handles exactly one request, writes the response, and then throws away everything it created — every object, every variable, every connection. The next request starts from an empty process, so nothing carries over in memory and anything that must survive lives in a database, a cache or a session store. This is why PHP scales by adding workers, why a leak inside one request is harmless, and why an in-process cache does nothing for you here.

The upside is that a whole category of bug cannot exist. A leaked object, a corrupted global, a connection left in a broken state — all of it is thrown away within milliseconds, so a request can only poison itself. Scaling is equally blunt: workers share nothing, so adding capacity means adding workers, and a crash takes down one request rather than a server.

The cost is that nothing survives. There is no in-process cache worth having, no connection pool inside the application, no background thread that keeps working after the response is sent, and no shared counter. Every one of those has to be moved outside the process: state to a database, a cache to Redis or Memcached, background work to a queue with a separate worker process consuming it. This is not a workaround — it is the intended design, and it is why PHP applications tend to be horizontally scalable by default while other stacks have to be made so deliberately.

There is now a second way to run PHP, and it is worth knowing it exists before you need it. Long-running servers — Swoole, RoadRunner, FrankenPHP, and Laravel Octane which wraps them — boot the application once and then serve many requests from the same process. Throughput rises sharply because the bootstrap disappears. So does the safety net: a leaked object now leaks for the life of the process, and a piece of state left on a singleton is visible to the next user's request. That is a real class of security bug, and it is the reason this mode is an optimisation you reach for deliberately, not a default.

The language people remember, and the one that exists

PHP 8 is a typed language when you ask it to be. Parameters, return values, properties and class constants all take type declarations; unions and intersections are expressible; and declare(strict_types=1) at the top of a file turns declarations from coercion hints into real checks. Without that line, passing the string "5" to an int parameter quietly converts it. With it, you get a TypeError. Put it in every file — the modern PHP ecosystem assumes it, and static analysis is far more useful when it can trust the signatures.

  • Enums

    A fixed set of cases as a real type, with methods and an optional backing value — the replacement for class constants and magic strings.

  • Readonly and promotion

    Constructor property promotion removes the boilerplate; readonly makes a property write-once, which is how value objects stop being accidentally mutable.

  • match

    An expression, not a statement: it returns a value, compares strictly, and throws if nothing matches — three defects of switch fixed at once.

  • Named arguments

    Call by parameter name, skipping optional ones. It makes a call with three booleans readable, and it makes parameter names part of your public API.

  • Attributes

    Structured metadata on classes and methods, read by reflection. Routes, validation rules and ORM mapping moved here from doc-block comments.

  • Fibers

    Interruptible functions — the primitive underneath async libraries. You will almost never write one directly; you will use a library that does.

Two things belong in every serious PHP project and neither ships with the language. Composer is the dependency manager and, through PSR-4, the autoloader — you will not write a single require for your own classes. Static analysis, PHPStan or Psalm, reads your type declarations and finds the null that reaches a method call before a user does; both work in levels, so an existing codebase can adopt them one notch at a time rather than in one impossible sweep.

On the JIT compiler added in PHP 8: be honest about it. It speeds up long-running numeric computation measurably and typical web request handling barely at all, because that work is dominated by I/O and by the database rather than by arithmetic. If your PHP is slow, the JIT is not the answer — the query log is.

Laravel, Symfony, and the standards under both

Almost no one starts from an empty directory. Two frameworks hold the field, and the choice between them is a choice about how much the framework decides for you. Under both sits PHP-FIG and its PSR standards — PSR-4 for autoloading, PSR-7 and PSR-15 for HTTP messages and middleware, PSR-12 for code style, PSR-11 for containers. That shared layer is why a cache library or a logger written for one framework usually drops into the other.

LaravelSymfony
StanceDecides for you. One idiomatic way to do most things, and it is present out of the box.Gives you components. You assemble and configure; less is implied.
Data accessEloquent, an active-record ORM — the model is the row, and it is fast to write.Doctrine, a data-mapper ORM — entities know nothing about the database.
Best fitProduct teams shipping features fast; small and mid-size teams; a broad standard library.Long-lived enterprise systems, complex domains, teams that want explicit boundaries.
Main riskConvenience invites business logic into models and controllers until nothing is testable.Ceremony and configuration surface slow a small team down for no return.

The honest summary: for a typical product team, Laravel gets you to a working feature sooner and the risk is architectural drift; for a system that a bank will still be running in twelve years, Symfony's explicitness is worth the extra ceremony. Both are maintained, both are fast enough, and the difference between a good and a bad codebase in either is discipline about where business logic lives — not the framework.

Talking to the database

PHP's reputation for SQL injection came from a real era of string-concatenated queries, and the fix has been standard for well over a decade: prepared statements. A prepared statement sends the query shape and the values separately, so a value can never be read as syntax. Every ORM and query builder in the ecosystem uses them underneath. The only way to reintroduce the vulnerability is to interpolate a variable into SQL by hand — which is exactly why any raw query with a $ inside it deserves a second look in review.

Two more habits separate a codebase that survives growth from one that does not. Wrap any sequence of writes that must succeed or fail together in a transaction, and keep the transaction short — a transaction held open across an HTTP call to a payment provider will lock rows for as long as that provider is slow. And keep migrations in version control alongside the code that needs them, so a deployment and its schema change move together; a schema that drifts from the code is the failure mode that turns a routine release into an outage.

How this shows up in real delivery

Because a worker cannot outlive its response, anything slow has to leave the request. Sending email, generating a PDF, calling a third-party API, resizing an image — all of it goes onto a queue, and a separate long-running worker process consumes it. Both major frameworks ship this, and it is not an advanced technique: it is the standard way PHP does background work, and a request that does any of those inline is a page your users will describe as "sometimes it just hangs".

Deployment follows the same logic. A release swaps the code and then resets OPcache, because the workers are still holding compiled bytecode for the previous version; forget that step and you get the confusing state where some requests run new code and some run old. Configuration comes from the environment, never from a committed file. Sessions must not live on a worker's local disk once there is more than one server, or a user's second request will land elsewhere and find themselves logged out.

Where it degrades

  • Business logic inside controllers and models, until a rule can only be tested through an HTTP request.
  • N+1 queries hidden behind a convenient ORM relation, invisible until the table has real data in it.
  • Slow work done inline — mail, PDFs, third-party calls — instead of on a queue.
  • Files without strict_types, so type declarations coerce instead of checking and give false confidence.
  • Raw SQL assembled by concatenation, which is the only remaining way to write an injection in modern PHP.
  • Sessions or uploads on local disk, which works on one server and breaks silently on two.
  • Moving to a long-running server for speed without auditing what state the singletons now keep between users.

When to use it

Use it when

  • Web applications and APIs where the work per request is short and the traffic scales horizontally.
  • Teams that need a working product quickly — the framework, ORM, queue and admin tooling all come as one package.
  • Content-heavy and e-commerce systems, where the hosting, the talent pool and the ecosystem are all deep.
  • Existing PHP systems — a modernised PHP 8 codebase with static analysis is a better bet than a rewrite.

Avoid it when

  • Long-lived connections — WebSockets, streaming, chat — where a per-request model fights you the whole way.
  • CPU-heavy processing and data pipelines; the runtime is not built for it and the ecosystem is elsewhere.
  • Anything needing real in-process concurrency, which the standard execution model simply does not offer.
  • Keeping state on a singleton once you move to Octane, Swoole or RoadRunner — that is a cross-user data leak.

Found this useful?

Share it with someone who is working on the same problem.