slick/http
Slick HTTP is a lightweight HTTP package for PHP, offering a clean API for building requests and handling responses. It integrates easily into Laravel or standalone projects and keeps external API calls simple, testable, and maintainable.
Install and confirm the shape of the package first. composer require slick/http — there is no service provider, no facade, and no config/ to publish. This is a plain library that autoloads PSR-7 messages, a PSR-15-style middleware dispatcher, session handling, a body-parser layer, and an HTTP client. Nothing registers itself in a Laravel app, so treat it as a dependency you call directly rather than a framework integration. Pin the version you adopt ("slick/http": "4.0.2") rather than floating on ^4.0 — see the note on v4.0.2 below for why.
Read these four directories in this order. They map cleanly to the package's four promises:
| Directory | What you get | Why you care |
|---|---|---|
src/Message/ |
Request, Response, ServerRequest, Uri, UploadedFile, stream classes |
The PSR-7 core; you will spend 90% of your time here |
src/Server/ |
MiddlewareStack, RequestHandler, middleware contract, body parsers (UrlEncodedParser) |
The composition layer for request pipelines and body decoding |
src/Session/ |
Session driver + handlers | Standalone session abstraction, not Laravel's session |
src/Client/ |
HTTP client | Outbound calls, separate from your app's Guzzle usage |
Open composer.json before anything else and check the php constraint and the psr/* requirements. Because adoption is essentially zero, the README is not a specification — the source is. Budget an hour to read src/Message/Response.php and src/Message/Stream/ end to end; they are small files and they define every behavior you will depend on.
Establish your baseline version before writing any code. The current release is v4.0.2, whose single change is: UrlEncodedParser is now registered as the parser for multipart/form-data. This is a behavior change to how the package decodes request bodies, shipped in a patch release — so record the exact version in your lockfile and treat body parsing as a "verify against source" area from day one.
First use case: build and emit a response outside of Laravel. The fastest feedback loop is constructing a message and stringifying it, because it exercises the stream, header, and status code paths in one shot:
use Slick\Http\Message\Response;
use Slick\Http\Message\Stream\TextStream;
$response = new Response(new TextStream('{"ok":true}'), 200, [
'Content-Type' => 'application/json',
]);
http_response_code($response->getStatusCode());
foreach ($response->getHeaders() as $name => $values) {
foreach ($values as $value) {
header(sprintf('%s: %s', $name, $value), false);
}
}
echo $response->getBody();
Verify the constructor argument order against the source before copying this — PSR-7 implementations disagree on whether body, status, or headers comes first, and the signature is not part of the PSR-7 spec. Once that works, your second step is the middleware stack, which is the actual reason to reach for this package.
Compose a request pipeline with the middleware stack. This is the package's differentiating feature and the pattern you will use most. The stack is a chain of middleware terminating in a final request handler; each middleware receives a request and a handler, and either short-circuits with a response or delegates inward.
use Slick\Http\Server\MiddlewareStack;
use Slick\Http\Server\RequestHandler;
$stack = new MiddlewareStack(new RequestHandler(function ($request) {
return new Response(new TextStream('reached the end'), 200);
}));
$stack->add(new CorrelationIdMiddleware());
$stack->add(new JsonErrorMiddleware());
$response = $stack->handle($serverRequest);
Confirm the ordering semantics of add() (FIFO vs LIFO) with a two-middleware test before you build anything real on top of it. Middleware order determines whether your error handler wraps your auth check or vice versa, and that is a bug you only want to find once.
Treat parsed bodies as untrusted until you have verified them — especially multipart. As of v4.0.2 the package routes multipart/form-data through UrlEncodedParser. URL-encoded parsing expects key=value&key=value pairs; a multipart body is boundary-delimited with per-part headers. Do not assume getParsedBody() gives you correct field data for a file upload, and do not assume it gives you UploadedFile objects. The safe patterns are:
UploadedFile class in src/Message/), not from the parsed body array.riverline/multipart-parser / a similar library.getParsedBody() returns. That single test is your protection against this area changing again in a future patch.Use it as a PSR-7 boundary layer inside a Laravel app, not as the app's HTTP kernel. Laravel runs on Symfony HttpFoundation, not PSR-7. The realistic day-to-day integration is at the edges: an inbound webhook controller, a package that hands you PSR-7 messages, or a queue worker making outbound calls. Convert at the boundary with symfony/psr-http-message-bridge and keep the PSR-7 objects confined to that seam:
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
// Illuminate request -> PSR-7
$psrRequest = app(PsrHttpFactory::class)->createRequest($request);
// PSR-7 response -> something Laravel can return
$symfonyResponse = app(HttpFoundationFactory::class)->createResponse($psrResponse);
return response($symfonyResponse->getContent(), $symfonyResponse->getStatusCode(), $symfonyResponse->headers->all());
This is the only pattern I would recommend for a Laravel codebase. Do not try to replace Laravel's router, container, or middleware with this package's server — you would be reimplementing the framework badly.
Prefer Laravel's request parsing over this package's for anything inbound-and-real. Because Illuminate\Http\Request already gives you input(), file(), and a battle-tested multipart story via Symfony, do not route production form handling through the Slick parsers. Use the Slick parser layer only when you need to decode a raw body you obtained outside a normal Laravel request cycle (a signed webhook payload you captured as a string, a queued job carrying a body). For JSON, decode with json_decode(..., true) directly — it is unambiguous and you own the error handling.
Pick streams deliberately. Use TextStream for in-memory string bodies, the PHP input stream wrapper for reading php://input, and plain PSR-7 stream objects when you need fopen-style resource semantics. For JSON APIs, construct the body once and set Content-Type explicitly rather than relying on any content negotiation the package may or may not do.
Reuse messages for testing. PSR-7 objects are immutable value-ish types, which makes them excellent test fixtures. If you need a factory implementation for other libraries (Guzzle, HTTPlug), reach for nyholm/psr7 or guzzlehttp/psr7 in require-dev rather than forcing this package to serve as the canonical factory everywhere.
Keep sessions separate from Laravel's. The package ships its own session driver. If you are inside Laravel, use Laravel's session and do not instantiate the Slick driver — mixing two session stores in one process produces header-already-sent errors and lost data. Only use the Slick session layer in standalone services where there is no framework session at all.
Wrap the client. Whatever HTTP client this package exposes, put it behind a small interface of your own so a change of dependency does not ripple through the codebase. With a package this young, the client API is the most likely thing to shift.
v4.0.2 changed body-parsing behavior in a patch release. multipart/form-data now resolves to UrlEncodedParser. Patch releases are supposed to be safe, so this is a reminder that the package is pre-1.0-in-spirit with respect to semver discipline: pin an exact version, and read every diff — including patch diffs — before composer update. If you already handle multipart in production code on 4.0.1, re-run your tests against 4.0.2 and diff the parsed output rather than trusting the "patch" label.
A URL-encoded parser on a multipart body will return garbage, not an error. Do not expect an exception you can catch. The likely outcome is a partially-populated or nonsensical array. Guard by inspecting Content-Type before you consume getParsedBody() and skipping the parsed body entirely for multipart/*. Assert on the shape, not on truthiness — an empty array and a wrong array both look fine to if ($parsed).
Immutability is the number one source of silent bugs. withHeader(), withBody(), withStatus(), and friends return new instances. Code like $response->withHeader('X-Foo', 'bar'); does nothing. Always reassign:
$response = $response->withHeader('X-Foo', 'bar'); // not a no-op
Rewind the body before emitting it. After you write into a stream, the pointer sits at the end and echo $response->getBody() outputs an empty string. rewind() first, and guard it — not every stream is seekable. This is the single most common "my response is blank" report for every PSR-7 library.
Do not copy constructor signatures from Guzzle examples. Argument order for Response and Request differs between implementations and is not standardized. Read the constructor in src/Message/ before writing a single line.
getSize() may return null. Streams of unknown length are legal in PSR-7. If you rely on Content-Length being accurate, compute it yourself or omit the header and let the SAPI chunk the response.
Header handling has two accessors with different shapes. getHeader() returns an array of values; getHeaderLine() returns the comma-joined string. Set-Cookie is the classic trap — it must not be collapsed via getHeaderLine().
Middleware error handling is your responsibility. The dispatcher generally does not catch exceptions for you. Put an outermost middleware that catches Throwable and converts it into a 500 response; without it, one bad middleware takes the process down with a stack trace in the response body.
PHP native sessions must already be started. The Slick session handlers assume a session context exists; calling them before session_start(), or after output has begun, fails with the usual "headers already sent" symptoms. In CLI contexts (queue workers, tests) sessions are effectively unavailable — do not design around them there.
Debugging PSR-7 objects via var_dump is close to useless. Dump readable projections instead: (string) $request->getUri(), $request->getMethod(), $response->getStatusCode(), (string) $response->getBody(). For parsed-body problems specifically, dump $request->getHeaderLine('Content-Type') and $request->getParsedBody() side by side — the content type tells you which parser ran, which is the only way to explain the array you are looking at.
Treat the low adoption as a real risk, not a footnote. Zero stars and zero dependents means no community pressure testing and no Stack Overflow corpus. The v4.0.2 parser change is exactly the kind of thing that would have been caught by a crowd. Mitigate concretely: pin an exact version in composer.json, read the diff of every update before applying it, and write characterization tests around any behavior you depend on — particularly middleware ordering, header merging, multipart/URL-encoded parsing, and stream semantics. If you find yourself building significant logic on top, vendor the relevant source files or fork.
Extension points are the interfaces plus the parser registry, not subclassing. Implement PSR-7 message interfaces for custom types, implement the middleware contract for cross-cutting concerns (correlation IDs, logging, CORS, error mapping), supply your own stream implementation when you need non-standard buffering, and — the newest lever — register your own body parser for the media types you care about (a proper multipart parser, a JSON parser with strict error handling) instead of living with whatever the default map gives you. Subclassing the concrete message classes buys you little because most methods are final-by-convention immutable factories.
Performance: keep it out of the hot path. PSR-7's immutable message churn costs more than Symfony's mutable request/response bags, and parsing bodies twice (once by Laravel, once by this package) doubles that. For a high-throughput Laravel app, converting every request to PSR-7 and back is a measurable regression. Convert at the boundaries only.
How can I help you explore Laravel packages today?