Конкатенація назв змінних і ключів масивів у Twig

OpenCartBot - 03.10.2026
Конкатенація назв змінних і ключів масивів у Twig

Шаблонізатор Twig обробляє назви змінних на етапі компіляції шаблону. Через це конструкція на кшталт {{ var_~language_id }} не дає змінну var_1 чи var_2. Twig сприймає var_ як окрему невизначену змінну, перетворює її на порожній рядок і приєднує до неї значення language_id. У результаті на сторінку виводиться просто «1». Із цією ситуацією стикається кожен, хто пише багатомовні модулі для OpenCart, де контролер передає в шаблон змінні title_1, title_2, entry_name_3 з ідентифікатором мови в назві. Twig має кілька способів зібрати назву змінної або ключ масиву з частин, і кожен із них має свої межі застосування залежно від версії шаблонізатора.

В Twig символ тильда (~) виконує конкатенацію рядків. Обидва операнди перетворюються на рядки, тому 'title_' ~ 5 дає 'title_5', а 'price_' ~ product.id ~ '_old' дає 'price_42_old'. Сам по собі оператор формує лише рядок-значення. Щоб цей рядок став назвою змінної, його потрібно передати в конструкцію, яка звертається до змінної або елемента масиву за ім'ям, обчисленим під час виконання. Таких конструкцій у Twig чотири: функція attribute(), квадратні дужки, динамічний оператор крапки та інтерполяція рядків у поєднанні з першими трьома.

Змінна _context і функція attribute()

Кожен шаблон Twig має службову змінну _context. Це асоціативний масив, у якому лежать усі змінні поточного контексту: передані з контролера, оголошені через {% set %} та глобальні. В OpenCart туди потрапляє весь масив $data, який контролер передає в $this->load->view(). Отже, змінна title_1 доступна як елемент _context з ключем 'title_1'.

Функція attribute() дозволяє звернутися до атрибута, методу або властивості об'єкта чи масиву, коли їхня назва зберігається у змінній або генерується виразом. Поєднання цих двох механізмів і дає класичний запис:

{{ attribute(_context, 'var_' ~ language_id) }}

Типовий приклад із форми налаштувань модуля OpenCart 3, де заголовок зберігається окремо для кожної мови:

{% for language in languages %}
  <div class="input-group">
    <span class="input-group-addon">
      <img src="language/{{ language.code }}/{{ language.code }}.png" title="{{ language.name }}" />
    </span>
    <input type="text"
           name="module_banner_title_{{ language.language_id }}"
           value="{{ attribute(_context, 'module_banner_title_' ~ language.language_id) }}"
           class="form-control" />
  </div>
{% endfor %}

Функція працює з будь-яким масивом або об'єктом, тому _context тут лише окремий випадок. Запис attribute(product, 'name_' ~ language_id) звертається до елемента масиву product, а attribute(order, 'get' ~ field|capitalize) викликає метод об'єкта. Третім аргументом функція приймає масив параметрів для методу: attribute(object, method_name, [arg1, arg2]).

Варто враховувати статус функції в новіших версіях. Функцію attribute оголошено застарілою починаючи з Twig 3.15, а її замінником є оператор крапки, який тепер приймає будь-який вираз у круглих дужках. Водночас функція залишиться доступною в Twig 4.0 для плавнішого переходу. Для шаблонів OpenCart 3, які працюють на Twig 2, це не має жодного значення. У проєктах на Twig 3.15 і вище виклик attribute() генерує deprecation-повідомлення в логах.

Квадратні дужки як універсальний спосіб

Оскільки _context є звичайним PHP-масивом, до нього можна звертатися через квадратні дужки з обчисленим ключем:

{{ _context['var_' ~ language_id] }}

Цей запис еквівалентний виклику attribute() для масивів, коротший і працює в усіх гілках Twig від 1.x до 3.x без жодних попереджень. Квадратні дужки підходять для вкладених структур, де динамічною є лише частина шляху:

