Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Creating & Mapping Layouts

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Ok, let's see what Layouts is all about. In this chapter, we'll, step-by-step, create & use a "layout", learning exactly how Layouts works its magic along the way.

To check out the Layouts admin section, head to /nglayouts/admin to find... a login form! The login form has nothing to do with the Layouts... it's just that the layouts admin area requires you to be logged in... and I've already added a login form to our site. There's even a user in the database! Log in with doggo@barkbite.com, password woof.

The Security Role Needed for the Admin Area

And when we submit... access denied! No worries: click down on the web debug toolbar's security icon... and go to "Access Decision". Yup: we were denied access because it was looking for a role called ROLE_NGLAYOUTS_ADMIN. To access the layouts admin area, we need to have this role.

The simplest way to add it is to go to config/packages/security.yaml. The user we're logged in as right now has ROLE_ADMIN. So, under role_hierarchy also give our user ROLE_NGLAYOUTS_ADMIN:

security:
... lines 2 - 6
role_hierarchy:
ROLE_ADMIN: [ROLE_USER, ROLE_NGLAYOUTS_ADMIN]
... lines 9 - 56

Creating our First Layout

And now if we click back, ta-da! Welcome to the layouts admin section! To understand what layouts does... it's best to see it in action. Start in this Layouts section... and click to create a new layout. This shows us about six different layout types we can choose from. As you'll see, these are much less important than they might seem at first, because, once you're in a layout, you can really do whatever you want, including floating things left and right. I typically choose "Layout 2". Call this "Homepage Layout" because I'm planning to use this on our homepage.

And... welcome to the layout editor! Quick tour: these items on the left side are called "blocks", and there are many different types, from simple title blocks to Google maps... to more complex things like lists and grids where you can render dynamic collections of things, like featured recipes. The main things we "do" on this page is choose a block on the left... then drag it onto one of the "zones" in the middle.

Putting Blocks onto the Layout

Grab a "Title" block and drag it somewhere onto the page... then give it some text. Cool!

It's a modest start, but, good enough! In the upper right, hit "Publish Layout".

And now that we have this new layout, open a second tab and go to the homepage to discover that... absolutely nothing changed! Let me actually rearrange my tabs.

Mapping a Layout

Anyways, nothing changed because, once you have a layout, you need to map it to a specific page or set of pages. That's the job of the layout mapping section. These are really the only two important sections in the admin area.

Here, add a new mapping and then go to Details. There are multiple ways that you can map a layout to a specific URL. You could use, for example, the path info, which is a fancy term that means "the URL, but without query parameters". Or you could use a path info prefix - like use this layout for all URLs that start with "/products". Or you can even map a layout to a specific route name.

Let's try that one. Hit "Add target". Then... let's go find our homepage route name: src/Controller/MainController.php. Here it is: app_homepage:

... lines 1 - 9
class MainController extends AbstractController
{
#[Route('/', name: 'app_homepage')]
public function homepage(RecipeRepository $recipeRepository): Response
{
... lines 15 - 22
}
}

Move back over, paste and hit "Save target".

We're going to talk about other ways to map or "activate" a layout for pages later. But route and path info are the simplest and flexible. They say:

If the current route or URL matches what we have here, use this layout.

Hit save changes. To choose which layout goes with this mapping, hit "Link layout" and select the only one: "Homepage Layout".

Awesome! So now when we go to the homepage, it should use the homepage layout. But... what does that even mean? Let's find out! Refresh and... we still don't see any difference! It's the same static page from Symfony!

Extending the Dynamic Base Layout

Oh, that's because we missed an important installation step. My bad! Go open the template for this page: templates/main/homepage.html.twig. Right now, we're extending base.html.twig:

{% extends 'base.html.twig' %}
... lines 3 - 60

And that template, like usual, has a block called body in the middle:

<!DOCTYPE html>
<html>
... lines 3 - 16
<body>
... lines 18 - 46
{% block body %}{% endblock %}
... lines 48 - 60
</body>
</html>

So it's a super traditional setup.

Now, change the extends to a dynamic variable called nglayouts.layoutTemplate:

{% extends nglayouts.layoutTemplate %}
... lines 2 - 60

Configuring the Base Layout

Try the page again. Error! That's progress! It says:

Base page layout, not specified. To render the page with Layouts, specify the base page layout with this config.

This will all make more sense in a minute. What it wants us to do is open config/packages/ and create a new file - which can be called anything - but let's call it netgen_layouts.yaml. Inside, add netgen_layouts and, below that, pagelayout set to our base.html.twig:

netgen_layouts:
pagelayout: 'base.html.twig'

I'll explain this all in a minute. If we refresh now... huh, same error! It's possible Symfony didn't see my new config file... so let me clear the cache to be sure:

php ./bin/console cache:clear

And now... yes! It works! Except... it's still the same static page! But, for the first time, down on the web debug toolbar, it shows that the "Homepage Layout" is being used. So it realized the layout should be used... it just doesn't seem to be rendering it.

Rendering the layout Block

To fix that, we need to do one last thing... then we'll back up and explain what's going on and how cool it is. In base.html.twig, around {% block body %}, add {% block layout %}... then after {% endblock %}:

