How to set content type header when not using Nette
- Crell
- Member | 9
The Latte documentation states:
https://latte.nette.org/en/tags#…
If the parameter is a full-featured MIME type, such as application/xml, it also sends an HTTP header Content-Type to the browser
I am using Latte outside of Nette, and this is not happening. It's not entirely surprising as I'm not sure how it should work, frankly, but the documentation is silent on how I would extract that value to send the content-type header myself. Is that documented anywhere? How can I get that value to send it myself?
- mskocik
- Member | 74
And HOW are you sending the template? This following minimal example sends
correct content-type
header.
$latte = new \Latte\Engine();
$latte->render(__DIR__ . '/template.latte');
When template is like:
{contentType application/xml}
<?xml version="1.0" encoding="UTF-8"?>
<my-xml>...</my-xml>
Last edited by mskocik (2025-02-10 10:20)
- nightfish
- Member | 526
@Crell
The code responsible for rendering {contentType}
as
header('Content-type: ...')
call is located at https://github.com/…TypeNode.php#…
The coreCaptured
property prevents outputting
header()
call when template is rendered via
renderToString()
.
The getReferenceType()
method expresses whether the current
template (e.g. currentTemplate.latte
) is standalone
(null
), is being extended (has a parent template which contains
{extends currentTemplate.latte}
or imported (parent template
contains {import currentTemplate.latte}
). header()
is
not called when {contentType}
is located in an imported
template.
If you need to use renderToString()
or need setting
{contentType}
in an imported template, I'll need more details about
how you work with templates to propose a workable solution.
- Crell
- Member | 9
Currently, I'm calling renderToString() from within a wrapper object, like so:
readonly class TemplateRenderer
{
public function __construct(
private Engine $latte,
private EventDispatcherInterface $dispatcher,
) {}
public function render(string $template, object|array $args = []): string
{
/** @var TemplatePreRender $event */
$event = $this->dispatcher->dispatch(new TemplatePreRender($template, $args));
return $this->latte->renderToString($event->template, $event->args);
}
}
The intent being that the event listeners can inject additional information into the arguments to the template. The returned string is then wrapped into a PSR-7 Response object, which eventually gets emitted.
Presumably, I need some other method call on the Latte Engine that's a bit
lower level and lets me extract the content type, if any, and then change
render()
to return both the rendered string and the content type if
any, which I can then also set on the Response object. I'm just unclear what
that first part would be, to get the content type.
- nightfish
- Member | 526
Crell wrote:
Presumably, I need some other method call on the Latte Engine that's a bit lower level and lets me extract the content type, if any, and then changerender()
to return both the rendered string and the content type if any, which I can then also set on the Response object. I'm just unclear what that first part would be, to get the content type.
@Crell
AFAIK there is no easy way built into Latte to retrieve the value
{contentType}
.
You can probably work around it by using something like:
public function render(string $template, object|array $args = []): string
{
/** @var TemplatePreRender $event */
$event = $this->dispatcher->dispatch(new TemplatePreRender($template, $args));
ob_start();
$this->latte->render($event->template, $event->args);
$renderedTemplate = ob_end_clean();
// the above code should set a `Content-Type` header when `{contentType}` is used inside of the template
// you can then extract the information from `header_list()`
// and possibly return it along with the `$renderedTemplate`
return $renderedTemplate;
}
If you don't insist on using a built-in tag {contentType}
, you
could create a custom tag – e.g. {returnContentType}
, which
could just save the information to a global storage (maybe a global template
variable, which you'll create just for this purpose), instead of emitting a
header()
call.
You could also just use a regex to extract {contentType ...}
from $template
– but that would obviously be limited to
situations when {contentType}
is placed in the main template, not
the included one, and the value of the tag is just a plain text, not a varible,
nor an expression.
- Crell
- Member | 9
I was able to get that mostly working! What I ended up with is this:
public function render(string $template, object|array $args = []): TemplateResult
{
/** @var TemplatePreRender $event */
$event = $this->dispatcher->dispatch(new TemplatePreRender($template, $args));
ob_start();
$this->latte->render($event->template, $event->args);
$rendered = ob_get_clean();
// If no content type is found, assume it's HTML.
$contentType = 'text/html';
foreach (array_map(strtolower(...), headers_list()) as $header) {
if (str_starts_with($header, 'content-type')) {
sscanf($header, 'content-type: %s', $contentType);
break;
}
}
return new TemplateResult($rendered, $contentType);
}
Which feels a bit clunky, but works fine in manual testing when viewing a
page. When I run PHPUnit over it, however, for whatever reason
headers_list()
is empty. Exact same URL. My tests aren't issuing an
internal HTTP request; it's all in-process passing a request object through.
Latte doesn't do any unit-test detection to disable the headers or something
like that, does it? Because otherwise I'm not sure why it's skipping that.
- nightfish
- Member | 526
@Crell The issue of headers_list()
vs. PHPUnit is not
Latte-specific, see https://stackoverflow.com/…2373/4930070 or https://github.com/…/issues/3409#…
for suggestions on how to work around it.
- Crell
- Member | 9
Woof. I think I have an approach that is working in all cases now, barely. :-) Here it is for future reference:
public function render(string $template, object|array $args = []): TemplateResult
{
// Latte doesn't offer an API to get the content type of the template.
// And it only send sit when you tell it to print the rendered output
// itself. So this is this least-bad way to capture the content type,
// until/unless Latte provides a useful API.
ob_start();
$this->latte->render($template, $args);
$rendered = ob_get_clean();
// When running on the CLI (such as tests), headers_list() doesn't work. If using XDebug,
// it has its own headers method that we can use instead. This is ugly,
// but the only way I know of that gets tests to pass.
// @see https://github.com/sebastianbergmann/phpunit/issues/3409#issuecomment-442596333
$headers = (PHP_SAPI === 'cli' && function_exists('xdebug_get_headers'))
? xdebug_get_headers()
: headers_list();
// If no content type is found, assume it's HTML.
$contentType = 'text/html';
foreach (array_map(strtolower(...), $headers) as $header) {
if (str_starts_with($header, 'content-type')) {
sscanf($header, 'content-type: %s', $contentType);
break;
}
}
return new TemplateResult($rendered, $contentType);
}
This feels like a place where improving the API to make the content type available would be quite helpful.