A Day in a Pile of Work

My personal Web development blog

Implementing Kohana Fragment as a Twig extension

I like the Kohana framework, but it’s really missing a decent View engine. Sure, PHP-written views are nicely implemented, but that does not just get the job done on sufficiently big project.

The moment I have discovered Twig, everything just became easy and efficient! I was in fact looking for an equivalent templating engine of jinja2: Twig has the exact same syntax.

Twig allows you to write views as simply as:

1
2
3
4
5
6
7
8
9
{% extends 'home' %}

{% block header %}

    {{ parent() }}

    Specific header content

{% endblock %}

The only ability to declare hierarchical view would have been enough for me. But it has more: filters, embedding, auto-escaping and so on…

Kohana has a very nice extension for Twig written by tommcdo. The extension is great because it simply extends the View class. It is completely transparent to the original view engine.

I did some changes like passing View global variables and supporting Twig tests.

A test in Twig is a statement using the is keyword. You may check if a value is null just by writting:

1
2
3
{% if var is null %}
    {{ var }}
{% endif %}

Although, I couldn’t find a decent caching solution that would bind to Kohana nicely. I did a little of research and I dig to find out I could just extend the Twig framework to implement new tag. Why not a fragment tag?

First thing I did was to enable custom Twig Extension loading that I would just write down in the Kohana cascading file system.

I started by adding an extensions field in the module configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

defined('SYSPATH') or die('No direct script access.');

return array(

    /**
     * Twig Extensions
     */
    'extensions' => array(
        new Twig_Extension_Fragment()
    )
);

Here, the fragment tag will be parsed until a endfragment tag is detected.

{% fragment "name" 15 true %}

    {# code here is cached for 15 seconds with the "name" key #}

{% endfragment %}

Twig extension can define new TokenParser. A token parser is pretty much a class that is instantiated and called whenever a tag it parses is found in the document.

The nodes within the fragment tag are captured by subparsing until the endfragment tag. Twig will delegate that work to appropriate token parser.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php

defined('SYSPATH') or die('No direct script access.');

/**
 * Parser for fragment/endfragment blocks.
 *
 * @package Twig
 * @author  Guillaume Poirier-Morency <guillaumepoiriermorency@gmail.com>
 * @license BSD-3-Clauses
 */
class Twig_TokenParser_Fragment extends Twig_TokenParser {

    /**
     * @param \Twig_Token $token
     *
     * @return boolean
     */
    public function decideFragmentEnd(Twig_Token $token) {

        return $token->test('endfragment');
    }

    public function getTag() {

        return 'fragment';
    }

    public function parse(Twig_Token $token) {

        $stream = $this->parser->getStream();

        $name = $this->parser->getExpressionParser()->parseExpression();
        $lifetime = $this->parser->getExpressionParser()->parseExpression();
        $i18n = $this->parser->getExpressionParser()->parseExpression();

        $stream->expect(Twig_Token::BLOCK_END_TYPE);
        $body = $this->parser->subparse(array($this, 'decideFragmentEnd'), true);
        $stream->expect(Twig_Token::BLOCK_END_TYPE);

        return new Twig_Node_Fragment($name, $lifetime, $i18n, $body, $token->getLine(), $this->getTag());
    }

}

The next step is to implement compiler directives for the node. These end in the Twig_Node_Fragment class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?php

defined('SYSPATH') or die('No direct script access.');

/**
 * Cache twig node.
 *
 * @package Twig
 * @author  Guillaume Poirier-Morency <guillaumepoiriermorency@gmail.com>
 * @license BSD-3-Clauses
 */
class Twig_Node_Fragment extends Twig_Node {

    /**
     *
     * @param type $name
     * @param type $lifetime
     * @param type $i18n
     * @param \Twig_NodeInterface $body
     * @param type $lineno
     * @param type $tag
     */
    public function __construct(Twig_Node_Expression $name, Twig_Node_Expression $lifetime, Twig_Node_Expression $i18n, Twig_NodeInterface $body, $lineno, $tag = null) {

        parent::__construct(array('body' => $body, 'name' => $name, 'lifetime' => $lifetime, 'i18n' => $i18n), array(), $lineno, $tag);
    }

    public function compile(Twig_Compiler $compiler) {

        $compiler
                ->addDebugInfo($this)
                ->write("if ( ! Fragment::load(")
                ->subcompile($this->getNode('name'))
                ->write(', ')
                ->subcompile($this->getNode('lifetime'))
                ->write(', ')
                ->subcompile($this->getNode('i18n'))
                ->write(')) {')
                ->indent();

        $compiler
                ->subcompile($this->getNode('body'))
                ->write('Fragment::save();')
                ->outdent()
                ->write('}');
    }

}

As you can see, the node is designed to generate PHP code. Twig does not interpret but rather compile down to PHP.

So far, I only need to find out how optional arguments for tag are implemented. It is not really convenient to specify every argument of Fragment::load every time it is used.

Fragment is a great tool to optimize region in a view and is often a convenient way to speedup a web application.

Posted on and tagged with Kohana and Twig.