]>
Commit | Line | Data |
---|---|---|
4f5b44bd NL |
1 | Extending Twig |
2 | ============== | |
3 | ||
4 | .. caution:: | |
5 | ||
6 | This section describes how to extend Twig as of **Twig 1.12**. If you are | |
7 | using an older version, read the :doc:`legacy<advanced_legacy>` chapter | |
8 | instead. | |
9 | ||
10 | Twig can be extended in many ways; you can add extra tags, filters, tests, | |
11 | operators, global variables, and functions. You can even extend the parser | |
12 | itself with node visitors. | |
13 | ||
14 | .. note:: | |
15 | ||
16 | The first section of this chapter describes how to extend Twig easily. If | |
17 | you want to reuse your changes in different projects or if you want to | |
18 | share them with others, you should then create an extension as described | |
19 | in the following section. | |
20 | ||
21 | .. caution:: | |
22 | ||
23 | When extending Twig without creating an extension, Twig won't be able to | |
24 | recompile your templates when the PHP code is updated. To see your changes | |
25 | in real-time, either disable template caching or package your code into an | |
26 | extension (see the next section of this chapter). | |
27 | ||
28 | Before extending Twig, you must understand the differences between all the | |
29 | different possible extension points and when to use them. | |
30 | ||
31 | First, remember that Twig has two main language constructs: | |
32 | ||
33 | * ``{{ }}``: used to print the result of an expression evaluation; | |
34 | ||
35 | * ``{% %}``: used to execute statements. | |
36 | ||
37 | To understand why Twig exposes so many extension points, let's see how to | |
38 | implement a *Lorem ipsum* generator (it needs to know the number of words to | |
39 | generate). | |
40 | ||
41 | You can use a ``lipsum`` *tag*: | |
42 | ||
43 | .. code-block:: jinja | |
44 | ||
45 | {% lipsum 40 %} | |
46 | ||
47 | That works, but using a tag for ``lipsum`` is not a good idea for at least | |
48 | three main reasons: | |
49 | ||
50 | * ``lipsum`` is not a language construct; | |
51 | * The tag outputs something; | |
52 | * The tag is not flexible as you cannot use it in an expression: | |
53 | ||
54 | .. code-block:: jinja | |
55 | ||
56 | {{ 'some text' ~ {% lipsum 40 %} ~ 'some more text' }} | |
57 | ||
58 | In fact, you rarely need to create tags; and that's good news because tags are | |
59 | the most complex extension point of Twig. | |
60 | ||
61 | Now, let's use a ``lipsum`` *filter*: | |
62 | ||
63 | .. code-block:: jinja | |
64 | ||
65 | {{ 40|lipsum }} | |
66 | ||
67 | Again, it works, but it looks weird. A filter transforms the passed value to | |
68 | something else but here we use the value to indicate the number of words to | |
69 | generate (so, ``40`` is an argument of the filter, not the value we want to | |
70 | transform). | |
71 | ||
72 | Next, let's use a ``lipsum`` *function*: | |
73 | ||
74 | .. code-block:: jinja | |
75 | ||
76 | {{ lipsum(40) }} | |
77 | ||
78 | Here we go. For this specific example, the creation of a function is the | |
79 | extension point to use. And you can use it anywhere an expression is accepted: | |
80 | ||
81 | .. code-block:: jinja | |
82 | ||
83 | {{ 'some text' ~ lipsum(40) ~ 'some more text' }} | |
84 | ||
85 | {% set lipsum = lipsum(40) %} | |
86 | ||
87 | Last but not the least, you can also use a *global* object with a method able | |
88 | to generate lorem ipsum text: | |
89 | ||
90 | .. code-block:: jinja | |
91 | ||
92 | {{ text.lipsum(40) }} | |
93 | ||
94 | As a rule of thumb, use functions for frequently used features and global | |
95 | objects for everything else. | |
96 | ||
97 | Keep in mind the following when you want to extend Twig: | |
98 | ||
99 | ========== ========================== ========== ========================= | |
100 | What? Implementation difficulty? How often? When? | |
101 | ========== ========================== ========== ========================= | |
102 | *macro* trivial frequent Content generation | |
103 | *global* trivial frequent Helper object | |
104 | *function* trivial frequent Content generation | |
105 | *filter* trivial frequent Value transformation | |
106 | *tag* complex rare DSL language construct | |
107 | *test* trivial rare Boolean decision | |
108 | *operator* trivial rare Values transformation | |
109 | ========== ========================== ========== ========================= | |
110 | ||
111 | Globals | |
112 | ------- | |
113 | ||
114 | A global variable is like any other template variable, except that it's | |
115 | available in all templates and macros:: | |
116 | ||
117 | $twig = new Twig_Environment($loader); | |
118 | $twig->addGlobal('text', new Text()); | |
119 | ||
120 | You can then use the ``text`` variable anywhere in a template: | |
121 | ||
122 | .. code-block:: jinja | |
123 | ||
124 | {{ text.lipsum(40) }} | |
125 | ||
126 | Filters | |
127 | ------- | |
128 | ||
129 | Creating a filter is as simple as associating a name with a PHP callable:: | |
130 | ||
131 | // an anonymous function | |
132 | $filter = new Twig_SimpleFilter('rot13', function ($string) { | |
133 | return str_rot13($string); | |
134 | }); | |
135 | ||
136 | // or a simple PHP function | |
137 | $filter = new Twig_SimpleFilter('rot13', 'str_rot13'); | |
138 | ||
139 | // or a class method | |
140 | $filter = new Twig_SimpleFilter('rot13', array('SomeClass', 'rot13Filter')); | |
141 | ||
142 | The first argument passed to the ``Twig_SimpleFilter`` constructor is the name | |
143 | of the filter you will use in templates and the second one is the PHP callable | |
144 | to associate with it. | |
145 | ||
146 | Then, add the filter to your Twig environment:: | |
147 | ||
148 | $twig = new Twig_Environment($loader); | |
149 | $twig->addFilter($filter); | |
150 | ||
151 | And here is how to use it in a template: | |
152 | ||
153 | .. code-block:: jinja | |
154 | ||
155 | {{ 'Twig'|rot13 }} | |
156 | ||
157 | {# will output Gjvt #} | |
158 | ||
159 | When called by Twig, the PHP callable receives the left side of the filter | |
160 | (before the pipe ``|``) as the first argument and the extra arguments passed | |
161 | to the filter (within parentheses ``()``) as extra arguments. | |
162 | ||
163 | For instance, the following code: | |
164 | ||
165 | .. code-block:: jinja | |
166 | ||
167 | {{ 'TWIG'|lower }} | |
168 | {{ now|date('d/m/Y') }} | |
169 | ||
170 | is compiled to something like the following:: | |
171 | ||
172 | <?php echo strtolower('TWIG') ?> | |
173 | <?php echo twig_date_format_filter($now, 'd/m/Y') ?> | |
174 | ||
175 | The ``Twig_SimpleFilter`` class takes an array of options as its last | |
176 | argument:: | |
177 | ||
178 | $filter = new Twig_SimpleFilter('rot13', 'str_rot13', $options); | |
179 | ||
180 | Environment aware Filters | |
181 | ~~~~~~~~~~~~~~~~~~~~~~~~~ | |
182 | ||
183 | If you want to access the current environment instance in your filter, set the | |
184 | ``needs_environment`` option to ``true``; Twig will pass the current | |
185 | environment as the first argument to the filter call:: | |
186 | ||
187 | $filter = new Twig_SimpleFilter('rot13', function (Twig_Environment $env, $string) { | |
188 | // get the current charset for instance | |
189 | $charset = $env->getCharset(); | |
190 | ||
191 | return str_rot13($string); | |
192 | }, array('needs_environment' => true)); | |
193 | ||
194 | Context aware Filters | |
195 | ~~~~~~~~~~~~~~~~~~~~~ | |
196 | ||
197 | If you want to access the current context in your filter, set the | |
198 | ``needs_context`` option to ``true``; Twig will pass the current context as | |
199 | the first argument to the filter call (or the second one if | |
200 | ``needs_environment`` is also set to ``true``):: | |
201 | ||
202 | $filter = new Twig_SimpleFilter('rot13', function ($context, $string) { | |
203 | // ... | |
204 | }, array('needs_context' => true)); | |
205 | ||
206 | $filter = new Twig_SimpleFilter('rot13', function (Twig_Environment $env, $context, $string) { | |
207 | // ... | |
208 | }, array('needs_context' => true, 'needs_environment' => true)); | |
209 | ||
210 | Automatic Escaping | |
211 | ~~~~~~~~~~~~~~~~~~ | |
212 | ||
213 | If automatic escaping is enabled, the output of the filter may be escaped | |
214 | before printing. If your filter acts as an escaper (or explicitly outputs html | |
215 | or JavaScript code), you will want the raw output to be printed. In such a | |
216 | case, set the ``is_safe`` option:: | |
217 | ||
218 | $filter = new Twig_SimpleFilter('nl2br', 'nl2br', array('is_safe' => array('html'))); | |
219 | ||
220 | Some filters may need to work on input that is already escaped or safe, for | |
221 | example when adding (safe) html tags to originally unsafe output. In such a | |
222 | case, set the ``pre_escape`` option to escape the input data before it is run | |
223 | through your filter:: | |
224 | ||
225 | $filter = new Twig_SimpleFilter('somefilter', 'somefilter', array('pre_escape' => 'html', 'is_safe' => array('html'))); | |
226 | ||
227 | Dynamic Filters | |
228 | ~~~~~~~~~~~~~~~ | |
229 | ||
230 | A filter name containing the special ``*`` character is a dynamic filter as | |
231 | the ``*`` can be any string:: | |
232 | ||
233 | $filter = new Twig_SimpleFilter('*_path', function ($name, $arguments) { | |
234 | // ... | |
235 | }); | |
236 | ||
237 | The following filters will be matched by the above defined dynamic filter: | |
238 | ||
239 | * ``product_path`` | |
240 | * ``category_path`` | |
241 | ||
242 | A dynamic filter can define more than one dynamic parts:: | |
243 | ||
244 | $filter = new Twig_SimpleFilter('*_path_*', function ($name, $suffix, $arguments) { | |
245 | // ... | |
246 | }); | |
247 | ||
248 | The filter will receive all dynamic part values before the normal filter | |
249 | arguments, but after the environment and the context. For instance, a call to | |
250 | ``'foo'|a_path_b()`` will result in the following arguments to be passed to | |
251 | the filter: ``('a', 'b', 'foo')``. | |
252 | ||
253 | Functions | |
254 | --------- | |
255 | ||
256 | Functions are defined in the exact same way as filters, but you need to create | |
257 | an instance of ``Twig_SimpleFunction``:: | |
258 | ||
259 | $twig = new Twig_Environment($loader); | |
260 | $function = new Twig_SimpleFunction('function_name', function () { | |
261 | // ... | |
262 | }); | |
263 | $twig->addFunction($function); | |
264 | ||
265 | Functions support the same features as filters, except for the ``pre_escape`` | |
266 | and ``preserves_safety`` options. | |
267 | ||
268 | Tests | |
269 | ----- | |
270 | ||
271 | Tests are defined in the exact same way as filters and functions, but you need | |
272 | to create an instance of ``Twig_SimpleTest``:: | |
273 | ||
274 | $twig = new Twig_Environment($loader); | |
275 | $test = new Twig_SimpleTest('test_name', function () { | |
276 | // ... | |
277 | }); | |
278 | $twig->addTest($test); | |
279 | ||
280 | Tests allow you to create custom application specific logic for evaluating | |
281 | boolean conditions. As a simple, example let's create a Twig test that checks if | |
282 | objects are 'red':: | |
283 | ||
284 | $twig = new Twig_Environment($loader) | |
285 | $test = new Twig_SimpleTest('red', function ($value) { | |
286 | if (isset($value->color) && $value->color == 'red') { | |
287 | return true; | |
288 | } | |
289 | if (isset($value->paint) && $value->paint == 'red') { | |
290 | return true; | |
291 | } | |
292 | return false; | |
293 | }); | |
294 | $twig->addTest($test); | |
295 | ||
296 | Test functions should always return true/false. | |
297 | ||
298 | When creating tests you can use the ``node_class`` option to provide custom test | |
299 | compilation. This is useful if your test can be compiled into PHP primitives. | |
300 | This is used by many of the tests built into Twig:: | |
301 | ||
302 | $twig = new Twig_Environment($loader) | |
303 | $test = new Twig_SimpleTest( | |
304 | 'odd', | |
305 | null, | |
306 | array('node_class' => 'Twig_Node_Expression_Test_Odd')); | |
307 | $twig->addTest($test); | |
308 | ||
309 | class Twig_Node_Expression_Test_Odd extends Twig_Node_Expression_Test | |
310 | { | |
311 | public function compile(Twig_Compiler $compiler) | |
312 | { | |
313 | $compiler | |
314 | ->raw('(') | |
315 | ->subcompile($this->getNode('node')) | |
316 | ->raw(' % 2 == 1') | |
317 | ->raw(')') | |
318 | ; | |
319 | } | |
320 | } | |
321 | ||
322 | The above example, shows how you can create tests that use a node class. The | |
323 | node class has access to one sub-node called 'node'. This sub-node contains the | |
324 | value that is being tested. When the ``odd`` filter is used in code like: | |
325 | ||
326 | .. code-block:: jinja | |
327 | ||
328 | {% if my_value is odd %} | |
329 | ||
330 | The ``node`` sub-node will contain an expression of ``my_value``. Node based | |
331 | tests also have access to the ``arguments`` node. This node will contain the | |
332 | various other arguments that have been provided to your test. | |
333 | ||
334 | Tags | |
335 | ---- | |
336 | ||
337 | One of the most exciting feature of a template engine like Twig is the | |
338 | possibility to define new language constructs. This is also the most complex | |
339 | feature as you need to understand how Twig's internals work. | |
340 | ||
341 | Let's create a simple ``set`` tag that allows the definition of simple | |
342 | variables from within a template. The tag can be used like follows: | |
343 | ||
344 | .. code-block:: jinja | |
345 | ||
346 | {% set name = "value" %} | |
347 | ||
348 | {{ name }} | |
349 | ||
350 | {# should output value #} | |
351 | ||
352 | .. note:: | |
353 | ||
354 | The ``set`` tag is part of the Core extension and as such is always | |
355 | available. The built-in version is slightly more powerful and supports | |
356 | multiple assignments by default (cf. the template designers chapter for | |
357 | more information). | |
358 | ||
359 | Three steps are needed to define a new tag: | |
360 | ||
361 | * Defining a Token Parser class (responsible for parsing the template code); | |
362 | ||
363 | * Defining a Node class (responsible for converting the parsed code to PHP); | |
364 | ||
365 | * Registering the tag. | |
366 | ||
367 | Registering a new tag | |
368 | ~~~~~~~~~~~~~~~~~~~~~ | |
369 | ||
370 | Adding a tag is as simple as calling the ``addTokenParser`` method on the | |
371 | ``Twig_Environment`` instance:: | |
372 | ||
373 | $twig = new Twig_Environment($loader); | |
374 | $twig->addTokenParser(new Project_Set_TokenParser()); | |
375 | ||
376 | Defining a Token Parser | |
377 | ~~~~~~~~~~~~~~~~~~~~~~~ | |
378 | ||
379 | Now, let's see the actual code of this class:: | |
380 | ||
381 | class Project_Set_TokenParser extends Twig_TokenParser | |
382 | { | |
383 | public function parse(Twig_Token $token) | |
384 | { | |
385 | $parser = $this->parser; | |
386 | $stream = $parser->getStream(); | |
387 | ||
388 | $name = $stream->expect(Twig_Token::NAME_TYPE)->getValue(); | |
389 | $stream->expect(Twig_Token::OPERATOR_TYPE, '='); | |
390 | $value = $parser->getExpressionParser()->parseExpression(); | |
391 | $stream->expect(Twig_Token::BLOCK_END_TYPE); | |
392 | ||
393 | return new Project_Set_Node($name, $value, $token->getLine(), $this->getTag()); | |
394 | } | |
395 | ||
396 | public function getTag() | |
397 | { | |
398 | return 'set'; | |
399 | } | |
400 | } | |
401 | ||
402 | The ``getTag()`` method must return the tag we want to parse, here ``set``. | |
403 | ||
404 | The ``parse()`` method is invoked whenever the parser encounters a ``set`` | |
405 | tag. It should return a ``Twig_Node`` instance that represents the node (the | |
406 | ``Project_Set_Node`` calls creating is explained in the next section). | |
407 | ||
408 | The parsing process is simplified thanks to a bunch of methods you can call | |
409 | from the token stream (``$this->parser->getStream()``): | |
410 | ||
411 | * ``getCurrent()``: Gets the current token in the stream. | |
412 | ||
413 | * ``next()``: Moves to the next token in the stream, *but returns the old one*. | |
414 | ||
415 | * ``test($type)``, ``test($value)`` or ``test($type, $value)``: Determines whether | |
416 | the current token is of a particular type or value (or both). The value may be an | |
417 | array of several possible values. | |
418 | ||
419 | * ``expect($type[, $value[, $message]])``: If the current token isn't of the given | |
420 | type/value a syntax error is thrown. Otherwise, if the type and value are correct, | |
421 | the token is returned and the stream moves to the next token. | |
422 | ||
423 | * ``look()``: Looks a the next token without consuming it. | |
424 | ||
425 | Parsing expressions is done by calling the ``parseExpression()`` like we did for | |
426 | the ``set`` tag. | |
427 | ||
428 | .. tip:: | |
429 | ||
430 | Reading the existing ``TokenParser`` classes is the best way to learn all | |
431 | the nitty-gritty details of the parsing process. | |
432 | ||
433 | Defining a Node | |
434 | ~~~~~~~~~~~~~~~ | |
435 | ||
436 | The ``Project_Set_Node`` class itself is rather simple:: | |
437 | ||
438 | class Project_Set_Node extends Twig_Node | |
439 | { | |
440 | public function __construct($name, Twig_Node_Expression $value, $line, $tag = null) | |
441 | { | |
442 | parent::__construct(array('value' => $value), array('name' => $name), $line, $tag); | |
443 | } | |
444 | ||
445 | public function compile(Twig_Compiler $compiler) | |
446 | { | |
447 | $compiler | |
448 | ->addDebugInfo($this) | |
449 | ->write('$context[\''.$this->getAttribute('name').'\'] = ') | |
450 | ->subcompile($this->getNode('value')) | |
451 | ->raw(";\n") | |
452 | ; | |
453 | } | |
454 | } | |
455 | ||
456 | The compiler implements a fluid interface and provides methods that helps the | |
457 | developer generate beautiful and readable PHP code: | |
458 | ||
459 | * ``subcompile()``: Compiles a node. | |
460 | ||
461 | * ``raw()``: Writes the given string as is. | |
462 | ||
463 | * ``write()``: Writes the given string by adding indentation at the beginning | |
464 | of each line. | |
465 | ||
466 | * ``string()``: Writes a quoted string. | |
467 | ||
468 | * ``repr()``: Writes a PHP representation of a given value (see | |
469 | ``Twig_Node_For`` for a usage example). | |
470 | ||
471 | * ``addDebugInfo()``: Adds the line of the original template file related to | |
472 | the current node as a comment. | |
473 | ||
474 | * ``indent()``: Indents the generated code (see ``Twig_Node_Block`` for a | |
475 | usage example). | |
476 | ||
477 | * ``outdent()``: Outdents the generated code (see ``Twig_Node_Block`` for a | |
478 | usage example). | |
479 | ||
480 | .. _creating_extensions: | |
481 | ||
482 | Creating an Extension | |
483 | --------------------- | |
484 | ||
485 | The main motivation for writing an extension is to move often used code into a | |
486 | reusable class like adding support for internationalization. An extension can | |
487 | define tags, filters, tests, operators, global variables, functions, and node | |
488 | visitors. | |
489 | ||
490 | Creating an extension also makes for a better separation of code that is | |
491 | executed at compilation time and code needed at runtime. As such, it makes | |
492 | your code faster. | |
493 | ||
494 | Most of the time, it is useful to create a single extension for your project, | |
495 | to host all the specific tags and filters you want to add to Twig. | |
496 | ||
497 | .. tip:: | |
498 | ||
499 | When packaging your code into an extension, Twig is smart enough to | |
500 | recompile your templates whenever you make a change to it (when | |
501 | ``auto_reload`` is enabled). | |
502 | ||
503 | .. note:: | |
504 | ||
505 | Before writing your own extensions, have a look at the Twig official | |
506 | extension repository: http://github.com/fabpot/Twig-extensions. | |
507 | ||
508 | An extension is a class that implements the following interface:: | |
509 | ||
510 | interface Twig_ExtensionInterface | |
511 | { | |
512 | /** | |
513 | * Initializes the runtime environment. | |
514 | * | |
515 | * This is where you can load some file that contains filter functions for instance. | |
516 | * | |
517 | * @param Twig_Environment $environment The current Twig_Environment instance | |
518 | */ | |
519 | function initRuntime(Twig_Environment $environment); | |
520 | ||
521 | /** | |
522 | * Returns the token parser instances to add to the existing list. | |
523 | * | |
524 | * @return array An array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances | |
525 | */ | |
526 | function getTokenParsers(); | |
527 | ||
528 | /** | |
529 | * Returns the node visitor instances to add to the existing list. | |
530 | * | |
531 | * @return array An array of Twig_NodeVisitorInterface instances | |
532 | */ | |
533 | function getNodeVisitors(); | |
534 | ||
535 | /** | |
536 | * Returns a list of filters to add to the existing list. | |
537 | * | |
538 | * @return array An array of filters | |
539 | */ | |
540 | function getFilters(); | |
541 | ||
542 | /** | |
543 | * Returns a list of tests to add to the existing list. | |
544 | * | |
545 | * @return array An array of tests | |
546 | */ | |
547 | function getTests(); | |
548 | ||
549 | /** | |
550 | * Returns a list of functions to add to the existing list. | |
551 | * | |
552 | * @return array An array of functions | |
553 | */ | |
554 | function getFunctions(); | |
555 | ||
556 | /** | |
557 | * Returns a list of operators to add to the existing list. | |
558 | * | |
559 | * @return array An array of operators | |
560 | */ | |
561 | function getOperators(); | |
562 | ||
563 | /** | |
564 | * Returns a list of global variables to add to the existing list. | |
565 | * | |
566 | * @return array An array of global variables | |
567 | */ | |
568 | function getGlobals(); | |
569 | ||
570 | /** | |
571 | * Returns the name of the extension. | |
572 | * | |
573 | * @return string The extension name | |
574 | */ | |
575 | function getName(); | |
576 | } | |
577 | ||
578 | To keep your extension class clean and lean, it can inherit from the built-in | |
579 | ``Twig_Extension`` class instead of implementing the whole interface. That | |
580 | way, you just need to implement the ``getName()`` method as the | |
581 | ``Twig_Extension`` provides empty implementations for all other methods. | |
582 | ||
583 | The ``getName()`` method must return a unique identifier for your extension. | |
584 | ||
585 | Now, with this information in mind, let's create the most basic extension | |
586 | possible:: | |
587 | ||
588 | class Project_Twig_Extension extends Twig_Extension | |
589 | { | |
590 | public function getName() | |
591 | { | |
592 | return 'project'; | |
593 | } | |
594 | } | |
595 | ||
596 | .. note:: | |
597 | ||
598 | Of course, this extension does nothing for now. We will customize it in | |
599 | the next sections. | |
600 | ||
601 | Twig does not care where you save your extension on the filesystem, as all | |
602 | extensions must be registered explicitly to be available in your templates. | |
603 | ||
604 | You can register an extension by using the ``addExtension()`` method on your | |
605 | main ``Environment`` object:: | |
606 | ||
607 | $twig = new Twig_Environment($loader); | |
608 | $twig->addExtension(new Project_Twig_Extension()); | |
609 | ||
610 | Of course, you need to first load the extension file by either using | |
611 | ``require_once()`` or by using an autoloader (see `spl_autoload_register()`_). | |
612 | ||
613 | .. tip:: | |
614 | ||
615 | The bundled extensions are great examples of how extensions work. | |
616 | ||
617 | Globals | |
618 | ~~~~~~~ | |
619 | ||
620 | Global variables can be registered in an extension via the ``getGlobals()`` | |
621 | method:: | |
622 | ||
623 | class Project_Twig_Extension extends Twig_Extension | |
624 | { | |
625 | public function getGlobals() | |
626 | { | |
627 | return array( | |
628 | 'text' => new Text(), | |
629 | ); | |
630 | } | |
631 | ||
632 | // ... | |
633 | } | |
634 | ||
635 | Functions | |
636 | ~~~~~~~~~ | |
637 | ||
638 | Functions can be registered in an extension via the ``getFunctions()`` | |
639 | method:: | |
640 | ||
641 | class Project_Twig_Extension extends Twig_Extension | |
642 | { | |
643 | public function getFunctions() | |
644 | { | |
645 | return array( | |
646 | new Twig_SimpleFunction('lipsum', 'generate_lipsum'), | |
647 | ); | |
648 | } | |
649 | ||
650 | // ... | |
651 | } | |
652 | ||
653 | Filters | |
654 | ~~~~~~~ | |
655 | ||
656 | To add a filter to an extension, you need to override the ``getFilters()`` | |
657 | method. This method must return an array of filters to add to the Twig | |
658 | environment:: | |
659 | ||
660 | class Project_Twig_Extension extends Twig_Extension | |
661 | { | |
662 | public function getFilters() | |
663 | { | |
664 | return array( | |
665 | new Twig_SimpleFilter('rot13', 'str_rot13'), | |
666 | ); | |
667 | } | |
668 | ||
669 | // ... | |
670 | } | |
671 | ||
672 | Tags | |
673 | ~~~~ | |
674 | ||
675 | Adding a tag in an extension can be done by overriding the | |
676 | ``getTokenParsers()`` method. This method must return an array of tags to add | |
677 | to the Twig environment:: | |
678 | ||
679 | class Project_Twig_Extension extends Twig_Extension | |
680 | { | |
681 | public function getTokenParsers() | |
682 | { | |
683 | return array(new Project_Set_TokenParser()); | |
684 | } | |
685 | ||
686 | // ... | |
687 | } | |
688 | ||
689 | In the above code, we have added a single new tag, defined by the | |
690 | ``Project_Set_TokenParser`` class. The ``Project_Set_TokenParser`` class is | |
691 | responsible for parsing the tag and compiling it to PHP. | |
692 | ||
693 | Operators | |
694 | ~~~~~~~~~ | |
695 | ||
696 | The ``getOperators()`` methods allows to add new operators. Here is how to add | |
697 | ``!``, ``||``, and ``&&`` operators:: | |
698 | ||
699 | class Project_Twig_Extension extends Twig_Extension | |
700 | { | |
701 | public function getOperators() | |
702 | { | |
703 | return array( | |
704 | array( | |
705 | '!' => array('precedence' => 50, 'class' => 'Twig_Node_Expression_Unary_Not'), | |
706 | ), | |
707 | array( | |
708 | '||' => array('precedence' => 10, 'class' => 'Twig_Node_Expression_Binary_Or', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), | |
709 | '&&' => array('precedence' => 15, 'class' => 'Twig_Node_Expression_Binary_And', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), | |
710 | ), | |
711 | ); | |
712 | } | |
713 | ||
714 | // ... | |
715 | } | |
716 | ||
717 | Tests | |
718 | ~~~~~ | |
719 | ||
720 | The ``getTests()`` methods allows to add new test functions:: | |
721 | ||
722 | class Project_Twig_Extension extends Twig_Extension | |
723 | { | |
724 | public function getTests() | |
725 | { | |
726 | return array( | |
727 | new Twig_SimpleTest('even', 'twig_test_even'), | |
728 | ); | |
729 | } | |
730 | ||
731 | // ... | |
732 | } | |
733 | ||
734 | Overloading | |
735 | ----------- | |
736 | ||
737 | To overload an already defined filter, test, operator, global variable, or | |
738 | function, define it again **as late as possible**:: | |
739 | ||
740 | $twig = new Twig_Environment($loader); | |
741 | $twig->addFilter(new Twig_SimpleFilter('date', function ($timestamp, $format = 'F j, Y H:i') { | |
742 | // do something different from the built-in date filter | |
743 | })); | |
744 | ||
745 | Here, we have overloaded the built-in ``date`` filter with a custom one. | |
746 | ||
747 | That also works with an extension:: | |
748 | ||
749 | class MyCoreExtension extends Twig_Extension | |
750 | { | |
751 | public function getFilters() | |
752 | { | |
753 | return array( | |
754 | new Twig_SimpleFilter('date', array($this, 'dateFilter')), | |
755 | ); | |
756 | } | |
757 | ||
758 | public function dateFilter($timestamp, $format = 'F j, Y H:i') | |
759 | { | |
760 | // do something different from the built-in date filter | |
761 | } | |
762 | ||
763 | public function getName() | |
764 | { | |
765 | return 'project'; | |
766 | } | |
767 | } | |
768 | ||
769 | $twig = new Twig_Environment($loader); | |
770 | $twig->addExtension(new MyCoreExtension()); | |
771 | ||
772 | .. caution:: | |
773 | ||
774 | Note that overloading the built-in Twig elements is not recommended as it | |
775 | might be confusing. | |
776 | ||
777 | Testing an Extension | |
778 | -------------------- | |
779 | ||
780 | Functional Tests | |
781 | ~~~~~~~~~~~~~~~~ | |
782 | ||
783 | You can create functional tests for extensions simply by creating the | |
784 | following file structure in your test directory:: | |
785 | ||
786 | Fixtures/ | |
787 | filters/ | |
788 | foo.test | |
789 | bar.test | |
790 | functions/ | |
791 | foo.test | |
792 | bar.test | |
793 | tags/ | |
794 | foo.test | |
795 | bar.test | |
796 | IntegrationTest.php | |
797 | ||
798 | The ``IntegrationTest.php`` file should look like this:: | |
799 | ||
800 | class Project_Tests_IntegrationTest extends Twig_Test_IntegrationTestCase | |
801 | { | |
802 | public function getExtensions() | |
803 | { | |
804 | return array( | |
805 | new Project_Twig_Extension1(), | |
806 | new Project_Twig_Extension2(), | |
807 | ); | |
808 | } | |
809 | ||
810 | public function getFixturesDir() | |
811 | { | |
812 | return dirname(__FILE__).'/Fixtures/'; | |
813 | } | |
814 | } | |
815 | ||
816 | Fixtures examples can be found within the Twig repository | |
817 | `tests/Twig/Fixtures`_ directory. | |
818 | ||
819 | Node Tests | |
820 | ~~~~~~~~~~ | |
821 | ||
822 | Testing the node visitors can be complex, so extend your test cases from | |
823 | ``Twig_Test_NodeTestCase``. Examples can be found in the Twig repository | |
824 | `tests/Twig/Node`_ directory. | |
825 | ||
826 | .. _`spl_autoload_register()`: http://www.php.net/spl_autoload_register | |
827 | .. _`rot13`: http://www.php.net/manual/en/function.str-rot13.php | |
828 | .. _`tests/Twig/Fixtures`: https://github.com/fabpot/Twig/tree/master/test/Twig/Tests/Fixtures | |
829 | .. _`tests/Twig/Node`: https://github.com/fabpot/Twig/tree/master/test/Twig/Tests/Node |