Циклы и итерации

Полное руководство по работе с циклами в шаблонах MNRFY

Цикл foreach

Базовый синтаксис

<!-- Простая итерация по массиву -->
{% foreach users as user %}
    <div class="user-item">
        <h3>{{ user.name }}</h3>
        <p>{{ user.email }}</p>
    </div>
{% endforeach %}

<!-- Итерация с ключами -->
{% foreach menu as key => item %}
    <li data-key="{{ key }}">
        <a href="{{ item.url }}">{{ item.title }}</a>
    </li>
{% endforeach %}

Переменная loop

В каждом цикле foreach доступна специальная переменная loop:

Свойство Описание Тип
loop.index Номер итерации (начиная с 1) number
loop.index0 Номер итерации (начиная с 0) number
loop.revindex Обратный номер итерации (от конца, начиная с 1) number
loop.revindex0 Обратный номер итерации (от конца, начиная с 0) number
loop.first Первая итерация boolean
loop.last Последняя итерация boolean
loop.length Общее количество элементов number
loop.parent Контекст родительского цикла object

Примеры использования переменной loop

{% foreach products as product %}
    <div class="product-item {{ loop.first ? 'first' : '' }} {{ loop.last ? 'last' : '' }}">
        <span class="item-number">#{{ loop.index }}</span>
        <h3>{{ product.name }}</h3>
        <p>{{ product.price | money('₽') }}</p>
        
        {% if loop.first %}
            <span class="badge badge-featured">Рекомендуемый</span>
        {% endif %}
        
        {% if loop.last %}
            <hr>
            <p>Всего товаров: {{ loop.length }}</p>
        {% endif %}
    </div>
{% endforeach %}

Чередование стилей

<table class="data-table">
    {% foreach users as user %}
        <tr class="{{ loop.index % 2 == 0 ? 'even' : 'odd' }}">
            <td>{{ loop.index }}</td>
            <td>{{ user.name }}</td>
            <td>{{ user.email }}</td>
        </tr>
    {% endforeach %}
</table>

Цикл for (диапазоны)

Числовые диапазоны

<!-- От 1 до 10 -->
{% for i in 1..10 %}
    <div class="item-{{ i }}">Элемент {{ i }}</div>
{% endfor %}

Обратный порядок

<!-- Обратный отсчет -->
<div class="countdown">
    {% for i in 10..1 %}
        <span class="countdown-number">{{ i }}</span>
    {% endfor %}
    <span class="countdown-go">Поехали!</span>
</div>

Генерация опций

<!-- Годы для даты рождения -->
<select name="birth_year">
    {% for year in 1950..2024 %}
        <option value="{{ year }}" {{ year == user.birth_year ? 'selected' : '' }}>
            {{ year }}
        </option>
    {% endfor %}
</select>

<!-- Рейтинг звездами -->
<div class="rating">
    {% for star in 1..5 %}
        <i class="fas fa-star {{ star <= product.rating ? 'active' : '' }}"></i>
    {% endfor %}
</div>

Вложенные циклы

Категории и товары

{% foreach categories as category %}
    <div class="category">
        <h2>{{ category.name }}</h2>
        <div class="category-products">
            {% foreach category.products as product %}
                <div class="product">
                    <h3>{{ product.name }}</h3>
                    <p>Категория #{{ loop.parent.index }}, Товар #{{ loop.index }}</p>
                    <p>{{ product.price | money('₽') }}</p>
                    
                    {% if product.images %}
                        <div class="product-gallery">
                            {% foreach product.images as image %}
                                <img src="{{ image.url }}" 
                                     alt="{{ product.name }}"
                                     class="{{ loop.first ? 'main-image' : 'thumb' }}">
                            {% endforeach %}
                        </div>
                    {% endif %}
                </div>
            {% endforeach %}
        </div>
    </div>
{% endforeach %}

Таблица с данными

