Макросы и переиспользуемые блоки

Создание функций для шаблонов и переиспользуемых блоков кода

Основы макросов

Макросы - это переиспользуемые функции в шаблонах, которые принимают параметры и возвращают 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. Теперь можете создавать сложные переиспользуемые элементы интерфейса.