Preprocessing Paragraphs: A Beginners Guide

Paragraphs is a powerful and popular contributed module for creating dynamic pages in Drupal 8. Preprocessing allows us easily to create and alter render arrays to generate tailored markup for any paragraphs type.

In this session you will learn about:

  • Getting started with preprocessing paragraphs and structuring your preprocessing methods.
  • Creating custom render arrays and overriding twig templates.
  • Referencing nested entities and pulling data into paragraph twig templates.
  • How to debug your preprocessing and twig files using contributed modules or composer packages without running out of memory.

You'll leave this session ready to preprocess paragraphs and have a plan of action to reference when debugging any issues. This session is perfect for site-builders or back/front-end devs that are new to preprocessing in Drupal 8 and Twig.

The slide deck for this presentation is now available.

Code samples for this presentation are on GitHub.


Larry Walangitan: Good afternoon everyone. This is Preprocessing Paragraphs, A Beginner's Guide. If you've trickled in, the slides are posted on the session notes so you can download and follow along there and there's a GitHub repo, a sample one in the slides as well. We'll pause on the link when we get to it in a few slides. A bit about me. My name is Larry Walangitan. I'm a senior developer at Chromatic. This is how you can find me on the web.

This is the dog that my wife and I just adopted, she was the partner and listened to me practice this a ton so just giving a shout out to her. I work for Chromatic, we're a fully distributed team. You can find us on the web at these two places and in real life at Booth 516. I'm going to pause here just for a second in case people want to check out the sample module that I have for preprocessing paragraphs.

Just to note that this is case sensitive so it's If you don't use the proper casing sometime-- In the last session that I gave this talk, it's like a view dependency so just be aware. I'm going to leave this up just for a few more seconds and we'll stop and start into what we're going to go over in our talk. The outcomes of this talk, I wanted to just start out and this is what I want you to leave this talk with.

First is that you would be able to preprocess your paragraphs and structure your preprocessing methods. This talk is quite opinionated in how I like to do things so take that with a grain of salt, but the steps that we're going to go through will apply for any situation we're using paragraphs. Also if you haven't worked with render arrays, this is going to be your chance to dive into custom render arrays and Twig templates.

Also a really a use case that everyone is familiar with, reference entities when you have fields that reference other nodes or taxonomy terms, pulling in that data into your Twig templates. Finally, maybe a very important one is when you're debugging, how do you do it without running out of memory or getting the white screen of death. That's what we're going to be going through today.

The prereqs for this is just a familiarity with Drupal 8, with Twig, with PHP, and the paragraphs module. For those that aren't super familiar, we're going to do a really quick overview. Paragraphs. What the paragraphs module gives us is an entity that has configurable fields. It can have different types. You can set up multiple different types of paragraphs, and it also uses view mode.

We can use view modes to control the way that our content is displayed. For the purposes of this talk, since we're talking about preprocessing, we want to have full control over the way that markup gets generated on the page for a paragraph. View modes are cool but we're going to gloss over that today. This is from, just a screenshot of what it looks like if you're not familiar.

On this there are several different paragraphs. First one is a text paragraph, it has a couple of fields, below that is a quote paragraph, and finally below that is a slideshow. As you can see in the editor you can select different paragraph types. You can drag them around the page. Editors have full control over creating these dynamic experiences as you set up your content type.

Here's a visual representation of what each paragraph can be thought of. On your left, my right, is what it looks like on the web page, and it's dissected on the other side of what each component that builds that web page would look like when you're making your content. A really strong use case for using paragraphs that I found professionally is the component library.

If you're not familiar with component libraries, a component library breaks up different parts that make up a web design into smaller isolated chunks. The goal is to encapsulate each components, markups, styles, the behavior so the HTML, CSS, and JavaScript, so that they can easily be mixed and matched across a variety of templates and contexts. This makes it easier to maintain a consistency and maintainability in a design system.

You can take a component from the library, structure it, and pair it with a paragraph, and preprocess that paragraph to reflect that markup exactly. Let's take a look at a few examples of what this looks like. Here we have a component that a front-end developer has created in our component library. At the bottom, you can see the markup that we need and what it's going to look like when it's rendered on the page.

When we're creating a paragraph, we can mimic this exactly and just pull in and create fields to create this sort of component on any page that we have. For a blockquote, we would need a field for a quote, one for citation source, and one for an attribution. Here's another example for an audio paragraph. A couple fields that we would need in our paragraph is just a link to the audio source, a label and description, and we could plug in the values from our fields in our paragraph into the markup and render this on the page.

This is the goal that we're aiming towards in this talk, but first, let's go back and talk about what preprocessing is. The first and main use case I see for preprocessing is data transformation. You have something that an editor or a user of your site is creating a node, they're creating content, and you want to do something with it, you don't want to just display on the page as is. Preprocessing gives us the ability to do that.

Another component is the separation of concerns. The logic can live outside of the template. With Twig templates were given a lot of different methods in the template that would allow us to do some transformations or do some filtering, some really cool things. This is great but if you have a developer that's coming on board that isn't super familiar with Twig but is familiar with PHP, keeping these sort of the display and the logic, separate is super helpful.

