megclaypool
7/23/2019 - 6:25 AM

How to set values of existing arrays in Twig

[How to set values of existing arrays in Twig]

I've got a few ways to solve this. The first method is a filter that can be added to twig which are a bit of a pain to set up but slick to use in twig templates.

I'll also include the vanilla method u

I'm saving this for future reference, because it's kinda neat, but it doesn't work well to set nested array values and I'm unwilling to work on it further when I was able to get the upsert filter to do what I want. I won't be including this in future projects unless something changes.

You have to add it to Pattern Lab: Create a set_element.function.php file in patterns/_twig-components/functions/
It should contain the following:

<?php

/**
 * @file
 * Add "set_element" function for Pattern Lab.
 *
 * To use: {% set arr = set_element(arr, 'key', 'value') %}
 */

$function = new Twig_SimpleFunction('set_element', function ($data, $key, $value) {
  // If $data is empty, we're going to make an array
  if (empty($data)) {
    $data = [];
  }
  // If it's an array, assign $value to $data[$key]
  if (is_array($data)) {
    $data[$key] = $value;
    return $data;
  }
  // If it's an object, assign $value to $data->$key
  elseif (is_object($data)) {
    $data->$key = $value;
    return $data;
  }
  // Or it's impossible and just put it back the way it was
  else {
    return $data;
  }
});

And to Timber: Add this inside the add_filter('timber/twig', function (\Twig_Environment $twig) { in RadicatiSite.php

// To use: {% set arr = set_element(arr, 'element', 'value') %}
$twig->addFunction(new Timber\Twig_Function('set_element', function ($data, $key, $value) {

  $exploded_data = explode(".", $data);
  var_dump($exploded_data);

  $temp = &$data;
  foreach ($exploded as $key) {
    $temp = &$temp[$key];
  }
  $temp = $value;
  unset($temp);


  // If $data is empty, we're going to make an array
  if (empty($data)) {
    $data = [];
  }
  // If it's an array, assign $value to $data[$key]
  if (is_array($data)) {
    $data[$key] = $value;
    var_dump($data);
    return $data;
  }
  // If it's an object, assign $value to $data->$key
  elseif (is_object($data)) {
    $data->$key = $value;
    return $data;
  }
  // Or it's impossible and just put it back the way it was
  else {
    return $data;
  }
}));

This filter is somewhat more complicated than it used to be because I wanted to be able to use it with objects (such as post!) without the filter returning an array. Instead, the object is converted into an array temporarily but after the merge it is converted back into an object of the original class :) Accomplishing this requires that the filter have access to a PHP function. Making that PHP function available to the filter without redefining it (and thus causing an error) every time the filter is called ended up being a bit tricky:

The PHP function is:

/**
 * Function to convert class of given object
 */
function convertObjectClass($array, $final_class)
{
  return unserialize(sprintf(
    'O:%d:"%s"%s',
    strlen($final_class),
    $final_class,
    strstr(serialize($array), ':')
  ));
}
  • To make it available to Pattern Lab, we include it in the upsert.filter.php, but enclose it in a conditional so if it's already declared it doesn't get redeclared. (I admit, this is kind of a weasel-y solution. I couldn't figure out where to put php functions to make them universally available to PL's twig functions and filters...)
  • To make it available to Timber/Twig in WordPress, copy and paste it into the bottom of RadicatiSite.php or some other PHP file that gets called in functions.php.

While you're there, add the filter to Timber/Twig by copying and pasting the following into the add_filter('timber/twig', function (\Twig_Environment $twig) { in RadicatiSite.php:

// I don't know why, but for some reason Timber\Twig_Filter (which is what the documentation recommends) was *not* working here, and was generating errors.
$twig->addFilter(new Twig_SimpleFilter('upsert', function ($arr1, $arr2) {

  if ($arr1 instanceof Traversable) {
    $arr1 = iterator_to_array($arr1);
  } elseif (!is_array($arr1)) {
    throw new Twig_Error_Runtime(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as first argument.', gettype($arr1)));
  }

  if ($arr2 instanceof Traversable) {
    $arr2 = iterator_to_array($arr2);
  } elseif (!is_array($arr2) && !is_object($arr2)) {
    $arr2 = [];
  }

  if (is_object($arr2)) {

    $class = get_class($arr2);

    return convertObjectClass(array_merge(
      (array) $arr2,
      (array) $arr1
    ), $class);
  }
  return array_merge($arr2, $arr1);
}));

Finally, add the filter to Pattern Lab (including the needed PHP function inside its little conditional): Create a upsert.filter.php file in patterns/_twig-components/functions/
It should contain the following:

<?php

/**
 * This filter is similar to the Twig merge filter:
 *
 * This will allow you to merge a null value, allowing
 * you to use the following pattern:
 *
 * {% set classes = ['class1', 'class2']|merge(classes) %}
 *
 * If classes is empty/null, then a new array will be
 * created containing class1 and class2.
 *
 * Note: This is compatible with objects, so if for some
 * reason you wanted to upsert an array into the post
 * object you could do that, and it would return an object
 * of the same type you upsert into.
 *
 * Please note that if you are upserting a key/value pair
 * and the key you upsert into the array already exists,
 * its value will be overwritten with the new value.
 *
 *
 * Example uses:
 * To add values, whose key will be the next avaiable
 * integer:
 * {% set post = ['one', 'two', 'three']|upsert(post) %}
 *
 * To add key/value pairs to your object:
 * {% set post = {'FOO': 'BAR', 'xyzzy': 'the cake is a lie'}|upsert(post) %}
 *
 * Or even:
 * {% set test = {'key1': 'value1', 'key2': ['value2a', 'value2b'], 'key3': {'key3a': 'value3a', 'key3b': 'value3b'}}|upsert(test) %}
 */

$filter = new Twig_SimpleFilter('upsert', function ($arr1, $arr2) {
  if ($arr1 instanceof Traversable) {
    $arr1 = iterator_to_array($arr1);
  } elseif (!is_array($arr1)) {
    throw new Twig_Error_Runtime(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as first argument.', gettype($arr1)));
  }

  if ($arr2 instanceof Traversable) {
    $arr2 = iterator_to_array($arr2);
  } elseif (!is_array($arr2) && !is_object($arr2)) {
    $arr2 = [];
  }

  if (is_object($arr2)) {

    $class = get_class($arr2);

    return convertObjectClass(array_merge(
      (array) $arr2,
      (array) $arr1
    ), $class);
  }
  return array_merge($arr2, $arr1);
});

if (!function_exists('convertObjectClass')) {

  /**
   * Function to convert class of given object
   */
  function convertObjectClass($array, $final_class) {
    return unserialize(sprintf(
      'O:%d:"%s"%s',
      strlen($final_class),
      $final_class,
      strstr(serialize($array), ':')
    ));
  }

}

Vanilla Default Method for Setting Values of an Existing Array in Twig Templates

The Problem

If you just try the obvious: {% set arr['element'] = 'value' %} You will get the following error:

Unexpected token "punctuation" of value "[" ("end of statement block" expected) in ...

The Solution

Instead, you have to use the merge filter: {% set arr = arr|merge({'element' = 'value'}) %}

Conditional with Ternary Operators

{% set card = card.level ? card|merge({'level': card.level}) : card|merge({'level': 1})%}

Super-Fancy Nested Array

(Holy crap, this convoluted spaghetti code is how you set a nested array value in twig! You have to merge a merge, as many layers deep as you need... Inception!) {% set card = card|merge({ button: card.button|merge({href: card.href}) }) %}