{{ module_description[language.language_id].title }}
{{ product_option['option_' ~ option.option_id]['value_' ~ value.id] }}
{{ settings[store_id]['theme_' ~ theme_code ~ '_width'] }}

Перший рядок показує найпоширеніший патерн OpenCart: масив, проіндексований ідентифікатором мови. Саме так ядро OpenCart зберігає описи товарів, категорій та інформаційних сторінок у формах адмінпанелі (product_description[language.language_id].name).

Квадратні дужки надійно працюють із масивами та з об'єктами, які реалізують інтерфейс ArrayAccess. Для виклику методу об'єкта за динамічною назвою потрібна функція attribute() або динамічна крапка.

Динамічний оператор крапки в Twig 3.15+

Починаючи з версії 3.15 оператор крапки приймає вираз у круглих дужках. Запис із _context набуває такого вигляду:

{{ _context.('var_' ~ language_id) }}
{{ product.('name_' ~ language_id) }}
{{ user.('get' ~ field|capitalize)() }}

Документація Twig називає два сценарії для цього синтаксису. Перший стосується атрибутів зі спецсимволами: запис user.('first-name') еквівалентний неробочому user.first-name, де дефіс сприймався б як оператор мінус. Другий сценарій стосується саме динамічних назв, обчислених зі змінної. До версії 3.15 обидва завдання вирішувалися через attribute().

Динамічна крапка поєднується з null-safe оператором ?., доданим у Twig 3.23, який повертає null замість винятку, коли лівий операнд дорівнює null. Запис order?.('shipping_' ~ method) безпечно спрацює, навіть якщо змінна order порожня.

У версії 3.28 механізм поширили на макроси. Раніше виклик макросу, назва якого відома лише під час виконання, вимагав функції attribute(), а оператор крапки макроси не підтримував. Тепер макрос обирається так:

{% import "forms.twig" as forms %}
{% set field = 'text' %}
{{ forms.(field)(name, value) }}

Інтерполяція рядків

Twig підтримує вбудовування виразів у рядки через синтаксис #{...}. Інтерполяція працює виключно в рядках у подвійних лапках. У одинарних лапках #{...} залишається звичайним текстом. Отже, такі два записи дають однаковий результат:

{{ attribute(_context, 'entry_' ~ field) }}
{{ attribute(_context, "entry_#{field}") }}

Інтерполяція зручна, коли назва складається з кількох змінних частин:

{{ _context["module_#{code}_status_#{store_id}"] }}

Усередині #{} допускається будь-який вираз, зокрема фільтри та звернення до атрибутів: "field_#{language.language_id}_#{type|lower}". Для назв з одним змінним фрагментом тильда читається простіше, для назв із трьома й більше фрагментами інтерполяція зменшує кількість лапок і операторів.

Динамічні ключі під час створення масивів

Конкатенація потрібна і в зворотному напрямку, коли шаблон сам формує масив із ключами на зразок title_1. Літерал хеша в Twig сприймає ключ без лапок як рядок, тому { key: value } створює елемент із ключем 'key'. Щоб ключ обчислювався, його беруть у круглі дужки:

{% set titles = {} %}
{% for language in languages %}
  {% set titles = titles|merge({ ('title_' ~ language.language_id): language.name }) %}
{% endfor %}

Тут прихована одна особливість фільтра merge. Для масивів він використовує PHP-функцію array_merge, яка перенумеровує цілочисельні ключі. Якщо записати titles|merge({ (language.language_id): language.name }), ключі 1, 2, 3 перетворяться на 0, 1, 2, і зв'язок з ідентифікатором мови зникне. Рядковий префікс на кшталт 'lang_' або 'title_' зберігає ключі в незмінному вигляді.

Змінну з динамічною назвою через {% set %} створити неможливо. Тег set очікує статичне ім'я, тому для таких задач використовують проміжний масив, як у прикладі вище, і звертаються до нього через квадратні дужки.

Перевірка існування та значення за замовчуванням

