Jump to content
marco

Template Twig Replace

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

Share this post


Link to post
Share on other sites

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

Share this post


Link to post
Share on other sites

Just updated the Twig library that is shipped with the module to the current release v1.15.0.

  • Like 1

Share this post


Link to post
Share on other sites

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

Share this post


Link to post
Share on other sites

Sorry, dumb/newbie question,

I can't find how to use this module, should I rename all the templates to .twig and put the logic (selectors, expressions) between <?php ?> tags?

Thanks in advance

Share this post


Link to post
Share on other sites

FYI, Twig syntax has support for conditional expressions, loops, etc. Look for clues in the TemplateTwig README.md file which also mentions using the Template Data Providers module to prepare data for the template.

Share this post


Link to post
Share on other sites

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

Share this post


Link to post
Share on other sites

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

Share this post


Link to post
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.

Share this post


Link to post
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.

Share this post


Link to post
Share on other sites

I am wondering, if the prependTemplateFile is supported here?

I try to play around with this module over the weekend but no luck. It seems that it does not recognize the prependTemplateFile.

Share this post


Link to post
Share on other sites

Hi peterfoeng,

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

Share this post


Link to post
Share on other sites

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!

Share this post


Link to post
Share on other sites

Ok, this works:

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

Share this post


Link to post
Share on other sites

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 :)

Share this post


Link to post
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).

Share this post


Link to post
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

Share this post


Link to post
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() }}

Share this post


Link to post
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.

Share this post


Link to post
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?

Share this post


Link to post
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. 

Share this post


Link to post
Share on other sites

I've just pushed a bugfix to github. The $ignoredTemplates variable wasn't populated when updating the module.

Share this post


