Monday, 11 May 2015

PHP: inadvertently looking at a coupla refactoring tactics with Twig

G'day:
Here we have details of me unsuccessfully trying to replicate an issue we are having with one of our Twig files... it's not seeing some data being passed to it. However along the way I got myself a bit more up to speed with a coupla refactoring tactics one can use with Twig.

This article doesn't cover everything about Twig (I don't know everything about Twig, for one thing), it just covers the troubleshooting steps I took too try to replicate the issue I was having.

Spoiler: I did not solve the problem.
Spoiler spoiler: or did I?

Baseline

Twig is a templating system for PHP, provided by the same outfit - SensioLabs - that is responsible for Silex, which is the micro-framework we use. They're pretty much designed to work together, and the docs have a fair bit of crossover.

To get a controller to use a twig file to represent a view, we just call Twig's render() function, which takes the name of a Twig file, and an array of data to pass to it:

Extending a Twig

The next facet of the issue was that the code I was looking at wasn't in the Twig itself, we were calling one Twig file and the code in question was in the Twig the first one was extending.

This time we have two methods routed: /twigextends/master/ and /twigextends/detail/. I'll leave the routing (Application.php) and controller provision (TwigExtends.php) out, but they're linked there if you want to look at the relevant files. Here's the controller though:

{# detail.html.twig #}
{% extends "extends/master.html.twig" %}{% block detailBlock %}This is text in the detail view which will override the content of the block in the master view<br>{% endblock %}

What have we got here?

the master twig can define blocks,

and the detail twig extends the master twig.

Within the detail twig, we can override the extended twig's block by implementing a block with the same name.

In the master twig we output a variable value,

which is set both in master and detail twig.

Again, simple!

If we hit /twigextends/master/, we see the block contents as defined by the master, as well as the variable value as set in the master:

This is unblocked text in the master view

This is text within a block in the master viewThis was set in the Master controller

On the other hand if we browse to /twigextends/detail, we get this:

This is unblocked text in the master viewThis is text in the detail view which will override the content of the block in the master viewThis was set in the Detail controller

Notes:

the detail twig overrides the "detailBlock" in the master.

and the variable correctly uses the value specified by whichever controller was used.

Another thing to note is that an extending twig cannot have any content of its own, it can only override blocks from the twig its extending. If I had this:

{# detail.html.twig #}
{% extends "extends/master.html.twig" %}
{% block detailBlock %}
This is text in the detail view which will override the content of the block in the master view<br>
{% endblock %}
This is unblocked text in the detail view<br>

I just get an error:

This makes sense... where would Twig render content from the extending twig in the context of the extended one? Still: I got caught out by it when I was knocking-out the sample code for this article, so figured it worth mentioning.

Including a twig

Another tactic for abstracting view code is simply using an include. Here are the twigs:

Note here that the same one controller controls all the data (in this case hard-coded in line, but you get the idea). We've got a fair bit of this in our code base, but are erring away from it now. The issue is that there's not really any decent decoupling between the controller and the subviews, which makes for very busy controllers. And also code repetition if the included twig is used in more than one place.

There's a better solution: using a sub request.

Sub-requests

Another tactic one can employ is to encapsulate a chunk of business logic & view together and render a sub request. This is similar to an include except instead of calling in a file, one calls in a route, and accordingly this takes the usual approach of route ➞ controller (➞ model) ➞ view. So it better compartmentalises to controlling of the logic needed for the view. Code will probably demonstrate that easier than describing it.

As the routing is relevant when using a sub request, I'll include all that code as well this time:

In the controller we just have the usual bumpf of setting some variables. You can see that the sub request receives a GET variable mainMessage.

{# main.html.twig #}
This is static text in the main twig<br>
This is the message passed from the main controller: {{ mainMessage }}<br>
<hr>
{{
render(
path("route.subRequestSub",{"mainMessage":mainMessage})
)
}}

This is how we call a sub request. Use call path() - passing it the route binding value - which returns the internal URL to the bound route (so /subrequest/sub/ in this case), and then we render() that. We pass parameters to the sub request via the second argument of path(). Here we're passing the value from the main request into the sub request.

{# sub.html.twig #}
This is static text in the sub twig<br>
This is the message passed from the main controller: {{ mainMessage }}<br>
This is the message passed from the sub controller: {{ subMessage }}<br>

This just demonstrates we've got both values. The output of all this is unsurprising:

This is static text in the main twig
This is the message passed from the main controller: This was set in the doMain() method

This is static text in the sub twig
This is the message passed from the main controller: This was set in the doMain() method
This is the message passed from the sub controller: This was set in the doSub() method

Sit. rep.

OK, so those are the building blocks of what we're using in our codebase, and somewhere along the way something's going amiss. I'd refactored some code to get a reference to a constant out of a view (and back into the controller where it belonged), and as part of doing this refactored a view which was being included into being a sub-request instead. at this point, the sub-request's controller was not receiving the values passed to it in the path() call. However everything I'd done here in isolation worked fine, so I have not yet repro'ed the situation.

Deeper abstraction

My first question was whether some combination of include / extend / subrequests was undoing me. So I've repro'ed that too.

This time I've contrived a page which is build from the following components:

actual.html.twigextends a layout.html.twig (and that extends doc.html.twig), and overrides the layout's blocks

the content for the header block is provided by an included header.html.twig file

header.html.twig itself includes status.html.twig

and status.html.twig calls a sub-request to get user.twig.html

which displays the user initially set in the controller.

And this outputs:

So that's a reasonable level of abstraction, and... it all works. Frown. So I still cannae replicate my problem.

Complex objects

The only other thing we could think of is that in out actual code we were passing around complex objects, not just strings. Whilst this should not be a problem in theory, we'd have issues with it before. And we wondered given the sub-request process uses the routing and passes values as "get" values, perhaps only simple values (as per in a URL query string) might be allowed.

So I made the example more complex. Most of the code for this example is the same as above, but the controller and user view changes, so I'll show those.

(all the rest of the stuff on the page was the same, so I omitted it).

Bamboozlement

OK, so I've pretty much replicated the perceived problem, so I was at my wits' end as to what the issue was. This morning I revisited the live code to see if I could find any clues as to why my code wasn't working. I checked a coupla pages, and it seems they were working: the correct info was being passed in and rendered correctly.

I re-enquired as to why we though this whole thing wasn't working, and my colleague pointed me to a different page, and - lo - it was not working. I revisited the code, back-tracked through to where the data was being set...

PEBCAC

(Problem exists between chair and colleague). But the data wasn't being set. On that particular page (and other pages like it which "weren't working") we simply weren't passing all the necessary values, so my code was picking up the defaults. As per its intent. The code was always working. Well the bits of it which had been implemented. The bits that hadn't been implemented? No, those did not work. Never had. Unsurprisingly.

Result!

This just made me laugh. I should have checked more closely initially as to what the perceived problem was, and not ass-u-me`d quite so much. I should also have not automatically decided because I had been working on code that when something about things didn't seem right once we came to look at it, that the error must lie with my own work. Fair's fair: it usually does lie with my code. But not in this instance.

Why is this a "result!". Because as part of troubleshooting this I spent a lot of time reading up on Twig and how it comes together, plus I decided to write it up here, so I took even more care than usual. Plus my examples were a lot more rigorous than usual too. So that's quite convenient.

Search This Blog

About

I've been a web developer since 2001. I have spent 13yrs as a CFML developer, and until Sept 2014, that was the main subject matter here. I've now been re-tasked as a PHP developer, so learning PHP will become the focus of this blog. But I also mess around with other languages too.

I tend to be a bit "forthright" in my opinions, I am indelicate, and I tend to swear too much. This will come out occasionally here: I make no apology for it.

Everything said here is my own opinion. Feel free to disagree with me :-)