<table class="data-table">
    <thead>
        <tr>
            {% foreach table_headers as header %}
                <th>{{ header.title }}</th>
            {% endforeach %}
        </tr>
    </thead>
    <tbody>
        {% foreach table_rows as row %}
            <tr class="{{ loop.index % 2 == 0 ? 'even' : 'odd' }}">
                {% foreach table_headers as header %}
                    <td>{{ row[header.field] }}</td>
                {% endforeach %}
            </tr>
        {% endforeach %}
    </tbody>
</table>

Условия в циклах

Пропуск элементов (continue)

{% foreach users as user %}
    {% if user.is_banned %}
        {% continue %}  <!-- Пропускаем заблокированных пользователей -->
    {% endif %}
    
    <div class="user-card">
        <h3>{{ user.name }}</h3>
        <p>{{ user.email }}</p>
        
        {% if user.is_premium %}
            <span class="badge badge-gold">Премиум</span>
        {% endif %}
    </div>
{% endforeach %}

Прерывание цикла (break)

{% foreach items as item %}
    <div class="item">
        <h3>{{ item.title }}</h3>
        <p>{{ item.description }}</p>
    </div>
    
    {% if loop.index >= 10 %}
        {% break %}  <!-- Показываем только первые 10 элементов -->
    {% endif %}
{% endforeach %}

Условные блоки

{% foreach products as product %}
    <div class="product {{ loop.index % 3 == 0 ? 'product-featured' : '' }}">
        <h3>{{ product.name }}</h3>
        <p>{{ product.price | money('₽') }}</p>
        
        {% if loop.index % 5 == 0 %}
            <div class="advertisement">
                <!-- Реклама каждые 5 товаров -->
                <p>Специальное предложение!</p>
            </div>
        {% endif %}
        
        {% if loop.last %}
            <div class="products-footer">
                <p>Показано {{ loop.length }} товаров</p>
            </div>
        {% endif %}
    </div>
{% endforeach %}

Цикл while

Базовое использование

{% php %}
    $counter = 1;
    $maxItems = 10;
{% endphp %}

{% while counter <= maxItems %}
    <div class="dynamic-item">
        <h3>Элемент {{ counter }}</h3>
        <p>Это динамически сгенерированный элемент</p>
    </div>
    
    {% php %}
        $counter++;
    {% endphp %}
{% endwhile %}

While с данными из базы

{% php %}
    $page = 1;
    $perPage = 20;
    $hasMore = true;
{% endphp %}

<div class="infinite-scroll">
    {% while hasMore %}
        {% set pageUsers = db('1752665380840')->paginate('users', page, perPage) %}
        
        {% if pageUsers.data %}
            {% foreach pageUsers.data as user %}
                <div class="user-item">
                    <h3>{{ user.name }}</h3>
                    <p>Страница {{ page }}</p>
                </div>
            {% endforeach %}
        {% endif %}
        
        {% php %}
            $hasMore = $pageUsers['next_page'] ? true : false;
            $page++;
        {% endphp %}
    {% endwhile %}
</div>

Работа с пустыми коллекциями

Проверка перед циклом

{% if users and users | length > 0 %}
    <div class="users-list">
        {% foreach users as user %}
            <div class="user-item">
                <h3>{{ user.name }}</h3>
                <p>{{ user.email }}</p>
            </div>
        {% endforeach %}
    </div>
{% else %}
    <div class="empty-state">
        <i class="fas fa-users fa-3x"></i>
        <h3>Пользователи не найдены</h3>
        <p>Добавьте первого пользователя</p>
        <a href="/users/create" class="btn btn-primary">Добавить пользователя</a>
    </div>
{% endif %}

Else в цикле