Link to post
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

  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By MoritzLost
      This is a new module that provides a simple solution to clearing all your cache layers at once, and an extensible interface to perform various cache-related actions.
      The simple motivation behind this module was that I was tired of manually clearing caches in several places after deploying a change on a live site. The basic purpose of this module is a simple Clear all caches link in the Setup menu which clears out all caches, no matter where they hide. You can customize what exactly the module does through it's configuration menu:
      Expire or delete all cache entries in the database, or selectively clear caches by namespace ($cache API) Clear the the template render cache. Clear out specific folders inside your site's cache directory (/site/assets/cache) Refresh version strings for static assets to bust client-side browser caches (this requires some setup, see the full documentation for details). This is the basic function of the module. However, you can also add different cache management action through the API and execute them through the module's interface. For this advanced usage, the module provides:
      An interface to see all available cache actions and execute them. A system log and logging output on the module page to see verify what the module is doing. A CacheControlTools class with utility functions to clear out different caches. An API to add cache actions, execute them programmatically and even modify the default action. Permission management, allowing you granular control over which user roles can execute which actions. The complete documentation can be found in the module's README.
      Beta release
      Note that I consider this a Beta release. Since the module is relatively aggressive in deleting some caches, I would advise you to install in on a test environment before using it on a live site.
      Let me know if you're getting any errors, have trouble using the module or if you have suggestions for improvement!
      In particular, can someone let me know if this module causes any problems with the ProCache module? I don't own or use it, so I can't check. As far as I can tell, ProCache uses a folder inside the cache directory to cache static pages, so my module should be able to clear the ProCache site cache as well, I'd appreciate it if someone can test that for me.
      Future plans
      If there is some interest in this, I plan to expand this to a more general cache management solution. I particular, I would like to add additional cache actions. Some ideas that came to mind:
      Warming up the template render cache for publicly accessible pages. Removing all active user sessions. Let me know if you have more suggestions!
      Links
      https://github.com/MoritzLost/ProcessCacheControl ProcessCacheControl in the Module directory

    • By joshua
      This module is (yet another) way for implementing a cookie management solution.
      Of course there are several other possibilities:
      - https://processwire.com/talk/topic/22920-klaro-cookie-consent-manager/
      - https://github.com/webmanufaktur/CookieManagementBanner
      - https://github.com/johannesdachsel/cookiemonster
      - https://www.oiljs.org/
      - ... and so on ...
      In this module you can configure which kind of cookie categories you want to manage:

      You can also enable the support for respecting the Do-Not-Track (DNT) header to don't annoy users, who already decided for all their browsing experience.
      Currently there are four possible cookie groups:
      - Necessary (always enabled)
      - Statistics
      - Marketing
      - External Media
      All groups can be renamed, so feel free to use other cookie group names. I just haven't found a way to implement a "repeater like" field as configurable module field ...
      When you want to load specific scripts ( like Google Analytics, Google Maps, ...) only after the user's content to this specific category of cookies, just use the following script syntax:
      <script type="optin" data-type="text/javascript" data-category="statistics" data-src="/path/to/your/statistic/script.js"></script> <script type="optin" data-type="text/javascript" data-category="marketing" data-src="/path/to/your/mareketing/script.js"></script> <script type="optin" data-type="text/javascript" data-category="external_media" data-src="/path/to/your/external-media/script.js"></script> <script type="optin" data-type="text/javascript" data-category="marketing">console.log("Inline scripts are also working!");</script> The type has to be "optin" to get recognized by PrivacyWire, the data-attributes are giving hints, how the script shall be loaded, if the data-category is within the cookie consents of the user. These scripts are loaded asynchronously after the user made the decision.
      If you want to give the users the possibility to change their consent, you can use the following Textformatter:
      [[privacywire-choose-cookies]] It's planned to add also other Textformatters to opt-out of specific cookie groups or delete the whole consent cookie.
      You can also add a custom link to output the banner again with a link / button with following class:
      <a href="#" class="privacywire-show-options">Show Cookie Options</a> <button class="privacywire-show-options">Show Cookie Options</button> This module is still in development, but we already use it on several production websites.
      You find it here: https://github.com/blaueQuelle/privacywire/tree/master
      Download: https://github.com/blaueQuelle/privacywire/archive/master.zip
      I would love to hear your feedback 🙂
      Edit: Updated URLs to master tree of git repo
       
    • By David Karich
      Admin Page Tree Multiple Sorting
      ClassName: ProcessPageListMultipleSorting
      Extend the ordinary sort of children of a template in the admin page tree with multiple properties. For each template, you can define your own rule. Write each template (template-name) in a row, followed by a colon and then the additional field names for sorting.
      Example: All children of the template "blog" to be sorted in descending order according to the date of creation, then descending by modification date, and then by title. Type:
      blog: -created, -modified, title  Installation
      Copy the files for this module to /site/modules/ProcessPageListMultipleSorting/ In admin: Modules > Check for new modules. Install Module "Admin Page Tree Multible Sorting". Alternative in ProcessWire 2.4+
      Login to ProcessWire backend and go to Modules Click tab "New" and enter Module Class Name: "ProcessPageListMultipleSorting" Click "Download and Install"   Compatibility   I have currently tested the module only under PW 2.6+, but think that it works on older versions too. Maybe someone can give a feedback.     Download   PW-Repo: http://modules.processwire.com/modules/process-page-list-multiple-sorting/ GitHub: https://github.com/FlipZoomMedia/Processwire-ProcessPageListMultipleSorting     I hope someone can use the module. Have fun and best regards, David
    • By dimitrios
      Hello,
      this module can publish content of a Processwire page on a Facebook page, triggered by saving the Processwire page.
      To set it up, configure the module with a Facebook app ID, secret and a Page ID. Following is additional configuration on Facebook for developers:
      Minimum Required Facebook App configuration:
      on Settings -> Basics, provide the App Domains, provide the Site URL, on Settings -> Advanced, set the API version (has been tested up to v3.3), add Product: Facebook Login, on Facebook Login -> Settings, set Client OAuth Login: Yes, set Web OAuth Login: Yes, set Enforce HTTPS: Yes, add "https://www.example.com/processwire/page/" to field Valid OAuth Redirect URIs. This module is configurable as follows:
      Templates: posts can take place only for pages with the defined templates. On/Off switch: specify a checkbox field that will not allow the post if checked. Specify a message and/or an image for the post.
      Usage
      edit the desired PW page and save; it will post right after the initial Facebook log in and permission granting. After that, an access token is kept.
       
      Download
      PW module directory: http://modules.processwire.com/modules/auto-fb-post/ Github: https://github.com/kastrind/AutoFbPost   Note: Facebook SDK for PHP is utilized.


×
×
  • Create New...