Replacing templating systems (Twig and Latte) with Components

The current market standard of server-side rendering in PHP is to use templating systems like Twig or Latte. This article will present an alternative that is type-safe (thus robust), easily testable and allow you to scale thanks to reusability.

In a nutshell: let's write some React components in PHP.

en 4. 4. 2019
PHP

Component as a pure function

When designing a part of the UI in server-side rendering, we can think about it as a function. For the given input it will produce (HTML) string. This function must be pure, hence must not access global state.

Pure functions are ultimately testable and reusable.

Interface for the component has just one method without arguments. This allows to separate:

  1. passing arguments into the function (via constructor)
  2. execution of the function (calling render method)
interface UiComponent {
    public function render(): string;
}

The __invoke magic method can be also utilized for this.

This is an example of the UiComponent itself:

final class UserComponent implements UiComponent {

    /** @var Uri */
    private $avatar;

    /** @var string */
    private $name;

    public function __construct(Uri $avatar, string $name)
    {
        $this->avatar = $avatar;
        $this->name = $name;
    }

    public function render(): string
    {
        return '<img src="' . Escaper::escapeAttribute((string)$this->avatar) . '" />'
            . Escaper::escapeHtml($this->name);
    }
}

Data is passed via the constructor. This is equivalent to props in React. It is important to pass only pure data (scalars and immutable value object) into the component. The component is a value-object by itself (its value is HTML string) and must not have any dependencies on services.

Composing the component tree

The composition of the components is achieved by passing inner components via constructor as a first parameter.

final class ListRowComponent implements UiComponent {

    /** @var UiComponent[]|string[] */
    private $children;

    /**
     * @param UiComponent[]|string[]|UiComponent|string $children
     */
    public function __construct($children)
    {
        $this->children = is_array($children) ? $children : [$children];
    }

    public function render(): string
    {
        return '<li>' . ArrayRenderer::render($this->children) . '</li>';
    }
}

The following example shows, how the whole page can be composed:

$menu = new MenuComponent(...);
$rows = [
    new ListRowComponent(new UserComponent(...)),
    new ListRowComponent(new UserComponent(...)),
];
$content = new UserListComponent($rows);

echo (new MainPageLayoutComponent($menu, $content))->render();
Usually, components that allow inner nodes ($children) accept any UiComponent. But component can also limit inner nodes to be of a certain type. In the example above MainPageLayoutComponent accepts only MenuComponent type as its first parameter.

Benefits

Type-safety

This is the biggest benefit of this approach. It allows you to do refactoring of the UI code from IDE safely (and natively, without the need for a special plugin) and prevents bugs by using both PHP runtime type-checking and static analysis (like PHPStan).

Unit-testability

Because each component is a pure function, testing is trivial. A good approach is to do snapshot testing. This means asserting whole HTML output of the component for a given input (i.e. props passed via constructor).

Example of such a test (detail of SnapshotAssertTrait is available here):

class TagComponentTest extends TestCase
{
    use SnapshotAssertTrait;

    public function testRenderTagWithHslStyle(): void
    {
        $component = new TagComponent('#12f457', 'Foo');
        $this->assertSnapshot(__DIR__ . '/__snapshots__/hslTag.html', $component);
    }
}

Reusability

This approach naturally leads to writing small functions that solve small parts of the UI and are reusable in different contexts.

Challenges

Escaping

All variables need to be escaped properly to prevent XSS. Currently, it is expected that it will be solved per each component. Using of Zend Escaper is favored.

A possible solution to this problem is briefly discussed in section Post-processing.

Barrier to entry

All those types, classes and other syntax sugar mean more code to write, especially at the beginning of adapting this practice. However, in the long run, it will pay off as the reusability of the components actually lowers the amount of the code that needs to be written.

Real world application

In Brand Embassy we successfully implemented this pattern for server-side rendering. And we open-sourced our implementation. We benefit heavily from having the same components (only written in JS) in our React applications. We maintain both JS components and PHP implementations simultaneously. It's much better to have it just twice (because of different technology) than having the same HTML code copied all over the Twig or Latte templates.

DISCLAIMER: These repositories are meant to be used only as an example and demonstration of this pattern. Using them directly is strongly discouraged. Some important implementation decisions have not been settled and the components' API will most likely change a lot in the future.

Next steps and possible extensions of this pattern

Post-processing

Components will not return a string but some object-structure. This may be a way to systematically solve escaping.

One implementation of this may be to use Twig or Latte inside of each component to provide this post-processing. But this will require further research (especially if it's possible to implement this without breaking type-safety).

Props as one object

Because PHP does not support named function arguments (RFC), it's hard to extend the constructor of the UiComponent while maintaining backwards compatibility.

First, the optionality of multiple arguments is painful:

new XyzComponent('', null, false, 'Here is something I want to set');

Second, passing default values in case of objects (for example enums) is problematic an must be hacked by using null and solved by if in constructor's body.

    public function __construct(?Color $color = null) 
    {
        $this->color = $color ?? Color::get(Color::POSITIVE);
    }

All of this can be solved by passing a single object into the component. Let's call this object props. A props-type must be defined for each component.

final class UserComponentProps {
    /** @var Uri */
    private $avatar;

    /** @var string */
    private $name;

    // Private construtor hides the complicated API
    private function __construct(Uri $avatar, ..., string $name = '')
    {
        $this->avatar = $avatar;
        // ... many optional properties
        $this->name = $name;
    }

    public static function create(string $name): self
    {
        return new self(new Uri('/img/default-avatar.png'), ..., $name);
    }

    // This allows the buiding of the props object with just some of optional properties
    public function withAvatar(Uri $avatar): self  
    {
        return new self($avatar, ..., $this->name);
    }

    // ... getters to emulate read-only properties

}

The API of the component is now simple:

final class UserComponent implements UiComponent {

    /** @var UserComponentProps */
    private $props;

    public function __construct(UserComponentProps $props)
    {
        $this->props = $props;
    }

    // ... render
}

This extension adds some more code to write but provides a way to achieve a nice and extendable API while maintaining the ability to preserve backwards compatibility.

Conclusion

This article proposes an alternative to traditional template-systems. Writing components as pure functions provides a powerful tool for bigger applications that need to scale. It also enables the creation of UI Frameworks similar to Blueprint or Material UI.

Questions, ideas and improvements are welcome. Please let me know on Twitter .

Našli jste chybu? Opravit!