<div class="products-grid">
    {% foreach products as product %}
        <div class="product-card">
            <h3>{{ product.name }}</h3>
            <p>{{ product.price | money('₽') }}</p>
        </div>
    {% else %}
        <div class="no-products">
            <i class="fas fa-box-open fa-4x"></i>
            <h2>Товары не найдены</h2>
            <p>В данной категории пока нет товаров</p>
            <a href="/products/create" class="btn btn-primary">Добавить товар</a>
        </div>
    {% endforeach %}
</div>

Сложные примеры

Группировка элементов

<!-- Группировка товаров по 3 в ряду -->
<div class="products-grid">
    {% foreach products as product %}
        {% if loop.index % 3 == 1 %}
            <div class="products-row">
        {% endif %}
        
        <div class="product-card">
            <h3>{{ product.name }}</h3>
            <p>{{ product.price | money('₽') }}</p>
        </div>
        
        {% if loop.index % 3 == 0 or loop.last %}
            </div>
        {% endif %}
    {% endforeach %}
</div>

Древовидная структура

<!-- Рекурсивное меню -->
{% macro render_menu(items, level) %}
    <ul class="menu-level-{{ level }}">
        {% foreach items as item %}
            <li class="menu-item">
                <a href="{{ item.url }}">{{ item.title }}</a>
                
                {% if item.children %}
                    {{ _self.render_menu(item.children, level + 1) }}
                {% endif %}
            </li>
        {% endforeach %}
    </ul>
{% endmacro %}

<nav class="main-menu">
    {{ _self.render_menu(menu_items, 0) }}
</nav>

Календарь

<!-- Генерация календаря -->
{% set currentMonth = date('m') %}
{% set currentYear = date('Y') %}
{% set daysInMonth = date('t') %}
{% set firstDayOfWeek = date('w', strtotime(currentYear ~ '-' ~ currentMonth ~ '-01')) %}

<div class="calendar">
    <div class="calendar-header">
        <h2>{{ date('F Y') }}</h2>
    </div>
    
    <div class="calendar-weekdays">
        {% for day in ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'] %}
            <div class="weekday">{{ day }}</div>
        {% endfor %}
    </div>
    
    <div class="calendar-days">
        <!-- Пустые ячейки для первой недели -->
        {% for i in 1..firstDayOfWeek %}
            <div class="calendar-day empty"></div>
        {% endfor %}
        
        <!-- Дни месяца -->
        {% for day in 1..daysInMonth %}
            {% set isToday = (day == date('d') and currentMonth == date('m')) %}
            <div class="calendar-day {{ isToday ? 'today' : '' }}">
                {{ day }}
            </div>
        {% endfor %}
    </div>
</div>

Производительность циклов

Оптимизация запросов

<!-- Плохо: N+1 запросов -->
{% foreach users as user %}
    <div class="user">
        <h3>{{ user.name }}</h3>
        {% set userPosts = db('1752665380840')->getItems('posts', {'user_id': user.id}) %}
        <p>Постов: {{ userPosts | length }}</p>
    </div>
{% endforeach %}

<!-- Хорошо: один запрос -->
{% set postCounts = db('1752665380840')->query('SELECT user_id, COUNT(*) as count FROM posts GROUP BY user_id') %}
{% set postCountsMap = {} %}
{% foreach postCounts as count %}
    {% set postCountsMap[count.user_id] = count.count %}
{% endforeach %}

{% foreach users as user %}
    <div class="user">
        <h3>{{ user.name }}</h3>
        <p>Постов: {{ postCountsMap[user.id] ?? 0 }}</p>
    </div>
{% endforeach %}

Ограничение количества элементов

<!-- Ограничение отображения -->
{% foreach items as item %}
    <div class="item">{{ item.title }}</div>
    
    {% if loop.index >= 50 %}
        <div class="show-more">
            <p>Показаны первые 50 элементов из {{ items | length }}</p>
            <button onclick="loadMore()">Показать еще</button>
        </div>
        {% break %}
    {% endif %}
{% endforeach %}
Следующий шаг: Изучите фильтры (pipes).