PHP-FIG and the PSR Standards: A Reference Guide
Understand what PHP-FIG is and why it exists, and know what each widely-used PSR (1, 3, 4, 6, 7, 11, 12, 14, 15, 16, 18) actually specifies, so you can recognize what interoperability guarantee a package is making when it claims PSR compliance and choose the right interface when designing your own code.
PHP-FIG (the PHP Framework Interop Group) is a body of PHP project representatives — originally framework maintainers, now a broader set of package and project maintainers — that publishes PSRs (PHP Standard Recommendations): shared interfaces and conventions that let independently-written packages interoperate without every author having to agree on a whole framework. A PSR-3-compliant logger from one vendor can be swapped into any code that only depends on the PSR-3 interface, regardless of which framework either side was built for — this is the entire point of PHP-FIG's existence, and it's why the modern PHP ecosystem is far more composable than it was before these standards existed.
Coding style and structural standards: PSR-1 and PSR-12
PSR-1 (Basic Coding Standard) is PHP-FIG's foundational, deliberately minimal standard: files should use only <?php and <?php echo tags, files should declare symbols (classes, functions, constants) OR cause side effects (like echoing output or making a request), but not both, class names should be declared in StudlyCaps, method names in camelCase, and class constants in UPPER_CASE. It's intentionally small — a baseline almost any PHP code can meet — rather than a complete style guide.
PSR-12 (Extended Coding Style) supersedes the older PSR-2 and is the actual detailed formatting standard most PHP tooling (PHP-CS-Fixer, PHPCS with the PSR-12 ruleset) enforces by default: 4-space indentation (never tabs), a 120-character soft line-length guideline, opening braces for classes and methods on their own line but for control structures on the same line, one blank line after the namespace declaration and after the use block, and consistent rules for visibility keywords, declare(strict_types=1) placement, and modern syntax like union types, attributes, and match expressions. PSR-12 is a style standard, not a functional interface — it exists purely so that code from different authors looks consistent and reduces diff noise and review friction when people from different projects collaborate.
Autoloading: PSR-4
PSR-4 defines how to map fully-qualified class names to file paths for autoloading, superseding the older PSR-0. It specifies that a namespace prefix corresponds to a 'base directory', and everything after that prefix in a class's fully-qualified name maps one-to-one to a subdirectory path plus a .php file matching the final class name exactly (including case). This is the standard Composer's own autoloader implements — the "psr-4" key in composer.json's autoload section is a direct, literal application of this spec, which is why it was covered concretely in the package-authoring lesson.
Logging and dependency injection: PSR-3 and PSR-11
PSR-3 (Logger Interface) defines Psr\Log\LoggerInterface with eight standard severity methods — emergency(), alert(), critical(), error(), warning(), notice(), info(), debug() — plus a generic log($level, $message, array $context = []). Because this is just an interface, any library can type-hint LoggerInterface in its constructor and accept whatever concrete logger the consuming application already uses (Monolog is the dominant real-world implementation), without the library needing to know or care which logging package the application chose.
PSR-11 (Container Interface) defines the minimal Psr\Container\ContainerInterface, with exactly two methods: get(string $id) (retrieve an entry, or throw a NotFoundExceptionInterface) and has(string $id): bool (check existence without triggering instantiation). It deliberately does NOT standardize how a container is configured or how entries get bound — that's each framework's own concern — it only standardizes the read-side lookup contract, which is exactly enough for a library to accept 'some dependency injection container' as a constructor argument without being locked to Symfony's, Laravel's, or PHP-DI's specific container implementation.
Caching: PSR-6 and PSR-16
PSR-6 (Caching Interface) defines a more structured, object-oriented caching contract built around CacheItemPoolInterface (the pool/repository you fetch items from and save them back to) and CacheItemInterface (an individual cache entry object exposing get(), set(), isHit(), and expiration controls). Its two-step 'fetch an item object, mutate it, save it back to the pool' flow is more ceremonious than most simple use cases need, but it supports more advanced patterns like deferred/batched saves.
PSR-16 (Simple Cache) was introduced specifically to address PSR-6's ceremony for the common case: Psr\SimpleCache\CacheInterface offers a flat, direct API — get($key, $default = null), set($key, $value, $ttl = null), delete($key), plus multi-key variants getMultiple()/setMultiple()/deleteMultiple() — much closer to how most developers actually think about a key-value cache. The two coexist deliberately: PSR-6 for libraries needing fine-grained cache item control, PSR-16 for the common case where a simple get/set is all that's needed.
HTTP: PSR-7, PSR-15, and PSR-18
PSR-7 (HTTP Message Interfaces) standardizes immutable value objects representing HTTP requests and responses — RequestInterface/ResponseInterface (and server-side ServerRequestInterface), plus UriInterface and StreamInterface for the URI and body. Every mutator method (withHeader(), withBody(), withStatus(), and so on) returns a new instance rather than modifying the object in place, which is what lets these objects pass safely through layered middleware pipelines without one layer's mutation silently affecting another's view of the same request.
PSR-15 (HTTP Server Request Handlers) builds directly on PSR-7, defining RequestHandlerInterface (a single handle(ServerRequestInterface): ResponseInterface method — the thing that ultimately produces a response) and MiddlewareInterface (a process(ServerRequestInterface, RequestHandlerInterface): ResponseInterface method — middleware that can inspect/modify the request, delegate to the next handler in the chain, and inspect/modify the resulting response on the way back out). This is the standard contract that makes a middleware written for one PSR-15-compliant framework usable, unmodified, in a completely different PSR-15-compliant framework.
PSR-18 (HTTP Client) standardizes the client side: ClientInterface with one method, sendRequest(RequestInterface): ResponseInterface, which throws a ClientExceptionInterface on failure. It deliberately reuses PSR-7's request/response objects rather than inventing its own, so a library needing to make an outbound HTTP call can type-hint ClientInterface and let the consuming application supply whichever PSR-18 implementation it prefers (Guzzle's PSR-18 adapter is a common choice) instead of the library hard-depending on a specific HTTP client package.
Events: PSR-14
PSR-14 (Event Dispatcher) standardizes the publish/subscribe pattern with two small interfaces: EventDispatcherInterface, with a single dispatch(object $event): object method that hands an event object to all registered listeners and returns it back (possibly mutated, since listeners receive the same instance), and ListenerProviderInterface, with getListenersForEvent(object $event): iterable, which is queried by the dispatcher to find which listeners should run for a given event's type. Notably, PSR-14 does not standardize a base Event class or how listeners get registered to a provider — an event can be any plain object at all, and registration mechanics are left to each implementation, keeping the interoperable surface deliberately minimal.
Representative PSR interface signatures
All of these are pure interfaces published as tiny standalone Composer packages under the psr/ vendor namespace (psr/log, psr/container, psr/simple-cache, psr/http-server-handler, psr/http-client) — you composer require the interface package, code against the interface, and let the application wire in a concrete implementation. PHP-FIG has continued evolving this catalogue since these were published — always check the current PHP-FIG site for the authoritative, up-to-date list and status of each PSR (some have been superseded, e.g. PSR-2 by PSR-12, PSR-0 by PSR-4).
// PSR-3 — Psr\Log\LoggerInterface (excerpt)
interface LoggerInterface {
public function error(string|\Stringable $message, array $context = []): void;
public function info(string|\Stringable $message, array $context = []): void;
public function log($level, string|\Stringable $message, array $context = []): void;
}
// PSR-11 — Psr\Container\ContainerInterface
interface ContainerInterface {
public function get(string $id);
public function has(string $id): bool;
}
// PSR-16 — Psr\SimpleCache\CacheInterface (excerpt)
interface CacheInterface {
public function get(string $key, mixed $default = null): mixed;
public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): bool;
public function delete(string $key): bool;
}
// PSR-15 — Psr\Http\Server\MiddlewareInterface
interface MiddlewareInterface {
public function process(
\Psr\Http\Message\ServerRequestInterface $request,
\Psr\Http\Server\RequestHandlerInterface $handler
): \Psr\Http\Message\ResponseInterface;
}
// PSR-18 — Psr\Http\Client\ClientInterface
interface ClientInterface {
public function sendRequest(\Psr\Http\Message\RequestInterface $reques
…Code Examples
PaymentProcessor depends only on the PSR-3 LoggerInterface, not on Monolog specifically — this is the entire value proposition of PSR standards: the class is fully decoupled from any particular logging library and works unchanged whether the application wires in Monolog, a simple file logger, or a test double implementing the same interface.
This middleware is written purely against PSR-15 and PSR-7 interfaces, so it works unmodified inside any PSR-15-compliant framework's middleware pipeline. Note how withAttribute() returns a new request instance rather than mutating the original — PSR-7's immutability guarantee is what makes it safe to pass this request further down the chain without worrying about hidden state changes from earlier middleware.
$request = $request->withAttribute('user_id', 42);
This entire service is built from PSR interfaces only — PSR-18 for the outbound HTTP call, PSR-7's request factory for building the request, and PSR-16 for caching the result — meaning it has zero hard dependency on Guzzle, Symfony HttpClient, or any specific cache backend. Any application can supply its own preferred concrete implementations of these three interfaces and this class works without modification.
<?php
use Psr\Log\LoggerInterface;
final class PaymentProcessor
{
public function __construct(private LoggerInterface $logger) {}
public function charge(float $amount): void
{
$this->logger->info('Charging {amount}', ['amount' => $amount]);
// ... actual charge logic ...
}
}
// Any PSR-3 logger works here without PaymentProcessor knowing which one:
$processor = new PaymentProcessor(new \Monolog\Logger('payments'));<?php
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
final class AuthMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if (!$request->hasHeader('Authorization')) {
return new \GuzzleHttp\Psr7\Response(401, [], 'Unauthorized');
}
// request is immutable — any modification returns a NEW instance
$request = $request->withAttribute('user_id', 42);
return $handler->handle($request); // delegate to the next layer
}
}<?php
use Psr\SimpleCache\CacheInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
final class WeatherService
{
public function __construct(
private ClientInterface $httpClient,
private RequestFactoryInterface $requestFactory,
private CacheInterface $cache,
) {}
public function getTemperature(string $city): float
{
$cacheKey = "weather_{$city}";
$cached = $this->cache->get($cacheKey);
if ($cached !== null) {
return $cached;
}
$request = $this->requestFactory->createRequest('GET', "https://api.example.com/weather?city={$city}");
$response = $this->httpClient->sendRequest($request); // PSR-18
$temp = (float) json_decode((string) $response->getBody(), true)['temp'];
$this->cache->set($cacheKey, $temp, ttl: 300); // PSR-16
return $temp;
}
}PSR interfaces as swappable seams between packages
Because your code depends on the PHP-FIG interface rather than a specific package, the application wires in whichever concrete implementation it prefers — this is the interoperability PHP-FIG exists to guarantee.
Your library code
|
v depends on INTERFACES only
+-------------------------------------------+
| LoggerInterface (PSR-3) |
| ContainerInterface (PSR-11) |
| CacheInterface (PSR-16) / CacheItemPoolInterface (PSR-6)
| ClientInterface (PSR-18) |
| MiddlewareInterface / RequestHandlerInterface (PSR-15)
+-------------------------------------------+
^ ^
| |
Monolog implements Guzzle implements
LoggerInterface ClientInterface + PSR-7
(chosen by the application, not your library)Important Rules
PSR-1/PSR-12 are style and structure standards; PSR-4 is the autoloading standard — none of the three define runtime interfaces.
PSR-3 (logging), PSR-11 (containers), PSR-6/PSR-16 (caching, structured vs simple), PSR-7 (HTTP messages), PSR-15 (HTTP middleware/handlers), PSR-18 (HTTP client), and PSR-14 (events) are all runtime interface standards published as tiny psr/ Composer packages.
Code against the PSR interface, not a specific implementation, whenever your code needs logging, caching, an HTTP client, DI lookup, or middleware — this is what lets your package work inside any framework that supplies a compliant implementation.
PSR-7 objects are immutable — every with*() method returns a new instance; never assume mutating methods change the object in place.
PHP-FIG's catalogue evolves — some PSRs supersede older ones (PSR-12 supersedes PSR-2, PSR-4 supersedes PSR-0); check the current PHP-FIG site for a PSR's live status before assuming an older number is still the active recommendation.
Where this shows up
Symfony's HttpClient, Guzzle, Laravel's HTTP facade, and Laminas's Diactoros all implement PSR-7/PSR-18, meaning HTTP-related libraries can be framework-agnostic almost for free. Middleware written for one PSR-15 microframework (Slim, Mezzio) often runs unmodified in another. Nearly every framework's dependency injection container implements PSR-11 alongside its own richer API, specifically so third-party packages can accept 'a container' without coupling to that framework. And virtually the entire PHP logging ecosystem — Monolog above all — exists as a PSR-3 implementation, which is why so many unrelated packages can accept 'a logger' as a constructor argument with zero framework coupling.
<?php class ReportGenerator { private \Monolog\Logger $logger; // hard dependency on a specific package public function __construct(\Monolog\Logger $logger) { $this->logger = $logger; } }
<?php use Psr\Log\LoggerInterface; class ReportGenerator { public function __construct(private LoggerInterface $logger) {} }
Type-hinting the concrete Monolog\Logger class instead of Psr\Log\LoggerInterface forces every consumer of ReportGenerator to use Monolog specifically, defeating the entire purpose of PSR-3 — depending on the interface instead lets any PSR-3-compliant logger be substituted freely by whichever application wires this class up.
[multiple-choice] What is the key functional difference between PSR-6 and PSR-16? A. PSR-6 is for logging and PSR-16 is for caching B. They are unrelated — PSR-6 is autoloading and PSR-16 is coding style C. Both standardize caching; PSR-6 uses a more structured item-pool object model (CacheItemPoolInterface/CacheItemInterface) while PSR-16 offers a flatter, simpler get/set/delete API for the common case D. PSR-16 is deprecated in favor of PSR-6
C. Both standardize caching; PSR-6 uses a more structured item-pool object model (CacheItemPoolInterface/CacheItemInterface) while PSR-16 offers a flatter, simpler get/set/delete API for the common case — Both PSR-6 and PSR-16 standardize caching, but at different levels of ceremony: PSR-6's CacheItemPoolInterface/CacheItemInterface pair supports fine-grained, deferred/batched cache operations, while PSR-16's CacheInterface offers a much simpler, direct get($key)/set($key, $value, $ttl) API for the common case where that extra structure isn't needed.
[multiple-choice] PSR-15's MiddlewareInterface::process() takes a ServerRequestInterface and a RequestHandlerInterface. What is the RequestHandlerInterface argument for? A. It represents the HTTP client used to make the original request B. It lets the middleware delegate to the next handler in the pipeline (by calling its handle() method) and receive back the response to inspect or modify before returning it C. It is only used for logging purposes and has no functional role D. It replaces the need for a Response object entirely
B. It lets the middleware delegate to the next handler in the pipeline (by calling its handle() method) and receive back the response to inspect or modify before returning it — The RequestHandlerInterface passed into process() represents 'the rest of the pipeline' — calling $handler->handle($request) runs the remaining middleware and the final handler, returning the resulting response, which the current middleware can then inspect or further modify before returning it up the chain itself.
You're writing a reusable rate-limiting library that needs to (a) log when a limit is exceeded, (b) store request counts somewhere fast with an expiration, and (c) let the consuming application supply its own storage backend. Name the three PSR interfaces you should type-hint against instead of hard-depending on specific packages, and say what each is for.
Psr\Log\LoggerInterface (PSR-3) for logging the limit-exceeded event; Psr\SimpleCache\CacheInterface (PSR-16) for storing request counts with a TTL/expiration via its simple get/set/ttl API (PSR-6's CacheItemPoolInterface would also work but is more ceremonious than needed for this flat key-count-plus-ttl use case). Type-hinting both interfaces in the constructor lets the consuming application supply whichever concrete logger and cache backend it already uses.
One is for logging, one is the simple flat-API caching standard (not the more ceremonious item-pool one), and think about what 'expiration' implies about the caching choice.