HTTP požadavky a odpovědi – Část 2.
V první
části popisuji metody presenteru, kterými můžeme ovládat HTTP
odpovědi aplikace. V této části se budu věnovat rozhaní
Nette\Application\IResponse
, se kterým lze HTTP odpovědi formovat
velmi přesně.
Nette\Application\IResponse
Metoda presenteru sendResponse()
přijímá jediný argument.
Objekt implementující rozhraní Nette\Application\IResponse
. Než
si napíšeme vlastní implementaci, pojďme si prohlédnout ty hotové ze
jmeného prostoru Nette\Application\Responses
.
Nejpoužívanější, nejužitečnější, ale zároveň nejméně známá je
TextResponse
. Pokud presenter vykoná nějakou akci a z Latte se
vykreslí HTML obsah, je to právě ona, které se použije. Ponechme ji
v klidu v zapomnění.
Kdo znáte Nette podrobněji, mohli jste si všimnout, že jsem v první
části této minisérie vůbec nezmínil metodu presenteru
forward()
. To proto, že s HTTP nemá nic společného. Metoda
sice vytvoří objekt třídy ForwardResponse
, tváří se jako
HTTP odpověď, odešle se tak, ale aplikace odpověď zachytí, HTTP
přesměrování neodešle a místo toho jen zavolá nový cíl
přesměrování. Prohlížeč nic neví, URL se nezmění.
Oproti tomu, třídu RedirectResponse
používají všechny
redirect*()
metody presenteru. Tady už jde o přesměrování
HTTP protokolem, prohlížeč načítá novou URL.
Zajímavější se může zdát JsonResponse
, ale opravdu se to
jen zdá. Tuto třídu využívá metoda presenteru sendJson()
a
tím bych její popis uzavřel.
Opravdu zajímavá je až třída FileResponse
. Odešle do
prohlížeče soubor a nabídne ho ke stažení. Představte si aplikaci
s účetnictvím. Spousta faktur ve formátu PDF. Ty ale nesmí být
přístupné jen tak někomu! Přístup je řízen aplikací, adresářová
struktura je následující:
app/ invoices/ <-- spousta faktur v PDF, sem se prohlížeč nedostane www/ <-- document root
A nyní presenter, který umožní stahování faktur pouze přihlášeným uživatelům.
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("Faktura s ID $id neexistuje.");
die();
}
$response = new FileResponse($invoice->file, "Faktura $invoice->number.pdf", 'application/pdf');
$this->sendResponse($response);
}
}
Presenter dostane v konstruktoru repozitář. V metodě
startup()
zajistíme přístup pouze pro přihlášené uživatele.
A pokud faktura existuje, akce invoice
ji odešle jako PDF soubor
do prohlížeče.
Třída FileResponse
za nás myslí na spoustu věcí. Správně
formátovaný název souboru při použití diakritiky, nebo navázání
přerušeného stahování. Není úplně snadné to vyřešit korektně. Také
celý soubor nenačítá do paměti, ale odesílá ho po blocích. Nutnost pro
velké soubory.
Třída FileResponse
je nenahraditelná. Téměř. Znáte modul
mod_xsendfile
pro Apache? Pokud ne, krátce ho představím.
Mějme adresář se soubory mimo document root. Stejně umístěný, jako ten
s fakturami z příkladu výše. Webový prohlížeč se do něj nedostane.
A vy přesto chcete, aby šlo soubory volně stahovat a navíc, abyste si mohli
přístupy logovat. Můžete použít logiku stejnou jako v příkladu s PDF
fakturami a použít FileResponse
. Má to ale jednu nevýhodu.
Pokud jsou soubory velké a klienti přistupují po pomalém připojení, může
stažení souboru trvat několik hodin. Mohou se nám nashromáždit třeba
i stovky paralelních stahování. A pro každé spojení běží jeden PHP
proces. Máte-li Apache a modul mod_xsendfile
, stačí odeslat HTTP
hlavičku X-SendFile
a běh PHP aplikace ukončit. O stažení
souboru se postará web server. Úvahy o tom, jak moc je to užitečné,
ponechám na vás. Mně se to hodí do příkladu na implementaci
Nette\Application\IResponse
. A třeba anonymní třídou, když
už je v PHP 7 máme. Následující útržek kódu je akce presenteru, která
stažení obslouží.
public function actionDownload(string $path): void
{
# $this->basePath - například /var/data/download
# $this->logger - smyšlený logovací nástroj
$file = $this->basePath . '/' . $path;
if (!is_file($file)) {
$this->error('File not found.');
die();
}
# Trocha té bezpečnosti
$file = realpath($file);
if (strpos($file, $basePath . '/') !== 0) {
$this->error(null, Nette\HTTP\IResponse::S403_FORBIDDEN);
die();
}
# Implementace 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);
}
Myslím, že nic složitého. Odesláním odpovědi se pouze nastaví zmiňovaná HTTP hlavička. Vše ostatní řeší Apache.
Podobnou funkcionalitu má i Nginx. Řízení se předává
hlavičkou X-Accel-Redirect
a její smysl je trochu odlišný.
Neposíláte absolutní cestu ve filesystému, ale URI, jejíž mapování na
konkrétní adresář nastavíte v konfiguraci.
Použití anonymní třídy je sice cool, ale kód není znovupoužitelný. Napíšeme si teď znovupoužitelnou třídu, response pro odesílání CSV, populární tabulkový export dat.
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) {
# Aby MS Excel správně zobrazil diakritiku. Ale jen pokud existují nějaké řádky.
fputs($fd, "\xEF\xBB\xBF");
$bom = false;
}
$row = $row instanceof \Traversable ? iterator_to_array($row) : (array) $row;
fputcsv($fd, $row, $this->delimiter);
}
fclose($fd);
}
}
Kód komentovat nebudu, věřím, že je srozumitelný. Použití je snadné.
Vytvoříme instanci třídy a předáme ji metodě presenteru
sendResponse()
.
Poslední připravenou implementací Nette\Application\IResponse
je CallbackResponse
. Je to univerzální třída a v konstruktoru
vyžaduje callback, který zavolá ze své send()
metody. Všechny
příklady uvedené výše lze přepsat pomocí CallbackResponse
.
S anonymní třídou trochu ztrácí na kouzlu, ale pro nějaké very lazy
odpovědi se stále hodí.
Ať už vám přišly uvedené implementace užitečné, nebo ne, rozhodně doporučuji podívat se na jejich kód, než začnete psát implementace vlastní. Je se čím inspirovat a lépe se vám to dostane pod kůži.
Tím jsem shrnul téměř vše, co jsem chtěl o HTTP v Nette aplikaci napsat. Třetí díl už bude jen taková třešnička.
Komentáře
Při přesměrování na přihlašovací formulář je vhodné mu předat adresu, na kterou se po přihlášení má vrátit. Jinak uživatel stránku musí znovu hledat.
Ano. To se v Nette řeší uložením požadavku a persistentním parametrem. https://pla.nette.org/…jnou-stranku
Chcete-li odeslat komentář, přihlaste se