Динамічна назва може вказувати на змінну, якої немає в контексті. Поведінка Twig у цьому випадку залежить від опції strict_variables. Коли вона вимкнена, звернення до відсутнього ключа повертає null і виводить порожній рядок. Коли ввімкнена, Twig кидає RuntimeError з текстом «Key "var_5" does not exist». Щоб шаблон стабільно працював за будь-якого налаштування, перевірку роблять явно:

{% if attribute(_context, 'title_' ~ language_id) is defined %}
  {{ attribute(_context, 'title_' ~ language_id) }}
{% endif %}

{{ _context['title_' ~ language_id]|default('') }}
{{ _context['title_' ~ language_id] ?? heading_title }}

Тест defined вміє перевіряти існування динамічного атрибута, а фільтр default та оператор ?? пригнічують помилку для невизначених ключів. Оператор ?? зручний для каскадних підстановок, коли за відсутності перекладу потрібно показати значення мови за замовчуванням.

Пріоритет операторів у виразах з конкатенацією

Тильда має свій пріоритет відносно арифметичних операторів і ??, і в Twig 3 він відрізняється від запланованого для Twig 4. Вираз 'var_' ~ language_id + 1 у Twig 3.x обчислюється як ('var_' ~ language_id) + 1, тобто намагається додати одиницю до рядка. Використання ~ разом із + чи - без дужок, що уточнюють пріоритет, викликає deprecation починаючи з Twig 3.15, а в Twig 4.0 оператори + та - матимуть вищий пріоритет за ~. Аналогічна зміна стосується оператора ??: запис foo ?? bar ~ baz вважається застарілим, і його слід переписати як (foo ?? bar) ~ baz для поведінки Twig 3 або foo ?? (bar ~ baz) для поведінки Twig 4.

Для динамічних назв це означає просте правило: будь-яку арифметику в ключі беруть у дужки. Запис _context['var_' ~ (language_id + 1)] однаково працює в усіх версіях і не генерує попереджень.

Сумісність у модулях для OpenCart 2.3, 3 та 4

Вибір синтаксису для модуля OpenCart визначається версією Twig, яку постачає ядро. Гілка 3.0.x у своєму composer.json вимагає twig/twig версії ^2.4.8, сучасна гілка OpenCart 4 вимагає twig/twig ^3.3.7, а частина власників магазинів на OpenCart 3 самостійно оновлює Twig до третьої версії заради підтримки PHP 8.x. Розподіл способів за сумісністю виглядає так:

  • _context['var_' ~ id] працює в Twig 1.x, 2.x і 3.x без попереджень і підходить для модулів, які розповсюджуються під кілька версій OpenCart одночасно.
  • attribute(_context, 'var_' ~ id) працює в усіх версіях і генерує deprecation-повідомлення лише в Twig 3.15 і новіших.
  • _context.('var_' ~ id) вимагає Twig 3.15+ і спричиняє синтаксичну помилку компіляції на OpenCart 3 зі штатним Twig 2.
  • Динамічний виклик макросу forms.(name)() вимагає Twig 3.28+.

Найстійкіше рішення для власних модулів полягає в тому, щоб узагалі не створювати змінних із ідентифікатором мови в назві. Контролер формує вкладений масив:

foreach ($languages as $language) {
    $language_id = $language['language_id'];
    $data['title'][$language_id] = $this->config->get('module_banner_title_' . $language_id);
}

Шаблон звертається до нього звичайними квадратними дужками:

<input type="text"
       name="module_banner_title[{{ language.language_id }}]"
       value="{{ title[language.language_id] }}"
       class="form-control" />

Такий запис компілюється однаково в Twig 2 і Twig 3, не залежить від _context і статусу функції attribute(). Поле форми з назвою module_banner_title[1] PHP отримує як масив, тому модель налаштувань зберігає всі переклади одним записом у таблиці setting з serialized = 1. Конкатенацію назв змінних у цьому підході залишають для випадків, коли структуру даних задає чужий код: ядро OpenCart, сторонній модуль або тема, змінювати яку немає можливості.


Рекомендовані модулі


Інші статті