Twig merge for json with string of numbers as key - php

I'm trying to merge hashes with Twig to output JSON.
My problem is that some of my keys are using strings of numbers and twig converts it to integers.
My code :
{% set rows = {} %}
{% for key, val in row %}
{% set rows = rows|merge({ (key) : val }) %}
{% endfor %}
{{ { 'report': { 'metric': metric, 'rows': rows, 'tot': tot, 'min': min, 'max': max } }|json_encode|raw }}
Which outputs
{"report":{"metric":"sessions","rows":["5","4","4","3","7","4","4"],"tot":"31","min":"0","max":"7"}}
I also tried replacing my keys with number_format, but since I'm stripping all non-numeric characters, the output is the same.
{% set rows = {} %}
{% for key, val in row %}
{% set rows = rows|merge({ (key)|number_format(0,'','') : val }) %}
{% endfor %}
{{ { 'report': { 'metric': metric, 'rows': rows, 'tot': tot, 'min': min, 'max': max } }|json_encode|raw }}
The result expected goes like this:
{"report":{"metric":"sessions","rows":{"20180423":"5","20180424":"4","20180425":"4","20180426":"3","20180427":"7","20180428":"4","20180429":"4"},"tot":"31","min":"0","max":"7"}}
Is there any way I can prevent Twig from changing my keys to integers?
Found this post, but it doesn't work for me since my keys are strings of numbers.
key value being replaced by 'key' when using merge() in twig

Twig's merge filter relies on PHP's array_merge function and the doc says :
Values in the input array with numeric keys will be renumbered with
incrementing keys starting from zero in the result array.
And string containing only numbers are considered numeric.
Solution:
The simplest solution would be to change the keys format from "20180423" to "2018-04-23" which would make it non-numeric.
If you really need to keep your numeric keys, you could create a custom filter to merge arrays the way you want:
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
class AppExtension extends AbstractExtension
{
public function getFilters()
{
return array(
new TwigFilter('mymerge', array($this, 'merge')),
);
}
public function merge($baseArray, $arrayToMerge)
{
foreach ($arrayToMerge as $key => $value) {
$baseArray[$key] = $value;
}
return $baseArray;
}
}
Then
{% set test = {"1234": "2", "2345": "3"} %}
{% set rows = {"test": "1"} %}
{% set rows = rows|mymerge(test) %}
{{ { 'report': { 'rows': rows } }|json_encode|raw }}
Would output
{"report":{"rows":{"test":"1","1234":"2","2345":"3"}}}

Related

Twig: How to concat and translate strings?

