I have 2 TWIG renderblock's working on a base and standard template. The two blocks render on the page, everything before {% extends "base.php" %} and after {% block body %}{% endblock %} in base.htm does not render, I see why as I have used renderblock not render which should render the whole template. 1 how do I get whole template to render and 2. {% block head %} will not render unless I use a for loop, I am sure there is a better way of doing this. Edit 1: added $data2.
API
$template = $twig->loadTemplate('event.htm');
echo $template->renderBlock('head', (array('heads' => $data2)));
echo $template->renderBlock('content', (array('events' => $data)));
base.htm
<html>
<head>
{% block head %}
{% for head in heads %}
<title>{{ head.title }}</title>
<meta charset="UTF-8">
</head>
<body>
<h1>{{ head.title2 }}</h1>
{% endfor %}
{% endblock %}
{% block body %}{% endblock %}
</body>
</html>
event.htm
{% extends "base.php" %}
{% block content %}
{% for event in events %}
{{event.uri}}
{{event.desc}}
{% else %}
no events!
{% endfor %}
{% endblock %}
$data2
Array ( [0] => Array ( [id] => 1 [uri] => /event1/1 [title] => some title ) )
1/ As soon as you are rendering a block, you get the content of that block, nothing more.
You need to render the whole template using:
$template = $twig->loadTemplate('event.htm');
echo $template->render(array(
'heads' => $data2,
'events' => $data,
));
2/ You need to use a loop because there are big chances $data2 contains an array or an object instead of the expected header. You should use a string instead, or know in which index you can access your header. This is difficult to help you as I don't know what does contain your $data2 variable, but an ugly solution could be to use the reset function this way:
echo $template->render(array(
'heads' => reset($data2),
'events' => $data,
));
2/ If you know which index (in this case 0) as suggested by Alain
echo $template->render(array(
'heads' => $data2[0],
'events' => $data,
));
Related
TL;DR: I am currently struggling with the idea how to elegantly generate CRUDs/GRID using Twig.
Long story:
I have a Phalcon app with AngularJs, templating system is Twig.
PHP is suppose to prepare a template for CRUD that contains:
List od entities, table that contains proper headers, columns, row items, action buttons
Simple and advanced search
Mass actions block
Main buttons - Add/Import/Export etc.
AngularJs is responsible for:
Paginating results in 2 modes: Ajax Loaded entities, All entities at once
Changing number of entities per page
Displaying various forms in modals - Add/Edit/Import
Filtering entities - simple and advanced search
Everything is working right now but I am not particularly fond of how it works.
For example a simple entity with few fields needs a following template:
{% extends 'index.twig' %}
{% block containerAttr %}ng-controller="InventoryController as ctrl"{% endblock %}
{% set ctrlName = 'ctrl' %}
{% set columnWidth = 'col-xs-12' %}
{% set tableClass = 'table table-bordered table-striped table-hover' %}
{% block header %}
{% include '#crud/crud/header.twig' %}
{% endblock %}
{% block content %}
{% embed '#crud/crud/main-box.twig' %}
{% block tableHeader %}
{% autoescape false %}
{{ crud.tableHeader('Id', 'id') }}
{{ crud.tableHeader('Nazwa', 'name') }}
<th class="col-xs-1">Actions</th>
{% endautoescape %}
{% endblock %}
{% block tableRow %}
<td ng-bind="e.id"></td>
<td ng-bind="e.name"></td>
{% endblock %}
{% endembed %}
{% embed '#crud/crud/form.twig' %}
{% block modalBody %}
{% autoescape false %}
<div class="row">
{{ crud.input('ctrl.entity.name', 'Name', 12) }}
</div>
{% endautoescape %}
{% endblock %}
{% endembed %}
{% embed '#crud/crud/upload.twig' %}{% endembed %}
{% embed '#crud/crud/advancedSearch.twig' %}{% endembed %}
<script>
window.xdata = {{ xdata | json_encode | raw }};
</script>
{% endblock %}
crud in this template is responsible for generating form fields compatible with AngularJs.
Idea is: To create new CRUDs/Grids with the least amount of work required. While still giving a room for some flexibility.
Right now to make a CRUD/Grid inside my application there is a really little work required:
PHP Controller needs to contain only one line:
class SubCategoryController extends CrudBase
{
protected $entityClass = InventoryCategory::class;
}
Thanks to extending it is really easy to customize every part of it.
Model needs to have just a few fields:
class InventoryCategory extends ModelBase
{
/** #var integer */
public $id;
/** #var string */
public $name;
public function initialize()
{
$this->setSource('inventory_subcategory');
$this::setup(['castOnHydrate' => true]);
}
}
Even the AngularJs controller contains only URLs and one line:
class InventoryCategoryController extends CrudBase {
constructor($injector, $scope) {
super($injector, $scope);
this.urlSave = '/inventory/category/save';
this.urlImport = '/inventory/category/import';
this.urlExport = '/inventory/category/export';
this.urlDelete = '/inventory/category/delete';
this.init({
advancedSearch: false
});
}
}
angular.module('App').controller('InventoryCategoryController', InventoryCategoryController);
Yes I use ES6, application is not compatible with older browsers anyways and it is admin panel so I do not care about older browsers.
But now only templates remains a tedious work to copy it over and change it. Not to mention they do look pretty ugly.
So my idea was to use Annotation Reader (Phalcon has one) to read fields from Model, then transform it into a base configuration, check if there is custom configuration in json and merge it. Then based on configuration, generate this whole template on the fly.
So the grand question is:
I was wondering if I can create some class that extends Twig_Template to generate this template on the fly?
I have seen Twig cache and it should be pretty easy but then how can i use it? How can i render it? Will extend still work?
In this regex101 demo, I'm trying to get the contents of each block:
{% block works %}
This works
{% endblock %}
{% block main_block %}
{% block sub_block %}
Does not work
{% endblock %} #ends here
This is not covered
{% endblock %}
It works right if a block has no blocks inside, but, in the second block (main_block) it fails to match all of its content because one block is found inside.
I need a regex pattern that captures both main_block and sub_block blocks, without ending on the first {% endblock %} tag.
Right now, my expression looks like this: \{\%\s*block\s?([a-z0-9\_]*?)\s?\%\}(.*?)(?>(?:{\%\s*block\s?([a-z0-9\_]*?)\s?\%\}|(?R))*\{\%\s?endblock\s?\1?\s?\%\}\is
EDIT: my question was marked as duplicate, but I don’t think it has to do with that problem, mine is in a different scenario where blocks could be inside of blocks indefinitely.
Regex Used: '/(\ *){%\sblock.*\s%}((?:.*|\n)*?)\1{%\sendblock.*\s%}/'
For this to work, it does assume that the blocks are properly formatted, but if they are formatted correctly, this will work in most cases (it uses the spaces before each block to determine whether the tags match or not).
Try this out for size:
<?php
function getBlockText($blockText)
{
$regex = '/(\ *){%\sblock.*\s%}((?:.*|\n)*?)\1{%\sendblock.*\s%}/';
$recursedMatches= [];
preg_match_all($regex, $blockText, $matches);
for ($i = 0; $i < count($matches[2]); $i++) {
if(preg_match($regex, $matches[2][$i])){
array_push($recursedMatches, getBlockText($matches[2][$i]));
} else {
array_push($recursedMatches, $matches[2][$i]);
}
}
return $recursedMatches;
}
$str = '{% block works %}
This works
{% endblock %}
{% block main_block %}
{% block sub_block %}
Does not work
{% endblock %} #ends here
This is not covered
{% block sub_block %}
Does not work
{% endblock %}
{% endblock %}';
print_r(getBlockText($str));
Output:
Array
(
[0] =>
This works
[1] => Array
(
[0] =>
Does not work
[1] =>
Does not work
)
)
I have an array of user records (0 indexed, from a database query), each of which contains an array of fields (indexed by field name). For example:
Array
(
[0] => Array
(
[name] => Fred
[age] => 42
)
[1] => Array
(
[name] => Alice
[age] => 42
)
[2] => Array
(
[name] => Eve
[age] => 24
)
)
In my Twig template, I want to get all the users where the age field is 42 and then return the name field of those users as an array. I can then pass that array to join(<br>) to print one name per line.
For example, if the age was 42 I would expect Twig to output:
Fred<br>
Alice
Is this possible to do in Twig out of the box, or would I need to write a custom filter? I'm not sure how to describe what I want in a couple of words so it may be that someone else has written a filter but I can't find it by searching.
Final solution was a mix of what has been posted so far, with a couple of changes. The pseudocode is:
for each user
create empty array of matches
if current user matches criteria then
add user to matches array
join array of matches
Twig code:
{% set matched_users = [] %}
{% for user in users %}
{% if user.age == 42 %}
{% set matched_users = matched_users|merge([user.name|e]) %}
{% endif %}
{% endfor %}
{{ matched_users|join('<br>')|raw }}
merge will only accept an array or Traversable as the argument so you have to convert the user.name string to a single-element array by enclosing it in []. You also need to escape user.name and use raw, otherwise <br> will be converted into <br> (in this case I want the user's name escaped because it comes from an untrusted source, whereas the line break is a string I've specified).
In twig you can merge the for ( .... in ....) with the if condition like :
{% for user in users if user.age == 42 %}
{{ user.name }}{{ !loop.last ? '<br>' }}
{% endfor %}
Edit: This syntax is deprecated, and we are advised to use |filter as a replacement for the for...if syntax.
Twig Filter: filter (The name of the filter is filter)
Twig Deprecated Features
You can apply a filter on the array you apply for loop on, like this:
{% for u in user|filter((u) => u.age == 42) -%}
<!-- do your stuff -->
{% endfor %}
{% for user in users %}
{% if user.age == 42 %}
{{ user.name|e }}<br>
{% endif %}
{% endfor %}
in alternative you can create an array of elements
{% set aUserMatchingCreteria %}
{% for user in users %}
{% if user.age == 42 %}
{% aUserMatchingCreteria = aUserMatchingCreteria|merge(user.name) %}
{% endif %}
{% endfor %}
{{ aUserMatchingCreteria|join('<br>') }}
Since Twig 2.10, the recommended way to conditionally exclude array elements is the filter filter. As was noted in some previous answers, loop.last has some issues, but you can simply flip the logic and use loop.first, which will work consistently:
{% for user in users|filter((u) => u.age == 42) %}
{{ loop.first ?: '<br/>' }}
{{ user.name|e }}
{% endfor %}
I am using twig files for some of the forms in my PHP application. I did everything following this link
http://twig.sensiolabs.org/doc/extensions/i18n.html#
which tells how twig files can be translated using Twig_Extensions_Extension_I18n. I believe i have followed all the steps but still i cannot seem to solve the problem.
This is how my PHP code looks like
require_once 'Twig/Autoloader.php';
Twig_Autoloader::register();
$twig_loader = new Twig_Loader_Filesystem(APP_PATH . 'corporate/views');
$GLOBALS['twig'] = new Twig_Environment($twig_loader, array(
'cache' => TMP_PATH . 'twig_cache',
'debug' => DEBUG,
'auto_reload' => true
));
$GLOBALS['twig']->addExtension(new Twig_Extensions_Extension_I18n());
return $GLOBALS['twig']->
render('path/to/twig/template'.$formTemplate,
array_merge($formVariables,
array(
"help" => isset($helpbox) ? $helpbox : array(),
"type" => $current_data['type'],
"integration_id" => $current_data['action_comp'],
"automation_type" => $current_data['automation_type']
)
)
);
There is another PHP file where i read all the twig files and run the following command as following.
$exe = 'xgettext --force-po --default-domain emarketeer --keyword="pgettext:1c,2" -c -j -o /tmp/messages.po '.escapeshellarg($file).' >/dev/null 2>&1';
system($exe);
this is how my twig template ($formTemplate) looks like
{% extends 'integrations/_mixed/automation_form_wrapper.twig' %}
{% block formular %} {% import "_utils/form.twig" as form %}
{{ form.boxlabel( 'Name (choose automation Name)'|trans, "Name") }}
{{ form.input("action_name", action_name.value, "", "",
action_name.collection ) }}
{% endblock %}
The imported twig template ('_utils/form.twig') as form looks like this
{% macro boxlabel(label, label_for, tooltip, class, extra) %} {% set
label_parts = label|split("\n") %} {{- label_parts[0] -}} {% if tooltip %} {% endif %} {% if label_parts[1] %}
{{ label_parts[1]}} {%
endif %} {% endmacro %}
{% macro input(name, value, type, class, extra) %}
{% endmacro %}
Can anyone please tell me if i am missing something. There will be a lot of code which i cannot defin or explain but i hope i have delivered the idea and the question. Any help would be greatly appreciated.
I'm new to Symfony, so this is most certain a simple mistake from my side.
I get the following error: Variable "worker" does not exist.
The template looks like this:
{% extends "NTSBSServiceBundle::layout.html.twig" %}
{% block body %}
<h1>Rapportera</h1>
{% for worker in workers if workers %}
{{ worker.name }}
{% else %}
<em>Det finns inga öppna protokoll för närvarande...</em>
{% endfor %}
{% endblock %}
And the controller method look like this:
/**
* List all open protocols, grouped by worker.
*
* #Route("/", name="report")
* #Method("GET")
* #Template()
*/
public function indexAction()
{
$em = $this->getDoctrine()->getManager();
$workers = $em->getRepository('NTSBSServiceBundle:Worker')->findAll();
return array(
'workers' => $workers,
);
}
I have checked, and $workers does contain entities from the database. The twig gets rendered. If I remove the for-loop, naturally the error message disappears.
Hoping that someone can explain to me what I'm doing wrong.
UPDATE:
Have confirmed that the correct controller is used by exiting in indexAction(). If i do a print_r of $workers, I get the following output:
Array
(
[0] => NT\SBSServiceBundle\Entity\Worker Object
(
[id:NT\SBSServiceBundle\Entity\Worker:private] => 2
[name:protected] => Worker 1
[mail:protected] => worker1#example.com
[phone:protected] => 123456789
)
[1] => NT\SBSServiceBundle\Entity\Worker Object
(
[id:NT\SBSServiceBundle\Entity\Worker:private] => 3
[name:protected] => Worker 2
[mail:protected] => worker2#example.com
[phone:protected] => 123456789
)
)
Also I have tried to change the rendering-method by changing from annotation to using the render-method, as such:
return $this->render('NTSBSServiceBundle:Report:index.html.twig',array( 'workers' => $workers ));
You can not do {% for i in x if x %}
You have to do
{% if x | length > 0 %}
{% for i in x %}
instructions
{% endfor %}
{% endif %}
Use twig doc : http://twig.sensiolabs.org/doc
I do always loop through arrays in Twig like that:
{% for b in books %}
{{ b.name }}
{% endfor %}
{% if not books %}
<i>{% trans %}utils.nothing{% endtrans %}</i>
{% endif %}
But your error looks like a variable is missing. What is your error message?
Symfony2 Variable “name” does not exist
or
Variable "worker" does not exist.