PHP Luminova: HTTP File Downloader
Download utility for sending files and generated content as downloadable responses. Supports file paths, streams, resources, and raw strings, with built-in handling for range requests, caching, server offloading via Apache X-Sendfile and Nginx X-Accel-Redirect.
The HTTP File Downloader is a practical utility for sending files to the browser without juggling headers, streams, and edge cases yourself.
It gives you one consistent way to handle downloads, from a file on disk, an already opened stream, or generated content in memory. Instead of writing different code for each case, you use the same interface to handle downloads.
It handles:
- Correct download headers (so the browser actually downloads instead of displaying)
- Partial downloads (resume support via HTTP
206) - Cache validation (
ETagandLast-Modified) - Efficient streaming to avoid memory issues
It also supports server-level offloading, instead of PHP doing all the work (which is slow for large files), you can hand the job over to the web server using:
- Apache
X-Sendfile - Nginx
X-Accel-Redirect
Download Source
The downloader accepts multiple download source:
- Filesystem path – the standard case for serving stored files
- PHP resource – useful if the file is already opened (
fopen) - PSR-7
StreamInterface– fits into middleware or modern HTTP stacks SplFileObject– object-oriented file handling- Raw string – for generated content like CSV, JSON, or exports
Usages
This section shows how to send files or content to the browser using the Downloader.If you’ve ever clicked a “Download” button on a website, this is the code behind it.
Basic file download
The simplest case: send a file to the browser.
use Luminova\Http\Downloader;
$dl = new Downloader('/var/storage/report.pdf', 'Q3-Report.pdf');
$dl->download();
// Optional: remove the file after sending (useful for temp files)
$dl->deleteSourceFile();
// Always close to release resources
$dl->close();Throttled streaming (rate limiting)
Useful when sending large files and you don’t want to:
- overload the server
- or saturate the user’s connection
// 16 KB per chunk, 5 ms delay between chunks
$dl->download(Downloader::X_DOWNLOAD, 16384, 5000);
$dl->close();Without throttling, PHP will try to push everything as fast as possible.That sounds good — until your server starts struggling under load.
Server Offloading
Instead of PHP doing all the work, let the web server handle it.This is faster, more stable, and uses fewer resources (Recommended for large files).
Apache X-Sendfile
PHP sends only headers while Apache reads and streams the file.
$dl = new Downloader('/var/storage/large-file.iso', 'large-file.iso');
$dl->download(Downloader::X_SENDFILE);
$dl->close();Nginx X-Accel-Redirect
This is the Nginx version of offloading.
use Luminova\Http\Downloader;
use function Luminova\Funcs\root;
$filepath = root('/writeable/storages/videos/', 'tour.mp4');
$storages = root('/writeable/storages/');
$redirect = '/protected/';
$dl = new Downloader($filepath, 'tour.mp4');
// Map real path → internal Nginx path
$dl->setAccelPaths($storages, $redirect);
$dl->download(Downloader::X_ACCEL);
$dl->close();Important:
Nginx does not accept real file paths,it only accepts an internal URL
So you map:
/var/www/.../writeable/storages/videos/tour.mp4
→ /protected/videos/tour.mp4Nginx configuration example
The internal; prevents direct access from the browser, only your app can trigger downloads
location /protected/ {
internal;
alias /var/www/example.com/writeable/storages/;
}Working with Streams
PSR-7 stream
use Luminova\Http\Message\Stream;
$stream = Stream::from('/tmp/export.csv', 'rb');
$dl = new Downloader($stream, 'export.csv', [
'Cache-Control' => 'no-store'
]);
$dl->download();
$dl->close();Raw string content
Good for generated files (CSV, JSON, logs, etc.), No temp file needed. Everything is sent directly from memory.
$csv = "id,name\n1,Alice\n2,Bob\n";
$dl = new Downloader($csv, 'users.csv', [
'Content-Type' => 'text/csv'
]);
$dl->download();
$dl->close();Resource or SplFileObject
To reuse already opened handlers and avoid reopening files.
$fp = fopen('/var/storage/data.bin', 'rb');
$dl = new Downloader($fp, 'data.bin');
$dl->download();
$dl->close();
$fo = new SplFileObject('/var/storage/data.bin', 'rb');
$dl = new Downloader($fo, 'data.bin');
$dl->download();
$dl->close();Note:
Calling
close()may also close the underlying resource.
Download as a Response (PSR-7)
Instead of sending output immediately, you can build a response.
use Luminova\Http\Downloader;
use Luminova\Http\Header;
$dl = new Downloader('/var/storage/archive.zip', 'archive.zip');
$response = $dl->getResponse();
// Send headers + status
Header::send($response->getHeaders(), status: $response->getStatusCode());
// Clear buffers before streaming
Header::clearOutputBuffers();
// Send body
$response->getBody()->send();
// Optional: inspect range info (resume support)
$info = $dl->getInfo();
$dl->close();Useful when you’re inside middleware or want full control over response handling
Static Proxy Methods
Shortcuts when you don’t want to manually create an instance.
use Luminova\Http\Downloader;
// Direct streaming
Downloader::send('/var/storage/archive.zip', 'archive.zip');
// Same as send(), but suppress errors
Downloader::trySend('/var/storage/archive.zip', 'archive.zip');
// Apache offload (fallback to PHP)
Downloader::sendFile('/var/storage/archive.zip', 'archive.zip');
// Nginx offload (fallback to PHP)
Downloader::sendAccel('/var/storage/archive.zip', 'archive.zip');
// Build response only (no output)
$response = Downloader::response('/var/storage/archive.zip', 'archive.zip');Quick guidance:
- Use
send()→ normal usage - Use
sendFile()/sendAccel()→ production (large files) - Use
response()→ middleware or API-style flow
Class Definition
- Class namespace:
\Luminova\Http\Downloader
Constants
| Constant | Value | Description |
|---|---|---|
Downloader::X_DOWNLOAD | 1 | PHP streams content directly (default) |
Downloader::X_SENDFILE | 2 | Apache / Lighttpd X-Sendfile offload |
Downloader::X_ACCEL | 3 | Nginx X-Accel-Redirect offload |
Properties
accelRealBase
Real filesystem base path stripped when building X-Accel-Redirect URIs.
protected string $accelRealBaseSet via setAccelPaths()
accelInternalPrefix
Nginx internal location prefix used for X-Accel-Redirect URIs.
protected string $accelInternalPrefixSet via setAccelPaths()
Methods
constructor
Initialize a new Downloader instance.
Accepts a file path, an open PHP resource, a PSR-7 StreamInterface, ora raw string of file contents as the download source.
public __construct(
Psr\Http\Message\StreamInterface|SplFileObject|resource|string $source,
?string $filename = null,
array<string,mixed> $headers = [],
?string $etag = null
): mixedParameters:
| Parameter | Type | Description |
|---|---|---|
$source | StreamInterface|SplFileObject|resource|string | File path, open resource, PSR-7 stream, or raw content. |
$filename | string|null | Filename shown in the browser download dialog. Falls back to the basename of the file path, or 'file_download'. |
$headers | array<string,mixed> | Additional HTTP headers merged into the response. |
$etag | string|null | ETag string for cache validation. Falls back to $headers['ETag'] when omitted. |
Throws:
- \Luminova\Exceptions\RuntimeException - If the source is null, the file path does notexist, or the path is not readable.
__callStatic
Static proxy that creates a Downloader instance and immediately dispatchesthe requested action.
The $arguments (forwarded to the constructor) and immediately dispatches the action named by $method.
public static __callStatic(string $method, array $arguments): mixedParameters:
| Parameter | Type | Description |
|---|---|---|
$method | string | Proxy method name. |
$arguments | array | Constructor arguments forwarded to __construct. |
Throws:
- \Luminova\Exceptions\RuntimeException - For unrecognized method names.
Note The static proxy does not expose
setAccelPaths(). Use the instance API when Nginx offloading requires path configuration.
setAccelPaths
Configure the real-to-internal path mapping used when serving files via Nginx's X-Accel-Redirect.
public setAccelPaths(string $realBasePath, string $internalPrefix = '/protected/'): staticParameters:
| Parameter | Type | Description |
|---|---|---|
$realBasePath | string | Absolute filesystem path that should be stripped from the source path. (e.g. '/var/www/storage') |
$internalPrefix | string | Nginx internal location prefix that replaces the stripped portion. (e.g. '/protected/') |
Return Value:
static - Returns the current instance for fluent chaining.
is
Check whether the resolved source type matches the given type string.
Possible type values: 'file', 'resource', 'stream', 'contents', 'fileObject'.
public is(string $type): boolParameters:
| Parameter | Type | Description |
|---|---|---|
$type | string | Type string to test against. |
Return Value:
bool - Return true if type matches $source, otherwise false.
isPartial
Returns true when the last download served a partial (HTTP 206) response.
public isPartial(): boolReturn Value:
bool - Return true if download is partial, otherwise false.
getType
Resolve the source type, optionally re-detecting it when not yet set.
public getType(): string|falseReturn Value:
string|false - The type string, or false when detection fails.
getInfo
Return metadata set during the last download() or getResponse() call.
Keys: offset (int), length (int), status (int HTTP status code).
public getInfo(): array<string,int>Return Value:
array<string,int> - Return associative array of download information.
getFilename
Return the filename that will be (or was) sent to the client.
public getFilename(): ?stringReturn Value:
string|null - Return download file name or null if not available.
getMime
Return the resolved MIME type of the download source.
public getMime(): ?stringReturn Value:
string|null - Return download MIME type.
getETag
Return the ETag string used for cache validation, or null if none.
public getETag(): ?stringReturn Value:
string|null - Return download ETag value or null if not available.
getSize
Return the resolved content length in bytes.
public getSize(): intReturn Value:
int - Return download content size or file size in bytes.
getFileTime
Return the last-modified Unix timestamp of the source, or 0 whenunavailable.
public getFileTime(): intReturn Value:
int - Return download file ast-modified Unix timestamp.
getSource
Return the original download source as supplied to the constructor.
public getSource(): mixedReturn Value:
mixed - Return the download source.
getHeaders
Return the current download response headers.
public getHeaders(): array<string,mixed>Return Value:
array<string,mixed> - Return an associate array of download prepared/sent headers.
getResponse
Build and return a PSR-7 Response without streaming to the client.
The returned Response wraps the source in a StreamInterface body.Call $downloader->close() after the body has been sent.
public getResponse(): Psr\Http\Message\ResponseInterfaceReturn Value:
\Psr\Http\Message\ResponseInterface - PSR-7 response ready to be dispatched.
Throws:
- \Luminova\Exceptions\RuntimeException - If no valid source is set.
Use this when your framework's dispatch layer is responsible for sendingthe response (e.g. middleware pipelines, etc.).
download
Stream the download directly to the client.
Sends all necessary HTTP headers (including Content-Disposition,Content-Length, range headers for partial content, and cachingheaders), then streams the source content in chunks.
When $mode is X_SENDFILE or X_ACCEL, the method emits only theappropriate offload header and lets the web server handle the transfer;this is only effective when the source is a 'file'.
public download(int $mode = self::X_DOWNLOAD, int $chunkSize = 8192, int $delay = 0): boolParameters:
| Parameter | Type | Description |
|---|---|---|
$mode | int | Transfer strategy. One of: - Downloader::X_DOWNLOAD – PHP streams the content (default).- Downloader::X_SENDFILE – Apache/Lighttpd X-Sendfile.- Downloader::X_ACCEL – Nginx X-Accel-Redirect. |
$chunkSize | int | Bytes read per iteration when streaming (default 8192). |
$delay | int | Microseconds to sleep between chunks, 0 disables throttling (default 0). |
Return Value:
bool - Returns true on success, false if the source yielded zero bytesor an offload header could not be set.
Throws:
- \Luminova\Exceptions\RuntimeException - When the source is missing or unreadable.
tryDownload
Exception-safe wrapper around download().
Catches any Throwable thrown during streaming and returns falseinstead of propagating the exception. Useful in fire-and-forget contextswhere you prefer a boolean result over exception handling.
public tryDownload(int $mode = self::X_DOWNLOAD, int $chunkSize = 8192, int $delay = 0): boolParameters:
| Parameter | Type | Description |
|---|---|---|
$mode | int | Transfer strategy (X_DOWNLOAD, X_SENDFILE, X_ACCEL). |
$chunkSize | int | Bytes per streaming chunk. |
$delay | int | Microseconds between chunks. |
Return Value:
bool - Returns true on success, false on failure or exception.
close
Close all open handles and release internal state.
public close(): voidCall this after streaming is complete, or after obtaining and sending aPSR-7 response. The instance should not be reused after
close().
deleteSourceFile
Delete the source file from disk.
public deleteSourceFile(): boolReturn Value:
bool - true on success, false when the source is not a file orthe unlink fails.
Note:
Only applicable when the source type is
'file'. Silently returnsfalsefor other source types.