An easy guide for react developers getting into PHP Twig template engine from Symfony.
Hello π,
My name is Alexander Schranz (alex_s_) and I'm fulltime Webdeveloper working on the SULU CMS and did based on that created a lot of websites.
With this article I wanted to make it easier for React Developers getting into the Twig Syntax.
Every Section will first Provide the React JS
βοΈ code and then the Twig
π±
followed by explanation about the difference between them.
At the start I want to mention here that Twig
was inspired a lot by
Jinja2 Template Engine
developed for the Django Python Framework, but was improved
over the years with other new features by the Symfony Team.
Before continue with the overview the best comparison is
what JSX
is for React
(JS), Twig
is for Symfony
(PHP).
So all Business Logic should live outside of it.
Table of Contents
- Outputting a variable
- Outputting raw HTML
- If statements
- Loops
- Base Template
- Providing props to templates
- Include and override parts
- Dynamic Include
- Conclusion
In the first example we will to the most simpliest thing just outputting a variable we did create in our component / template.
// src/views/My.js
import React from 'react';
class My extends React.Component {
render() {
const title = 'My Title';
return <h1>{title}</h1>;
}
}
{# templates/pages/my.html.twig #}
{% set title = 'My Title' %}
<h1>{{ title }}</h1>
Outputting a variable in Twig and JSX is very similar instead
of {variable}
you use {{ variable }}
. The spaces are optional
but recommended by the Twig coding standards.
There are cases where you use CKEditor or other rich text editors to create content which will provide you raw HTML code you would like to render in this case you want not that your variable you want to outputted does get escaped.
// src/views/My.js
import React from 'react';
class My extends React.Component {
render() {
const someHtml = '<p>My Text editor content</p>';
return <div dangerouslySetInnerHTML={{_html: someHtml}} />;
}
}
{# templates/pages/my.html.twig #}
{% set someHtml = '<p>My Text editor content</p>' %}
<div>
{{ someHtml|raw }}
</div>
Both (React JS
and Twig
) escape variables by default so no HTML
injected without a call of an additional prop or filter.
There are in Twig other ways to disable autoescape
over a whole
content. See for this the official Twig Docs.
I personally recommend only using the |raw
filter todo this kind
of things.
// src/views/My.js
import React from 'react';
class My extends React.Component {
render() {
var title = 'Test' ;
const list = ['Test', 'Test 2'];
// Basic equal if statement
if (title == 'test') {
title = 'Basic equal if statement';
// Not equal if statement
} else if (title != 'test') {
title = 'Not equal if statement';
// Typesafe if statemenet
} else if (title === false) {
title = 'Typesafe if statemenet';
// Typesafe not if statemenet
} else if (title !== false) {
title = 'Typesafe not if statemenet';
// In array if statemenet
} else if (list.includes(title)) {
title = 'In array if statemenet';
// Not in array if statemenet
} else if (!list.includes(title)) {
title = 'Not in array if statemenet';
// Greater if statement
} else if (title > 10) {
title = 'Greater if statement';
// And if statement
} else if (title > 10 && title < 10) {
title = 'And if statement';
// Or if statement
} else if (title > 10 || title != 0) {
title = 'Or if statement';
// Else if statement
} else {
title = 'Else if statement';
}
return <div>
{title}
{title ? title : 'Other'}
</div>;
}
}
{# templates/pages/my.html.twig #}
{% set title = 'Test' %}
{% set list = ['Test', 'Test 2'] %}
{# Basic equal if statement #}
{% if title == 'test' %}
{% set title = 'Basic equal if statement' %}
{# Basic not equal if statement #}
{% elseif title != 'test' %}
{% set title = 'Basic not equal if statement' %}
{# Typesafe if statement #}
{% elseif title same as(false) %} {# Very uncommon in Twig todo typesafe checks #}
{% set title = 'Typesafe if statement' %}
{# Typesafe not if statement #}
{% elseif title not same as(false) %} {# Very uncommon in Twig todo typesafe checks #}
{% set title = 'Typesafe not if statement' %}
{# In array if statement #}
{% elseif title in list %}
{% set title = 'In array if statement' %}
{# Not in array if statement #}
{% elseif title not in list %}
{% set title = 'Not in array if statement' %}
{# Greater if statement #}
{% elseif title > 10 %}
{% set title = 'Greater if statement' %}
{# And if statement #}
{% elseif title > 10 and title < 10 %}
{% set title = 'And if statement' %}
{# Or if statement #}
{% elseif title > 10 or title != 0 %}
{% set title = 'Or if statement' %}
{# Else if statement #}
{% else %}
{% set title = 'Else if statement' %}
{% endif %}
<div>
{{ title }}
{{ title ?: 'Other' }}
</div>
For if statement the if tag
is used. Where in JavaScript it is very common to have
typesafe check with ===
. This is very uncommon in twig
because Twig is really focused being a template engine and
not a language where you should build business logic.
It is still possible in Twig doing a typesafe check using the same as test.
Twig like PHP supports also the ternary operator (?:
) and the
null-coalescing operator (??
). Read more about them in the
Twig operators documentation.
// src/views/My.js
import React from 'react';
class My extends React.Component {
render() {
const list = [{
title: 'List Item 1',
}, {
title: 'List Item 2',
}];
return <ul>
{list.map((item) => {
return <li>
{item.title}
</li>;
})}
</ul>;
}
}
{# templates/pages/my.html.twig #}
{% set list = [
{
title: 'List Item 1',
},
{
title: 'List Item 2',
}
] %}
<ul>
{% for item in list %}
<li>{{ item.title }}</li>
{% endfor %}
</ul>
Where in JavaScript
different way of looping exist in twig
you will work with the for
to loop over variables.
Twig here supports some range function to make developing of templates easier for example:
{# Output numbers from 0 - 10 #}
{% for i in 0..10 %}
* {{ i }}
{% endfor %}
{# Output letters from a-z #}
{% for i in 'a'..'z' %}
* {{ i }}
{% endfor %}
{# Output letters from A-Z #}
{% for letter in 'a'|upper..'z'|upper %}
* {{ letter }}
{% endfor %}
By default inside the for
loop Twig provide some magic variable
called loop
which will provide you the following data:
{% for i in 0..10 %}
{{ loop.index }} {# index beginning with 1 #}
{{ loop.index0 }} {# index beginning with 0 #}
{{ loop.revindex }} {# reverted index #}
{{ loop.revindex0 }} {# reverted index0 #}
{{ loop.first }} {# boolean flag if its the first item of this loop #}
{{ loop.last }} {# boolean flag if its the last item of this loop #}
{{ loop.length }} {# the number of items of this loop #}
{{ loop.parent }} {# access the parent loop variable if using nested loops #}
{% endfor %}
Where in React JS
you maybe need a counter
or accessing the length
in Twig
the loop
variable is available inside every loop and make
it easy to add CSS classes to first or last items of a loop.
See here also the Twig documentation about the for tag.
As React JS
has access to all JavaScript
array available function
Twig also has it tricks to make outputting of html very simple.
So for example let see that we have variable which content is the following:
["1", "2", "3", "4", "5"]
and we want to render the following HTML:
<div>
<div class="row">
<div class="col-4">
1
</div>
<div class="col-4">
2
</div>
<div class="col-4">
3
</div>
</div>
<div class="row">
<div class="col-4">
4
</div>
<div class="col-4">
5
</div>
<div class="col-4">
</div>
</div>
</div>
This can in Twig easily be rendered by the batch
function
the following way:
<div>
{% for row in list|batch(3, '') %}
<div class="row">
{% for item in row %}
<div class="col-4">
{{ item }}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
Read here also the offical documentation about the Twig batch function.
The first you mostly create when building a website is a base template or base component which contains all things which your templates have in common like Navigation, Footer, Header.
In react for rendering anything we first need to create a
index.html
where the react application is started which also
contains the minimal required html for a page.
<!-- src/index.html --->
<!doctype html>
<html lang="en">
<head>
</head>
<body id="app">
<script src="app.js"></script>
</body>
</html>
In react a base template could look like the following.
// src/views/Base.js
import React from 'react';
import Head from 'next/head'
import Navigation from './components/Navigation';
import Header from './components/Header';
import Footer from './components/Footer';
class Base extends React.Component {
render() {
const {
header
} = this.props;
const headerComponent = header ? header : <Header />;
return <div>
<Head>
<title>{this.props.title}</title>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
</Head>
<Navigation />
<Header />
<div className="content">
{this.props.children}
</div>
<Footer />
</div>;
}
}
A default template build on top of the base template will look like the following way:
// src/views/Default.js
import React from 'react';
import Base from './components/Base';
class Default extends React.Component {
render() {
const title = 'Some Title';
return <Base title={title}>
<h1>{title}</h1>
</Base>;
}
}
A homepage template build on top of the base template could look this way which will override the Header component:
// src/views/Homepage.js
import React from 'react';
import Base from './components/Base';
import HeaderBig from './components/HeaderBig';
class Homepage extends React.Component {
render() {
const title = 'Some Title';
return <Base title={title}
header={<HeaderBig />}>
<h1>{title}</h1>
</Base>;
}
}
This way we created a reusable base which can be used in all components again.
To implement the same as we did above we need to create the following
in our Twig structure. As Twig is server rendered by PHP in Symfony
we will create the base.html.twig
which will contain the html which
was in react rendered by index.html
and Base.js
.
{# templates/base.html.twig #}
<!doctype html>
<head lang="en">
<title>{title}</title>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
</head>
<body>
{% include 'include/navigation.html.twig' %}
{% block header %}
{% include 'include/header.html.twig' %}
{% endblock %}
{% block content %}{% endblock %}
{% include 'include/footer.html.twig' %}
</body>
</html>
And now we create the 2 templates file which use this base.html.twig
file.
{# templates/pages/default.html.twig #}
{% extends 'base.html.twig' %}
{% block content %}
<h1>{title}</h1>
{% endblock %}
We will also create the homepage.html.twig
with a custom header.
{# templates/pages/homepage.html.twig #}
{% extends 'base.html.twig' %}
{% block header %}
{% include 'include/header-big.html.twig' %}
{% endblock %}
{% block content %}
<h1>{title}</h1>
{% endblock %}
As you see in this example the extends
will say from which template
they should inherit and with the block
feature of Twig we can then
provide content or override the defined blocks
like header
and
content
in our Twig template.
Instead of importing a components like its done react we will use
in Twig the include
block/method of Twig to render the content of another twig
template.
Has in this case a look at the extends tag documentation and include tag documentation from the official Twig Documentation.
// src/views/Parent.js
import React from 'react';
import Child from './components/Child';
class Parent extends React.Component {
render() {
const someTitle = 'Some Title';
return <Child title={someTitle} />;
}
}
{% set title = 'Some Title' %}
{% include 'includes/child.html.twig' %}
Where in React JS
the only way to give properties from a parent
to a child is giving them as a props
in Twig
by default a
included Template
has access to all variables which were defined
or are accessible in the parent template.
Twig still provide ways to provide properties to included template this way:
{% include 'includes/child.html.twig' with { title: 'Some Title' } %}
If you don't want that the included template can access the variables
defined in the parent template this can be avoided using the only
keyword:
{% set someTitle = 'Some Title' %}
{% include 'includes/child.html.twig' with { title: someTitle } only %}
Read more about the include tag in the official Twig Include Documentation.
Sometimes you need to include and override a specific template or Component. In this section we will look at this parts.
// src/views/Parent.js
import React from 'react';
import Child from './components/Child';
import CustomHeader from './components/CustomHeader';
import CustomFooter from './components/CustomFooter';
class Parent extends React.Component {
render() {
return <Child
header={<CustomHeader />}
footer={<CustomFooter />}>
{this.props.children}
</Child>;
}
}
// src/component/Child.js
import React from 'react';
import Child from './components/Child';
import Header from './components/Header';
import Footer from './components/Footer';
class Parent extends React.Component {
render() {
const HeaderComponent = this.props.header ? this.props.header : <Header />;
const FooterComponent = this.props.footer ? this.props.footer : <Footer />;
return <div>
{HeaderComponent}
{FooterComponent}
</div>;
}
}
{# templates/pages/parent.html.twig #}
{% include 'includes/other-child.html.twig' %}
{# templates/pages/other-child.html.twig #}
{% extends 'includes/child.html.twig' %}
{% block header %}
{% include 'includes/custom-header.html.twig' %}
{% endblock %}
{% block content %}
Some Content
{% endblock %}
{% block footer %}
{% include 'includes/custom-footer.html.twig' %}
{% endblock %}
{# templates/pages/child.html.twig #}
{% block header %}
{% include 'includes/header.html.twig' %}
{% endblock %}
{% block content %}
{% endblock %}
{% block footer %}
{% include 'includes/footer.html.twig' %}
{% endblock %}
{# templates/pages/parent.html.twig #}
{% embed 'includes/child.html.twig' %}
{% block header %}
{% include 'includes/custom-header.html.twig' %}
{% endblock %}
{% block content %}
Some Content
{% endblock %}
{% block footer %}
{% include 'includes/custom-footer.html.twig' %}
{% endblock %}
{% endembed %}
{# templates/pages/child.html.twig #}
{% block header %}
{% include 'includes/header.html.twig' %}
{% endblock %}
{% block content %}
{% endblock %}
{% block footer %}
{% include 'includes/footer.html.twig' %}
{% endblock %}
In Twig there are different solutions for this kind of task.
Where mostly you would work in Twig with extends
from a
specific base.html.twig
and maybe have 1-3 different kind
of this base template, its very uncommon in Twig that you
include a component template and override a part of it.
Mostly Twig devs will go here with a additional Twig template
and use extends
and in the other template an include
tag
to the new template.
Twig provide with the embed
tag here to combine a include
and extends
in one template and avoid so the additional
template.
As this pattern is very uncommon you should not overuse the
embed
tag as it make the templates unreadable.
There are cases mostly when working with some kind of block editors where you would need to have an own component / template for every block type your block editors does provide.
In react this kind of things would need additional services and some kind of registry to be this dynamic. I will currently not provide an example and just show in the next step how it would be done in twig.
{# templates/pages/default.html.twig #}
{% set blocks = [
{
"type": "text",
"title": "My Title",
"description": "My Description"
},
{
"type": "quote",
"quote": "My Quote",
}
] %}
{% for content in blocks %}
{% include 'includes/block-types/' ~ block.type ~ '.html.twig' %}
{% endfor %}
{# templates/includes/block-types/text.html.twig #}
<div class="text">
<h3 class="text-title">{{ content.title }}</h3>
<p class="text-description">{{ content.description }}</p>
</div>
{# templates/includes/block-types/quote.html.twig #}
<div class="quote">
<blockquote>{{ content.quote }}</blockquote>
</div>
In twig as you can just concat the string and include a template dynamic you don't need any additional registry or other service to map between the block type and template. And as by default a included template as access to the parent variables you are also not force to map all properties from the parent template to the child template as the child template can just access it.
If you still want todo that you could go with something like:
{% for block in blocks %}
{% include 'includes/block-types/' ~ block.type ~ '.html.twig' with {
content: block,
} only %}
{% endfor %}
Twig and React JS (JSX) are in some cases very similar how they
render and output things. Twig is really focused of just creating
HTML or the outputted file format, like what JSX
was designed for.
Where in React JS
you have access to all JavaScript functions,
Twig
is very strict here as it is designed that all your Business Logic
lives in your PHP code before Twig is callded.
So if your Twig code begins to look messy or you have a lot of logic
statements in it, it is a hint that you did accidentally move your
Business Logic which should be handled in your PHP Controller
into your template rendering, which should be avoided.
I hope with this article I can give React Developers an easier way to get into Twig.
For getting an overview of all available Twig functionality I recommend to read over the Official Twig Documentation.
Thank you for reading this!
More articles on Github from me can be found here.