HTTP requests and responses – Part 2
In the first
part I describe presenter methods, by which we can control HTTP responses
of the application. In this section, I will address the interface
Nette\Application\IResponse
, with which HTTP responses can be
formed very accurately.
Nette\Application\IResponse
The presenter method sendResponse()
accepts a single argument.
Object implementing interface Nette\Application\IResponse
. Before
we write our own implementation, let's look at the ready ones
Nette\Application\Responses
.
It is the most widely used, the most useful, but also the least known
TextResponse
. If the presenter takes an action and the HTML content
is rendered from Latte, it is the one that is used. Let her stay calm in
oblivion.
If you know Nette in more detail, you may have noticed that I did not
mention the presenter method at all in the first part of this mini-series
forward()
. That's because it has nothing to do with HTTP. The
method creates a class object ForwardResponse
, looks like an HTTP
response, sends it, but the application intercepts the response, does not send
the HTTP redirection, and instead calls a new redirection target. The browser
does not know anything, the URL does not change.
In contrast, class RedirectResponse
is used by all
redirect*()
presenter methods. This is a redirect by HTTP protocol,
the browser retrieves a new URL.
JsonResponse
may seem more interesting, but it really only
seems. This class is used by the presenter method sendJson()
and
thus I would conclude its description.
Really interesting is the class FileResponse
. Sends a file to
the browser and offers it for download. Imagine an app with user accounts. Lots
of PDF invoices. But they must not be accessible to anyone! The access is
controlled by the application, the directory structure is as follows:
app/ invoices/ <-- a lot of invoices in PDF, the browser can't get here www/ <-- document root
And now presenter, which allows downloading of invoices only to logged users.
use Nette\Application\Responses\FileResponse;
final class DownloadPresenter extends Nette\Application\UI\Presenter
{
private $invoices;
public function __construct(InvoiceRepository $invoices)
{
$this->invoices = $invoices;
}
public function startup(): void
{
parent::startup();
if (!$this->getUser()->isLoggedIn()) {
$this->redirect(':Admin:Sign:in');
die();
}
}
public function actionInvoice(int $id): void
{
try {
$invoice = $this->invoices->get($id);
} catch (InvoiceNotFoundException $e) {
$this->error("An invoice with ID $id does not exist.");
die();
}
$response = new FileResponse($invoice->file, "Invoice $invoice->number.pdf", 'application/pdf');
$this->sendResponse($response);
}
}
The presenter gets a repository in the constructor. In the method
startup()
, we will only grant access to logged-in users. And if an
invoice exists, the action invoice
sends it as a PDF file to the
browser.
Class FileResponse
does a lot of things for us. Properly
formatted file name when using diacritics or interrupted download. It is not
quite easy to solve it correctly. Also, the entire file is not read into memory
but is sent in blocks. The necessity for large files.
Class FileResponse
is irreplaceable. Nearly. Do you know the
Apache module mod_xsendfile
? If not, I will briefly
introduce it.
Let's have a directory with files outside of the document root. Just as
placed as the one with the invoices from the example above. The web browser does
not access it. And yet you want to be able to download the files freely and,
moreover, to log your accesses. You can use the same logic as in the example
with PDF invoices and use FileResponse
. But it has one drawback. If
the files are large and clients access after a slow connection, it may take
several hours to download the file. We may have accumulated hundreds of parallel
downloads, for example. And for each connection runs one PHP process. If you
have Apache and module mod_xsendfile
, just send HTTP header
X-SendFile
and stop running the PHP application. The web server
will take care of the file download. I will leave it to you to consider how
useful it is. This fits me in the implementation example
Nette\Application\IResponse
. And maybe an anonymous class when we
already have it in PHP 7. The following code snippet is a presenter action that
handles the download.
public function actionDownload(string $path): void
{
# $this->basePath - for example /var/data/download
# $this->logger - fictional logging tool
$file = $this->basePath . '/' . $path;
if (!is_file($file)) {
$this->error('File not found.');
die();
}
# A little bit of security
$file = realpath($file);
if (strpos($file, $basePath . '/') !== 0) {
$this->error(null, Nette\HTTP\IResponse::S403_FORBIDDEN);
die();
}
# Implementation IResponse
$response = new class($file) implements Nette\Application\IResponse
{
private $file;
public function __construct(string $file)
{
$this->file = $file;
}
public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response)
{
$response->setHeader('X-SendFile', $this->file);
}
};
$this->logger->notice("File download from $_SERVER[REMOTE_ADDR]: $path");
$this->sendResponse($response);
}
I think nothing complicated. Sending a response only sets the HTTP header. Everything else is solved by Apache.
Nginx has a similar functionality. The control is passed through
the header X-Accel-Redirect
and its meaning is slightly different.
You are not sending an absolute path in the file system, but a URI whose mapping
to a specific directory is set in the configuration.
Using an anonymous class is cool, but the code is not reusable. Let's write a reusable class, response for sending CSV, a popular spreadsheet data export.
final class CsvResponse implements Nette\Application\IResponse
{
private $fileName;
private $rows;
private $delimiter;
public function __construct(string $fileName, iterable $rows, string $delimiter = ',')
{
$this->fileName = $fileName;
$this->rows = $rows;
$this->delimiter = $delimiter;
}
public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response)
{
$response->setContentType('text/csv', 'utf-8');
$response->setHeader('Content-Description', 'File Transfer');
# Trochu vykrademe Nette\Application\Responses\FileResponse
$tmp = str_replace('"', "'", $this->fileName);
$response->setHeader(
'Content-Disposition',
"attachment; filename=\"$tmp\"; filename*=utf-8''" . rawurlencode($this->fileName)
);
$bom = true;
$fd = fopen('php://output', 'wb');
foreach ($this->rows as $row) {
if ($bom) {
# In order for MS Excel to display diacritics correctly. But only if there are any lines
fputs($fd, "\xEF\xBB\xBF");
$bom = false;
}
$row = $row instanceof \Traversable ? iterator_to_array($row) : (array) $row;
fputcsv($fd, $row, $this->delimiter);
}
fclose($fd);
}
}
I will not comment on the code, I believe it is understandable. Use is
easy. Create an instance of the class and pass it to the presenter method
sendResponse()
.
The latest implementation Nette\Application\IResponse
is
CallbackResponse
. It is a universal class and requires a callback
in the constructor to be called in send()
method. All of the
examples above can be overridden with CallbackResponse
. The
anonymous class a bit lost in charm, but some very lazy answers are still
useful.
Whether you find these implementations useful or not, I strongly recommend looking at their code before you write your own implementations. There is something to inspire and it gets better under your skin.
This summarized almost everything I wanted to write about HTTP in the Nette application. The third part will be just such a cherry.
Sign in to submit a comment