How to set content type header when not using Nette

Crell
Member | 9
+
0
-

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
+
0
-

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
+
0
-

@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
+
0
-

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
+
0
-

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 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.

@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
+
0
-

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
+
0
-

@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
+
0
-

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.