Jump to content

Template Twig Replace


marco
 Share

Recommended Posts

Hi folks,

as announced yesterday, I just published my new Template Twig Replace module on github.

The module lets you write Twig templates directly, making calls like $page->twig('my_template.twig') in an otherwise empty php template unnecessary.

It also seamlessly integrates with my other module Template Data Providers that can be found here and here.

Be sure to consult the README.md for further information.

Please leave your comments, opinions, and suggestions here in this forum or at github and I'll try to answer as quick as possible.

Honorable mentions go to porl and his Template Twig module for inspiring this little piece of code.

Regards,

Marco

  • Like 4
Link to comment
Share on other sites

  • 3 months later...

Hi,

I just released the new minor version 1.0.4 of the TemplateTwigReplace module.

The module now lets you access the Twig_Environment instance used for rendereing pages and chunks via a public getter. So now you may customize the twig environment (e.g. by adding custom filters) to better meet your needs.

Please take a look at the modified readme.

Regards,

Marco

  • Like 1
Link to comment
Share on other sites

  • 2 weeks later...

Marco, do you have any pointers on how best to use Twig as a tool within the normal PW templating system? I want to have some of my PW template files prepare data, specify a path to a Twig template file and then invoke Twig to insert the data and return the result. I don't want Twig to be the default way of rendering pages.

Thanks.

Edit: I guess my question is really whether your module can be used in that way or should I just make something much more basic.

Answer!  :) 

I answered my own question. Porl's TemplateTwig module turned out to be perfect for what I'm doing. I just made one small change to point it to a newer version of Twig than the one distributed with the module.

This is part of a larger MVC-ish scheme, part of which is just like Apesia's Template Delegate approach using PW's TemplateFile class. In templates/markup/layouts/ I have the TemplateFile PHP file ('whatever.php') which, depending on what needs to be done, can prepare variables and echo them out directly or pass them in an array to Twig which will look for a similarly named file ('whatever.php.twig') of twig markup. Just to make things easier, the system behind all this passes that 'twig' filename to the TemplateFile class file so all it has to do is prepare an array of data and then: echo $page->twig->render($this->twig, $arr);
 

  • Like 1
Link to comment
Share on other sites

  • 4 weeks later...

Hey SteveB, Hey Manaus,

as Marco can't answer currently I'll try to answer your questions as far as I can.

@SteveB the answer is: No. As you've mentioned you can't just use PHP in Template Twig Replace. It is designed to replace the whole rendering Process and expects the input to be twig compatible. You could use Twig filters to achieve PHP execution though.

The way we (Marco works at the same company I do) use Twig Replace & Data Providers is somewhat similar to the delegate approach. We use some basic "wrapper" Templates and extend them (using blocks) with template based code.

Here is an example we actually use in a blog page from an actual project:

Base Template (_base_template.twig in /templates) which is extended later on (every %block acts as a placeholder which can be filled with stuff from templates or chunks):

<!DOCTYPE html>
<!--[if lt IE 9 ]><html class="no-js lt-ie9" lang="de"><![endif]-->
<!--[if IE 9 ]><html class="no-js ie9" lang="de"><![endif]-->
<!--[if !(IE)]><!--><html class="no-js modern" lang="de"><!--<![endif]-->
<html lang="de" xmlns:fb="http://ogp.me/ns/fb#">
    <head>
        {% include 'includes/htmlhead.twig' %}
        {% block htmlhead %} {% endblock htmlhead %}
    </head>
    <body>
        <div class="wrapper">
            {% include 'includes/header.twig' %}
            {% block stage %}{% endblock stage %}
            <main class="main">
                {% block mainContent %}{% endblock mainContent %}
            </main>
            {% include 'includes/footer.twig' %}
        </div>
        {% include 'includes/javascript.twig' %}
        {% block beforebody %} {% endblock beforebody %}
    </body>
</html>

Blog Page (blog.twig in /templates) which displays an overview of the last articles:

{% extends '_base-template.twig' %}

