Detect if twig macro argument was passed or null - macros

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();
}
}

Related

Eloquent paginate function in Slim 3 project using twig

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>

Imported Twig macro displays nothing i.e. not working

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

Access form.vars.value in Symfony subform using Twig

I am working on a Symfony 2.7 WebApp and I would like to use a custom Form Widget for one of the entites. The Widgets needs to access the form.vars.value. This works fine as long as the Widget is uses within the main form. But when using the Widget in a subform, form.vars.value is empty.
The classes used within the form:
class AdressBookEntry {
// The main phone number of this contact: Type PhoneNumber
protected $mainPhoneNumber;
//...getter and setter for mainPhoneNumber
// An array of Addresses
protected $addresses;
//...getter and setter for addresses
...
}
class Address {
// The phone number of this address: Type PhoneNumber
protected $phoneNumber;
//...getter and setter for phoneNumber
...
}
class PhoneNumber {
...
}
The custom Form Types for theses classes:
// Custom FormType for AddressBookEntries
class AdressBookEntryType extends AbstractType {
...
public function buildForm(FormBuilderInterface $builder, array $options) {
// Type 'phone_number_edit' is registered in services.yml
$builder
->add('mainPhoneNumber', 'phone_number_edit', array(
'label' => '...',
...
))
->add('addresses', 'collection', array(
'label' => '...',
...
));
}
}
// Custom FormType for Address
class AddressType extends AbstractType {
...
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('mainPhoneNumber', 'phone_number_edit', array(
'label' => '...',
...
))
...;
}
}
The custom Widget for the PhoneNumberEdit
{% block phone_number_edit_widget %}
...
{{ dump(form.vars.value) }}
...
The PhoneNumberEdit for the main form (representing the AddressBookEntry) works fine. The dump statement shows the content of the assigned PhoneNumber object.
Within the Subform of the addresses collection however, the form.vars.value variable is empty. The dump shows just "".
So, how do I access form.vars.value within the subform? How can the widget recognize wether it is being uses within the main form or a subform?
UPDATE:
Some additional information as asked in the comments:
#Jeet: As described before the dump shows an empty value/string: ""
#DOZ: This is the Twig code:
{{ form_start(form) }}
{{ form_errors(form) }}
{{ form_row(form.name) }}
{{ form_widget(mainPhoneNumber) }}
<ul data-prototype"{{ _self.addressItem(form.addresses.vars.prototype)|e('html_attr') }}" >
{% for address in form.addresses %}
{{ _self.addressItem(address) }}
{% endfor %}
</u>
...
{{ form_end(form) }}
{% macro addressItem(address) %}
<li>
{{ form_widget(address.phoneNumber) }}
...
</li>
{% endmacro %}
Use value instead of form.vars.value
{% block phone_number_edit_widget %}
...
{{ dump(value) }}
...

Symfony2 multiple forms in one template

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;

symfony 2 form display default value

I'm working with Symfony 2.0.14 and I would like to display the default value in my form template.
Well a FormType is bound to an entity, when I want to add extra field, I know the option property_path = false allow to add non-entity fields, right ?
When I m in the opposite case, I want to set an entity field without a form field.
Ok I just have to give a default entity to "createForm".
Howewver how can I render it in my template form ?
Controller code :
public function newAction(Request $request)
{
$game = new Game();
$local = new Role();
$visitor = new Role();
$local->setType('LOCAL');
$visitor->setType('VISITOR');
$game->addRole($local);
$game->addRole($visitor);
$form = $this->createForm(new GameType(), $game);
GameType code :
public function buildForm(FormBuilder $builder, array $options){
$builder->add('teams', 'collection', array( 'type' => new RoleType()));
}
RoleType code :
public function buildForm(FormBuilder $builder, array $options){
$builder->add('type', 'text'); // <= I would like read only for end-User
$builder->add('score', 'integer');
form template :
{% for role in form.teams %}
<li>
<div class="role-team">
{{ role.type }} {# WRONG way, how to do ? #}
{{ form_row(role.score) }}
</div>
</li>
{% endfor %}
If you want just to display your entity field value (without passing the entire entity to the view) you can print it with:
{{ form.vars.value.type }}
(assuming your role entity has type property).
EDIT: i realized you're inside the loop. Try figuring out the right property path using:
{% for role in form.teams %}
{% debug role %}
{% endfor %}