Skip to Content

Компоненты и их использование

Компоненты в Flute CMS включают:

  • Yoyo-компоненты (FluteComponent) для интерактивной логики без JS
  • Blade UI-компоненты для единообразной верстки форм и блоков (например, <x-card>, <x-forms.field>, <x-input>)

ModuleServiceProvider автоматически регистрирует Yoyo-компоненты из Components/ (kebab-case, без суффикса Component). Тематические Blade-компоненты регистрируются в Template с кешем.

Архитектура компонентов

Базовый класс FluteComponent

<?php namespace Flute\Core\Support; use Clickfwd\Yoyo\Component; use Flute\Core\Contracts\FluteComponentInterface; abstract class FluteComponent extends Component implements FluteComponentInterface { public function validate(array $rules, ?array $data = null, array $messages = []) { // Валидация данных компонента } public function getValidatorErrors() { // Получение ошибок валидации } }

Интерфейс FluteComponentInterface

<?php namespace Flute\Core\Contracts; interface FluteComponentInterface { // Базовый контракт для Yoyo-компонентов Flute }

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

Структура компонента

      • YourComponent.php

Простой компонент

<?php namespace Flute\Modules\Blog\Components; use Flute\Core\Support\FluteComponent; class SimpleCounterComponent extends FluteComponent { // Публичные свойства компонента public int $count = 0; // Приватные свойства protected string $title = 'Счетчик'; /** * Увеличение счетчика */ public function increment() { $this->count++; // Отправка события в браузер $this->emitEvent('counter-incremented', [ 'newCount' => $this->count ]); } /** * Сброс счетчика */ public function reset() { $oldCount = $this->count; $this->count = 0; $this->emitEvent('counter-reset', [ 'oldCount' => $oldCount ]); } /** * Рендеринг компонента (используем $this->view) */ public function render() { return $this->view('blog::components.simple-counter', [ 'title' => $this->title, 'count' => $this->count ]); } }

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

{{-- resources/views/components/simple-counter.blade.php --}} <div class="counter-component"> <h3>{{ $title }}</h3> <div class="counter-display"> <span class="count">{{ $count }}</span> </div> <div class="counter-controls"> <button class="btn btn-primary" yoyo:post="increment()"> <x-icon path="plus" /> Увеличить </button> <button class="btn btn-secondary" yoyo:post="reset()"> <x-icon path="undo" /> Сбросить </button> </div> </div>

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

Карточки

<x-card class="my-3"> <h3 class="mb-2">{{ __('def.title') }}</h3> <p>{{ __('def.description') }}</p> </x-card>

Поля форм

<form method="POST" action="{{ route('example.store') }}"> @csrf <x-forms.field class="mb-3"> <x-forms.label for="name" required>@t('def.name'):</x-forms.label> <x-input name="name" id="name" :value="old('name')" /> @error('name')<div class="text-danger">{{ $message }}</div>@enderror </x-forms.field> <x-forms.field class="mb-3"> <x-forms.label for="category">@t('def.category'):</x-forms.label> <select id="category" name="category" class="form-select"> @foreach($categories as $c) <option value="{{ $c->id }}">{{ $c->name }}</option> @endforeach </select> </x-forms.field> <x-forms.field class="mb-3"> <x-forms.label for="content" required>@t('def.content'):</x-forms.label> <textarea id="content" name="content" class="form-control" rows="6"></textarea> </x-forms.field> <button class="btn btn-primary" type="submit">@t('def.save')</button> </form>

Примечания:

  • <x-input> — универсальный инпут (поддерживает type, mask, yoyo, multiple, filePond)
  • <x-forms.field> — контейнер поля с лейблом/подсказкой/ошибкой
  • Используйте эти компоненты в админке и темах для единообразия UI
  • <x-icon> — встроенный компонент иконок

Свойства компонентов

Публичные и приватные свойства

<?php class ProductFormComponent extends FluteComponent { // Публичные свойства (доступны в шаблоне и могут обновляться) public string $productName = ''; public float $price = 0.0; public array $categories = []; public ?int $selectedCategory = null; public bool $isActive = true; // Приватные свойства (только для внутренней логики) private ProductService $productService; private array $validationRules = []; public function mount() { $this->productService = app(ProductService::class); // Загрузка категорий $this->categories = $this->productService->getCategories(); // Установка значений по умолчанию if (!$this->selectedCategory && count($this->categories) > 0) { $this->selectedCategory = $this->categories[0]['id']; } } }

FluteComponent сам заполняет публичные свойства из запроса; $props задавать не нужно. Исключения можно описать в $excludesVariables.

Динамические свойства

<?php class DynamicFormComponent extends FluteComponent { public array $formFields = []; /** * Дополнительные свойства, которые можно создать на лету */ protected function getDynamicProperties(): array { return [ 'customField1', 'customField2', 'dynamicValue' ]; } public function addField() { $fieldId = 'field_' . count($this->formFields); $this->formFields[] = $fieldId; // Создание динамического свойства $this->{$fieldId} = ''; } public function removeField($fieldId) { if (($key = array_search($fieldId, $this->formFields)) !== false) { unset($this->formFields[$key]); unset($this->{$fieldId}); } } }

Используйте динамические свойства осторожно: они не получают автокомплит и усложняют сопровождение. Предпочитайте явные публичные свойства.

Методы компонентов

Методы жизненного цикла

<?php class LifecycleComponent extends FluteComponent { public function mount() { // Вызывается при инициализации компонента // Здесь происходит загрузка данных, установка начальных значений $this->loadInitialData(); $this->setupValidationRules(); } public function boot(array $variables, array $attributes) { // Вызывается перед mount // Здесь происходит обработка входящих данных parent::boot($variables, $attributes); // Дополнительная обработка $this->processInputData(); } public function render() { // Вызывается при рендеринге компонента // Возвращает представление компонента // Проверка прав доступа if (!$this->userCanView()) { return $this->view('errors.access-denied'); } return $this->view('components.lifecycle-example', [ 'data' => $this->prepareRenderData() ]); } }

Обработчики событий

<?php class EventHandlerComponent extends FluteComponent { public string $status = 'idle'; public array $messages = []; /** * Обработка изменения статуса */ public function updatedStatus($newStatus) { // Вызывается при изменении свойства status $this->messages[] = "Статус изменен на: {$newStatus}"; // Валидация нового значения if (!in_array($newStatus, ['idle', 'processing', 'completed', 'error'])) { $this->status = 'idle'; $this->messages[] = "Недопустимый статус"; } // Отправка события $this->emitEvent('status-changed', [ 'oldStatus' => $this->status, 'newStatus' => $newStatus ]); } /** * Обработка отправки формы */ public function submitForm() { $this->status = 'processing'; try { // Валидация данных $this->validateForm(); // Обработка данных $result = $this->processFormData(); $this->status = 'completed'; $this->messages[] = 'Форма успешно обработана'; // Перенаправление $this->redirectTo('/success'); } catch (\Exception $e) { $this->status = 'error'; $this->messages[] = $e->getMessage(); } } /** * Обработка клика по кнопке */ public function handleButtonClick($buttonId) { $this->messages[] = "Нажата кнопка: {$buttonId}"; // Выполнение действия в зависимости от кнопки switch ($buttonId) { case 'save': $this->saveData(); break; case 'delete': $this->deleteData(); break; case 'refresh': $this->refreshData(); break; } } }

Валидация в компонентах

Простая валидация

<?php class ValidationComponent extends FluteComponent { public string $email = ''; public string $password = ''; public string $confirmPassword = ''; public function submitForm() { // Валидация данных $isValid = $this->validate([ 'email' => 'required|email', 'password' => 'required|min:8', 'confirmPassword' => 'required|same:password' ]); if (!$isValid) { // Получение ошибок валидации $errors = $this->getValidatorErrors(); foreach ($errors->all() as $error) { $this->flashMessage($error, 'error'); } return; } // Обработка валидных данных $this->processValidData(); } }

Продвинутая валидация

<?php class AdvancedValidationComponent extends FluteComponent { public string $username = ''; public string $email = ''; public array $tags = []; public function submitForm() { // Правила валидации $rules = [ 'username' => 'required|string|min-str-len:3|max-str-len:20|unique:users,username', 'email' => 'required|email|unique:users,email', 'tags' => 'array|max:5', 'tags.*' => 'string|max-str-len:50|exists:tags,name' ]; // Сообщения об ошибках $messages = [ 'username.required' => 'Имя пользователя обязательно', 'username.unique' => 'Это имя пользователя уже занято', 'email.unique' => 'Этот email уже используется', 'tags.max' => 'Максимум 5 тегов', 'tags.*.exists' => 'Один из тегов не существует' ]; // Валидация $isValid = $this->validate($rules, null, $messages); if (!$isValid) { return; } // Обработка валидных данных $this->createUser(); } /** * Кастомная валидация */ public function validateUsername() { if (str_contains($this->username, 'admin')) { $this->inputError('username', 'Имя пользователя не может содержать слово "admin"'); return false; } return true; } /** * Валидация с дополнительными правилами */ public function createUser() { // Кастомная валидация if (!$this->validateUsername()) { return; } // Создание пользователя $user = User::create([ 'username' => $this->username, 'email' => $this->email, 'tags' => $this->tags ]); $this->flashMessage('Пользователь успешно создан', 'success'); $this->redirectTo('/users'); } }

Работа с событиями

Отправка событий

<?php class EventEmitterComponent extends FluteComponent { public array $notifications = []; /** * Создание уведомления */ public function createNotification($type, $message) { $notification = [ 'id' => uniqid(), 'type' => $type, 'message' => $message, 'timestamp' => now()->toISOString() ]; $this->notifications[] = $notification; // Отправка события в браузер $this->emitEvent('notification-created', $notification); } /** * Обновление данных */ public function refreshData() { // Отправка события о начале загрузки $this->emitEvent('data-loading-start'); try { // Загрузка данных $data = $this->loadData(); // Отправка события об успешной загрузке $this->emitEvent('data-loaded', [ 'data' => $data, 'count' => count($data) ]); } catch (\Exception $e) { // Отправка события об ошибке $this->emitEvent('data-loading-error', [ 'error' => $e->getMessage() ]); } } /** * Массовая отправка событий */ public function bulkOperations() { $operations = ['create', 'update', 'delete']; foreach ($operations as $operation) { $this->emitEvent('operation-start', [ 'operation' => $operation ]); // Выполнение операции $result = $this->{$operation . 'Operation'}(); $this->emitEvent('operation-complete', [ 'operation' => $operation, 'result' => $result ]); } $this->emitEvent('all-operations-complete'); } }

Прослушивание событий в JavaScript

{{-- resources/views/components/event-emitter.blade.php --}} <div class="event-emitter-component"> <div class="notifications"> @foreach($notifications as $notification) <div class="notification notification--{{ $notification['type'] }}"> {{ $notification['message'] }} </div> @endforeach </div> <button yoyo:post="refreshData()">Обновить данные</button> <button yoyo:post="bulkOperations()">Массовые операции</button> </div> @push('scripts') <script> // Прослушивание событий компонента document.addEventListener('notification-created', function(event) { const notification = event.detail; // Добавление уведомления в DOM const notificationElement = createNotificationElement(notification); document.querySelector('.notifications').appendChild(notificationElement); // Автоматическое удаление через 5 секунд setTimeout(() => { notificationElement.remove(); }, 5000); }); document.addEventListener('data-loading-start', function() { showLoadingSpinner(); }); document.addEventListener('data-loaded', function(event) { hideLoadingSpinner(); updateDataDisplay(event.detail.data); showSuccessMessage(`Загружено ${event.detail.count} элементов`); }); document.addEventListener('data-loading-error', function(event) { hideLoadingSpinner(); showErrorMessage(event.detail.error); }); document.addEventListener('operation-start', function(event) { console.log(`Начата операция: ${event.detail.operation}`); }); document.addEventListener('operation-complete', function(event) { console.log(`Завершена операция: ${event.detail.operation}`); }); document.addEventListener('all-operations-complete', function() { showSuccessMessage('Все операции завершены'); }); function createNotificationElement(notification) { const div = document.createElement('div'); div.className = `notification notification--${notification.type}`; div.innerHTML = notification.message; return div; } function showLoadingSpinner() { // Показать спиннер загрузки } function hideLoadingSpinner() { // Скрыть спиннер загрузки } function updateDataDisplay(data) { // Обновить отображение данных } function showSuccessMessage(message) { // Показать сообщение об успехе } function showErrorMessage(message) { // Показать сообщение об ошибке } </script> @endpush

Подтверждения и модальные окна

Система подтверждений

<?php class ConfirmationComponent extends FluteComponent { public array $items = []; public string $selectedItemId = ''; /** * Удаление элемента с подтверждением */ public function deleteItem($itemId) { // Поиск элемента $item = $this->findItemById($itemId); if (!$item) { $this->flashMessage('Элемент не найден', 'error'); return; } // Создание подтверждения $this->withConfirmation( "delete_item_{$itemId}", 'error', "Вы действительно хотите удалить элемент '{$item['name']}'?", function () use ($item) { // Действие при подтверждении $this->performDeletion($item); $this->flashMessage('Элемент успешно удален', 'success'); }, 'Удаление элемента', 'Да, удалить', 'Отмена' ); } /** * Пакетная обработка с подтверждением */ public function batchDelete() { $selectedItems = $this->getSelectedItems(); if (empty($selectedItems)) { $this->flashMessage('Не выбраны элементы для удаления', 'warning'); return; } $count = count($selectedItems); $this->withConfirmation( 'batch_delete', 'warning', "Вы действительно хотите удалить {$count} элементов?", function () use ($selectedItems) { foreach ($selectedItems as $item) { $this->performDeletion($item); } $this->flashMessage("Удалено {$count} элементов", 'success'); }, 'Массовое удаление', 'Да, удалить все', 'Отмена' ); } /** * Критическое действие с дополнительным подтверждением */ public function criticalAction() { $this->withConfirmation( 'critical_action', 'error', 'Это действие нельзя отменить. Все данные будут потеряны.', function () { // Сначала показываем дополнительное предупреждение $this->confirm( 'critical_action_confirm', 'error', 'ПОДТВЕРДИТЕ: Вы понимаете, что это действие необратимо?', 'Критическое действие', 'Да, я понимаю последствия', 'Отмена' ); }, 'Критическое действие', 'Продолжить', 'Отмена' ); } /** * Обработка подтверждения критического действия */ public function confirmCriticalAction() { // Выполнение критического действия $this->performCriticalAction(); $this->flashMessage('Критическое действие выполнено', 'success'); } }

Шаблон с подтверждениями

{{-- resources/views/components/confirmation-example.blade.php --}} <div class="confirmation-component"> <div class="items-list"> @foreach($items as $item) <div class="item"> <span>{{ $item['name'] }}</span> <button class="btn btn-danger btn-sm" yoyo:post="deleteItem({{ $item['id'] }})"> Удалить </button> </div> @endforeach </div> @if(count($selectedItems ?? []) > 0) <div class="batch-actions"> <button class="btn btn-warning" yoyo:post="batchDelete()"> Удалить выбранные ({{ count($selectedItems) }}) </button> </div> @endif <button class="btn btn-danger" yoyo:post="criticalAction()"> Критическое действие </button> </div>

Модальные окна

<?php class ModalComponent extends FluteComponent { public string $modalTitle = ''; public string $modalContent = ''; /** * Открытие модального окна */ public function openModal($title, $content) { $this->modalTitle = $title; $this->modalContent = $content; $this->modalOpen('custom-modal'); } /** * Открытие формы редактирования */ public function openEditForm($itemId) { $item = $this->findItemById($itemId); if (!$item) { $this->flashMessage('Элемент не найден', 'error'); return; } $this->modalTitle = 'Редактирование элемента'; $this->modalContent = $this->renderEditForm($item); $this->modalOpen('edit-modal'); } /** * Сохранение изменений */ public function saveChanges() { // Валидация и сохранение $this->validateAndSave(); // Закрытие модального окна $this->modalClose('edit-modal'); // Обновление списка $this->refreshItemsList(); } /** * Открытие подтверждения */ public function openConfirmation($action, $itemId) { $this->modalTitle = 'Подтверждение действия'; $this->modalContent = $this->renderConfirmationDialog($action, $itemId); $this->modalOpen('confirmation-modal'); } /** * Рендеринг модального окна */ public function render() { return $this->view('components.modal-example'); } }

Примеры из реального кода

ProductPurchaseComponent (из модуля Shop)

Этот компонент демонстрирует все основные возможности:

  1. Управление состоянием — свойства для товара, сервера, времени
  2. Валидация — проверка данных перед покупкой
  3. Подтверждения — диалоги подтверждения покупки
  4. События — отправка событий в браузер
  5. AJAX взаимодействие — обновление данных без перезагрузки
  6. Модальные окна — интеграция с системой модальных окон
<?php // Адаптированный пример из ProductQuickViewComponent class ProductPurchaseComponent extends FluteComponent { public int $productId; public ?int $time = null; public ?int $serverId = null; public function mount(int $productId) { $this->productId = $productId; $this->loadProduct(); $this->initializeDefaults(); } public function updatedServerId($serverId) { $this->serverId = (int) $serverId; $this->emitEvent('server-changed', [ 'serverId' => $this->serverId ]); } public function buyProduct() { // Валидация if (!$this->validatePurchase()) { return; } // Подтверждение $this->withConfirmation( 'purchase_confirm', 'primary', 'Подтвердить покупку товара?', function () { $this->processPurchase(); } ); } public function render() { return $this->view('shop::components.product-purchase', [ 'product' => $this->getProduct(), 'servers' => $this->getAvailableServers(), 'prices' => $this->getProductPrices() ]); } }

Лучшие практики

Организация компонентов

Единая ответственность

Каждый компонент должен решать одну задачу.

Переиспользование

Компоненты должны быть максимально переиспользуемыми.

Четкое API

Публичные методы должны иметь понятные названия.

Документирование

Все публичные методы должны быть документированы.

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

  1. Кеширование данных — избегайте лишних запросов к БД
  2. Ленивая загрузка — загружайте данные только при необходимости
  3. Оптимизация событий — не отправляйте лишние события
  4. Минификация ресурсов — сжимайте CSS и JavaScript

Безопасность

  1. Валидация данных — всегда проверяйте входные данные
  2. Авторизация — проверяйте права доступа к действиям
  3. CSRF защита — используйте встроенную защиту от CSRF
  4. Санитизация — очищайте пользовательский ввод

Поддерживаемость

  1. Тестирование — пишите тесты для компонентов
  2. Логирование — логируйте важные действия
  3. Обработка ошибок — корректная обработка исключений
  4. Документирование — ведите документацию по компонентам

Итоги

Компоненты Flute CMS предоставляют мощный инструмент для создания интерактивных веб-приложений. Они сочетают простоту использования с высокой функциональностью, позволяя создавать сложные интерфейсы без необходимости перезагрузки страниц.

Основные преимущества компонентов:

  • Реактивность — автоматическое обновление интерфейса
  • AJAX взаимодействие — бесшовная работа с сервером
  • Событийная модель — гибкая система событий
  • Валидация — встроенная система проверки данных
  • Безопасность — защита от CSRF и других атак
  • Переиспользование — возможность повторного использования компонентов