PSR-18, and the HTTP client we stopped writing

Three packages in the same application, each with its own curl wrapper, each with its own idea of what a timeout means.

// the interface is one method
interface ClientInterface
{
    public function sendRequest(RequestInterface $request): ResponseInterface;
}

// which means a package takes the client rather than making one
public function __construct(
    private ClientInterface $http,
    private RequestFactoryInterface $requests,
) {}

// and the application decides on retries, timeouts, logging
// and the middleware stack, once.

The value is not the abstraction, it is that configuration moves to the place that knows about the environment. A package that constructs its own client cannot be given a proxy, a retry policy or a request log without an option for each, and it will have an option for each eventually. The factory interfaces from PSR-17 are the part that makes this practical — without them the package still has to name a concrete request implementation.