What I would like to achieve is to output some dynamic text coming from DB filled with unpredictable number of placeholders to be filled with some query parameters.
Basically it is an automation/notification system whereby upon user or admin's interaction with the website, some automation tasks will get triggered and added to DB. My closest shot at almost handling it is by using twig |replace filter in connection with twig extension. The problem is that I get to see the replaced text with the raw data not their parsed value. I guess it's better to look at my code. Your help is greatly appreciated.
DB Schema 'AutomationMsgTemplate, aka: amt'
raw_msg(text type) | format_keys(text type) | format_values(text type)
You are %USERNAME% | USERNAME | row.user.username
%No% %ORD_STATS% created | NO, ORD_STATUS | row.notif.x, row.order.y
My Service 'MsgFormatHelper' (facilitating twig ext)
public function renderMsgUsingSprintFormat(AutomationMsgTemplate $amt, GeneratedAutomationTask $gat): array
{
$formatter_keys = $amt->getFormatKeys();
$formatter_values = $amt->getFormatValues();
$formatter_keys_arr = explode(",", $formatter_keys);
$formatter_values_arr = explode(",", $formatter_values);
$formatter_ready = array_combine($formatter_keys_arr, $formatter_values_arr);
return $formatter_ready;
}
Twig extension
class AppExtension extends AbstractExtension implements ServiceSubscriberInterface
{
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function getFunctions(): array
{
return [
new TwigFunction('msgSprint', [$this, 'renderMsg'])
];
}
public function renderMsg($amt, $gat): array
{
return $this->container
->get(MsgFormatHelper::class)
->renderMsgUsingSprintFormat($amt, $gat);
}
public static function getSubscribedServices(): array
{
return [
MsgFormatHelper::class
];
}
}
And finally inside the twig
{% for i, row in incompleteTasks %}
{% for amt in row.aet.automationMsgTemplates %}
// MOMENT OF TRUTH IS BELOW:
{% set format_arr = msgSprint(amt, row) %}
{% set processedMsg = amt.message|replace(format_arr) %}
{{ processedMsg }}
//nope, output is like: You are row.user.username
//sure enough below, as a debug test, works as intended
{# {% set processedMsg = temp.message|replace({'%USERNAME%': row.ats.studentCourse.student.username}) %} #}
{% endfor %}
{% endfor %}
i try to create some kind of formbuilder that outputs html of generated forms using the symfony form extenstion (using all the nice stuff like valdation, error hightlighting and such).
is use symfony 5 with twig 3.0
the created class
class FormBuilder{
private $twig;
private $formFactory;
public function __construct()
{
$defaultFormTheme = 'contact.html.twig';
$loader = new FilesystemLoader($_SERVER['DOCUMENT_ROOT'].'../template/forms');
$this->twig = new Environment($loader, ['cache' => $_SERVER['DOCUMENT_ROOT'].'../var/cache/'.$_ENV['APP_ENV'].'/twig']);
$formEngine = new TwigRendererEngine([$defaultFormTheme], $this->twig);
$this->twig->addRuntimeLoader(new FactoryRuntimeLoader([
FormRenderer::class => function () use ($formEngine) {
return new FormRenderer($formEngine);
},
]));
$this->twig->addExtension(new FormExtension());
$this->formFactory = Forms::createFormFactoryBuilder()
->getFormFactory();
}
/**
* #return string
*/
public function getContactForm():string
{
$form = $this->formFactory->createBuilder()
->add('content', TextareaType::class)
->getForm();
return $this->twig->render('contact.html.twig', [
'contact_form' => $form->createView(),
]);
}
}
and call it somewhere else with
$fb = new FormBuilder();
var_dump($fb->getContactForm());
it doesn't matter if the template looks like
{{ form_start(contact_form) }}
{{ form_widget(contact_form) }}
<input type="submit"/>
{{ form_end(contact_form) }}
or
{{ form(contact_form) }}
there is always the and runtime error:
An exception has been thrown during the rendering of a template ("No block "form" found while rendering the form.").
or in first template example instead of "form" it yells about "form_start".
searching for hours now, but seems i to blind to find the missing spot..
any suggestions or tips how to include the form function in twig outside the symfony controller?
with adding themes and (because of translation in the themes) translation it now works
public function __construct()
{
// name of theme template
$defaultFormTheme = 'form_div_layout.html.twig';
$appVariableReflection = new \ReflectionClass('\Symfony\Bridge\Twig\AppVariable');
$vendorTwigBridgeDirectory = dirname($appVariableReflection->getFileName());
//where the forms reside
$viewsDirectory = realpath($_SERVER['DOCUMENT_ROOT'].'../template/forms');
//init twig with directories
$this->twig = new Environment(new FilesystemLoader([
$viewsDirectory,
$vendorTwigBridgeDirectory.'/Resources/views/Form',
]));
//apply theme to twig/renderer
$formEngine = new TwigRendererEngine([$defaultFormTheme], $this->twig);
$this->twig->addRuntimeLoader(new FactoryRuntimeLoader([
FormRenderer::class => function () use ($formEngine) {
return new FormRenderer($formEngine);
},
]));
//add form extenstion
$this->twig->addExtension(new FormExtension());
// this is for the filter |trans
$filter = new TwigFilter('trans', function ($context, $string) {
return Translation::TransGetText($string, $context);
}, ['needs_context' => true]);
// load the i18n extension for using the translation tag for twig
// {% trans %}my string{% endtrans %}
$this->twig->addFilter($filter);
$this->twig->addExtension(new Translation());
//prepare factory for later use
$this->formFactory = Forms::createFormFactoryBuilder()
->getFormFactory();
}
just needs additional php extension 'gettext' and the translation extension for twig from https://github.com/JBlond/twig-trans (the original seems to support only twig up to v2.x)
thanks again Jakumi for the solving hint :)
this is my ConfigureShowFields using sonata bundle :
I want to transform my data in ConfigureShowFields to pdf , is possible ??
Its possible using
https://github.com/KnpLabs/KnpSnappyBundle
Firstly install and configure the KnpSnappyBundle
Then:
create custom action in your admin:
protected function configureRoutes(RouteCollection $collection)
{
$collection->add('pdf', $this->getRouterIdParameter().'/pdf');
}
Create the logic for this action in the admin controller
public function pdfAction(Request $request)
{
$id = $request->get($this->admin->getIdParameter());
$object = $this->admin->getObject($id);
if (!$object) {
throw $this->createNotFoundException(sprintf('unable to find the object with id : %s', $id));
}
$this->admin->checkAccess('show', $object);
$this->admin->setSubject($object);
$response = $this->render(
'#App/Admin/pdf.html.twig',
[
'action' => 'print',
'object' => $object,
'elements' => $this->admin->getShow(),
],
null
);
$cacheDir = $this->container->getParameter('kernel.cache_dir');
$name = tempnam($cacheDir.DIRECTORY_SEPARATOR, '_print');
file_put_contents($name, $response->getContent());
$hash = base64_encode($name);
$options['viewport-size'] = '769x900';
$url = $this->container->get('router')->generate('app_print', ['hash' => $hash], Router::ABSOLUTE_URL);
$pdf = $this->container->get('knp_snappy.pdf')->getOutput($url, $options);
return new Response(
$pdf,
200,
[
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'filename="show.pdf"',
]
);
}
Create the pdf view to render the show without admin menu or headers.
App/Admin/pdf.html.twig
{% extends '#SonataAdmin/CRUD/base_show.html.twig' %}
{% block html %}
<!DOCTYPE html>
<html lang="en" dir="ltr">
{% block head %}
{{ parent() }}
{% endblock %}
<body style="background: none">
{% block sonata_admin_content %}
{{ parent() }}
{% endblock %}
</body>
</html>
{% endblock %}
Create a print controller in the app.
/**
* #Route(path="/core/print")
*/
class PrinterController
{
/**
* #Route(path="/{hash}", name="app_print")
*
* #param string $hash
*
* #return Response
*/
public function indexAction($hash)
{
$file = base64_decode($hash);
if (!file_exists($file)) {
throw new NotFoundHttpException();
}
$response = new Response(file_get_contents($file));
unlink($file);
return $response;
}
}
NOTE: This controller is used to use knpSnappy with a url instead of string, in order to avoid conflicts with assets, e.g. images etc. If you don't need print images or styles, simply use $this->get('knp_snappy.pdf')->generateFromHtml() to generate the pdf from the response instead of send to another url and remove the part when is used the cache to create a temporal rendered file.
I have divs with CSS that represent boxes, they wrap html code.
<div class="box indent">
<div class="padding">
my code here
</div>
</div>
I created a "layoutbundle" where every HTML wrapper (such as boxes, tabs, grids, and so on) is put inside separate twig files. In such a way, views on other bundles can be implemented with other layouts.
But I get tired of includes. Every small html wrapper requires an include, and I wonder if there is a simpler way to wrap HTML code.
Let's have an example with a simple box. Actually, I created several files :
A box.html.twig file that contain the box and include the content :
<div class="box indent">
<div class="padding">
{% include content %}
</div>
</div>
Several box-content.html.twig files, containing content of my boxes.
And finally, I create a box in a view by doing :
{%
include 'AcmeDemoBundle:layout:box.html.twig'
with {
'content': 'ReusableBundle:feature:xxx.html.twig'
}
%}
Is there a way to create wrappers such as :
a) I declare once a new wrapper :
{% wrapperheader "box" %}
<div class="box indent">
<div class="padding">
{% endwrapperheader %}
{% wrapperfooter "box" %}
</div>
</div>
{% endwrapperfooter %}
b) And then in my pages, I use :
{% wrapper "box" %}
{# here my content #}
{% endwrapper %}
I think I'll need to add new tag extensions in Twig, but first I want to know if something similar is natively possible.
The block method
This method was proposed by Sebastiaan Stok on GitHub.
This idea uses the block function. It writes the given block contents, and can be called several times.
Wrappers file :
{# src/Fuz/LayoutBundle/Resources/views/Default/wrappers.html.twig #}
{% block box_head %}
<div class="box indent">
<div class="padding">
{% enblock %}
{% block box_foot %}
</div>
</div>
{% enblock %}
Feature page :
{{ block('box_head') }}
Some content
{{ block('box_foot') }}
The wrap extension with macros
This idea was proposed by Charles on GitHub.
First you declare a macro in a macro.html.twig file.
{% macro box(content) %}
<div class="box indent">
<div class="padding">
{{ content | raw }}
</div>
</div>
{% endmacro %}
Amd then, instead of calling {{ macros.box('my content') }} (see the doc you develop a {% wrap %} tag that will handle the macro call, with what's between [% wrap %} and {% endwrap %} as parameter.
This extension was easy to develop. I thought it could be hard to access macros, but in fact, they are stored in the context as objects, and calls can be compiled easily.
Just some changes : we will use the following syntax :
{# to access a macro from an object #}
{% wrap macro_object macro_name %}
my content here
{% endwrap %}
{# to access a macro declared in the same file #}
{% wrap macro_name %}
macro
{% endwrap %}
In the following code, don't forget to change namespaces if you want to get it work!
First, add the extension in your services.yml:
parameters:
fuz_tools.twig.wrap_extension.class: Fuz\ToolsBundle\Twig\Extension\WrapExtension
services:
fuz_tools.twig.wrap_extension:
class: '%fuz_tools.twig.wrap_extension.class%'
tags:
- { name: twig.extension }
Inside your bundle, create a Twig directory.
Add the extension, it will return the new TokenParser (in english: it will declare the new tag).
Twig/Extension/WrapExtension.php:
<?php
// src/Fuz/ToolsBundle/Twig/Extension/WrapExtension.php
namespace Fuz\ToolsBundle\Twig\Extension;
use Fuz\ToolsBundle\Twig\TokenParser\WrapHeaderTokenParser;
use Fuz\ToolsBundle\Twig\TokenParser\WrapFooterTokenParser;
use Fuz\ToolsBundle\Twig\TokenParser\WrapTokenParser;
class WrapExtension extends \Twig_Extension
{
public function getTokenParsers()
{
return array (
new WrapTokenParser(),
);
}
public function getName()
{
return 'wrap';
}
}
Then add the TokenParser itself, it will be met when the parser will find a {% wrap %} tag. This TokenParser will check if the tag is called correctly (for our example, it has 2 parameters), store those parameters and get the content between {% wrap %} and {% endwrap %}`.
Twig/TokenParser/WrapTokenParser.php:
<?php
// src/Fuz/ToolsBundle/Twig/TokenParser/WrapTokenParser.php
namespace Fuz\ToolsBundle\Twig\TokenParser;
use Fuz\ToolsBundle\Twig\Node\WrapNode;
class WrapTokenParser extends \Twig_TokenParser
{
public function parse(\Twig_Token $token)
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$object = null;
$name = $stream->expect(\Twig_Token::NAME_TYPE)->getValue();
if ($stream->test(\Twig_Token::BLOCK_END_TYPE))
{
if (!$this->parser->hasMacro($name))
{
throw new \Twig_Error_Syntax("The macro '$name' does not exist", $lineno);
}
}
else
{
$object = $name;
$name = $stream->expect(\Twig_Token::NAME_TYPE)->getValue();
}
$this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE);
$body = $this->parser->subparse(array ($this, 'decideWrapEnd'), true);
$this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE);
return new WrapNode($object, $name, $body, $token->getLine(), $this->getTag());
}
public function decideWrapEnd(\Twig_Token $token)
{
return $token->test('endwrap');
}
public function getTag()
{
return 'wrap';
}
}
Next, we need a compiler (a Node in the twig dialect), it will generate the PHP code associated with our {% wrap %} tag.
This tag is an alias of {{ macro_object.box(content) }}, so I wrote that line in a template and watched the resulting code in the resulting generated php file (stored in your app/cache/dev/twig directory). I got :
echo $this->getAttribute($this->getContext($context, "(macro object name)"), "(name)", array("(body)"), "method");
So my compiler became :
Twig/Node/WrapNode.php:
<?php
// src/Fuz/ToolsBundle/Twig/Node/WrapNode.php
namespace Fuz\ToolsBundle\Twig\Node;
class WrapNode extends \Twig_Node
{
public function __construct($object, $name, $body, $lineno = 0, $tag = null)
{
parent::__construct(array ('body' => $body), array ('object' => $object, 'name' => $name), $lineno, $tag);
}
public function compile(\Twig_Compiler $compiler)
{
$compiler
->addDebugInfo($this)
->write('ob_start();');
$compiler
->addDebugInfo($this)
->subcompile($this->getNode('body'));
if (is_null($this->getAttribute('object')))
{
$compiler
->write(sprintf('echo $this->get%s(ob_get_clean());', $this->getAttribute('name')) . "\n");
}
else
{
$compiler
->write('echo $this->getAttribute($this->getContext($context, ')
->repr($this->getAttribute('object'))
->raw('), ')
->repr($this->getAttribute('name'))
->raw(', array(ob_get_clean()), "method");')
->raw("\n");
}
}
}
Note : to know how work the subparsing / subcompiling, I read the spaceless extension source code.
That's all! We get an alias that let us use macros with a large body. To try it:
macros.html.twig:
{% macro box(content) %}
<div class="box indent">
<div class="padding">
{{ content | raw }} {# Don't forget the raw filter! #}
</div>
</div>
{% endmacro %}
some layout.html.twig:
{% import "FuzLayoutBundle:Default:macros.html.twig" as macros %}
{% wrap macros box %}
test
{% endwrap %}
{% macro test(content) %}
some {{ content | raw }} in the same file
{% endmacro %}
{% wrap test %}
macro
{% endwrap %}
Outputs:
<div class="box indent">
<div class="padding">
test
</div>
</div>
some macro in the same file
The wrapperheader, wrapperfooter, wrapper extension
This method is the one I tell you about in my question. You can read / implement it if you want to train yourself with token parsers, but functionnaly, that's less nice than the previous method.
In a wrapper.html.twig file, you declare all wrappers :
{% wrapperheader box %}
<div class="box">
{% endwrapper %}
{% wrapperfooter box %}
</div>
{% endwrapperfooter %}
In your features twig files, you use your wrappers :
{% wrapper box %}
This is my content
{% endwrapper %}
The following extension has 3 issues :
There is no way to store data (such as context variables) in the Twig Environnement. So when you define a {% wrapperheader NAME %}, you have basically no clean way to check if a header for NAME is already defined (in this extension, I use static properties).
When you include a twig file, it is parsed at runtime, not immediately (I mean, the included twig template is parsed while the generated file is executed, and not when the include tag is parsed). So that's not possible to know if a wrapper exists on a previousely included file when you parse the {% wrapper NAME %} tag. If your wrapper does not exist, this extension just displays what's between {% wrapper %} and {% endwrapper %} without any notice.
The idea of this extension is : when the parser meet a wrapperheader and wrapperfooter tag, the compiler store the content of the tag somewhere for a later use with the wrapper tag. But the twig context is passed to {% include %} as a copy, not by reference. So that's not possible to store the {% wrapperheader %} and {% wrapperfooter %} information inside that context, for an usage at upper level (in files that include files). I needed to use a global context too.
Here is the code, take care to change your namespaces.
First we need to create an extension that will add new token parsers to Twig.
Inside services.yml of a bundle, add the following lines to activate the extension :
parameters:
fuz_tools.twig.wrapper_extension.class: Fuz\ToolsBundle\Twig\Extension\WrapperExtension
services:
fuz_tools.twig.wrapper_extension:
class: '%fuz_tools.twig.wrapper_extension.class%'
tags:
- { name: twig.extension }
Inside your bundle, create a Twig directory.
Create the following Twig\Extension\WrapperExtension.php file :
<?php
// src/Fuz/ToolsBundle/Twig/Extension/WrapperExtension.php
namespace Fuz\ToolsBundle\Twig\Extension;
use Fuz\ToolsBundle\Twig\TokenParser\WrapperHeaderTokenParser;
use Fuz\ToolsBundle\Twig\TokenParser\WrapperFooterTokenParser;
use Fuz\ToolsBundle\Twig\TokenParser\WrapperTokenParser;
class WrapperExtension extends \Twig_Extension
{
public function getTokenParsers()
{
return array(
new WrapperHeaderTokenParser(),
new WrapperFooterTokenParser(),
new WrapperTokenParser(),
);
}
public function getName()
{
return 'wrapper';
}
}
Now we need to add the token parsers : our syntax is {% wrapper NAME %} ... {% endwrapper %} and the same with wrapperheader and wrapperfooter. So those token parsers are used to declare the tags, to retrive the wrapper's NAME, and to retrieve the body (what's between wrapper and endwrapper`).
The token parser for wrapper: Twig\TokenParser\WrapperTokenParser.php:
<?php
// src/Fuz/ToolsBundle/Twig/TokenParser/WrapperTokenParser.php
namespace Fuz\ToolsBundle\Twig\TokenParser;
use Fuz\ToolsBundle\Twig\Node\WrapperNode;
class WrapperTokenParser extends \Twig_TokenParser
{
public function parse(\Twig_Token $token)
{
$stream = $this->parser->getStream();
$name = $stream->expect(\Twig_Token::NAME_TYPE)->getValue();
$this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE);
$body = $this->parser->subparse(array($this, 'decideWrapperEnd'), true);
$this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE);
return new WrapperNode($name, $body, $token->getLine(), $this->getTag());
}
public function decideWrapperEnd(\Twig_Token $token)
{
return $token->test('endwrapper');
}
public function getTag()
{
return 'wrapper';
}
}
The token parser for wrapperheader: Twig\TokenParser\WrapperHeaderTokenParser.php:
<?php
// src/Fuz/ToolsBundle/Twig/TokenParser/WrapperHeaderTokenParser.php
namespace Fuz\ToolsBundle\Twig\TokenParser;
use Fuz\ToolsBundle\Twig\Node\WrapperHeaderNode;
class WrapperHeaderTokenParser extends \Twig_TokenParser
{
static public $wrappers = array ();
public function parse(\Twig_Token $token)
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$name = $stream->expect(\Twig_Token::NAME_TYPE)->getValue();
if (in_array($name, self::$wrappers))
{
throw new \Twig_Error_Syntax("The wrapper '$name''s header has already been defined.", $lineno);
}
self::$wrappers[] = $name;
$this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE);
$body = $this->parser->subparse(array($this, 'decideWrapperHeaderEnd'), true);
$this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE);
return new WrapperHeaderNode($name, $body, $token->getLine(), $this->getTag());
}
public function decideWrapperHeaderEnd(\Twig_Token $token)
{
return $token->test('endwrapperheader');
}
public function getTag()
{
return 'wrapperheader';
}
}
The token parser for wrapperfooter: Twig\TokenParser\WrapperFooterTokenParser.php:
<?php
// src/Fuz/ToolsBundle/Twig/TokenParser/WrapperFooterTokenParser.php
namespace Fuz\ToolsBundle\Twig\TokenParser;
use Fuz\ToolsBundle\Twig\Node\WrapperFooterNode;
class WrapperFooterTokenParser extends \Twig_TokenParser
{
static public $wrappers = array ();
public function parse(\Twig_Token $token)
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$name = $stream->expect(\Twig_Token::NAME_TYPE)->getValue();
if (in_array($name, self::$wrappers))
{
throw new \Twig_Error_Syntax("The wrapper '$name''s footer has already been defined.", $lineno);
}
self::$wrappers[] = $name;
$this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE);
$body = $this->parser->subparse(array($this, 'decideWrapperFooterEnd'), true);
$this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE);
return new WrapperFooterNode($name, $body, $token->getLine(), $this->getTag());
}
public function decideWrapperFooterEnd(\Twig_Token $token)
{
return $token->test('endwrapperfooter');
}
public function getTag()
{
return 'wrapperfooter';
}
}
The token parsers retrieve all the required information, we now need to compile those information into PHP. This PHP code will be generated by the twig engine inside a Twig_Template implementation (you can find generated classes in your cache directory). It generates code in a method, and the context of included files is not available (because the context array is not given by reference). In such a way, this is not possible to access what's inside the included file without a global context. That's why here, I use static attributes... That's not nice at all but I don't know how to avoid them (if you have ideas, please let me know! :)).
Compiler for the wrapper tag : Twig\Nodes\WrapperNode.php
<?php
// src/Fuz/ToolsBundle/Twig/Node/WrapperNode.php
namespace Fuz\ToolsBundle\Twig\Node;
class WrapperNode extends \Twig_Node
{
public function __construct($name, $body, $lineno = 0, $tag = null)
{
parent::__construct(array ('body' => $body), array ('name' => $name), $lineno, $tag);
}
public function compile(\Twig_Compiler $compiler)
{
$compiler
->addDebugInfo($this)
->write('if (isset(\\')
->raw(__NAMESPACE__)
->raw('\WrapperHeaderNode::$headers[')
->repr($this->getAttribute('name'))
->raw('])) {')
->raw("\n")
->indent()
->write('echo \\')
->raw(__NAMESPACE__)
->raw('\WrapperHeaderNode::$headers[')
->repr($this->getAttribute('name'))
->raw('];')
->raw("\n")
->outdent()
->write('}')
->raw("\n");
$compiler
->addDebugInfo($this)
->subcompile($this->getNode('body'));
$compiler
->addDebugInfo($this)
->write('if (isset(\\')
->raw(__NAMESPACE__)
->raw('\WrapperFooterNode::$footers[')
->repr($this->getAttribute('name'))
->raw('])) {')
->raw("\n")
->indent()
->write('echo \\')
->raw(__NAMESPACE__)
->raw('\WrapperFooterNode::$footers[')
->repr($this->getAttribute('name'))
->raw('];')
->raw("\n")
->outdent()
->write('}')
->raw("\n");
}
}
Compiler for the wrapperheader tag : Twig\Nodes\WrapperHeaderNode.php
<?php
// src/Fuz/ToolsBundle/Twig/Node/WrapperHeaderNode.php
namespace Fuz\ToolsBundle\Twig\Node;
/**
* #author alain tiemblo
*/
class WrapperHeaderNode extends \Twig_Node
{
static public $headers = array();
public function __construct($name, $body, $lineno = 0, $tag = null)
{
parent::__construct(array ('body' => $body), array ('name' => $name), $lineno, $tag);
}
public function compile(\Twig_Compiler $compiler)
{
$compiler
->write("ob_start();")
->raw("\n")
->subcompile($this->getNode('body'))
->write(__CLASS__)
->raw('::$headers[')
->repr($this->getAttribute('name'))
->raw('] = ob_get_clean();')
->raw("\n");
}
}
Compiler for the wrapperfooter tag : Twig\Nodes\WrapperFooterNode.php
<?php
// src/Fuz/ToolsBundle/Twig/Node/WrapperFooterNode.php
namespace Fuz\ToolsBundle\Twig\Node;
class WrapperFooterNode extends \Twig_Node
{
static public $footers = array();
public function __construct($name, $body, $lineno = 0, $tag = null)
{
parent::__construct(array ('body' => $body), array ('name' => $name), $lineno, $tag);
}
public function compile(\Twig_Compiler $compiler)
{
$compiler
->write("ob_start();")
->raw("\n")
->subcompile($this->getNode('body'))
->write(__CLASS__)
->raw('::$footers[')
->repr($this->getAttribute('name'))
->raw('] = ob_get_clean();')
->raw("\n");
}
}
The implementation is ok now. Let's try it!
Create a view named wrappers.html.twig :
{# src/Fuz/LayoutBundle/Resources/views/Default/wrappers.html.twig #}
{% wrapperheader demo %}
HEAD
{% endwrapperheader %}
{% wrapperfooter demo %}
FOOT
{% endwrapperfooter %}
Create a view named what you want.html.twig :
{# src/Fuz/HomeBundle/Resources/views/Default/index.html.twig #}
{% include 'FuzLayoutBundle:Default:wrappers.html.twig' %}
{% wrapper demo %}
O YEAH
{% endwrapper %}
This shows up :
HEAD O YEAH FOOT
There's a fairly straight forward method with Twig variables and macros.
<div class="box indent">
<div class="padding">
my code here
</div>
</div>
Create a macro:
{% macro box(content) %}
<div class="box indent">
<div class="padding">
{{ content }}
</div>
</div>
{% endmacro %}
And call it like this:
{% set content %}
my code here
{% endset %}
{{ _self.box(content) }}
Not particularly elegant but less mountains of code!
I want to do the following code:
{% set rooms = [] %}
{% set opts = {
'hasStudio': 'Studio',
'has1Bed': '1 BR',
'has2Bed': '2 BR',
'has3Bed': '3 BR',
'has4BedPlus': '4 BR+'
}
%}
{% for key, val in opts %}
{% if bldg.{key} is none %} {# PROBLEM HERE.. HOW TO FIND THIS MEMBER!? #}
{{ val }}?
{% elseif bldg.{key} %}
{{ val }}
{% else %}
No {{ val }}
{% endif %}
{% endfor %}
How do I call the member properties of bldg that are named by the value of key? I want to get the values of
bldg.hasStudio
bldg.has1Bed
bldg.has2Bed
etc....
Short answer: not directly / natively possible ... yet.
Apparently they added a new function to Twig 1.2 called attribute() which addresses exactly that need.
But as up to this day you can only download Twig 1.1.2; so 1.2 is probably not shipped with SF2 - though I cannot find a version number. (1.2 is available now!)
I tried to solve that with different tricks, but to no avail; 1.2 will fix it.
New in version 1.2: The attribute function was added in Twig 1.2.
attribute can be used to access a “dynamic” attribute of a variable:
{{ attribute(object, method) }}
{{ attribute(object, method,arguments) }}
{{ attribute(array, item) }}
But what you can do though is add a method to your class that takes care of whatever you need. something like that:
php:
class C
{
public $a = 1;
public $b = 2;
public function getValueForKey($k)
{
return $this->$k;
}
}
[ providing an instance of C to the template as 'obj' ]
twig:
{% set x = "a" %}
{{ obj.getValueForKey(x) }}
will output '1'
Use brackets syntax: bldg[key]
I wrote my own twig extension to do this. You would use it in the way that I wanted:
{% set keyVariable = 'propertyName' %}
{{ obj.access(keyVariable) }}
{# the above prints $obj->propertyName #}
Here is it:
// filename: Acme/MainBundle/Extension/AccessTwigExtension.php
namespace Acme\MainBundle\Extension;
class AccessTwigExtension extends \Twig_Extension
{
public function getFilters()
{
return array(
'access' => new \Twig_Filter_Method($this, 'accessFilter'),
);
}
public function getName()
{
return 'access_twig_extension';
}
// Description:
// Dynamically retrieve the $key of the $obj, in the same order as
// $obj.$key would have done.
// Reference:
// http://twig.sensiolabs.org/doc/templates.html
public function accessFilter($obj, $key)
{
if (is_array($obj)) {
if (array_key_exists($key, $obj)) {
return $obj[$key];
}
} elseif (is_object($obj)) {
$reflect = new \ReflectionClass($obj);
if (property_exists($obj, $key) && $reflect->getProperty($key)->isPublic()) {
return $obj->$key;
}
if (method_exists($obj, $key) && $reflect->getMethod($key)->isPublic()) {
return $obj->$key();
}
$newKey = 'get' . ucfirst($key);
if (method_exists($obj, $newKey) && $reflect->getMethod($newKey)->isPublic()) {
return $obj->$newKey();
}
$newKey = 'is' . ucfirst($key);
if (method_exists($obj, $newKey) && $reflect->getMethod($newKey)->isPublic()) {
return $obj->$newKey();
}
}
return null;
}
}
To use it in my program, I also had to add a few lines to my dependency injection:
//filename: Acme/MainBundle/DependencyInjection/AcmeMainInjection.php
// other stuff is here....
public function load(array $configs, ContainerBuilder $container)
{
// other stuff here...
$definition = new Definition('Lad\MainBundle\Extension\AccessTwigExtension');
$definition->addTag('twig.extension');
$container->setDefinition('access_twig_extension', $definition);
// other stuff here...