Макросы и переиспользуемые блоки
Создание функций для шаблонов и переиспользуемых блоков кода
Основы макросов
Макросы - это переиспользуемые функции в шаблонах, которые принимают параметры и возвращают HTML код. Они полезны для создания сложной логики форматирования и вывода данных.
Создание простого макроса
<!-- /src/macros/ui.html -->
{# Макрос для создания кнопки #}
{% macro button(text, type, size, icon, class) %}
<button type="{{ type ?? 'button' }}"
class="btn btn-{{ size ?? 'md' }} {{ class ?? 'btn-primary' }}">
{% if icon %}
<i class="fas fa-{{ icon }}"></i>
{% endif %}
{{ text }}
</button>
{% endmacro %}
{# Макрос для создания бейджа #}
{% macro badge(text, type, pill) %}
<span class="badge bg-{{ type ?? 'primary' }} {{ pill ? 'rounded-pill' : '' }}">
{{ text }}
</span>
{% endmacro %}
{# Макрос для статусных индикаторов #}
{% macro status_indicator(active, text_active, text_inactive) %}
<span class="status-indicator {{ active ? 'active' : 'inactive' }}">
<i class="fas fa-circle"></i>
{{ active ? (text_active ?? 'Активен') : (text_inactive ?? 'Неактивен') }}
</span>
{% endmacro %}
Использование макросов
<!-- Импорт макросов -->
{% import 'macros/ui.html' as ui %}
<!-- Использование макросов -->
{{ ui.button('Сохранить', 'submit', 'lg', 'save', 'btn-success') }}
{{ ui.button('Отмена', 'button', 'md', 'times', 'btn-secondary') }}
<p>Статус заказа: {{ ui.badge('Выполнен', 'success', true) }}</p>
{{ ui.status_indicator(user.is_active, 'Пользователь активен', 'Пользователь заблокирован') }}
Сложные макросы
Макрос для форм
<!-- /src/macros/forms.html -->
{# Макрос для поля ввода #}
{% macro input(name, label, type, value, required, placeholder, help, errors) %}
<div class="form-group mb-3">
{% if label %}
<label for="{{ name }}" class="form-label">
{{ label }}
{% if required %}<span class="text-danger">*</span>{% endif %}
</label>
{% endif %}
<input type="{{ type ?? 'text' }}"
id="{{ name }}"
name="{{ name }}"
class="form-control {{ errors[name] ? 'is-invalid' : '' }}"
value="{{ value ?? old(name) }}"
{{ required ? 'required' : '' }}
{% if placeholder %}placeholder="{{ placeholder }}"{% endif %}>
{% if help %}
<div class="form-text">{{ help }}</div>
{% endif %}
{% if errors[name] %}
<div class="invalid-feedback">{{ errors[name] }}</div>
{% endif %}
</div>
{% endmacro %}
{# Макрос для выпадающего списка #}
{% macro select(name, label, options, value, required, placeholder, help, errors) %}
<div class="form-group mb-3">
{% if label %}
<label for="{{ name }}" class="form-label">
{{ label }}
{% if required %}<span class="text-danger">*</span>{% endif %}
</label>
{% endif %}
<select id="{{ name }}" name="{{ name }}"
class="form-select {{ errors[name] ? 'is-invalid' : '' }}"
{{ required ? 'required' : '' }}>
{% if placeholder %}
<option value="">{{ placeholder }}</option>
{% endif %}
{% foreach options as option_value => option_label %}
<option value="{{ option_value }}"
{{ (value ?? old(name)) == option_value ? 'selected' : '' }}>
{{ option_label }}
</option>
{% endforeach %}
</select>
{% if help %}
<div class="form-text">{{ help }}</div>
{% endif %}
{% if errors[name] %}
<div class="invalid-feedback">{{ errors[name] }}</div>
{% endif %}
</div>
{% endmacro %}
{# Макрос для текстового поля #}
{% macro textarea(name, label, value, rows, required, placeholder, help, errors) %}
<div class="form-group mb-3">
{% if label %}
<label for="{{ name }}" class="form-label">
{{ label }}
{% if required %}<span class="text-danger">*</span>{% endif %}
</label>
{% endif %}
<textarea id="{{ name }}" name="{{ name }}"
class="form-control {{ errors[name] ? 'is-invalid' : '' }}"
rows="{{ rows ?? 3 }}"
{{ required ? 'required' : '' }}
{% if placeholder %}placeholder="{{ placeholder }}"{% endif %}>{{ value ?? old(name) }}</textarea>
{% if help %}
<div class="form-text">{{ help }}</div>
{% endif %}
{% if errors[name] %}
<div class="invalid-feedback">{{ errors[name] }}</div>
{% endif %}
</div>
{% endmacro %}
Использование макросов форм
{% import 'macros/forms.html' as forms %}
<form method="POST" action="/users">
{{ csrf() }}
{{ forms.input(
'login',
t('form.login'),
'text',
null,
true,
t('form.enter_login'),
t('form.login_help'),
errors
) }}
{{ forms.input(
'email',
t('form.email'),
'email',
null,
true,
null,
null,
errors
) }}
{{ forms.select(
'role',
t('form.role'),
{
'user': t('role.user'),
'moderator': t('role.moderator'),
'admin': t('role.admin')
},
null,
true,
t('form.select_role'),
null,
errors
) }}
{{ forms.textarea(
'bio',
t('form.bio'),
null,
4,
false,
t('form.tell_about_yourself'),
t('form.bio_help'),
errors
) }}
<button type="submit" class="btn btn-primary">
{{ t('button.create') }}
</button>
</form>
Макросы для таблиц
Макросы для отображения данных
<!-- /src/macros/tables.html -->
{# Макрос для ячейки с датой #}
{% macro date_cell(value, format, relative) %}
{% if value %}
{% if relative %}
<span title="{{ value | date('Y-m-d H:i:s') }}">
{{ value | time_ago }}
</span>
{% else %}
{{ value | date(format ?? 'Y-m-d H:i') }}
{% endif %}
{% else %}
<span class="text-muted">—</span>
{% endif %}
{% endmacro %}
{# Макрос для статусной ячейки #}
{% macro status_cell(value, statuses) %}
{% set status_config = statuses[value] ?? {'text': value, 'class': 'secondary'} %}
<span class="badge bg-{{ status_config.class }}">
{% if status_config.icon %}
<i class="fas fa-{{ status_config.icon }}"></i>
{% endif %}
{{ status_config.text }}
</span>
{% endmacro %}
{# Макрос для аватара пользователя #}
{% macro user_avatar_cell(user, size) %}
<div class="d-flex align-items-center">
{% if user.avatar %}
<img src="{{ user.avatar }}"
class="rounded-circle me-2"
width="{{ size ?? 32 }}"
height="{{ size ?? 32 }}"
alt="{{ user.name }}">
{% else %}
<div class="avatar-placeholder rounded-circle me-2 d-flex align-items-center justify-content-center"
style="width: {{ size ?? 32 }}px; height: {{ size ?? 32 }}px; background: #dee2e6;">
{{ user.name | first | upper }}
</div>
{% endif %}
<div>
<div class="fw-bold">{{ user.name }}</div>
{% if user.email %}
<small class="text-muted">{{ user.email }}</small>
{% endif %}
</div>
</div>
{% endmacro %}
{# Макрос для действий в таблице #}
{% macro action_buttons(actions, item) %}
<div class="btn-group btn-group-sm">
{% foreach actions as action %}
{% if not action.condition or _self.check_condition(action.condition, item) %}
{% if action.type == 'link' %}
<a href="{{ _self.build_url(action.url, item) }}"
class="btn btn-outline-{{ action.style ?? 'primary' }}"
{% if action.title %}title="{{ action.title }}"{% endif %}
{% if action.confirm %}onclick="return confirm('{{ action.confirm }}')"{% endif %}>
{% if action.icon %}
<i class="fas fa-{{ action.icon }}"></i>
{% endif %}
{{ action.label ?? '' }}
</a>
{% elseif action.type == 'button' %}
<button type="button"
class="btn btn-outline-{{ action.style ?? 'primary' }}"
onclick="{{ action.onclick }}"
{% if action.title %}title="{{ action.title }}"{% endif %}
{% if action.confirm %}data-confirm="{{ action.confirm }}"{% endif %}>
{% if action.icon %}
<i class="fas fa-{{ action.icon }}"></i>
{% endif %}
{{ action.label ?? '' }}
</button>
{% endif %}
{% endif %}
{% endforeach %}
</div>
{% endmacro %}
{# Вспомогательные функции #}
{% macro build_url(template, item) %}
{% set url = template %}
{% foreach item as key, value %}
{% set url = url | replace('{' ~ key ~ '}', value) %}
{% endforeach %}
{{ url }}
{% endmacro %}
{% macro check_condition(condition, item) %}
{% set parts = condition | split(' ') %}
{% set field = parts[0] %}
{% set operator = parts[1] ?? '==' %}
{% set expected = parts[2] ?? true %}
{% switch operator %}
{% case '==' %}
{{ item[field] == expected }}
{% case '!=' %}
{{ item[field] != expected }}
{% case '>' %}
{{ item[field] > expected }}
{% case '<' %}
{{ item[field] < expected }}
{% default %}
{{ item[field] }}
{% endswitch %}
{% endmacro %}
Использование макросов таблиц
{% import 'macros/tables.html' as tables %}
{% set user_statuses = {
'0': {'text': 'Активен', 'class': 'success', 'icon': 'check'},
'1': {'text': 'Заблокирован', 'class': 'danger', 'icon': 'ban'}
} %}
{% set actions = [
{
'type': 'link',
'url': '/users/{id}',
'icon': 'eye',
'label': 'Просмотр',
'style': 'info',
'title': 'Просмотр профиля'
},
{
'type': 'link',
'url': '/users/{id}/edit',
'icon': 'edit',
'label': 'Редактировать',
'style': 'warning',
'condition': 'ban != 1'
},
{
'type': 'button',
'onclick': 'deleteUser({id})',
'icon': 'trash',
'label': 'Удалить',
'style': 'danger',
'confirm': 'Удалить пользователя?',
'condition': 'role != admin'
}
] %}
<table class="table table-striped">
<thead>
<tr>
<th>Пользователь</th>
<th>Статус</th>
<th>Регистрация</th>
<th>Последний визит</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% foreach users as user %}
<tr>
<td>{{ tables.user_avatar_cell(user, 40) }}</td>
<td>{{ tables.status_cell(user.ban, user_statuses) }}</td>
<td>{{ tables.date_cell(user.created_at, 'd.m.Y') }}</td>
<td>{{ tables.date_cell(user.last_login, null, true) }}</td>
<td>{{ tables.action_buttons(actions, user) }}</td>
</tr>
{% endforeach %}
</tbody>
</table>
Макросы для виджетов
Статистические виджеты
<!-- /src/macros/widgets.html -->
{# Макрос для статистической карточки #}
{% macro stat_card(title, value, icon, color, change, link) %}
<div class="stat-card {{ link ? 'clickable' : '' }}"
{% if link %}onclick="window.location.href='{{ link }}'"{% endif %}>
<div class="stat-card-icon bg-{{ color ?? 'primary' }}">
<i class="fas fa-{{ icon ?? 'chart-bar' }}"></i>
</div>
<div class="stat-card-content">
<h3 class="stat-value">{{ value | number }}</h3>
<p class="stat-title">{{ title }}</p>
{% if change %}
<div class="stat-change {{ change > 0 ? 'positive' : 'negative' }}">
<i class="fas fa-{{ change > 0 ? 'arrow-up' : 'arrow-down' }}"></i>
{{ change | abs }}%
</div>
{% endif %}
</div>
</div>
{% endmacro %}
{# Макрос для прогресс-бара #}
{% macro progress_bar(value, max, label, color, striped, animated) %}
{% set percentage = (value / max * 100) | round(1) %}
<div class="progress-wrapper">
{% if label %}
<div class="progress-label d-flex justify-content-between">
<span>{{ label }}</span>
<span>{{ value }} / {{ max }} ({{ percentage }}%)</span>
</div>
{% endif %}
<div class="progress">
<div class="progress-bar bg-{{ color ?? 'primary' }}
{{ striped ? 'progress-bar-striped' : '' }}
{{ animated ? 'progress-bar-animated' : '' }}"
role="progressbar"
style="width: {{ percentage }}%"
aria-valuenow="{{ value }}"
aria-valuemin="0"
aria-valuemax="{{ max }}">
</div>
</div>
</div>
{% endmacro %}
{# Макрос для карточки с загрузкой #}
{% macro loading_card(height, text) %}
<div class="loading-card d-flex flex-column justify-content-center align-items-center"
style="height: {{ height ?? '200px' }};">
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">Loading...</span>
</div>
{% if text %}
<p class="text-muted">{{ text }}</p>
{% endif %}
</div>
{% endmacro %}
{# Макрос для пустого состояния #}
{% macro empty_state(icon, title, description, action_text, action_url) %}
<div class="empty-state text-center py-5">
<div class="empty-state-icon mb-3">
<i class="fas fa-{{ icon ?? 'inbox' }} fa-3x text-muted"></i>
</div>
{% if title %}
<h3 class="empty-state-title">{{ title }}</h3>
{% endif %}
{% if description %}
<p class="empty-state-description text-muted">{{ description }}</p>
{% endif %}
{% if action_text and action_url %}
<a href="{{ action_url }}" class="btn btn-primary">
{{ action_text }}
</a>
{% endif %}
</div>
{% endmacro %}
Использование виджетов
{% import 'macros/widgets.html' as widgets %}
<!-- Статистические карточки -->
<div class="row">
<div class="col-md-3">
{{ widgets.stat_card(
'Всего пользователей',
totalUsers,
'users',
'primary',
12.5,
'/admin/users'
) }}
</div>
<div class="col-md-3">
{{ widgets.stat_card(
'Активные заказы',
activeOrders,
'shopping-cart',
'success',
-3.2,
'/admin/orders'
) }}
</div>
<div class="col-md-3">
{{ widgets.stat_card(
'Выручка за месяц',
monthlyRevenue | money('₽'),
'dollar-sign',
'warning',
25.8
) }}
</div>
</div>
<!-- Прогресс-бары -->
<div class="row mt-4">
<div class="col-md-6">
{{ widgets.progress_bar(
usedStorage,
totalStorage,
'Использование дискового пространства',
'info',
false,
false
) }}
</div>
<div class="col-md-6">
{{ widgets.progress_bar(
completedTasks,
totalTasks,
'Выполнение плана',
'success',
true,
true
) }}
</div>
</div>
<!-- Пустое состояние -->
{% if not orders or orders | length == 0 %}
{{ widgets.empty_state(
'shopping-cart',
'Заказов пока нет',
'Здесь будут отображаться все заказы пользователей',
'Посмотреть каталог',
'/catalog'
) }}
{% endif %}
Рекурсивные макросы
Макрос для древовидного меню
<!-- /src/macros/navigation.html -->
{# Рекурсивный макрос для многоуровневого меню #}
{% macro render_menu(items, level, current_path) %}
{% if items and items | length > 0 %}
<ul class="nav-menu level-{{ level }}">
{% foreach items as item %}
{% set is_active = current_path == item.url or
(item.children and _self.has_active_child(item.children, current_path)) %}
<li class="nav-item {{ is_active ? 'active' : '' }}
{{ item.children ? 'has-children' : '' }}">
<a href="{{ item.url }}" class="nav-link">
{% if item.icon %}
<i class="fas fa-{{ item.icon }}"></i>
{% endif %}
{{ item.title }}
{% if item.badge %}
<span class="nav-badge">{{ item.badge }}</span>
{% endif %}
{% if item.children %}
<i class="nav-arrow fas fa-chevron-right"></i>
{% endif %}
</a>
{% if item.children %}
{{ _self.render_menu(item.children, level + 1, current_path) }}
{% endif %}
</li>
{% endforeach %}
</ul>
{% endif %}
{% endmacro %}
{# Вспомогательный макрос для проверки активных дочерних элементов #}
{% macro has_active_child(children, current_path) %}
{% foreach children as child %}
{% if child.url == current_path %}
{{ true }}
{% elseif child.children and _self.has_active_child(child.children, current_path) %}
{{ true }}
{% endif %}
{% endforeach %}
{{ false }}
{% endmacro %}
{# Макрос для хлебных крошек из древовидной структуры #}
{% macro breadcrumb_from_tree(items, current_path, breadcrumb) %}
{% foreach items as item %}
{% if item.url == current_path %}
{{ breadcrumb | push(item) | json }}
{% elseif item.children %}
{% set result = _self.breadcrumb_from_tree(
item.children,
current_path,
breadcrumb | push(item)
) %}
{% if result %}
{{ result }}
{% endif %}
{% endif %}
{% endforeach %}
{% endmacro %}
Использование рекурсивных макросов
{% import 'macros/navigation.html' as nav %}
{% set menu_items = [
{
'title': 'Главная',
'url': '/',
'icon': 'home'
},
{
'title': 'Каталог',
'url': '/catalog',
'icon': 'shopping-bag',
'children': [
{
'title': 'Электроника',
'url': '/catalog/electronics',
'children': [
{'title': 'Смартфоны', 'url': '/catalog/electronics/phones'},
{'title': 'Ноутбуки', 'url': '/catalog/electronics/laptops'}
]
},
{'title': 'Одежда', 'url': '/catalog/clothing'},
{'title': 'Книги', 'url': '/catalog/books'}
]
},
{
'title': 'О нас',
'url': '/about',
'icon': 'info-circle'
}
] %}
<nav class="main-navigation">
{{ nav.render_menu(menu_items, 0, request.path) }}
</nav>
<!-- Хлебные крошки -->
{% set breadcrumb_data = nav.breadcrumb_from_tree(menu_items, request.path, []) | json_decode %}
{% if breadcrumb_data %}
<nav class="breadcrumb-nav">
<ol class="breadcrumb">
{% foreach breadcrumb_data as crumb %}
{% if loop.last %}
<li class="breadcrumb-item active">{{ crumb.title }}</li>
{% else %}
<li class="breadcrumb-item">
<a href="{{ crumb.url }}">{{ crumb.title }}</a>
</li>
{% endif %}
{% endforeach %}
</ol>
</nav>
{% endif %}
Лучшие практики
Организация макросов
- Группируйте макросы по функциональности в отдельные файлы
- Используйте описательные имена параметров
- Предусматривайте значения по умолчанию для необязательных параметров
- Документируйте сложные макросы комментариями
Оптимизация производительности
<!-- Хорошо: кеширование результатов -->
{% macro expensive_calculation(data) %}
{% set cache_key = 'calc_' ~ (data | json | md5) %}
{% set cached_result = cache_get(cache_key) %}
{% if cached_result %}
{{ cached_result }}
{% else %}
{% set result = heavy_calculation(data) %}
{% php %}
cache_set($cache_key, $result, 3600); // 1 час
{% endphp %}
{{ result }}
{% endif %}
{% endmacro %}
Переиспользуемость
<!-- Создавайте гибкие макросы -->
{% macro card(title, content, footer, class, style, size) %}
<div class="card {{ class ?? '' }}
{{ size ? 'card-' ~ size : '' }}
{{ style ? 'card-' ~ style : '' }}">
{% if title %}
<div class="card-header">{{ title }}</div>
{% endif %}
<div class="card-body">{{ content }}</div>
{% if footer %}
<div class="card-footer">{{ footer }}</div>
{% endif %}
</div>
{% endmacro %}
Отлично! Вы изучили все основы компонентов, макетов и макросов MNRFY. Теперь можете создавать сложные переиспользуемые элементы интерфейса.