I tried the answers of this question, but it's not working. It's weird and I cannot reproduce the behavior:
I cleared the cache
Concat works (it output's the right string)
The concated string as a variable is not translateable
The concated string pasted as a string is translateable
Edit: obj2arr casts the object to an array, to make it iterable. prepareForTwig is just using trim(), etc. - the string is outputted correctly.
Edit 2: {% set transVar = (key|prepareForTwig) %} (without prefix) doesn't work as well.
yml:
# Resources/translations/messages.en.yml
my:
keywords:
keyword1: K1
keyword2: K2
# ...
twig:
{# my.twig.html #}
{% for key, value in data|obj2arr %}
{% set transVar = 'my.keywords.' ~ (key|prepareForTwig)) %}
{{ transVar }}<br/> {# output, e.g.: my.keywords.keyword1 #}
{{ transVar|trans}}<br/> {# output, e.g.: my.keywords.keyword1 #}
{{ 'my.keywords.keyword1'|trans }} {# output: K1 #}
{% endfor %}
EDIT:
CustomTwigExtension.php
class CustomTwigExtension extends \Twig_Extension
{
public function getFilters()
{
return array(
new \Twig_SimpleFilter('obj2arr', array($this, 'obj2arrFilter')),
new \Twig_SimpleFilter('prepareForTwig', array($this, 'prepareForTwigFilter')),
);
}
public function obj2arrFilter($obj)
{
return (array)$obj;
}
public function prepareForTwigFilter($str) {
$str = trim($str);
$str = strtolower($str);
$str = substr($str, 2, strlen($str)); // obj2arr() prefixes "*"
return $str;
}
public function getName()
{
return 'custom_twig_extension';
}
}
Thanks in advance!
Others examples didn't seem to work in my case, this is what I ended up using:
{{ "my.prefix.#{ myVariable }.my.postfix" | trans }}
The translation string has to be between double quotes.
Every answer of outputting concated translationstrings was right, example:
{% 'my.prefix.' ~ extendByKeyword | trans }}
The problem was a weird generated space:
(array) $obj added * (there are two spaces after the star) as a prefix to the key-variable.
I used substr() to get rid of the * (star + 1 space), but didn't notice/expect the second space.
After comparing the strings with strlen(), I found the cause.
Thanks to #Artamiel and #CarlosGranados for your help.

Incremental for-loop in twig

I'm trying to add values in an array using twig. Does twig have an incremental for-loop feature? I'm aware of the standard for-loop in twig (e.g. {% for k in v %}), but I'm interested in a loop where I may specify things as detailed below in pure PHP:
<?php
//adding values in an array
$quantities = array('23', '23', '4', '45', '45');
$sum = 0;
for ($i = 0; $i < count($quantities); $i++) {
$sum += $quantities[$i];
}
echo "Sum: " . $sum . "\n";
?>
This is pretty much exactly what I'm looking to do with twig.
Thanks for any help, All.
You can check the Loop options in Twig
{% for quantity in quantities %}
// Do your stuff here with each individual quantity
// If you want to access the index ($i in your php sample)
{{ quantities[loop.index0] }}
{% endfor %}
Just take it as a reference and check the link provided to adapt it to your configuration.
Your approach could be improved even in php "natively"
Do you know about array_sum?
And of course, in twig you can create a twig_extension that in tandem with twig_filter could help you obtain what you want in a smart way.
To create a twig_extension with custom filter (remember to follow link that I have provided for "twig estension"):
public function getFilters()
{
return array(
new \Twig_SimpleFilter('sum', 'array_sum'),
);
}
Then you can use sum keyword into your twig template, as a filter of course
{% set sum = quantities|array_sum %}
If you really want the twig representation of your PHP code, this will be:
{% set sum = 0 %}
{% for value in quantities %}
{% set sum = sum + value %}
{% endfor %}
Sum: {{ sum }}
Anyway, #DonCallisto's approach is better.

Symfony2 Twig Get Total Count for Child Entity

The following entities exist, Farm, Barn and Animals. A Farm can have many Barns and a Barn many Animals.
When displaying a Farm in a TWIG template the number of Animals should be shown as well.
What is the best way to do this?
I have create a TWIG extension which allows me to easily show the number of barns.
public function totalFieldFilter($data, $getField='getTotal') {
$total = count($data->$getField());
return $total;
}
In my template I would use {{ farm|totalField('getBarns') }}, I could easily extend this to write another custom function like so:
public function totalFieldFilter($farm) {
$total = 0;
foreach($farm->getBarns() AS $barn) {
$total += count($barn->getAniamls());
}
return $total;
}
Although this would work, is there a better way and can it be made more generic? What if I wanted to count Legs on Animals? Or how many Doors a Barn has, I would have to write a custom TWIG extension each time.
Use Entity accessors :
{% for farm in farms %}
{{ farm.name }}
{% set barns = farm.getBarns() %}
Barns count = {{ barns|length }}
{% for barn in barns %}
{% set animals = barn.getAnimals() %}
{{ barn.name }} animals count : {{ animals|length }}
{% endfor %}
{% endfor %}
You are looking for the length filter
When used with an array, length will give you the number of items. So, if your array is farm.barns, you can just use {{ farm.barns|length }}

Setting element of array from Twig

How can I set member of an already existing array from Twig?
I tried doing it next way:
{% set arr['element'] = 'value' %}
but I got the following error:
Unexpected token "punctuation" of value "[" ("end of statement block"
expected) in ...
There is no nice way to do this in Twig. It is, however, possible by using the merge filter:
{% set arr = arr|merge({'element': 'value'}) %}
If element is a variable, surround it with brackets:
{% set arr = arr|merge({(element): 'value'}) %}
I ran into this problem but was trying to create integer indexes instead of associative index like 'element'.
You need to protect your index key with () using the merge filter as well:
{% set arr = arr|merge({ (loop.index0): 'value'}) %}
You can now add custom index key like ('element'~loop.index0)
If initialization only need:
{% set items = { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'unknown' } %}
I have tried #LivaX 's answer but it does not work , merging an array where keys are numeric wont work ( https://github.com/twigphp/Twig/issues/789 ).
That will work only when keys are strings
What I did is recreate another array ( temp) from the initial array (t) and make the keys a string , for example :
{% for key , value in t%}
{% set temp= temp|merge({(key~'_'):value}) %}
{% endfor %}
t keys : 0 , 1 , 2 ..
temp keys : 0_, 1_ , 2_ ....
You can also use the following syntax:
{% set myArray = myArray + myArray2 %}
Just use this like {% set arr={'key':'value'} %} (with no blank space after the :), it works well.
But when I use it inside a for loop, to make it an array, it does not work outside of the for scope.
{% for group in user.groups %}
{% set foo={'loop.index0':'group.id'} %}
{% set title={'loop.index0':'group.title'} %}
{{ title }} //it work
{% else %}
{% set foo={'0':'-1'} %}
{% set title={'0':'未分组'} %}
{% endfor %}
{{ title }} //it does not work, saying title is not defined
{% set links = {} %}
{# Use our array to wrap up our links. #}
{% for item in items %}
{% set links = links|merge({ (loop.index0) : {'url': item.content['#url'].getUri(), 'text': item.content['#title']} }) %}
{% endfor %}
{%
set linkList = {
'title': label,
'links': links
}
%}
{% include '<to twig file>/link-list.twig'%}
Thanks for this thread -- I was also able to create an array with (loop.index0) and send to twig.
I've found this issue very annoying, and my solution is perhaps orthodox and not inline with the Twig philosophy, but I developed the following:
$function = new Twig_Function('set_element', function ($data, $key, $value) {
// Assign value to $data[$key]
if (!is_array($data)) {
return $data;
}
$data[$key] = $value;
return $data;
});
$twig->addFunction($function);
that can be used as follows:
{% set arr = set_element(arr, 'element', 'value') %}
Adding my answer in case anyone needs to update the array when merge doesn't work because it just appends to the end of an array instead of providing the ability to change an existing value.
Let's say you have an array words_array like below:
Object {
0: "First word"
1: "Second word"
2: "Third word"
}
In order to update "Second word", you can do the following:
{% set words_array = {(1): 'New word'} + words_array %}
The resulting array would be:
Object {
0: "First word"
1: "New word"
2: "Third word"
}
You can take it one step further if you are using a loop and use the loop.index0 variable something like the following:
{% for word in words_array %}
{% if word == 'Second word' %}
{% set words_array = {(loop.index0): 'New word'} + words_array %}
{% endif %}
{% endfor %}
You can declare the array as follows
{% set arr = [{'element1': 'value1','element2' : 'value2'},{'element1': 'value1','element2' : 'value2'},{'element1': 'value1','element2' : 'value2'}] %}
I had a multi dimension array. The only solution I could find out is create a new temporary array and update/add the information, which was further passed on to another twig function.
I had this problem sometime ago. Imagine you have an array like this one:
data = {
'user': 'admin',
'password': 'admin1234',
'role': 'admin',
'group': 'root',
'profile': 'admin',
'control': 'all',
'level': 1,
'session': '#DFSFASADASD02',
'pre_oa': 'PRE-OA',
'hepa_oa': 'HEPA-OA',
'pre_ra': 'HEPA-RA',
'hepa_ra': 'HEPA-RA',
'deodor_ra': 'DEODOR-RA'
}
So, you want to show this data in two rows, but remove the password from that list. To this end, split in 2 arrays will be easy with the slice filter. However, we have to remove the password. For that reason, I'm using this snippet. The idea is to put all the elements lesser than the data elements size divided by 2. To calculate this we use the filter length. Now to get the index of the current element we user loop.index. And finally we *push an associative element in the left or right array. An associative array has two components key and value. To reference an array key in twit we operator () and we use the merge filter to push into the array as shown here {% set left_list = left_list|merge({ (key): value }) %}
This is the complete solution.
{% set left_list = {} %}
{% set right_list = {} %}
{% set limit = data|length // 2 %}
{% for key, value in data|cast_to_array %}
{% if key != 'password' %}
{% if loop.index <= limit %}
{% set left_list = left_list|merge({ (key): value }) %}
{% else %}
{% set right_list = right_list|merge({ (key): value }) %}
{% endif %}
{% endif %}
{% endfor %}
{% for key, value in left_list %}
<p>
<label for="{{key}}">{{key}}</label>
<input type="text" name="{{key}}" id="{{key}}" value="{{value}}"
class="text ui-widget-content ui-corner-all">
</p>
{% endfor %}

Reference array from key in for loop with Twig

In Twig I am trying to iterate over a potentially incomplete array using a fixed-length for loop so I can show what values are empty.
In PHP this would be simplified to:
for($i =0; $i <= $limit; $i++) {
if($data[$i]) {
echo $data[$i];
}
)
The only thing is that in Twig I am having problems using the key (index) of the loop to reference a value in an array, this is what I've tried and expected to work, but doesn't:
{% for i in range(0, limit-1) %}
{{ data.i }}
{% endfor %}
I could obviously use array_pad() to pad out my array in my controller, but surely there must be a way to do this in twig?
How about this:
{% for i in range(0, limit-1) %}
{% if data[i] is defined %}
{{ data[i] }}
{% endif %}
{% endfor %}

Categories