The future is bright

November 30, 2022

I usually don't post here about things not related to my own add-ons, but I feel like things have changed in ExpressionEngine recently for the better that I want to talk about.

Since Packet Tide has taken stewardship of ExpressionEngine there has been a steady stream of much needed improvements to ExpressionEngine's core. Just to highlight a few:

  • Improved CLI tools
  • Vertical Grid
  • Revamped control panel interface
  • Native entry cloning
  • Multi-Factor Authentication
  • Front-end Content Management
  • Structure, Pro Variables, and Pro Search made first party add-ons
  • New File Manager with support for subfolders and cloud storage (finally!)
  • Adoption of PSR-12 to the codebase
  • Conditional field display
  • Performance improvements
  • The list goes on...

That is a long list of a lot of new and well received updates.

One of ExpressionEngine's biggest strength, which is what arguably attracted a lof of designers back in the EE1 and EE2 days was it's template language. It was super simple to learn and you didn't need to know PHP. Compared to Wordpress' template soup, it was a revelation. Over time though, it started to became it's biggest weakness. "Template parse order" became a well known phrase in the community. As soon as you needed some complex template logic, the order in which you tried to call template tags cause tags to not work as expected, or simply not render any output at all. Unless you had a firm understanding of how it worked, it was often a brick wall for some people. Even for the most seasoned ExpressionEngine developers, the parse order could make a seemingly simple task an afternoon of work. Shortly after Packet Tide acquired ExpressionEngine they announced that Twig would potentially be supported, but the community didn't know exactly what that meant. Did that mean the existing template parser would go away and Twig would be a wholesale replacement? Or would the two work together hand in hand to allow developers to adopt and transition to Twig?

Fast forward to EE Conf 2022 in Philadelphia on October 6th. I was excited to go to the conference for 2 reasons:

  1. It would be the first time in 3 years I could see other members of the community due to Covid.
  2. Packet Tide was presenting, and their session title was "ExpressionEngine Decoupled"

Being a developer, the word "decoupled" makes me all warm and fuzzy inside. I knew this presentation would be special, and it exceeded my expectations. Packet Tide announced a new product called Coilpack, which is

a Composer package that brings Twig, Blade, GraphQL, REST, and much more to ExpressionEngine.

A few slides into the presentation I saw this:

{% set entry = exp.channel.entries.channel('about').status('Default Page').first() %}
{{ entry.title }}

After my initial shock wore off the gears in my head started turning. As an add-on developer that interfaces with the template language through PHP code my next natural reaction to the announcment was "Oh my, how much refactoring of my add-ons will I need to do?"

Fast forward again to November 22nd when I received an invite to the Coilpack alpha release. I spent the next few days over the Thanksgiving holiday break carving out some time to dig into Coilpack.

So what is Coilpack? Well, it's a Laravel app that basically boots ExpressionEngine into it. It allows you to add custom routing, but if no custom routes are defined, it just goes straight to ExpressionEngine and loads it as if Coilpack wasn't even there. Everything in ExpressionEngine just works.

Once Coilpack is checked out, in my case in the document root along side ExpressionEngine's system folder, admin.php, and index.php files my directory structure looks like this:

/.ddev
/coilpack
/system
/themes
/index.php
/admin.php

There is an .env file in the coilpack directory where you tell the Laraval app what your database connection is to ExpressionEngine. Since I'm using ddev to run my local environment I also had to make a simple change in the .ddev/config.yaml file. All I had to do was change the docroot value to point to the /coilpack/public directory instead of the root directory where ExpressionEngine's system folder is located. This tells Apache where to look for the bootstrap index.php file. As mentioned above, it's booting up Laravel, which is serving as a facade to ExpressionEngine.

This is what the change in the .ddev/config.yaml looks like. The previous value is just blank (ddev defaults to /var/www/html).

docroot: "coilpack/public"

After I got a basic Twig template rendering in the browser I turned my attention to Bloqs, which is probably the most complicated add-on I have in terms of template tags and rendering. I dove straight into the deep end of the pool. I figured if I could get Bloqs to render in Twig, then everything else would be a piece of cake. Luckily most of the hard work was already handled in Bloqs by it's data model. I have a class to specifically handle building the nested tree data set, a database adapter to fetch the data, and various other methods to turn database rows into a collection of Block and Atom objects in PHP. I knew the hard part was going to be turning roughly 800+ lines of PHP code that handles transforming this collection into something the ExpressionEngine template parser can output. Most of the logic doing this has to account for nested bloqs, as well as creating template variables such as {bloqs:shortname}, {bloqs:is:first_child}, etc.

Packet Tide did an amazing job decoupling the Twig and Blade template parsing from the ExpressionEngine template parser. The important thing to remember is that you don't have to use Twig if you don't want to. The existing template language is there, untouched, just as it's always been. Coilpack just adds a buffet of new options.

