Шаблоны и компоненты
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 автоматически ищет шаблоны в нескольких местах:
Это значит, что @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])
@endforeachYoyo 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">×</button>
@endif
</div>{{-- ❌ Плохо: захардкоженные значения --}}
<div class="alert alert--info">
Какое-то сообщение
</div>Производительность
- Используйте
@onceдля кода, который должен выполниться один раз - Минимизируйте вложенность
@include(каждый include = файловая операция) - Для тяжёлых данных используйте кеширование в контроллере, а не в шаблоне