which PHP FIG standard solves which problem
PSR standards are the reason why any logger, any cache adapter and any HTTP client are interchangeable in practically every modern PHP project, without application code needing to change. Anyone who knows the most important PSR standards, from logging through caching to event dispatching, chooses libraries more deliberately and avoids unnecessary coupling to a single vendor.
Table of Contents
- 1. What PSR standards solve and what they do not
- 2. PSR-3: the logger interface as a shared language
- 3. PSR-6 and PSR-16: two cache standards, one purpose
- 4. PSR-7 and PSR-17: HTTP messages and factories
- 5. PSR-11: container interface for dependency injection
- 6. PSR-14: event dispatcher as decoupled communication
- 7. PSR-18: HTTP client without a Guzzle dependency in code
- 8. Distinction from PSR-1, PSR-4 and PSR-12
- 9. PSR standards compared directly
- 10. Summary
- 11. FAQ
1. What PSR standards solve and what they do not
PSR standards are recommendations from the PHP Framework Interop Group, PHP FIG for short, a coalition of major PHP projects such as Symfony, Laminas and WordPress. Their purpose is exclusively interoperability: a PSR standard defines an interface, usually one or several interfaces, that libraries program against instead of against a concrete implementation. That way an implementation can be swapped later without the calling code needing any changes.
A common misunderstanding: PSR standards are not framework requirements. No PSR standard dictates how an application must be structured, which framework is used, or what the business logic looks like. PSR-3, for instance, only requires a logger to offer methods such as info, warning or error with certain parameters, not where the log lines are actually written to. This deliberate restriction to pure interfaces is the reason PSR standards have stayed practically unchanged and stable for over a decade.
The PSR standards at the center of this article, PSR-3, PSR-6 and PSR-16, PSR-7 and PSR-17, PSR-11, PSR-14 and PSR-18, each cover a recurring infrastructure problem: logging, caching, HTTP messages, dependency injection containers, event communication and HTTP requests. Anyone familiar with these six areas understands most of what modern PHP libraries require in terms of interfaces today.
2. PSR-3: the logger interface as a shared language
PSR-3 defines the LoggerInterface with eight methods matching the RFC 5424 log levels: emergency, alert, critical, error, warning, notice, info and debug. Each of these methods accepts a message and an optional context array for structured extra data. Libraries programmed against PSR-3 know nothing about Monolog, nothing about the Symfony logger, and nothing about whether log lines end up in a file, in syslog or in a central log aggregator.
The practical benefit of this PSR standard shows up when swapping the logging implementation. An application that initially uses Monolog and later switches to a cloud native logging solution only needs to adjust the container entry for LoggerInterface, not a single line of application code changes. The NullLogger object from PSR-3 itself is also a useful default object for tests and for optional logger parameters that are not strictly needed in production.
<?php
declare(strict_types=1);
namespace Mironsoft\Billing;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
final class InvoiceProcessor
{
public function __construct(
private readonly LoggerInterface $logger = new NullLogger(),
) {
}
public function process(int $invoiceId): void
{
$this->logger->info('Processing invoice', ['invoice_id' => $invoiceId]);
try {
// ... business logic, independent of the concrete logger
} catch (\Throwable $e) {
$this->logger->error('Invoice processing failed', [
'invoice_id' => $invoiceId,
'exception' => $e->getMessage(),
]);
throw $e;
}
}
}
3. PSR-6 and PSR-16: two cache standards, one purpose
Two parallel PSR standards exist for caching: PSR-6 with CacheItemPoolInterface and CacheItemInterface, and PSR-16 as a deliberately leaner alternative with the SimpleCache interface. PSR-6 supports concepts such as deferred storage and explicit committing of several cache items at once, while PSR-16 with get, set, delete and has targets simple use cases directly, without the detour through a cache item object.
In practice, most applications use PSR-16 because the API is more direct and does not require explicit object creation for every cache access. Libraries that implement more complex cache strategies, for example with tags or deferred persistence of several values, tend to reach for PSR-6 instead. Symfony Cache implements both PSR standards simultaneously through the same underlying adapter architecture, so applications do not have to choose between the two interfaces but can use both in parallel.
<?php
declare(strict_types=1);
namespace Mironsoft\Catalog;
use Psr\SimpleCache\CacheInterface;
final class ProductPriceResolver
{
public function __construct(
private readonly CacheInterface $cache,
) {
}
public function getPrice(string $sku): float
{
$cacheKey = "product_price_{$sku}";
// PSR-16: works with any adapter (Redis, Memcached, filesystem, array)
$cached = $this->cache->get($cacheKey);
if ($cached !== null) {
return (float) $cached;
}
$price = $this->calculatePrice($sku);
$this->cache->set($cacheKey, $price, ttl: 3600);
return $price;
}
private function calculatePrice(string $sku): float
{
// ... expensive price calculation
return 19.99;
}
}
4. PSR-7 and PSR-17: HTTP messages and factories
PSR-7 defines immutable objects for request, response, stream, URI and uploaded file, the foundation of practically every modern PHP HTTP layer. The decisive design aspect: all PSR-7 objects are immutable. A call such as withHeader returns a new instance with the changed header instead of mutating the original object. That prevents unexpected side effects when the same request object gets passed through several middleware layers.
PSR-17 complements PSR-7 with factory interfaces used to create new request, response and stream objects without directly instantiating a concrete implementation such as Guzzle PSR-7 or Nyholm PSR-7. This PSR standard is especially relevant for library authors: an HTTP middleware can accept PSR-17 factories through constructor injection and thereby works with any PSR-7 compatible implementation, regardless of which one the application developer actually installed.
<?php
declare(strict_types=1);
namespace Mironsoft\Http;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
final class JsonResponseFactory
{
public function __construct(
private readonly ResponseFactoryInterface $responseFactory,
private readonly StreamFactoryInterface $streamFactory,
) {
}
// Works with any PSR-7/PSR-17 implementation (Nyholm, Guzzle, Laminas)
public function create(array $data, int $status = 200): ResponseInterface
{
$body = $this->streamFactory->createStream(json_encode($data, JSON_THROW_ON_ERROR));
return $this->responseFactory
->createResponse($status)
->withHeader('Content-Type', 'application/json')
->withBody($body);
}
}
5. PSR-11: container interface for dependency injection
PSR-11 defines the leanest interface among all PSR standards: ContainerInterface with only two methods, get and has. The purpose is explicitly limited: PSR-11 does not standardize how a container configures objects or resolves dependencies, only how an already configured container is queried from the outside. This deliberate restriction lets frameworks such as Symfony, Laminas or PHP-DI keep their own, often very different internal configuration logic.
In practice, libraries rarely use PSR-11 directly for their own objects, but rather to function as a consumer of a foreign container themselves, for example in middleware systems such as PSR-15, which need to resolve handler class names from a container. Important: direct access to a container within application code, the so called service locator pattern, is generally considered an anti pattern and should be consistently replaced by constructor injection of the actually needed dependency.
6. PSR-14: event dispatcher as decoupled communication
PSR-14 standardizes event dispatching through two central interfaces: EventDispatcherInterface with a single dispatch method, and ListenerProviderInterface, which returns matching listeners for a given event object. Unlike classic observer implementations with event names as strings, PSR-14 works in a typed way: the event object itself determines through its class which listeners get called, which significantly eases IDE support and static analysis.
Another central feature of this PSR standard is the optional StoppableEventInterface interface. If an event implements this interface, a listener can stop further processing, for example when an earlier listener has already made a final decision. Symfony EventDispatcher fully implements PSR-14, so event listeners written in Symfony also work outside the framework, as long as the application uses PSR-14 compatible event objects.
<?php
declare(strict_types=1);
namespace Mironsoft\Orders;
use Psr\EventDispatcher\StoppableEventInterface;
final class OrderPlacedEvent implements StoppableEventInterface
{
private bool $stopped = false;
public function __construct(
public readonly int $orderId,
public readonly float $totalAmount,
) {
}
public function stopPropagation(): void
{
$this->stopped = true;
}
public function isPropagationStopped(): bool
{
return $this->stopped;
}
}
// Dispatching against the PSR-14 interface, not a concrete implementation
$event = $dispatcher->dispatch(new OrderPlacedEvent(orderId: 4821, totalAmount: 129.90));
7. PSR-18: HTTP client without a Guzzle dependency in code
PSR-18 defines ClientInterface with a single method, sendRequest, that accepts a PSR-7 RequestInterface and returns a PSR-7 ResponseInterface. Before PSR-18, every library that needed to send HTTP requests either hard wired a specific Guzzle version or implemented its own, incompatible abstraction. Both approaches regularly led to version conflicts when two dependencies required different Guzzle major versions.
With PSR-18 as a PSR standard, a library merely declares a dependency on psr/http-client, without specifying which concrete implementation, Guzzle, Symfony HttpClient or curl based alternatives such as php-http/curl-client, is actually used. The php-http/discovery package practically complements PSR-18 by automatically finding an installed, compatible implementation at runtime, in case the application has not explicitly configured one in the container.
<?php
declare(strict_types=1);
namespace Mironsoft\Payment;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
final class PaymentGatewayClient
{
public function __construct(
private readonly ClientInterface $httpClient,
private readonly RequestFactoryInterface $requestFactory,
) {
}
// No dependency on Guzzle, Symfony HttpClient or any concrete implementation
public function charge(string $token, float $amount): bool
{
$request = $this->requestFactory
->createRequest('POST', 'https://gateway.example.com/charges')
->withHeader('Content-Type', 'application/json');
$response = $this->httpClient->sendRequest($request);
return $response->getStatusCode() === 200;
}
}
8. Distinction from PSR-1, PSR-4 and PSR-12
Not every PSR standard defines a runtime interface. PSR-1 and PSR-12 regulate pure code style, such as indentation, brace placement and naming conventions, with no effect whatsoever on runtime behavior. PSR-4, in turn, standardizes only autoloading, meaning the mapping of namespaces to directory structures, likewise without any interface or runtime behavior.
This distinction matters because developers frequently lump all PSR standards together. The standards covered in this article, PSR-3, PSR-6 and PSR-16, PSR-7 and PSR-17, PSR-11, PSR-14 and PSR-18, define actual interfaces that application code programs against at runtime. PSR-1, PSR-4 and PSR-12, by contrast, concern only static structure and formatting, not runtime behavior, and are accordingly enforced by completely different tools, such as PHP CS Fixer for PSR-12 and Composer itself for PSR-4.
9. PSR standards compared directly
The table below arranges the most important PSR standards by application area, central interface and typical usage location in a PHP application.
| PSR Standard | Application Area | Central Interface | Typical Usage Location |
|---|---|---|---|
| PSR-3 | Logging | LoggerInterface |
Application and error logging |
| PSR-6 / PSR-16 | Caching | CacheInterface |
Redis, Memcached, filesystem cache |
| PSR-7 / PSR-17 | HTTP messages | RequestInterface |
Middleware, router, HTTP layer |
| PSR-11 | Dependency injection | ContainerInterface |
Framework container queries |
| PSR-14 | Event communication | EventDispatcherInterface |
Decoupled domain events |
| PSR-18 | HTTP client | ClientInterface |
API calls to external services |
It is notable that all six PSR standards listed in this table each define only a single or a few, very focused interfaces. This deliberate leanness is no accident, it is the central design principle of the PHP FIG: the smaller an interface, the easier it is to implement, the less often it changes, and the more stable interoperability remains over the years.
Mironsoft
PHP architecture, interoperability and Composer tooling
Code programmed against PSR standards instead of concrete libraries?
We review your existing architecture for unnecessary coupling to a single vendor and replace it with PSR-3, PSR-6/16, PSR-7/17, PSR-11, PSR-14 and PSR-18 compatible interfaces.
Architecture Review
Analyzing where direct dependencies are used instead of PSR interfaces
Refactoring
Replacing hard wired implementations with PSR compliant interfaces
Training
Practical workshop on PSR standards and their use in daily team work
10. Summary
The most important PSR standards each solve a specific interoperability problem: PSR-3 standardizes logging through LoggerInterface, PSR-6 and PSR-16 standardize caching with different API depth, PSR-7 and PSR-17 standardize immutable HTTP messages together with factories, PSR-11 standardizes the pure querying of a dependency injection container, PSR-14 standardizes typed event communication, and PSR-18 standardizes the sending of HTTP requests without a fixed binding to Guzzle or any other concrete implementation.
The common denominator across all six standards is deliberate leanness: each interface covers exactly one use case, without framework specific assumptions. Anyone who programs their own libraries against these PSR standards instead of against concrete implementations makes code interchangeable, testable and independent of the choice of a particular framework or vendor for years to come.
PSR Standards Overview — The Essentials at a Glance
Logging and Caching
PSR-3 LoggerInterface for logging, PSR-6/PSR-16 for interchangeable cache implementations without vendor lock-in.
HTTP Layer
PSR-7/PSR-17 for immutable HTTP messages and factories, PSR-18 for requests without a fixed Guzzle binding.
Container and Events
PSR-11 for pure container querying, PSR-14 for typed, decoupled event communication between components.
Distinction
PSR-1, PSR-4 and PSR-12 govern code style and autoloading, no runtime interfaces, clearly separate from the standards covered here.