Finally customization. This is what is the main piece of preprocessing is the custom markup, the render arrays that we're creating for each paragraph type. We have a preprocessing recipe. Think of it this way, with every single paragraph type we're going to apply the same steps to preprocess them. First, a couple of things we need to do is make sure we download and install the paragraphs module, configure a new paragraph, and then create a custom module.

The sample code base that I have on GitHub, you can take that and take a look at it. Please do not install it, it is not something for that, it's just totally for learning purposes. Our preprocessing recipe, there are four steps. Each paragraph type is defined in the hook theme of our module. Second, all preprocessing is going to be handled by class. If you're not familiar with object-oriented programming, you can just think of a class as a blueprint.

Step three, each paragraph type has a corresponding method. Inside of our class, we have functions that will preprocess each one of the paragraph types that we have set up in our configuration. Finally, each paragraph type has a corresponding Twig template, where our preprocessing and our structured markup all comes together and wraps it all up. Those are the four steps that you have to keep in mind throughout this and let's start with step one.

In hook_theme, now this is in our module. In the custom module that we created, there are three different array keys that we need to be aware of. First is the base hook. Second, our variables that we're going to be creating, and finally the path to our template. Inside of our hook_theme function, for our paragraph foo, which is the machine name like an example machine name. We're calling our base hook, we have our variables, and we have our path.

Let's define what this base hook is because in my own journey of working with paragraphs, this was probably the critical component of unlocking preprocessing. Base hooks, they're defined in another module's theme. Because paragraphs module is a contributed module, it has a base hook that we can use an extend. We're going to call base hook paragraph in our hook theme, which will allow us to override and extend that base hook with our own custom variables and assign its own custom template.

Variable is our next one, and that is, we're just defining any custom variables that we need to coincide with either fields that we've defined in our paragraph or pieces on the page from our component library that we mentioned earlier. Finally, path is where a paragraph's custom template lives. The way I have it set up is just reflective of what is set up in the sample repo on GitHub. If you're not familiar with what's happening here, we're just concatenating path, which is available in hook_theme with some custom strings so that we know exactly where this template lives.

Our preprocessing class. We've set it up here in the src/paragraphPreprocessor.php. Now, this class is defined this way and it's pretty opinionated so that if someone's coming into your project and you're working with another developer and they want to know where the preprocessing is happening for a paragraph, this is where I put everything. I recommend this setup where there's one class that handles all of the methods that does the preprocessing for paragraph types specifically.

Now, our preprocess method in our example module, we have preprocessFoo. All it's doing is retrieving data and returning variables by reference for the render array that we defined in our hook_theme. At its most basic level, the preprocessing methods that we have are preparing variables for our templates. This is an example of what a preprocessFoo method would look like.

As you can see, I've defined content here since that's the main array value that we are most interested in. Inside of there, we can dig deeper to get field names using machine names and go even further to pull in markup and text keys, as we've done here. In the comments, is what it would look like when we output it to our paragraph template. Our paragraph template structure follows the machine names so paragraph--foo.html.twig. Inside of that, you can see that we have defined a couple of divs and some headers.

The steps that we've gone through, this is just one very, very basic example of a paragraph that we've created. Now, this next thing that I'm going to go over, maybe a bit more advanced, but we're going to optimize the workflow that we have right here, so it's a bit easier to maintain in the future. We are going to do that by creating a service. In our custom module, we have a services.yml file and we're going to create a chromatic_paragraphs.paragraphs_preprocess service, which is going to call the class that we have created to handle the preprocessing of our paragraphs.

Now, we've done this so that it's a bit easier to use as a developer. It's a bit more structured and also it gives us the ability to run unit tests in the future. In our services.yml file, this is how it would be set up. Now, if you're thinking where is-- there's a missing piece in all of this. How does this all link up? It is in our module file. We need to call the actual hook to invoke our methods.

We have a template_preprocess_paragraph_Foo inside of our module file. You might see this and think usually it's hook_preprocess sort of thing. Here we're calling template preprocess, we're doing this specifically because template preprocess prepares variables for us that are available in the Drupal 8 API and we want to return the render array of variables that we've created in the hook_theme. We're going to call our new service inside of this function, inside of a module.

A bit of things here at the top, we are defining a constant for our service. We're going to call our customer service inside of template_preprocess_paragraph_Foo. Inside of that, we're going to prepare our variables for our Twig template in the method that's on our class. That's how everything connects from creating our class to our methods and calling this template function allows us to access those methods and generate the markup that we want in our Twig template.

Now, really, quite a few use cases that are unique for nested entities. Essentially, we're just retrieving data from entity references and we always follow these two steps. First, is to load in the reference entity, whether it's a node or a taxonomy term or a user, and then just return the values on that note. It's pretty simple. Let's take a look. I apologize, the text here is a little bit small.

Here we're first just checking to make sure that the field that we're referencing is populated, that it has actual data on it so that we can dig deeper into that data to get a value. Here I'm just checking for a reference node. If it exists, I'm going to pull in the header field and assign it to the header variable that we have set up.

