Writing phtml Templates: the Escaper, Best Practices, No Raw Output Without Escaping
Writing phtml Templates: the Escaper, Best Practices, No Raw Output Without Escaping
~7 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026
At first glance, a .phtml template in Hyvä looks like plain PHP mixed into HTML - and for the most part, it is. The key difference from "quickly mixing some PHP into HTML" is the consistent use of the escaper for every dynamic output.
The escaper: $escaper
Every standard template gets an instance of \Magento\Framework\Escaper made available as $escaper. The most important methods:
escapeHtml($string)- for regular text content (the standard case, needed most often).escapeHtmlAttr($string)- for values inside HTML attributes, e.g.altortitle.escapeUrl($string)- for URLs, e.g. inhreforsrc.escapeJs($string)- for values embedded in JavaScript inside a<script>block.
<?php
/** @var \Magento\Framework\Escaper $escaper */
/** @var \Mironsoft\TeamPage\ViewModel\TeamMembers $teamViewModel */
?>
<?php foreach ($teamViewModel->getTeamMembers() as $member): ?>
<article>
<h3><?= $escaper->escapeHtml($member['name']) ?></h3>
<img src="<?= $escaper->escapeUrl($member['photo_url']) ?>"
alt="<?= $escaper->escapeHtmlAttr($member['name']) ?>">
</article>
<?php endforeach; ?>Achtung: Raw output like <?= $member['name'] ?> without the escaper is an XSS vulnerability as soon as the value comes (even partly) from user input or the admin - and even for seemingly safe values, it's simply the wrong habit. In this project: every dynamic output goes through the matching escaper, no exceptions.
@var annotations at the top of the file
At the top of every template you'll find /** @var Type $variable */ comments for $block, $escaper, and every bound ViewModel. That's not just documentation - PhpStorm and PHPStan use these annotations for autocompletion and static analysis, which wouldn't otherwise be possible in plain .phtml files without real type declarations.
Keep template logic minimal
A template should essentially just output what a ViewModel or block supplies it with - loops and simple conditions are normal, but complex business logic belongs in the ViewModel, not the template. A good rule of thumb: if a condition needs more than a one-line PHP expression, it probably belongs as its own method in the ViewModel.
<?php // Better avoided: complex logic directly in the template
<?php if (count(array_filter($members, fn($m) => $m['category'] === 'design')) > 0): ?>
<?php // Better: a finished, descriptive method on the ViewModel
<?php if ($teamViewModel->hasDesignTeamMembers()): ?>Keep Tailwind classes readable
When an element has many utility classes, it's worth formatting the opening tag across several lines instead of producing one very long line - that keeps future diffs small and the classes easy to scan. Chapters 11-13 cover Tailwind in Hyvä templates in detail.