php-http/httplug-pack
Convenience Composer metapackage for HTTPlug that installs the HTTP client abstraction with a PSR-7 implementation and message factories, giving PHP apps a quick, interoperable setup for sending HTTP requests.
This is a Symfony Flex "pack" — a composer.json with "type": "symfony-pack" and no PHP code. Installing it pulls in a curated set of HTTPlug packages (historically: php-http/httplug-bundle, php-http/message, and a guzzle6-adapter). So the first thing to understand is: you are really assessing httplug-bundle + a PSR-18-ish client adapter, not this repo.
Minimal steps:
composer require php-http/httplug-pack with symfony/flex present — Flex resolves it as a pack and installs its require block, then the httplug-bundle recipe writes config/packages/httplug.yaml.config/packages/httplug.yaml. This is where clients, plugins, and profiling are configured.bin/console debug:container httplug or php bin/console debug:container --show-private Http\\Client\\HttpClient). The default client is registered as httplug.clients.default and aliased to Http\Client\HttpClient.use Http\Client\HttpClient;
use Http\Message\RequestFactory;
use Http\Message\StreamFactory;
class GitHubApi
{
public function __construct(
private HttpClient $client,
private RequestFactory $requestFactory,
private StreamFactory $streamFactory,
) {}
public function user(string $login): array
{
$request = $this->requestFactory->createRequest('GET', "https://api.github.com/users/{$login}")
->withHeader('Accept', 'application/vnd.github+json');
$response = $this->client->sendRequest($request);
return json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR);
}
}
That's the day-one win: autowirable HTTP client + autowirable PSR-7 factories, with all the wiring done by the bundle.
Sync vs async. Http\Client\HttpClient is the synchronous entry point; Http\Client\HttpAsyncClient gives you sendAsyncRequest() returning a Promise. Guzzle, Curl, and Socket adapters all implement both. Use async when fanning out N API calls:
$promises = array_map(
fn (string $id) => $this->asyncClient->sendAsyncRequest(
$this->requestFactory->createRequest('GET', "/orders/{$id}")
),
$ids
);
$responses = \Http\Promise\Utils::settle($promises)->wait();
Note: HTTPlug predates GuzzleHttp\Promise's modern Utils — the promise interface is Http\Promise\Promise, and the implementation is usually Guzzle's. This dual-namespace situation is a common source of confusion.
Named clients. Define more than one client for different base URLs / auth:
httplug:
clients:
default: ~
github:
factory: 'httplug.factory.guzzle7'
plugins: ['httplug.plugin.logger', 'app.plugin.github_auth']
config:
base_uri: 'https://api.github.com'
Then inject via #[Autowire(service: 'httplug.clients.github')] (Symfony 6.1+) or an explicit services.yaml alias. Named clients are not autowired by type — this is the single most common friction point.
Plugins as middleware. HTTPlug's killer feature is composable Http\Client\Common\Plugin middleware, configured under httplug.plugins. Useful built-ins from the wider ecosystem:
httplug/retry-plugin — retry on 5xx / connection errors with backoff.httplug/logger-plugin — PSR-3 logging of request/response.httplug/cache-plugin — PSR-6/PSR-16 response caching.httplug/redirect-plugin — follow redirects.httplug/error-plugin — throw exceptions on 4xx/5xx.Register custom plugins as services and reference them in the plugins: array of a client — order matters (outermost first).
Testing. Swap the client for Http\Mock\Client (php-http/mock-client) in config/services_test.yaml:
services:
Http\Client\HttpClient: '@Http\Mock\Client'
Then $client->addResponse($response) / $client->getRequests() in tests. This is far cleaner than mocking Guzzle directly.
Interop with Symfony's own client. If you migrate to symfony/http-client later, you can keep the same autowiring by registering its adapter (php-http/symfony-http-client-adapter) or moving to PSR-18 (Psr\Http\Client\ClientInterface) — HTTPlug clients are not PSR-18 out of the box.
This pack is a 2019 artifact. Last release April 2019, 38 stars, ~0 dependents. The require block almost certainly pins guzzle6-adapter (Guzzle 6 is EOL) and httplug-bundle 1.x, which is fine on PHP 7.x but increasingly awkward on PHP 8.2+. For anything new, require the pieces you need directly instead of the pack:
composer require php-http/httplug-bundle php-http/guzzle7-adapter php-http/message
Flex "unpacking". If the version constraints conflict with your app, Flex will refuse to install the pack; composer unpack php-http/httplug-pack (or removing it from composer.json and requiring its deps individually) is the escape hatch. Because the recipe is old, it may also write an obsolete httplug.yaml — always read it, don't trust it.
Bundle vs PSR-18 vs Symfony HttpClient. Three HTTP abstractions now coexist: HTTPlug (Http\Client\HttpClient), PSR-18 (Psr\Http\Client\ClientInterface), and Symfony's own (Symfony\Contracts\HttpClient\HttpClientInterface). HTTPlug's request/response objects are PSR-7, which is good; but its client interfaces are not PSR-18. If a library asks for PSR-18, you need an adapter. If you are greenfield, prefer symfony/http-client — it implements PSR-18, has native Symfony integration, and is actively maintained.
Adapter discovery is implicit. httplug-bundle uses discovery (php-http/discovery) to pick a factory. If you install two adapters, behavior can be non-obvious. Pin factory: explicitly in httplug.clients.<name>.config to remove ambiguity. composer why php-http/guzzle6-adapter is your friend.
Debugging. In dev, enable the bundle's profiler panel (httplug: profiling: enabled: true in recent 1.x) — it shows requests, responses, and timings in the web debug toolbar. For prod issues, add httplug.plugin.logger backed by Monolog; it logs at debug level by default, so set the channel to info if you want it in prod. curl errors often surface only via the adapter's exception chain (Http\Client\Exception\NetworkException), so unwrap with getPrevious() when reporting.
Plugins compose in reverse intuition. The list is applied outer-to-inner, so a retry plugin listed before the logger will retry the logged request (logging each attempt); the opposite order logs only the final call. Decide intentionally.
Async + Symfony's HttpKernel. Do not leave promises unresolved before the kernel terminates — Utils::settle()->wait() or you leak sockets and log confusing "destructor called with unfinished promise" warnings.
Migration pointers. If you are inheriting a codebase on this pack: (1) replace guzzle6-adapter → guzzle7-adapter first (drop-in, one line); (2) migrate Http\Client\HttpClient consumers to Psr\Http\Client\ClientInterface where they only need sync calls; (3) eventually replace httplug-bundle with symfony/http-client and delete the pack. Because the interfaces are similar but distinct, step (2) often means touching every call site — budget for it, or add a thin HttplugClient decorator over Psr18Client to bridge incrementally.
How can I help you explore Laravel packages today?