Putting it all together, let's go through our steps that we've done. First, each paragraph type, we've defined each one of them in our hook_theme. Step two, all preprocessing is handled by a class. This step in step two, you only have to set it up just once. Step three, for every single paragraph type that we have, we have a corresponding method. Finally, each paragraph type has a corresponding Twig template.

I want to clarify that you can create paragraphs and exclude them from your custom preprocessing, if you don't need custom preprocessing, you don't have to do it. This is specifically if you want to preprocess your paragraphs.

When it all goes wrong, what do you do when things aren't working correctly? You run into issues with memory and you're using Kint and things don't work. I have two tools that have served me very well. One of the tools is for the front end inside of your actual templates, the Twig VarDumper module. It's a great tool. The second one is the Symfony Component VarDumper. Both of these are added in the same way with the composer to your projects. I found this a much better performance than Kint in the past.

These are just two personal tools that I enjoy using when I'm developing my paragraphs. Another trick, this is from one of the Drupal Slacks from Morton. If you are constantly running out of memory, if you paste this piece of code into your Twig template or any sort of template, you'll go two levels deep into the render array and see what keys and values are available for you so that there's less loaded into memory and you can debug a bit easier if you have a white screen of death. This is more of a last-ditch option if the other ones don't work.

You're probably thinking, why didn't we do any of this in the theme? Why did we have to create a custom module? This comes from personal experience. When we have a shared codebase that we want to share paragraphs across all of the sites, but we want the branding and the output to be different. We're preprocessing things, but we have different themes to manage the actual theming layer.

The steps that we would do if we wanted to just do this on a theme on a single layer are going to be very similar with how we've done it in the paragraphs for our custom module. Let's take a look at what this looks like for a theme. Step one, all preprocessing is going to be handled in our .theme file. Step two is going to be instead of defining a class, we're just going to have hook_theme. We're going to make sure that each paragraph type is defined in hook_theme.

In step three, we are going to be making sure that there's a template override function that corresponds to each paragraph type that we have set up. Then finally, each paragraph type is going to have a corresponding Twig template. That's a pretty quick overview and a rundown of how we would do preprocessing from both of the custom module and from a theme. I've set aside some time for some questions and answers. Real quick, the contributions are Friday, so be sure to check those out and be sure to leave some survey results. We have some time for questions and answers, if anyone has some. Thanks.


Speaker: Thanks. Is there any performance advantage to doing this rather than keeping all the logic out of the Twig template and staying out of the render arrays and the Twig template?

Larry Walangitan: That's a good question. The performance advantages of these, I would say, it's pretty nominal. I feel like it's more of a preference, like the performance would be the development velocity versus time-on-page and that sort of thing. I haven't done those sort of metrics, so that's a really good question. Maybe we can connect and I can go back.

Speaker: The main [unintelligible 00:20:15] in that is this is more of the best practice sort of idea?

Larry Walangitan: Yes.

Speaker: You built a module that is basically theming paragraph types. How do you tie together-- you have those fields that you created in those paragraphs. Do you feature out those paragraph types with the fields? How do you tie the module to the structure itself?

Larry Walangitan: This module is very barebones. Usually, there'd be some config associated with it either in features or in config sync to tie everything together. There are some missing components that we glossed over and assumed, but we follow the same practices that we do with a node or any other identity type to handle that sort of management.

Speaker: Thanks.

Larry Walangitan: Thanks. [pause 00:21:11]

Speaker: Hey.

Larry Walangitan: Hey.

Speaker: A quick question. Basically, in your philosophy then if you're building multiple projects for different clients with different front-ends, using a module would be the best way to go. How about if you had a consistent front-end that you'd been using, for example. In other words, if you used the same theme for multiple projects, would there an advantage in that situation?

Larry Walangitan: That's a great question. Just to clarify, so you have the same theme apply to different-

Speaker: Let's say you work for an organization and you built a theme or multiple themes that are consistent throughout different groups, you may have different sites, but they always use the same front-end versus having multiple paragraphs everywhere with different themes. I mean, with different patterns in the front-end. Maybe they'll be using your own custom theme versus Bootstrap or AT Vim or something like that.

Larry Walangitan: I think with that it ultimately will boil down to the preference of you and your developers on where you want your customizations to live. My own personal preference is mostly if we can have a custom module to handle any sort of transformation of data, we will put it there and keep our themes pretty lean in the preprocessing side.

Speaker: Got you. You basically abstract it a little bit to make it simpler for if you decide to pivot on something, then you don't have to worry about moving the preprocess yet to another theme or something else you just worry about your patterns.

Larry Walangitan: Yes, it becomes a bit easier. The impetus for this talk came from working with people that weren't familiar with Drupal, and they were like, "What is the .theme? How does this all work?" Abstracting in a way, felt like an easier way for other developers to join the project and start contributing quicker. It ultimately boils down to what you and your organization is most familiar with and comfortable with.

Speaker: It makes sense why you were doing it if that was the reason.

Larry Walangitan: Sure. Thanks.

Speaker: Thanks.

Larry Walangitan: Thanks. Great. If there's no other questions or answers, you'll find me around. Thanks, everyone. We're going to get a bit early.


[00:23:55] [END OF AUDIO]