What PHP Standards Recommendations PSRs Solve and Which Matter
PHP Standards Recommendations solve interop problems across frameworks—here is which PSRs matter, which are stable, and how the PHP-FIG process works.
What PHP Standards Recommendations Solve and Which Matter
A PHP Standards Recommendation (PSR) is a published specification from the PHP Framework Interop Group (PHP-FIG), a body of voting members drawn from across the PHP ecosystem. It exists because PHP projects written by different teams use different conventions for the same thing, and those differences break tooling, slow onboarding, and make packages incompatible. Not every PSR succeeded. Some stalled in draft, some were abandoned, and a handful became the de facto contracts that Composer, CI pipelines, and code review enforce today. This guide maps each PSR to the concrete problem it was designed to fix, distinguishes a specification enforced by tooling from a popular practice that spread without a vote, and names the PSRs that failed, using the PHP-FIG voting record as the authority.
PHP-FIG Process and Which PSRs Are Binding
How a Specification Becomes a PSR
PHP-FIG membership requires a project or organisation to nominate a representative who votes on proposed specifications. A proposal moves through four stages: Pre-Draft, Draft, Review, and Accepted. The vote that matters is the Entrance Vote to move from Draft to Review, which requires a two-thirds majority of the voting membership. A specification that clears that vote enters a review period before a final Acceptance Vote. The voting record is public, and it shows exactly where a PSR stalled.
PSR-0 autoloading passed in 2010 and was deprecated by PSR-4 in 2014. PSR-2 coding style passed in 2012 and was deprecated by PSR-12 in 2019. The deprecation mechanism is formal: a PSR that replaces an earlier one carries a note stating the superseded specification. PSR-1, the basic coding standard, remains in effect because PSR-12 extends it rather than replacing it. PSR-6, the caching interface, was accepted but saw limited adoption because projects that needed caching had already built their own abstractions. PSR-5 PHPDoc standard entered Draft in 2013 and never reached Review. PSR-14, the event dispatcher, was accepted but sits outside the daily workflow of most PHP developers building CRUD applications.
Which PSRs Tooling Enforces
The binding PSRs are the ones Composer enforces at install time and the ones PHP CS Fixer and PHP_CodeSniffer flag. PSR-4 autoloading is binding: if your namespace declaration does not match the directory structure, Composer refuses to load the class. PSR-12 is binding when a team configures its CI to fail builds that violate it. PSR-7 and PSR-15 are binding when a framework or middleware library type-hints against the interfaces. A PSR that no tool enforces is a suggestion, not a specification.
PSR-4 Autoloading
The PSR-4 autoloading standard fixes a single failure: PHP's require and include statements that break when a file moves. Before PSR-4, a developer had to write a manual require_once chain or configure a classmap. PSR-4 maps a fully qualified namespace prefix to a base directory, and the autoloader locates the file by substituting directory separators for namespace separators and appending .php. Composer's autoloader implements this in the vendor/composer/autoload_psr4.php file, generated during every composer install and composer update.
Declare the mapping in the project's composer.json under the autoload.psr-4 key. A typical entry maps "App\\": "src/" so that class App\Service\Payment lives in src/Service/Payment.php. The specification requires case-sensitive file names on case-insensitive file systems, which means a mismatch between the class name and the file name fails on a production Linux server even if it works on a local macOS or Windows machine. This is the most common PSR-4 failure on mixed-OS teams.
The autoloading specification does not cover functions or constants. It covers classes, interfaces, and traits only. Composer's files autoload key handles functions, and that mechanism sits outside the PSR. PSR-4 replaced PSR-0 in 2014, and the migration removed the underscore-to-directory-separator rule that PSR-0 required for PEAR-style class names. Projects still running on PSR-0 autoloading are at least a decade behind current tooling expectations.
PSR-12 Coding Style
The PSR-12 coding style guide solves the problem of code reviews that waste time arguing about bracket placement, line length, and whitespace. It extends PSR-1 and replaces PSR-2, adding rules for features that did not exist in 2012: declare(strict_types=1) placement, nullable type declarations, void return types, and short closures. The specification mandates a 4-space indent, a soft limit on line length, and a blank line before each namespace declaration.
PSR-12 is enforceable. PHP_CodeSniffer ships with a PSR-12 ruleset that flags violations. PHP CS Fixer applies automatic fixes for most rules. A CI pipeline that runs phpcs --standard=PSR-12 on every pull request is the enforcement mechanism that moves the specification from a document to a contract. Teams that skip the CI step are treating PSR-12 as a practice, not a specification.
The specification covers the structure of a PHP file from the opening ?>
PSR-7 HTTP Message Interface
The PSR-7 HTTP message interface solves the problem of every PHP framework having its own Request and Response objects. Before PSR-7, a middleware written for Slim could not run in Laravel without an adapter layer. PSR-7 defines seven interfaces: RequestInterface, ResponseInterface, ServerRequestInterface, StreamInterface, UriInterface, UploadedFileInterface, and MessageInterface, which RequestInterface and ResponseInterface extend. The interfaces describe HTTP messages as immutable objects. A method call on a PSR-7 request returns a new instance with the change applied, which prevents middleware from modifying a request that another middleware still needs in its original form.
Adoption and the Immutability Constraint
The specification was accepted in 2015 and is implemented by every major PHP HTTP library. Guzzle uses PSR-7 for its HTTP client. Slim 4 type-hints against PSR-7 interfaces throughout its routing and middleware pipeline. Laravel ships with Symfony's HTTP Foundation, not PSR-7, but provides the psr-http-message-bridge package that adapts between them. The bridge exists because PSR-7's immutability is a design constraint that conflicts with Symfony's mutable request and response objects.
The StreamInterface Trap
StreamInterface is the part of PSR-7 that most newcomers get wrong. A PSR-7 message body is a stream, not a string. Calling $response->getBody()->__toString() reads the stream to the end and returns a string. But the stream's pointer is now at the end, and a second call returns an empty string. Rewind the stream with rewind() to fix this, but only if the stream is seekable. The php://temp stream is seekable. The php://input stream is not. That difference is the source of a class of bugs that appear only when a project moves from a development server to a production server with a different SAPI.
PSR-15 HTTP Handlers and Middleware
The PSR-15 HTTP server request handler and middleware specification solves the problem that PSR-7 left open: how middleware processes a request and produces a response. PSR-7 defines the messages. PSR-15 defines the handler that processes them. A PSR-15 middleware implements MiddlewareInterface with a single process() method that receives a ServerRequestInterface and a RequestHandlerInterface, and returns a ResponseInterface. The RequestHandlerInterface defines a handle() method that passes the request to the next handler in the stack.
The specification creates a contract where middleware can decide whether to pass the request to the next handler, modify the request before passing it, or return a response immediately and short-circuit the chain. A cache middleware that finds a cached response returns it without calling handle(). An authentication middleware that finds no credentials returns a 401 response and stops the chain. A logging middleware calls handle() and then logs the response, wrapping the next handler without changing the request.
PSR-15 was accepted in 2018 and is the specification that Slim 4 and Mezzio build their middleware pipelines on. Laravel's middleware system predates PSR-15 and uses its own interface, but the concepts map directly. The specification also defines a MiddlewareDispatcherInterface for composing middleware into a pipeline, though the dispatcher is less widely implemented than the middleware interface itself. The key practical distinction: PSR-15 middleware is server-side only. It does not cover HTTP client middleware. That is a separate concern that PSR-18 addresses.
PSRs That Stalled and PSRs to Skip
Specifications That Never Passed
PSR-5, the PHPDoc standard, entered Draft in 2013 and never passed the Entrance Vote to move to Review. The draft defined @param, @return, @throws, and other tags with precise syntax rules, but the PHP-FIG membership could not reach consensus on type representation. The draft remains in Draft status, and the PHP community has converged on a de facto convention drawn from phpDocumentor and PHPStan annotations instead. PSR-19, the PHPDoc tags standard, was an attempt to revive the work in 2020 and also stalled in Draft.
Accepted but Narrow
PSR-6, the caching interface, was accepted in 2015 but saw limited adoption. Symfony's Cache component became the default caching layer for most PHP projects, and the PSR-6 interfaces added an abstraction that few projects needed. PSR-16, the simple cache interface, was accepted in 2017 as a companion to PSR-6 and is more commonly used because it defines a single get/set/delete contract with no pool/item separation.
PSR-11, the container interface, was accepted in 2016 and defines two methods: get() and has(). It is implemented by every major dependency injection container, but a developer rarely interacts with it directly. The container resolves behind the scenes, and the interface matters only when swapping one container implementation for another.
PSR-3, the logger interface, was accepted in 2012 and defines nine log methods that map to severity levels defined in RFC 5424. Every PHP logging library implements it, and it is the one PSR that appears in the method signature of third-party code more often than any other except PSR-7.
PSR-14, the event dispatcher, was accepted in 2019. It defines an EventDispatcherInterface with a dispatch() method and a ListenerProviderInterface. It is the newest of the accepted PSRs that a PHP developer is likely to encounter, and it appears more in framework internals than in application code.
What Separates a Specification From a Practice
A PHP specification is a document that passed a PHP-FIG vote and carries a PSR number. A practice is a pattern that many projects follow without a formal specification. Composer's vendor directory layout is a practice: no PSR defines it, but every PHP project assumes it. The .env file pattern is a practice popularised by the vlucas/phpdotenv library. Laravel's directory structure is a practice that the framework enforces through its installer and artisan commands, not through a PSR.
The confusion between a specification and a practice arises most often with coding style. PSR-12 is the specification that PHP-FIG published. The Laravel coding style is a practice that overlaps with PSR-12 but differs on specifics like the spacing around the concatenation operator. A project that runs phpcs with the PSR-12 ruleset enforces the specification. A project that follows Laravel's style guide without automated enforcement is following a practice.
The Framework Interop Group voting record is the only authority that distinguishes the two. A PSR number is a claim that a specification passed a vote. Check the PHP-FIG website to see whether the claim is true. PSR-2 passed and was deprecated. PSR-5 never passed. PSR-12 passed and is current. This guide is a map of which claims hold up under that check, not a list of every document that uses the word "standard".
Common Questions
Does every PHP project need to implement every PSR?
No. PSR-4 autoloading is required because Composer depends on it. PSR-12 is optional but widely enforced in CI. PSR-7 and PSR-15 matter only if your project handles HTTP messages or middleware. PSR-6 and PSR-16 matter only if your project needs a caching abstraction beyond what its framework provides.
What happens if I ignore PSR-4 in a new project?
Composer refuses to autoload your classes. You can work around this with a classmap entry in composer.json, but every package you install still uses PSR-4, and your build step runs a manual autoloader dump that breaks the moment someone runs composer install on a different machine.
Why did PSR-5 fail to pass?
The PHP-FIG membership could not agree on a syntax for array types, generics, and union types in PHPDoc. The draft has been in Draft status since 2013, and the community has standardised on phpDocumentor and PHPStan annotation formats without a PSR.
How do I check which PSRs a library implements?
Look at the composer.json require section for psr/* packages. A library that implements PSR-7 lists psr/http-message. One that implements PSR-15 lists psr/http-server-middleware. The packagist.org page for each psr/* package shows the implementing libraries in the "Dependents" tab.