{% block stage %}
    <div class="stage row">
        <img class="stage__background" src="{{ config.urls.root }}dummy-data/stage_default.jpg">
        <div class="stage__inner large-12 large-offset-2 columns">
            <h1 class="stage__headline stage__headline--default headline">xxx</h1>
        </div>
    </div>
{% endblock %}

{%  block mainContent %}
    <main class="article row" role="main">
        <div class="large-offset-2 large-10 columns">

            {% for article in articles %}
                {{ page.renderChunk('includes/blogpost_preview.twig',article,'blog') }}
            {% endfor %}

            {{ page.renderChunk('includes/pager.twig',articles) }}

        </div>
        {% include 'includes/sidebar.twig' %}
        

    </main>



{% endblock mainContent%}

The Coressponding "DataProvider"/Controller (BlogPage.php in /templates/dataproviders):

<?php

/**
 * Class definition of BlogPage
 *
 * @version 1.0.2
 * @copyright Copyright (c) 2013, neuwaerts GmbH
 * @filesource
 */

/**
 * Class BlogPage
 */
class BlogPage extends \nw\DataProviders\PageDataProvider {

    public function populate() {

        // load articles
        $today = date('Y-m-d');
        $this->articles = wire('pages')->find('template=blog_article, blog_articledate<=' . $today . ', sort=-date, limit=5');
        $pages = wire('pages');
        $this->categories = $pages->get('/blog/kategorien/')->children();
		$this->authors = $pages->get('/blog/autoren/')->children();
    }
}

And (just to make it complete and show some more twig stuff) here is the chunk for displaying the blog post previews that is included in the template. The chunk gets the article as a param and has it's own controller where you can place logic (i.e. to find related articles), too. 

<article class="article__preview clearfix">
	<div class="article__content">
		<header class="article__header clearfix">
			<h2 class="article__headline"><a href="{{article.url}}">{{ article.headline ?: article.title }}</a></h2>
			<div class="article__meta">
				{% for author in article.blog_author %}
					{% if loop.length > 1 %}
					<span class="meta__author"><img class="article__avatar" src="{{ author.blog_authorimage.size(20,20).url }}" alt="{{ author }}">{{ author.title }}</span>,
					{% endif %}
					{% if loop.last %}
						<span class="meta__author"><img class="article__avatar" src="{{ author.blog_authorimage.size(20,20).url }}" alt="{{ author }}">{{ author.title }}</span>
					{% endif %}
				{% endfor %}
					 | <time datetime="{{ article.blog_articledate|date('Y-d-m', config.timezone) }} ">{{ article.blog_articledate|date('d. F Y', config.timezone) }}</time>
			</div>
		</header>
		{% if includeTemplate == 'blog' %}
				{% if article.blog_keyvisualsize == 'small' %}
					{% set size = 280 %}
				{% else  %}
					{% set size = 755 %}
				{% endif %}
				{% if article.blog_articlekeyvisual %}<img src="{{article.blog_articlekeyvisual.width(size).url}}" alt="{{article.blog_articlekeyvisual.description}}" class="article__keyvisual {{article.blog_keyvisualsize}}">{% endif %}
				<div class="article__text">
					{% if article.body|length > 350 %}
						{{ article.body[:350] }}...
					{% else %}
							{{ article.body }}
					{% endif %}
				</div>
		{% endif %}
		<div class="article__category">
			{% for category in article.blog_category %}
				{% if loop.length > 1 %}
				{{ category.title }}, 
				{% endif %}
				{% if loop.last %}
						<a class="category__link" href="{{ config.urls.root }}blog/kategorien/{{category.name}}">{{ category.title }}</a>
				{% endif %}
			{% endfor %}
			<div class="article__read-more right">
				<a class="read-more" href="{{article.url}}">Artikel lesen</a>
			</div>
		</div>
	</div>
</article>

So we're basically splitting everything in includes (DRY), template specific stuff and Controllers. That's the way we believe is the "cleanest" and best maintainable. If you'd like to output some default stuff (i.e. headline and body) you're free to add it to the base template and just overwrite the block that they are in on demand.

@Manaus: You don't HAVE to rename everything into .twig - but you CAN. If you like to do this you'll have to set this explicitly in site/config.php: $config->templateExtension = 'twig';