As an add-on developer there are a few of ways to prepare your add-on output for Twig or Blade. The first, and probably easiest is if you're already using the ee()->TMPL->parse_variables() method in your add-on tags, then you don't have to change anything. It'll just work in Twig. So if your mod.my_add.php methods end in something like this, there is a very high likelyhood you'll have little to no work to do to support Twig.

return ee()->TMPL->parse_variables(ee()->TMPL->tagdata, $varsArray);

If your module tags do not use parse_variables(), you can add this to the end of your module tag PHP methods and likely call it a day.

if(method_exists(ee()->TMPL, 'set_data')) {
    ee()->TMPL->set_data($varsArray); 
}

Lastly, if you have more complex rendering to do, you can tell Coilpack to use a specific class to handle the Twig rendering by adding this to your addon.setup.php file. The file name and namespace can be whatever you want. It'll just have to extend a Fieldtype class and implement an apply() function, which handles returning the data to Twig. This is what I opted to do for Bloqs due to the nature of it's complex data structure. This allows me to keep the old template parsing 100% separate from the new stuff.

'coilpack' => [
    'fieldtypes' => [
        'bloqs' => 'BoldMinded\Bloqs\Tags\Replace', 
    ]
],

A couple hours and roughly 120 lines of code later, I had a working Twig template outputting an entire Bloqs field. Remember earlier when I said it took about 800+ lines of PHP to handle the rendering of a Bloqs field for the ExpressionEngine template parser? Yeah, this was a lot less work. It's probably a lot more stable too because I don't have to worry about string parsing errors. I just have to return an object or array of data and Twig will handle the rest. The following is actual working Twig code rendering a Bloqs field.

{!-- template:twig --}
{% for entry in exp.channel.entries.channel('pages').entry_id('4').get() %}
    <h1>{{ entry.title }}</h1>
    {% for bloq in entry.bloqs %}
        {% for atom in bloq.getAtoms() %}
            {{ atom }}
        {% endfor %}
        {% for child in bloq.getChildren() %}
            {{ child.heading }}
            {{ child.sub_heading }}
        {% endfor %}
    {% endfor %}
{% else %}
    <span>No blog entries</span>
{% endfor %}

Why use Twig (or Blade)?

The ExpressionEngine template language may be a comfortable familiarity to some, and the thought of switching may seem scary, but I highly recommend giving Twig a try. Why exactly is it superior to the native ExpressionEngine template parser?

  1. "Parse order" will mostly be a thing of the past. Yes, there are also parse/rendering order issues in Twig, but since the Twig templates are compiled into PHP, it's a much more natural and expected behavior.
  2. {{ dump(my_variable) }} - With Twig, you're actually using objects and arrays in your templates, not just strings. If you're unsure what data is available to you, just use dump(). It'll output all the public properties that you have access to. Unfortunately it won't tell you what public methods are available by default (this can be achieved with a Twig extension). Speaking of public methods on an object... you can call methods directly in Twig, just like you do in PHP. The above example is calling getChildren(), which is a public method in Bloqs... if you're in the PHP code. There was no way to access that in a native ExpressionEngine template.
  3. Documentation. It's well documented and widely used. If you're unsure how to do something, just google it and you'll likely find an answer. As much as we love ExpressionEngine, this just isn't the case with it's proprietary template language.
  4. Twig is very common in the PHP world. Most PHP developers have encountered it before through other frameworks like Symfony and Yii, so it'll be familar ground to them. Onboarding new developers to an ExpressionEngine project will be much faster. Trust me, I've worked with a lot of PHP developers and watching them struggle to learn ExpressionEngine's template parser and it's idiosyncrasies is no fun.
  5. It's faster. Complicated ExpressionEngine templates can be slow. It's reading and parsing massive strings of text. A Twig template compiles into PHP code, so it's faster and less error prone.

What's Next?

After testing Bloqs I took a look at a few of Publisher's template tags. Three of them just worked with no change necessary because they used the parse_variables() method. Another one needed to call the set_data() method. Obviously I have more testing to do with all my add-ons, but based on my experience so far fully supporting Twig and Blade as ExpressionEngine templates should be pretty painless.

Coilpack also opens the door for more opportunities as well. For instance, Laravel comes with a fully baked queuing system that works with services like Amazon SQS. Importing thousands of entries with DataGrab can sometimes be a pain due to limited system resources. DataGrab was never built to handle importing 5,000 or 10,000+ entries. Once you get into that range you should really be using a publishing queue. Once my add-ons are ready for Twig I'll be looking into improving DataGrab's ability to handle massive datasets with Laravel's Queue.

Wrapping it up

All my initial concerns with Coilpack have been quelled. I was fully expecting to spend the next few months refactoring half of the code in my add-ons, but that just isn't going to be the case. Major props to the Packet Tide team for pulling this off.