<!DOCTYPE html>
<html>
... lines 3 - 16
<body>
... lines 18 - 46
{% block layout %}
{% block body %}{% endblock %}
{% endblock %}
... lines 50 - 62
</body>
</html>

Refresh one more time. And... whoa! Our page is gone! Okay, we still have the nav and footer... which come from above and below the blocks in base.html.twig, but the actual contents of our page are gone and replaced by the dynamic title block! What Black Magic is this?

The Layouts Template Inheritance Magic

First, before I explain, let me say that there are much faster ways to start with Netgen Layouts: they have starter projects for normal Symfony apps, Sylius apps and Ibexa CMS apps. But we did all this set up work manually on purpose... because I really want you to understand how Layouts works: it's surprisingly simple.

First, our page is still hitting our normal route - app_homepage - and it's still executing our normal controller and still rendering our normal template. No magic there at all.

But then, we extend nglayouts.layoutTemplate. What does that point to? If there is no layout mapped to a particular page, nglayouts.layoutTemplate will resolve to base.html.twig. That's thanks to the config we added here:

netgen_layouts:
pagelayout: 'base.html.twig'

But if layouts does find a layout mapping for this page, then nglayouts.layoutTemplate resolves to a core Layouts template. In this case, if you hit Shift+Shift, it's called layout2.html.twig... since we selected "Layout 2".

This renders the dynamic layout via these nglayouts_render_zone tags: each of these refers to a different section - or "zone" - inside our layout.

Anyways, what's really important is that it renders the layout into a Twig block called layout. It then extends ngLayouts.pageLayoutTemplate, which resolves to our base.html.twig.

The end result is that our page renders completely normally and it still extends base.html.twig... but a block called layout has been added that holds the contents of the dynamic layout.

That's why we didn't see any changes on the page at first. Until we actually included {% block layout %} in base.html.twig, the layout was loading... we just weren't rendering it anywhere.

The takeaway is this: if you're on a page that does not map to a layout, everything is exactly the same as always. But if you are on a page that maps to a layout, it simply means that you now have a block called layout whose contents are equal to whatever you have inside of that layout.

Extending the Dynamic Layout on All Pages

So as I mentioned earlier, we don't have to add layouts to every page on our site: we could add it to the homepage and be done! But every page that we want to support layouts needs to extend nglayouts.layoutTemplate. The nice thing is, even if we extend this, nothing happens unless we actually map a layout to this page. So, there's no downside to using it everywhere. I'll quickly update login.html.twig to use it:

{% extends nglayouts.layoutTemplate %}
... lines 2 - 39

then list.html.twig and show.html.twig:

{% extends nglayouts.layoutTemplate %}
... lines 2 - 33

{% extends nglayouts.layoutTemplate %}
... lines 2 - 38

I can really move fast when I need to!

Back in the browser, the recipe list and recipe show pages still look the same... because no layout is resolved. But they're now ready to use layouts, if we want to.

Now, as interesting as it is to dynamically control the content on the homepage, we uh, kind of did too much! All of our old content is gone. Is it possible to mix dynamic content with some of the static content from our homepage Twig template? Absolutely. And that's a big part of what makes layouts special. That's next.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.7", // v3.7.0
        "doctrine/doctrine-bundle": "^2.7", // 2.7.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.13", // 2.13.3
        "easycorp/easyadmin-bundle": "^4.4", // v4.4.1
        "netgen/layouts-contentful": "^1.3", // 1.3.2
        "netgen/layouts-standard": "^1.3", // 1.3.1
        "pagerfanta/doctrine-orm-adapter": "^3.6",
        "sensio/framework-extra-bundle": "^6.2", // v6.2.8
        "stof/doctrine-extensions-bundle": "^1.7", // v1.7.0
        "symfony/console": "5.4.*", // v5.4.14
        "symfony/dotenv": "5.4.*", // v5.4.5
        "symfony/flex": "^1.17|^2", // v2.2.3
        "symfony/framework-bundle": "5.4.*", // v5.4.14
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/proxy-manager-bridge": "5.4.*", // v5.4.6
        "symfony/runtime": "5.4.*", // v5.4.11
        "symfony/security-bundle": "5.4.*", // v5.4.11
        "symfony/twig-bundle": "5.4.*", // v5.4.8
        "symfony/ux-live-component": "^2.x-dev", // 2.x-dev
        "symfony/ux-twig-component": "^2.x-dev", // 2.x-dev
        "symfony/validator": "5.4.*", // v5.4.14
        "symfony/webpack-encore-bundle": "^1.15", // v1.16.0
        "symfony/yaml": "5.4.*", // v5.4.14
        "twig/extra-bundle": "^2.12|^3.0", // v3.4.0
        "twig/twig": "^2.12|^3.0" // v3.4.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "symfony/debug-bundle": "5.4.*", // v5.4.11
        "symfony/maker-bundle": "^1.47", // v1.47.0
        "symfony/stopwatch": "5.4.*", // v5.4.13
        "symfony/web-profiler-bundle": "5.4.*", // v5.4.14
        "zenstruck/foundry": "^1.22" // v1.22.1
    }
}