I'm trying to implement some sort of macro autoloading.
The idea is to define a bunch of macros and use them on all the next template files.
Here's how I'm trying to do it:
<?php
define('ROOT_FRONT', '/path/to/files/');
define('LAYOUT_DIR', ROOT_FRONT . 'layout/');
include(ROOT_FRONT . 'lib/Twig/Autoloader.php');
Twig_Autoloader::register();
$twig_loader = new Twig_Loader_Filesystem(array(LAYOUT_DIR, ROOT_FRONT));
$twig = new Twig_Environment($twig_loader, array(
'charset' => 'ISO-8859-15',
'debug' => !!preg_match('#\.int$#', $_SERVER['SERVER_NAME']),
'cache' => $_SERVER['DOCUMENT_ROOT'] . '/cache/twig/'
));
$macro_code = '';
foreach(array_filter(
array_diff(
scandir(LAYOUT_DIR . 'macros/'),
array('..','.')
),
function($file)
{
return strtolower(pathinfo($file, PATHINFO_EXTENSION)) == 'twig'
&& is_file(LAYOUT_DIR . 'macros/' . $file);
}
) as $file)
{
$info = pathinfo($file);
$macro_code .= '{% import \'macros/' . $info['basename'] . '\' as macros_' . $info['filename'] . ' %}';
}
$twig
->createTemplate($macro_code)
->render(array());
$twig->display('index.twig', array());
If I have a file, say, macro/clearfix.twig, it will generate this template code, inside $macro_code:
{% import 'macros/clearfix' as macros_clearfix %}
The code inside macro/clearfix.twig is something like this:
{% macro clearfix(index, columns) %}
{% if index is divisible by(columns) %}
<div class="clearfix visible-md-block visible-lg-block"></div>
{% endif %}
{% if index is even %}
<div class="clearfix visible-sm-block"></div>
{% endif %}
{% endmacro %}
And then, inside the index.twig, I have this:
{{ macros_clearfix.clearfix(index=2, columns=6) }}
But nothing is displayed.
However, the following code works:
{% set index = 2 %}
{% set columns = 6 %}
{% if index is divisible by(columns) %}
<div class="clearfix visible-md-block visible-lg-block"></div>
{% endif %}
{% if index is even %}
<div class="clearfix visible-sm-block"></div>
{% endif %}
What could I possibly be doing wrong?
Am I misunderstanding something or applying this incorrectly?
TL;DR:
Twig requires you to load the macros inside the file where they will be used.
Just create custom functions to do what you want.
Twig (at least v1.30) doesn't implement macro inheritance.
This requires that you load every single macro you want to use on every single file.
The only way to do this is with functions, entirelly written in PHP.
This is what I've settled with:
index.php:
<?php
define('ROOT_FRONT', '/path/to/files/');
define('LAYOUT_DIR', ROOT_FRONT . 'layout/');
include(ROOT_FRONT . 'lib/Twig/Autoloader.php');
Twig_Autoloader::register();
$twig_loader = new Twig_Loader_Filesystem(array(LAYOUT_DIR, ROOT_FRONT));
$twig = new Twig_Environment($twig_loader, array(
'charset' => 'ISO-8859-15',
'debug' => !!preg_match('#\.int$#', $_SERVER['SERVER_NAME']),
'cache' => $_SERVER['DOCUMENT_ROOT'] . '/cache/twig/'
));
// ~ magic happens here ~
foreach(include(LAYOUT_DIR . 'fn.php') as $k => $fn)
{
$twig->addFunction(new Twig_SimpleFunction("fn_$k", $fn));
}
$twig->display('index.twig', array());
fn.php:
<?php
return array(
'clearfix' => function($index, $columns){
$html = '';
if(!($index % $columns))
{
$html .= '<div class="clearfix visible-md-block visible-lg-block"></div>';
}
if(!($index & 1))
{
$html .= '<div class="clearfix visible-sm-block"></div>';
}
return $html;
}
);
index.twig:
{{ fn_clearfix(index=2, columns=6) }}
This way, all your code is neatly indexed, new functions are created automatically and it is pretty easy to extend it to your liking.
Probably this is the worst idea, but it does the job.
Macros
As of Twig 2.0, macros imported in a file are not available in child templates anymore (via an include call for instance). You need to import macros explicitly in each file where you are using them.
From https://twig.symfony.com/doc/1.x/deprecated.html
Related
How can I use paginate function from Eloquent in Slim 3 project using twig ?
This is in my controller :
$posts = Sound::paginate(2);
$this->container->view->render($response, 'admin/sounds/index.twig', [
'posts' => $posts
]);
This is the view :
{{ posts.links() }}
But it doesn't work as well as I expected :
Warning: call_user_func() expects parameter 1 to be a valid callback, no array or string given in **PATH_TO_PROJECT**\vendor\illuminate\pagination\AbstractPaginator.php on line 412
Fatal error: Call to a member function make() on null in **PATH_TO_PROJECT**\vendor\illuminate\pagination\LengthAwarePaginator.php on line 90
What I have to do to make it work ?
Can you try this:
{{ posts.links }}
I presume that links is a getter that returns links. If not, this won't work like you expect.
First, you need to include illuminate/pagination in your project (it's not included with illuminate/database):
composer require illuminate/pagination
Now paginator needs to know how to resolve current page. You should make sure this is done before using paginator, I personally put it where I'm setting up dependencies:
// $container is application's DIC container.
// Setup Paginator resolvers
Illuminate\Pagination\Paginator::currentPageResolver(function ($pageName = 'page') use ($container) {
$page = $container->request->getParam($pageName);
if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) {
return $page;
}
return 1;
});
Then in your twig template you can output pagination links. But please you should notice that paginator generates some HTML code which needs to be written to output as is so you'll need to tell twig to ignore escaping for links:
{{ posts.links | raw }}
Sorry for the late :
I didn't keep the project, I don't remember exactly how I did, but this : https://github.com/romanzipp/PHP-Slim-Pagination looks like what I did.
$app->get('/posts', function(Request $req, Response $res, $args = []) use ($cache) {
$page = ($req->getParam('page', 0) > 0) ? $req->getParam('page') : 1;
$limit = 5; // Number of posts on one page
$skip = ($page - 1) * $limit;
$count = Post::getCount([]); // Count of all available posts
return $this->view->render($res, 'post-list.twig', [
'pagination' => [
'needed' => $count > $limit,
'count' => $count,
'page' => $page,
'lastpage' => (ceil($count / $limit) == 0 ? 1 : ceil($count / $limit)),
'limit' => $limit,
],
// return list of Posts with Limit and Skip arguments
'posts' => Post::getList([
'limit' => $limit,
'skip' => $skip,
])
]);
});
In template :
{% if pagination.needed %}
<div class="ui pagination menu">
{% for i in 1..pagination.lastpage %}
<a class="{% if i == pagination.page %}active{% endif %} item" href="?page={{ i }}">{{ i }}</a>
{% endfor %}
</div>
{% endif %}
<div class="ui container">
{% for post in posts %}
<a class="item">
{# Post contents (title, url, ...) #}
</a>
{% endfor %}
</div>
I need to know when an argument for a twig macro is defined vs when a null was passed as the value. If I use "is defined" then that accounts for both conditions, as twig seems to set all undefined arguments to null.
For example, here are two calls, the first calling the macro without the argument, and the second with a null value for the argument:
{% import 'macros.twig' as macros %}
{{ macros.method() }}
{{ macros.method(null) }}
And this would be the macro definition:
{% macro method(value) %}
{# condition to determine if value is undefined or null? #}
{% endmacro %}
To have a closer look into what Twig does with the definition of macro's I've added the compiled source. It as you say, twig sets all the variables default to null, so testing whether a variable was passed to a macro will be hard
twig
{% macro vars(foo, bar, foobar) %}
{% endmacro %}
{% import _self as forms %}
{{ forms.vars(null, false) }}
compiled source
// line 1
public function macro_vars($__foo__ = null, $__bar__ = null, $__foobar__ = null, ...$__varargs__)
{
$context = $this->env->mergeGlobals(array(
"foo" => $__foo__,
"bar" => $__bar__,
"foobar" => $__foobar__,
"varargs" => $__varargs__,
));
$blocks = array();
ob_start();
try {
return ('' === $tmp = ob_get_contents()) ? '' : new Twig_Markup($tmp, $this->env->getCharset());
} finally {
ob_end_clean();
}
}
I've got a problem integrating Twig and Zend Framework 1, with the Zwig library alb/zwig,
Versions are
alb/zwig version 1.0.1
twig/twig 1.14.2
zf1 1.12.3
The result is:
module controller action call is fine
layout.twig is properly displayed (parent() works)
index.twig specific block content is not displayed
Trials:
several downgrades, no step by step debug performed
application/configs/application.ini
twig.templateDir = APPLICATION_PATH "/modules/%module%/views/scripts/"
;twig.options.cache = APPLICATION_PATH "/../cache/twig"
autoloaderNamespaces[] = "Twig_"
autoloaderNamespaces[] = "Zwig_"
autoloaderNamespaces[] = "ZFDebug_"
resources.modules[] = ""
resources.layout.layoutPath = APPLICATION_PATH "/modules/Layout/views/scripts/"
resources.layout.layout = "layout"
application/Bootstrap.php
public function _initLayout() {
$layout = $this->getPluginResource("layout")->getLayout();
$layout->setViewSuffix("twig");
}
protected function _initTwig() {
Twig_Autoloader::register();
$config = Zend_Registry::get('config');
$templatePath = array();
$view = new Zwig_View(array(
'encoding' => 'UTF-8',
'helperPath' => array(
),
));
$loader = new Twig_Loader_Filesystem();
$d = new DirectoryIterator($config->resources->frontController->moduleDirectory);
foreach ($d as $fileInfo) {
zif (!$fileInfo->isDot() && $fileInfo->isDir()) {
$moduleName = $fileInfo->getFilename();
$templatePath =
str_replace(
'%module%', $moduleName, $config->twig->templateDir
);
$loader->addPath($templatePath, strtolower($moduleName));
}
}
$zwig = new Zwig_Environment($view, $loader, array(
'debug' => true,
'cache' => APPLICATION_PATH . '/cache/twig/',
//'auto_reload' => true,
));
$view->setEngine($zwig);
$view->doctype(Zend_View_Helper_Doctype::XHTML1_STRICT);
$viewRenderer = new Zend_Controller_Action_Helper_ViewRenderer($view, array(
'viewSuffix' => 'twig',
));
Zend_Controller_Action_HelperBroker::addHelper($viewRenderer);
return $view;
}
application/modules/Login/controllers/IndexController.php
class Login_IndexController extends Zend_Controller_Action
{
public function init()
{
/* Initialize action controller here */
}
public function indexAction()
{
// action body
$this->view->toto = "texte";
}
}
application/modules/Layout/views/scripts/layout.twig
{# empty Twig template #}
Titre de la page
{% block header %}
{%endblock header %}
{% block content %}
eéâà
{% endblock content %}
{% block sidebar %}
{% endblock sidebar %}
ceci est un contenu de page
child template
# cat application/modules/Login/views/scripts/index/index.twig
{% extends '#layout/layout.twig' %}
{% block content %}
{{ parent() }}
{{ toto }}
{% endblock content %}
Did you change the default view suffix?
$this->getHelper('viewRenderer')->setViewSuffix('twig');
This should also be settable from your config file.
I found the problem, in application/Bootstrap.php _initLayout() function and application.ini related conf must be removed. This conflicts with twig inheritance.
application/Booststrap.php
/*
public function _initLayout() {
$layout = $this->getPluginResource("layout")->getLayout();
$layout->setViewSuffix("twig");
}
*/
application.ini
;resources.layout.layoutPath = APPLICATION_PATH "/modules/Layout/views/scripts/"
;resources.layout.layout = "layout"
My app consists of Zones that can have many Devices.
When viewing a Zone, it needs to display a control for each Device in the Zone.
Each Device is completely independent, so embedding the Device forms in a Zone form seems unnecessary - I only want to deal with changes to one device at a time.
Currently I'm creating a form for each device and passing them to the Zone view template:
public function viewAction($zone_id)
{
$zone = $this->getZoneById($zone_id);
$forms = array();
foreach ($zone->getDevices() as $device) {
$forms[] = $this->createForm(new DeviceType(), $device)->createView();
}
return $this->render('AcmeBundle:Zones:view.html.twig', array('zone' => $zone, 'deviceForms' => $forms));
}
And then in the view template, I'm looping through the forms:
{% for form in deviceForms %}
{% include 'AcmeBundle:Devices:control.html.twig'
with {'zone':zone, 'form':form}
%}
{% endfor %}
This seems to be working ok, but I really need to change the template that renders based on the 'type' of Device. What's the cleanest way to do this? I can do something like:
{% if form.vars.data.type == 'foo' %}
{% include 'AcmeBundle:Devices:control-foo.html.twig'
with {'zone':zone, 'form':form}
%}
{% elseif form.vars.data.type == 'bar' %}
{% include 'AcmeBundle:Devices:control-bar.html.twig'
with {'zone':zone, 'form':form}
%}
{% endif %}
but this seems like putting too much logic in the template? It would be better to assign the template to render to the form object somehow, but I've no idea if this is possible?
You must add an option 'template' or whatever in the FormType via the controller,
In the FormType you must declare the default option 'template' and pass it the the form view.
public function viewAction($zone_id)
{
$zone = $this->getZoneById($zone_id);
$forms = array();
//You define a config for each type of device (you should use parameters)
$templates = array(
'foo' => 'AcmeBundle:Devices:control-foo.html.twig',
'bar' => 'AcmeBundle:Devices:control-bar.html.twig',
);
foreach ($zone->getDevices() as $device) {
//define your template here.
$type = $device->getType();
//add a template option in the form.
$options['template'] == $templates[$type];
$forms[] = $this->createForm(new DeviceType(), $device, $options)->createView();
}
return $this->render('AcmeBundle:Zones:view.html.twig', array('zone' => $zone, 'deviceForms' => $forms));
}
Now in the DeviceType you should set the defaults options in the form, they will be merged with options we create in the controller.
public function getDefaultOptions(array $options) {
return array(
//...other options...
//this is the default template of this form
'template' => 'AcmeBundle:Devices:control.html.twig'
);
}
Then set the attribute on the form in the Builder
public function buildForm(FormBuilder $builder, array $options)
{
$builder->setAttribute('template', $options['template']);
//...your fields here...
}
And finally, set the var template in the view.
public function buildView(FormView $view, FormInterface $form)
{
$view->set('template', $form->getAttribute('template'));
}
Now you can read the "template" option in twig, and include the corresponding template
{% for form in deviceForms %}
{% include form.get('template') with {'zone':zone, 'form':form} %}
{% endfor %}
Do not forget to add lines at the beginning of the FormType
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormBuilder;
I've got a problem with displaying collection in my form.
When displaying my entity collection I've got something like this :
0
Name: myInputName
Address: myInputAddress
1
Name: myInputName
Address: myInputAddress
My question is why Symfony2 display the index...
And this for all saved entities into my collection...
Here the code I use:
$builder
->add('person', 'collection', array(
'label' => ' ',
'type' => new PersonType(),
'prototype' => true,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
))
;
In my twig file:
<div>
{{ form_widget(edit_form) }}
</div>
Help please
Sam
Removing indexes (labels) for collection items:
$builder
->add('person', 'collection', array(
...
'options' => array('label' => false)
))
;
Use key entry_options instead of options for Symfony 3 and 4
If you want to add custom labels per row you can produce the form yourself:
{{ form_start(edit_form) }}
{% for person in form.persons %}
{{ form_row(person, {'label': 'custom label per item' }) }}
{% endfor %}
{{ form_end(edit_form) }}
Note: tested on Symfony 2.3 & 2.4
This one is some days ago but because I was facing same question for Symfony 3 the answer of sectus is the correct one.
Use the
'entry_options' => ['label'=>false],
option within your builder to hide he object item.
Best Regards
You can custom the rendering of your collection for don't display the index with, by example:
{% block _FORMNAME_person_widget %}
{% spaceless %}
{% for child in form %}
{{ form_widget(child.Name) }}
{{ form_widget(child.Address) }}
{% endfor %}
{% endspaceless %}
{% endblock %}
I know this has been closed for a while. And not sure if this has been solved elsewhere. This issue is actually pretty simple to fix and I am surprised there is no documentation about this anywhere. In the PersonType or any type that is used in a collections just modify the vars['name'] in the buildView to be what you want displayed as the label.
public function buildView(FormView $view, FormInterface $form, array $options)
{
// Adjust the view based on data passed
$this->vars['name'] = $form->getData();
// Or...
$this->vars['name'] = 'Some random string';
}
If you want it dynamic, you would use the object by form->getData(). Since, in my problem, I am using a form theme, overriding the twig is not really an option for me.
Hope this helps someone.
Using #MrBandersnatch's solution below, I had to use $view->vars['name'] instead of $this->vars['name'] (Symfony 2.3).
(apologies for not adding this as a comment on #MrBandersnatch's answer, I've not got enough reputation yet).