Skip to Content
Разработка модулейШаблоны и компоненты

Шаблоны и компоненты

Flute CMS использует Blade — мощный шаблонизатор от Laravel. Шаблоны позволяют отделить HTML-разметку от PHP-логики и переиспользовать код.

Что такое Blade

Blade — это шаблонизатор, который:

  • Позволяет вставлять PHP-код в HTML через специальный синтаксис
  • Поддерживает наследование шаблонов (layouts)
  • Автоматически экранирует данные (защита от XSS)
  • Компилирует шаблоны в PHP для высокой производительности

Структура шаблонов модуля

      • index.blade.php
      • show.blade.php
      • create.blade.php
ПапкаДля чего
pages/Полные страницы (список статей, карточка товара и т.д.)
components/Переиспользуемые части (карточки, кнопки, формы)
layouts/Базовые макеты (обычно наследуют макет темы)

Регистрация шаблонов

В провайдере модуля шаблоны регистрируются автоматически через bootstrapModule(). Но можно зарегистрировать вручную с кастомным namespace:

// По умолчанию namespace = имя модуля в kebab-case // Модуль Blog → namespace 'blog' $this->loadViews('Resources/views', 'blog'); // Теперь можно использовать: // view('blog::pages.index')

Основы Blade

Вывод данных

{{-- Выводит значение с экранированием (безопасно) --}} <h1>{{ $article->title }}</h1> {{-- БЕЗ экранирования (используйте только для доверенного HTML) --}} <div class="content">{!! $article->content !!}</div> {{-- Вывод с значением по умолчанию --}} <p>Автор: {{ $article->author->name ?? 'Аноним' }}</p>

Всегда используйте {{ }} для пользовательских данных. Синтаксис {!! !!} используйте только для HTML, который вы сами сформировали.

Условия

@if($articles->count() > 0) <div class="articles"> {{-- Отображаем статьи --}} </div> @elseif($showEmpty) <p>Статей пока нет, но скоро появятся!</p> @else <p>Статьи не найдены</p> @endif {{-- Сокращённая форма --}} @unless($user->isAdmin()) <p>Вы не администратор</p> @endunless {{-- Проверка на существование --}} @isset($article) <h1>{{ $article->title }}</h1> @endisset @empty($articles) <p>Список пуст</p> @endempty

Циклы

{{-- Обычный foreach --}} @foreach($articles as $article) <div class="article"> <h2>{{ $article->title }}</h2> <p>{{ $article->excerpt }}</p> </div> @endforeach {{-- С проверкой на пустоту --}} @forelse($articles as $article) <div class="article">{{ $article->title }}</div> @empty <p>Статей нет</p> @endforelse {{-- Внутри цикла доступна переменная $loop --}} @foreach($items as $item) @if($loop->first) <p>Это первый элемент</p> @endif <p>{{ $loop->iteration }}. {{ $item->name }}</p> @if($loop->last) <p>Это последний элемент</p> @endif @endforeach

Переменная $loop содержит:

  • $loop->index — индекс (начиная с 0)
  • $loop->iteration — номер итерации (начиная с 1)
  • $loop->first — это первая итерация?
  • $loop->last — это последняя итерация?
  • $loop->count — общее количество элементов

Наследование шаблонов

Базовый макет

Обычно модули используют макет темы:

{{-- Resources/views/pages/index.blade.php --}} {{-- Наследуем макет темы --}} @extends('flute::layouts.app') {{-- Заполняем секцию title --}} @section('title', 'Блог') {{-- Заполняем секцию content --}} @section('content') <div class="container"> <h1>Последние статьи</h1> @foreach($articles as $article) <article class="article-card"> <h2>{{ $article->title }}</h2> <p>{{ Str::limit($article->excerpt, 200) }}</p> <a href="{{ route('blog.show', $article->id) }}">Читать далее</a> </article> @endforeach </div> @endsection {{-- Добавляем стили в секцию head --}} @push('styles') @at('Modules/Blog/Resources/assets/scss/blog.scss') @endpush

Создание своего макета

Если нужен специфичный макет для модуля:

{{-- Resources/views/layouts/blog.blade.php --}} @extends('flute::layouts.app') @section('content') <div class="blog-wrapper"> <aside class="blog-sidebar"> {{-- Боковая панель блога --}} @include('blog::components.sidebar') </aside> <main class="blog-content"> {{-- Здесь будет контент дочерних страниц --}} @yield('blog-content') </main> </div> @endsection
{{-- Resources/views/pages/index.blade.php --}} @extends('blog::layouts.blog') @section('blog-content') {{-- Контент попадёт в main.blog-content --}} <h1>Статьи</h1> ... @endsection

Система fallback

Flute автоматически ищет шаблоны в нескольких местах:

Текущая тема

app/Themes/текущая_тема/views/

Стандартная тема

app/Themes/standard/views/

Это значит, что @extends('flute::layouts.app') найдёт файл в активной теме, а если его там нет — в стандартной.


