Search code examples
phptemplatesmodel-view-controllertwigtemplate-engine

How to render Twig / PHP / HTML template blocks as an arbitrary array of exploded subsections?


I've been beating my head against this for a while, because while it's "easy" to do in raw PHP, the result is not extendable due to PHP's lack of decent built-in templating syntax. So I installed Twig assuming it would have a built-in explode function for iterating amongst blocks. But there seems to be no way of dividing content into an arbitrary number of subsections.

I considered using {% embed %}, but the document is to be styled according to which sections appear in the parent template. And in what order (which is variable; this is for a form with a lot of business logic in it.)

I've built the form in PHP and gotten it to work as a "very pretty" static page with easily an arbitrary number of subsections, all working and interactive regardless of which are displayed (based on e.g. user privileges), but templatizing it is the challenge.

The static (twigless) version relies on me exporting the parsed content to an array which can then be wrapped with the appropriately-styled div's for each visible section. This works, but requires me to use included html and object buffering which looks awful and would not be easy to maintain:

$content = include("form_content_html.php"); // return an object-buffered array
// I was including the escaped values here using if isset($data) and echo short tags.

foreach ($content as $section) { /* do something; */ }

$template = include("form_template_subsection.php");

$formview->addSubsectionTemplate($template);

echo $formview->addSubsection($content,$i++,$type); 
// fill section of $type with $content[$i] if isset

echo $formview->addSubsection($content,$i++,$sometype);
// $types have different css class for certain effects

// etc. not the best approach.

(I need to escape all user / db content before binding it to the form values, but since the form fields are highly customized I separated them into their own content layer so that's the only layer that has content that needs to be escaped at present.)

So forget that, now I'm using Twig. (note: not Symfony2, as the project is on a shared webhost.)

I don't think there's a way of {%extend%}ing the parent template such that some of the child template blocks are "dropped into" the named parent template containers, with the rest ignored; which is what I'm looking for since that way I can put all the logic into the top level (which sections of the form to make visible, etc.) and pass in the values first.

Note that the style of the form sections is dictated by the structure of the parent template, not by the child content. E.g. section 1 may display content 1 and call css on content 2 in section 2; or section 1 and 2 with the same css may display different content.

If I split up the form into 15 content-only subtemplates, and then conditionally included them in the parent file, it would work. But that would seem to defeat the purpose of using a template engine? Although I suppose Twig makes it much easier to work with included files containing html snippets in this way, so let me know if that is a preferred solution.

Should I divide the content node up into numbered {% block1 %}, {% block2 %} etc. subsections and then renderBlock in my PHP view class?

If Twig had a {% section %}...{% section %} syntax allowing you to split up the template into chunks and then parse each chunk as an array of block elements... well, I half-expected that, and wish I could add one myself.


Solution

  • I've found the solution, I think:

    (From the Twig documentation)

    The set tag can also be used to 'capture' chunks of text:

    {% set foo %}
        <div id="pagination">
        ...
        </div> 
    {% endset %}
    

    Ergo, set can be used to iterate over anonymous consecutive blocks:

    {% set i=0, out=[] %}{# declare top scope #}
    
    {% block initialize_content %}{# use once #}
        {% set i=0, out=[] %}{# prevent dupes #}
        {% set foo %}
    
            <div id="pagination">...</div> 
    
        {% endset %}{% set out=out|merge({ i: foo}) %}{% set i=i+1 %}{% set foo %}
    
            {{ escape_variables_here }} ... more html
    
        {% endset %}{% set out=out|merge({ i: foo}) %}{% set i=i+1 %}{% set foo %}
    
            {{ variables_all_scoped_to_same_context }} ... more html
    
            {# so dividers can be moved "up and down" if necessary ... #}    
    
        {% endset %}{% set out=out|merge({ i: foo}) %}{% set i=i+1 %}{% set foo %}
    
            {# ... like this. ^^a tag/macro could probably define this #}
    
        {% endset %}{% set out=out|merge({ i: foo}) %}
        {# or loop over i = 0..n or i = loop.index0 #}
    
    {% endblock initialize_content %} {# end setter #}
    {% block content %}
    
        {% if key is defined and out.key is defined %}{{ out.key |raw }}{% endif %}
    
        {# output top-scoped var (outputs nothing if setter not called) #}
        {# the setter doesn't have to be in its own block, but that allows
        the variables in the content to be redefined in setter's scope. #}
    
    {% endblock %}
    

    (cf. Setting element of array from Twig )