Enclosing everything in a <?php is not required (in fact it will output this as plaintext: <?php).

  • Like 1
Link to comment
Share on other sites

  • 2 months later...

I'm sure this is a great module, I haven't tested it but thanks for your work.

I have two concerns with the module's approach though:

1.

The Twig templating engine should be stand alone/a seperate module, which is not autoloaded. The TemplateTwigReplace module then should load the Engine and work with it, this way other modules can use the Twig Engine by loading the stand alone module as well without running into any issues.

2. warning: extensive use of the word "logic" =)

I think the whole purpose of Twig is to sperate presentation from logic, so it should only be used for simple logic. Replacing the whole template system with Twig makes it necessary to write all logic (e.g. selecting data, merging data et al) in Twig templates which make them almost equally ugly as php templates with much logic in them.

I think a better way would be to use regular php templates as controllers, have them do some heavy logic lifting and then pass data to Twig views which should only be concerned with outputting/formatting strings and iterate through loops and so on.

PS: oh, and thanks, I was having problems with passed PW data to Twig so I looked at your code and discovered Page::$issetHas = true;. I then found out that Ryan implemented that feature after @porl had the same problems while coding his Twig module, so thanks to Ryan too =)

  • Like 1
Link to comment
Share on other sites

Hey owzim,

thanks for your input.

1.) I'm not sure about if it's a good idea to not include twig with the plugin. As pw is not using any dependency/package manager like composer there would always be the need of updating the twig pw-module when twig itself is updated.

2.) The Plugin was actually developed as an addition to "TemplateDataproviders" which adresses exactly the issues you've mentioned. Look at the source code from my last post: All the "heavy lifting " is done within the controllers.

Link to comment
Share on other sites

1.) I'm not sure about if it's a good idea to not include twig with the plugin. As pw is not using any dependency/package manager like composer there would always be the need of updating the twig pw-module when twig itself is updated.

Good point, have not thought of that. There should be an extra flag for the module info to specifiy the versions of the required modules. That would imply that modules should be provided in different versions, handled by ModulesManger perhaps. I think I might open an extra thread for that in the feature requests forum. Still, don't you see issues when another module uses Twig too? The only way to avoid issues would be to namespace the Twig classes. but thats extra work after each Twig update, right?

2.) The Plugin was actually developed as an addition to "TemplateDataproviders" which adresses exactly the issues you've mentioned. Look at the source code from my last post: All the "heavy lifting " is done within the controllers.

I saw that a couple of minutes after my post and wanted to edit it today, you beat me to it. I'll look into that, looks promising.

Link to comment
Share on other sites

  • 3 weeks later...
  • 1 month later...

Awesome module, much thanks for that. One question though, how would one accomplish the following in twig?

(I’m trying to exclude the current page from it’s array of siblings)

Vanilla PHP:

foreach($page->siblings('id!='.$page->id){

This is my twig code:

{%  for sibling in page.siblings('id!=page.id') %}

I understand that page.id will not be rendered as the page it’s id, since it’s a string etc. But what is the proper way to accomplish this? I’d need to do some string concatenation within the selector there. I know I can create a dataprovider to provide a proper wirearray with actual siblings, but it seems a bit overkill.

Thanks in advance!

Link to comment
Share on other sites

  • 4 months later...

Hi peterfoeng,

twig has another concept of extending templates with predefined code. It's called "extend" and is documented here.

For markup generation I do use extends. But in another case I needed more logic which does not generate markup and therefore I wanted to use  

$config->prependTemplateFile

and this does not work.

I want to redirect the user depending on his browser language setting "Accept-Language".

I didn't want to create an own module (beforeHook) containing only a few lines of code which have to differ from project to project.

I added some code in the module to make that work. Maybe this is not the best way to solve this issue. 

Any hints would be appreciated :)

Link to comment
Share on other sites

Hi Bea, 

thanks a lot. I did see that the rendering didn't support the 2.5 attach/prepend settings but didn't have the time to implement it.

I've merged your pull-request (and restored the indention to tabs ;)!!!). Furthermore I've added a new config setting that lets you exclude templates from beeing rendered with twig. This is needed if you're using FormBuilder (and i can think of some more use cases).

Link to comment
Share on other sites

tzz tabs   ;)