Компоненты

Blade-компоненты (include)

Простейший способ переиспользовать код:

{{-- Resources/views/components/article-card.blade.php --}} {{-- @props объявляет переменные, которые принимает компонент --}} @props([ 'article', // Обязательный параметр 'showExcerpt' => true // Необязательный, по умолчанию true ]) <article class="article-card"> <h2 class="article-card__title"> <a href="{{ route('blog.show', $article->id) }}"> {{ $article->title }} </a> </h2> @if($showExcerpt && $article->excerpt) <p class="article-card__excerpt"> {{ Str::limit($article->excerpt, 150) }} </p> @endif <div class="article-card__meta"> <span>{{ $article->author->name }}</span> <time>{{ $article->created_at->format('d.m.Y') }}</time> </div> </article>

Использование:

{{-- Передаём параметры --}} @include('blog::components.article-card', [ 'article' => $article, 'showExcerpt' => false ]) {{-- Или в цикле --}} @foreach($articles as $article) @include('blog::components.article-card', ['article' => $article]) @endforeach

Yoyo Live-компоненты

Yoyo позволяет создавать интерактивные компоненты, которые обновляются без перезагрузки страницы (как React/Vue, но на PHP).

Создание Yoyo-компонента

<?php namespace Flute\Modules\Blog\Components; use Clickfwd\Yoyo\Component; /** * Компонент фильтрации статей. * Обновляется при изменении полей формы. */ class ArticleFilter extends Component { // Публичные свойства автоматически сохраняются между запросами public string $search = ''; public string $category = ''; public string $sortBy = 'date'; /** * Вызывается при инициализации компонента. */ public function mount() { // Можно установить начальные значения $this->category = request()->get('category', ''); } /** * Метод, вызываемый при отправке формы. */ public function filter() { // Метод filter будет вызван при yoyo:method="filter" // Свойства уже обновлены из формы } /** * Сбросить фильтры. */ public function reset() { $this->search = ''; $this->category = ''; $this->sortBy = 'date'; } /** * Рендер компонента. */ public function render() { // Загружаем данные на основе текущих фильтров $query = rep(Article::class)->select()->where('published', true); if ($this->search) { $query->where('title', 'LIKE', "%{$this->search}%"); } if ($this->category) { $query->where('category_id', $this->category); } $query->orderBy($this->sortBy === 'date' ? 'created_at' : 'views', 'DESC'); $articles = $query->fetchAll(); $categories = rep(Category::class)->findAll(); return view('blog::components.article-filter', [ 'articles' => $articles, 'categories' => $categories, ]); } }

Шаблон Yoyo-компонента

{{-- Resources/views/components/article-filter.blade.php --}} <div> {{-- Форма фильтрации --}} <form class="filter-form"> {{-- yoyo:val привязывает поле к свойству компонента --}} <input type="text" yoyo:val="search" placeholder="Поиск..." class="filter-input"> <select yoyo:val="category" class="filter-select"> <option value="">Все категории</option> @foreach($categories as $cat) <option value="{{ $cat->id }}">{{ $cat->name }}</option> @endforeach </select> <select yoyo:val="sortBy" class="filter-select"> <option value="date">По дате</option> <option value="views">По популярности</option> </select> {{-- Кнопка сбрасывает фильтры --}} <button type="button" yoyo:on="click" yoyo:method="reset"> Сбросить </button> </form> {{-- Результаты обновляются автоматически --}} <div class="articles-grid"> @forelse($articles as $article) @include('blog::components.article-card', ['article' => $article]) @empty <p>Ничего не найдено</p> @endforelse </div> </div>

Регистрация и использование

Компоненты регистрируются автоматически через bootstrapModule() или вручную:

$this->loadComponents(); // Все компоненты из папки Components/

Использование в шаблоне:

{{-- Имя компонента = имя класса в kebab-case без суффикса Component --}} {{-- ArticleFilter → article-filter --}} @yoyo('article-filter') {{-- С параметрами --}} @yoyo('article-filter', ['category' => 'news'])

Yoyo-компоненты работают через AJAX. При изменении yoyo:val полей компонент автоматически перерендеривается на сервере и обновляется на странице.


Виджеты

Виджеты — это блоки, которые можно размещать на страницах через админку. В отличие от компонентов, виджеты имеют настройки.

Создание виджета

<?php namespace Flute\Modules\Blog\Widgets; use Flute\Core\Widgets\WidgetInterface; class RecentArticlesWidget implements WidgetInterface { /** * Отрисовка виджета. * * @param array $settings Настройки из админки */ public function render(array $settings = []): string { $limit = $settings['limit'] ?? 5; $showDate = $settings['show_date'] ?? true; $articles = rep(Article::class) ->select() ->where('published', true) ->orderBy('created_at', 'DESC') ->limit($limit) ->fetchAll(); // Возвращаем HTML return view('blog::widgets.recent-articles', [ 'articles' => $articles, 'showDate' => $showDate, ])->render(); } /** * Уникальное имя виджета. */ public function getName(): string { return 'recent-articles'; } /** * Описание для админки. */ public function getDescription(): string { return 'Показывает последние статьи блога'; } /** * Доступные настройки виджета. * Отображаются в админке при добавлении виджета. */ public function getSettings(): array { return [ 'limit' => [ 'type' => 'number', 'label' => 'Количество статей', 'default' => 5, 'min' => 1, 'max' => 20, ], 'show_date' => [ 'type' => 'boolean', 'label' => 'Показывать дату', 'default' => true, ], ]; } }

Шаблон виджета

{{-- Resources/views/widgets/recent-articles.blade.php --}} <div class="widget widget--recent-articles"> <h3 class="widget__title">Последние статьи</h3> <ul class="widget__list"> @foreach($articles as $article) <li class="widget__item"> <a href="{{ route('blog.show', $article->id) }}"> {{ $article->title }} </a> @if($showDate) <time class="widget__date"> {{ $article->created_at->format('d.m.Y') }} </time> @endif </li> @endforeach </ul> </div>

Использование виджета

{{-- В шаблонах --}} {!! widget('recent-articles', ['limit' => 10]) !!}

Встроенные директивы

Подключение ресурсов

Директива @at() подключает CSS, JS или изображения:

{{-- SCSS компилируется автоматически --}} @at('Modules/Blog/Resources/assets/scss/blog.scss') {{-- JS файл --}} @at('Modules/Blog/Resources/assets/js/blog.js') {{-- Файл из темы --}} @at('Themes/standard/assets/sass/app.scss')

Переводы

{{-- Функция перевода --}} <h1>{{ __('blog.title') }}</h1> {{-- С параметрами --}} <p>{{ __('blog.articles_count', ['count' => $count]) }}</p> {{-- Директива --}} @lang('blog.welcome')

Авторизация и права

{{-- Проверка авторизации --}} @auth <p>Привет, {{ user()->name }}!</p> @endauth @guest <a href="{{ route('login') }}">Войти</a> @endguest {{-- Проверка прав --}} @can('manage_blog') <a href="{{ route('admin.blog') }}">Управление блогом</a> @endcan @cannot('delete_articles') <p>У вас нет прав на удаление</p> @endcannot

Иконки

{{-- Встроенный компонент для иконок --}} <x-icon name="heart" /> <x-icon name="settings" class="icon-lg" />

Стили и скрипты

Push и Stack

Позволяют добавлять стили/скрипты из дочерних шаблонов в родительский:

{{-- В родительском шаблоне (layout) --}} <head> <link rel="stylesheet" href="/css/app.css"> @stack('styles') {{-- Сюда вставятся стили из @push --}} </head> <body> @yield('content') <script src="/js/app.js"></script> @stack('scripts') {{-- Сюда вставятся скрипты из @push --}} </body>
{{-- В дочернем шаблоне --}} @extends('flute::layouts.app') @push('styles') <link rel="stylesheet" href="/modules/blog/css/blog.css"> @endpush @section('content') <h1>Блог</h1> @endsection @push('scripts') <script src="/modules/blog/js/blog.js"></script> @endpush

Подключение SCSS из провайдера

public function boot(\DI\Container $container): void { // SCSS компилируется автоматически $this->loadScss('Resources/assets/scss/blog.scss'); $this->bootstrapModule(); }

SCSS компилируется через scssphp и кешируется в public/assets/css/cache/. В production файлы минифицируются.


Кеширование шаблонов

Автоматическое кеширование

  • Blade-шаблоны компилируются в PHP и сохраняются в storage/app/views/
  • SCSS компилируется и кешируется в public/assets/css/cache/
  • В development-режиме шаблоны перекомпилируются при изменении
  • В production кеш сохраняется до ручной очистки

Очистка кеша

// Через код template()->clearCache(); // Через консоль // php flute template:cache:clear

Советы по организации

Структура файлов

Хорошо:

  • Используйте пространства имён модулей (blog::pages.index)
  • Выносите повторяющийся код в компоненты
  • Разделяйте на папки: pages/, components/, layouts/

Плохо:

  • Дублирование HTML в разных файлах
  • Огромные файлы с логикой и разметкой
  • Хардкод путей вместо route()

Компоненты

{{-- ✅ Хорошо: переиспользуемый компонент с параметрами --}} @props(['type' => 'info', 'dismissible' => false]) <div class="alert alert--{{ $type }} @if($dismissible) alert--dismissible @endif"> {{ $slot }} @if($dismissible) <button type="button" class="alert__close">&times;</button> @endif </div>
{{-- ❌ Плохо: захардкоженные значения --}} <div class="alert alert--info"> Какое-то сообщение </div>

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

  • Используйте @once для кода, который должен выполниться один раз
  • Минимизируйте вложенность @include (каждый include = файловая операция)
  • Для тяжёлых данных используйте кеширование в контроллере, а не в шаблоне