php-http/discovery
Auto-discovery for HTTP clients and factories: finds PSR-18 clients and PSR-17/PSR-7 implementations at runtime, so libraries can depend on interfaces without forcing a specific vendor. Includes a Composer plugin for optional auto-installation.
Install the package:
composer require php-http/discovery
This adds the discovery layer to your project—no runtime dependencies required.
Immediate use case (library author):
In your package, inject HTTP client without binding to a concrete implementation:
use Http\Discovery\Psr18Client;
$client = Psr18Client::create();
Internally, it auto-selects a PSR-18 client (e.g., guzzlehttp/guzzle, symfony/http-client) and matching PSR-17 factories.
Check supported implementations:
Run composer show | grep -E '(psr-18|psr-7|psr-17)' to see what implementations you already have.
Avoid hard dependencies in libraries:
Require only php-http/discovery and PSR interface psr/http-client:
{
"require": {
"php-http/discovery": "^1.17",
"psr/http-client": "^1.0"
}
}
Let users install their preferred client (e.g., guzzlehttp/guzzle:^7.0, symfony/http-client:^6.0).
Use discovery-aware classes in app code (for prototyping or small apps):
use Http\Discovery\Psr18Client;
use Http\Discovery\Psr17Factory;
$client = Psr18Client::create();
$requestFactory = Psr17Factory::createServerRequestFactory();
$request = $requestFactory->createServerRequest('GET', 'https://api.example.com');
$response = $client->sendRequest($request);
Configure preferred implementations (for apps where you control dependencies):
composer config extra.discovery.psr/http-client-implementation GuzzleHttp\\Client::class
composer config extra.discovery.psr/http-factory-implementation GuzzleHttp\\Psr7\\HttpFactory::class
Then run composer install to let the plugin auto-install missing deps if enabled.
Disable auto-installation in dev (to avoid surprising dependency resolution):
composer config allow-plugins.php-http/discovery false
Composer plugin may auto-install dependencies unless disabled or a suitable candidate exists. This can break lockfile reproducibility—always composer update after changing extra.discovery.* config.
Multiple implementations conflict:
If you have nyholm/psr7 and guzzlehttp/psr7 installed, discovery picks one arbitrarily. Pin one explicitly using extra.discovery to avoid flaky behavior.
Psr18Client vs direct HttpClientDiscovery:
Prefer Psr18Client (simple, modern) over legacy HttpClientDiscovery, which is deprecated in v1.18.0+.
No runtime dependency ≠ zero cost:
Discovery scans installed packages at runtime using reflection—usually negligible, but avoid using it in hot paths (e.g., per-request in CLI tools). Cache discovery results or wire clients via DI instead.
Magento/Laminas compatibility:
Use ClassDiscovery::safeClassExists() instead of raw class_exists() in legacy environments to avoid autoloader conflicts.
Debugging discovery failures:
Wrap discovery calls in try/catch(Http\Discovery\Exception\DiscoveryFailedException $e) to list candidates that were skipped:
try {
$client = Psr18Client::create();
} catch (\Http\Discovery\Exception\DiscoveryFailedException $e) {
error_log('No client found. Candidates: ' . print_r($e->getCandidates(), true));
}
Testing tip:
For unit tests, disable discovery by mocking Psr18Client or use Http\Discovery\Strategy\MockClientStrategy (v1.2.0+) to inject test doubles deterministically.
How can I help you explore Laravel packages today?