I really like the new config option, I build my own contact form because I could not implement Form Template Processor.  Could not get to run the following code using twig:

$form = $modules->get('FormTemplateProcessor'); 
$form->template = $templates->get('my_contact_form_template'); // required
$form->requiredFields = array('fullname', 'email');
$form->email = 'your@email.com'; // optional, sends form as email
$form->parent = $page; // optional, saves form as page
echo $form->render(); // draw form or process submitted form
Link to comment
Share on other sites

*shameless plug*
We're using TemplateDataProviders for exactly this kind of jobs (preparing data before it's rendered). Maybe you should try it out, too.
In your case I'd use output buffer and store it inside a variable to output it using twig later on.
 
dataproviders/ContactPage.php
<?php

class ContactPage extends \nw\DataProviders\PageDataProvider {

	public function populate() {
		$modules = wire('modules');
		$templates = wire('templates');

		$form = $modules->get('FormTemplateProcessor'); 
		$form->template = $templates->get('my_contact_form_template'); // required
		$form->requiredFields = array('fullname', 'email');
		$form->email = 'your@email.com'; // optional, sends form as email
		$form->parent = $page; // optional, saves form as page

		ob_start();
		echo $form->render(); // draw form or process submitted form
		$this->formMarkup = ob_get_contents();
		ob_end_clean();
	}

}

contact.twig

{{ formMarkup }}
 
 
Alternately you could add the Form Object to the templates namespace and render it directly using the method call within twig:
 
dataproviders/ContactPage.php
class ContactPage extends \nw\DataProviders\PageDataProvider {

	public function populate() {
		$modules = wire('modules');
		$templates = wire('templates');

		$form = $modules->get('FormTemplateProcessor'); 
		$form->template = $templates->get('my_contact_form_template'); // required
		$form->requiredFields = array('fullname', 'email');
		$form->email = 'your@email.com'; // optional, sends form as email
		$form->parent = $page; // optional, saves form as page

		$this->contactForm = $form;
	}

}
contact.twig
{{ contactForm.render() }}
Link to comment
Share on other sites

thanks, I will try this  ;)

Tried to update to version 1.0.7 and got the following error:

Notice: Undefined index: ignoredTemplates in /../site/modules/TemplateTwigReplace/TemplateTwigReplace.module on line 194

The problem is that I could not submit the settings and there is no entry for the new config optionignoredTemplates atm. The second error message occurs because the entry admin is not saved as well so I got the following message (it tries to render the admin template using twig):

paths->adminTemplates . 'controller.php');

I added a simple if-condition and everything works as expected.

Link to comment
Share on other sites

Why not write this:

ob_start();
echo $form->render(); // draw form or process submitted form
$this->formMarkup = ob_get_contents();
ob_end_clean();

Like this:

$this->formMarkup = $form->render();

?

Why generate output, create an output buffer, to then assign the buffer to a variable, if you can assign the form output directly to the var?

Link to comment
Share on other sites

 

Why not write this:

ob_start();
echo $form->render(); // draw form or process submitted form
$this->formMarkup = ob_get_contents();
ob_end_clean();

Like this:

$this->formMarkup = $form->render();

?

Why generate output, create an output buffer, to then assign the buffer to a variable, if you can assign the form output directly to the var?

 

Yeah - you're right. That's why I've added the second option.


thanks, I will try this  ;)

Tried to update to version 1.0.7 and got the following error:

Notice: Undefined index: ignoredTemplates in /../site/modules/TemplateTwigReplace/TemplateTwigReplace.module on line 194

The problem is that I could not submit the settings and there is no entry for the new config optionignoredTemplates atm. The second error message occurs because the entry admin is not saved as well so I got the following message (it tries to render the admin template using twig):

paths->adminTemplates . 'controller.php');

I added a simple if-condition and everything works as expected.

I'll look into this issue asap. It was working in my test setup. I must have missed something. 

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...