Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Writing Custom Renderers and Custom Column Classes

Writing Custom Renderers and Custom Column Classes

~9 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026

Chapter 17 already built a custom column class for the actions column. This chapter generalizes the pattern: how do you decide whether a PHP column class is enough, or whether a custom JavaScript component is needed - and how do you build either one cleanly?

Two levels of custom rendering

  • PHP column class (prepareDataSource()) - reshapes the raw data per row before it reaches the browser. Enough for anything expressible as a plain text/HTML value or an extra data field.
  • Custom JavaScript component (KnockoutJS template + uiComponent) - needed as soon as the browser itself needs interactivity (click handlers, dynamic reloading) beyond a simple link with confirm.

For testimonials, the first level is enough: a rating column that renders the number 1-5 as a star string instead of a bare number.

A custom rating column

app/code/Mironsoft/Testimonial/Ui/Component/Listing/Column/Rating.php
<?php

declare(strict_types=1);

namespace Mironsoft\Testimonial\Ui\Component\Listing\Column;

use Magento\Framework\Escaper;
use Magento\Framework\View\Element\UiComponent\ContextInterface;
use Magento\Framework\View\Element\UiComponentFactory;
use Magento\Ui\Component\Listing\Columns\Column;

/**
 * Renders the numeric rating as a star string, e.g. "★★★★☆".
 */
class Rating extends Column
{
    private const MAX_RATING = 5;

    /**
     * @param ContextInterface $context Rendering context.
     * @param UiComponentFactory $uiComponentFactory Factory for nested UI components.
     * @param Escaper $escaper Escapes the rendered star string for safe HTML output.
     * @param array<string, mixed> $components Nested UI components.
     * @param array<string, mixed> $data Additional UI Component data configuration.
     */
    public function __construct(
        ContextInterface $context,
        UiComponentFactory $uiComponentFactory,
        private readonly Escaper $escaper,
        array $components = [],
        array $data = [],
    ) {
        parent::__construct($context, $uiComponentFactory, $components, $data);
    }

    /**
     * Replaces the raw rating integer with a rendered star string.
     *
     * @param array<string, mixed> $dataSource Raw grid data source.
     * @return array<string, mixed>
     */
    public function prepareDataSource(array $dataSource): array
    {
        if (!isset($dataSource['data']['items'])) {
            return $dataSource;
        }

        $fieldName = $this->getData('name');

        foreach ($dataSource['data']['items'] as &$item) {
            $rating = max(0, min(self::MAX_RATING, (int) $item[$fieldName]));
            $stars = str_repeat('★', $rating) . str_repeat('☆', self::MAX_RATING - $rating);
            $item[$fieldName] = $this->escaper->escapeHtml($stars);
        }

        return $dataSource;
    }
}
<column name="rating" class="Mironsoft\Testimonial\Ui\Component\Listing\Column\Rating">
    <settings>
        <filter>textRange</filter>
        <label translate="true">Rating</label>
    </settings>
</column>

Achtung: $this->escaper->escapeHtml() isn't a formality here: grid JavaScript templates partly render column values unbound as HTML. A column renderer that returns user input (not just computed values like here) unescaped opens a stored XSS hole directly in the admin grid.

When is PHP no longer enough?

An example that would need a custom JavaScript component: clicking the star column to change the rating directly in the grid, without a reload. That would require a custom RequireJS module (extending Magento_Ui/js/grid/columns/column) plus its own KnockoutJS template - noticeably more effort than the PHP renderer above, and outside the scope of this series. Chapter 20 shows inline edit as a ready-made, already built-in middle ground for exactly this use case.

The boundary, restated

As already mentioned in chapter 1: Rating here is a textbook example of UI Components enforcing their own conventions. The class must extend Column and implement prepareDataSource() - an ArgumentInterface ViewModel couldn't be used here. The pure formatting logic (number to star string) could still easily be extracted into a separate, testable utility method if it were needed in multiple places across the project.