Files
citrus/app/Services/TemplateService.php
T

611 lines
26 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Services;
use App\Models\HeaderTemplate;
use App\Models\SectionTemplate;
use App\Models\FooterTemplate;
use Illuminate\Database\Eloquent\Model;
use Filament\Forms\Components\{
TextInput, Textarea, Select, Checkbox, CheckboxList,
Radio, Toggle, ToggleButtons, DateTimePicker, DatePicker,
TimePicker, FileUpload, RichEditor, MarkdownEditor,
ColorPicker, TagsInput, KeyValue, CodeEditor, Hidden, Slider
};
class TemplateService
{
/**
* Generate dynamic form fields based on template placeholders
* For default data (template forms), use dataKey = 'default_data'
* For page data (page forms), use dataKey = 'header_data' or 'footer_data'
*
* @param array|null $defaultData Template's default_data array
*/
public static function generateDynamicFields($template, string $dataKey, ?array $existingData = null, ?array $defaultData = null): array
{
if (!$template) {
return [];
}
$placeholders = self::parsePlaceholders($template->html_content);
$fields = [];
foreach ($placeholders as $placeholder) {
// Handle special.* prefix - remove it for processing
$normalizedPlaceholder = $placeholder;
if (str_starts_with($placeholder, 'special.')) {
$normalizedPlaceholder = substr($placeholder, 8); // Remove 'special.' prefix
}
$parts = explode('.', $normalizedPlaceholder);
if (count($parts) < 2) {
continue; // Skip invalid placeholders
}
$type = $parts[0];
// Skip special.* placeholders - they are special placeholders (page.*, menu, staticMenu), not form fields
if (str_starts_with($placeholder, 'special.')) {
continue;
}
// Skip custom.* placeholders - they are blade components, not form fields
if ($type === 'custom') {
continue;
}
// Skip page.* placeholders - they are special placeholders, not form fields
if ($type === 'page') {
continue;
}
// Skip menu and staticMenu placeholders - they are special placeholders, not form fields
if ($normalizedPlaceholder === 'menu' || $normalizedPlaceholder === 'staticMenu') {
continue;
}
// For form fields, we expect type.field format (2 parts)
if (count($parts) !== 2) {
continue; // Skip invalid placeholders
}
[$type, $name] = $parts;
$label = str($name)->title()->replace('_', ' ')->toString();
// Get existing value from existingData if provided
$existingValue = $existingData[$placeholder] ?? null;
// Get default value from template's default_data
$defaultValue = $defaultData[$placeholder] ?? null;
// If existing value is empty (null, empty string, empty array), use default
$isValueEmpty = $existingValue === null
|| $existingValue === ''
|| (is_array($existingValue) && empty($existingValue));
// Use default if existing value is empty
$finalValue = $isValueEmpty && $defaultValue !== null ? $defaultValue : $existingValue;
$field = match($type) {
// Text Input Variants
'text' => TextInput::make("{$dataKey}.{$placeholder}")
->label($label)
->maxLength(255),
'email' => TextInput::make("{$dataKey}.{$placeholder}")
->label($label)
->email()
->maxLength(255),
'url' => TextInput::make("{$dataKey}.{$placeholder}")
->label($label)
->url()
->maxLength(500),
'tel' => TextInput::make("{$dataKey}.{$placeholder}")
->label($label)
->tel()
->maxLength(20),
'number' => TextInput::make("{$dataKey}.{$placeholder}")
->label($label)
->numeric(),
'password' => TextInput::make("{$dataKey}.{$placeholder}")
->label($label)
->password()
->dehydrated(fn ($state) => filled($state))
->maxLength(255),
// Text Area & Editors
'textarea' => Textarea::make("{$dataKey}.{$placeholder}")
->label($label)
->rows(4)
->maxLength(5000),
'richtext' => RichEditor::make("{$dataKey}.{$placeholder}")
->label($label)
->toolbarButtons([
'bold', 'italic', 'underline', 'link',
'bulletList', 'orderedList', 'h2', 'h3',
])
->maxLength(50000),
'markdown' => MarkdownEditor::make("{$dataKey}.{$placeholder}")
->label($label)
->toolbarButtons([
'bold', 'italic', 'strike', 'link',
'heading', 'bulletList', 'orderedList', 'codeBlock',
])
->maxLength(50000),
'code' => CodeEditor::make("{$dataKey}.{$placeholder}")
->label($label)
//->lineNumbers()
->maxLength(50000),
// Date & Time
'date' => DatePicker::make("{$dataKey}.{$placeholder}")
->label($label)
->native(false),
'datetime' => DateTimePicker::make("{$dataKey}.{$placeholder}")
->label($label)
->native(false)
->seconds(false),
'time' => TimePicker::make("{$dataKey}.{$placeholder}")
->label($label)
->native(false)
->seconds(false),
// File Uploads
'image' => FileUpload::make("{$dataKey}.{$placeholder}")
->label($label)
->image()
->disk('public')
->directory('templates/images')
->imageEditor()
->imageEditorAspectRatios([null, '16:9', '4:3', '1:1'])
->maxSize(5120),
'images' => FileUpload::make("{$dataKey}.{$placeholder}")
->label($label)
->image()
->disk('public')
->directory('templates/images')
->multiple()
->imageEditor()
->maxSize(5120)
->maxFiles(10),
'file' => FileUpload::make("{$dataKey}.{$placeholder}")
->label($label)
->disk('public')
->directory('templates/files')
->maxSize(10240),
'files' => FileUpload::make("{$dataKey}.{$placeholder}")
->label($label)
->disk('public')
->directory('templates/files')
->multiple()
->maxSize(10240)
->maxFiles(10),
// Selection & Options
'select' => Select::make("{$dataKey}.{$placeholder}")
->label($label)
->options(self::getSelectOptions($name))
->searchable(),
'multiselect' => Select::make("{$dataKey}.{$placeholder}")
->label($label)
->multiple()
->options(self::getSelectOptions($name))
->searchable(),
'checkbox' => Checkbox::make("{$dataKey}.{$placeholder}")
->label($label)
->inline(false),
'checkboxlist' => CheckboxList::make("{$dataKey}.{$placeholder}")
->label($label)
->options(self::getSelectOptions($name))
->columns(2),
'radio' => Radio::make("{$dataKey}.{$placeholder}")
->label($label)
->options(self::getSelectOptions($name))
->inline()
->inlineLabel(false),
'toggle' => Toggle::make("{$dataKey}.{$placeholder}")
->label($label)
->inline(false),
'togglebuttons' => ToggleButtons::make("{$dataKey}.{$placeholder}")
->label($label)
->options(self::getSelectOptions($name))
->inline()
->grouped(),
// Color & Visual
'color' => ColorPicker::make("{$dataKey}.{$placeholder}")
->label($label),
// Structured Data
'tags' => TagsInput::make("{$dataKey}.{$placeholder}")
->label($label)
->separator(','),
'keyvalue' => KeyValue::make("{$dataKey}.{$placeholder}")
->label($label)
->keyLabel('Key')
->valueLabel('Value')
->reorderable(),
// Advanced
'slider' => Slider::make("{$dataKey}.{$placeholder}")
->label($label)
->minValue(0)
->maxValue(100)
->step(1),
'hidden' => Hidden::make("{$dataKey}.{$placeholder}"),
// Default
default => TextInput::make("{$dataKey}.{$placeholder}")
->label($label)
->maxLength(255)
->helperText("Unknown type: {$type}"),
};
// Set default value: use finalValue (which falls back to default if existing is empty)
if ($finalValue !== null) {
$field->default($finalValue);
}
$fields[] = $field;
}
return $fields;
}
/**
* Parse placeholders from HTML content
* Format: {type.field_name} or {type.field-name} or {special.type.field}
*/
public static function parsePlaceholders(string $html): array
{
// Pattern: {type.field} or {special.type.field} or {type.field.field2} etc.
preg_match_all('/\{([a-z]+(?:\.[a-z][a-z_-]*)+)\}/i', $html, $matches);
return array_unique($matches[1] ?? []);
}
/**
* Get select options for a field
* Can be extended to load from database or config
*/
protected static function getSelectOptions(string $fieldName): array
{
return config("template-options.{$fieldName}", [
'option_1' => __('Option 1'),
'option_2' => __('Option 2'),
'option_3' => __('Option 3'),
]);
}
/**
* Replace placeholders in HTML with actual data
* Supports both {text.title} and {{text.title}} formats
* Also supports {menu} and {staticMenu} placeholders for menu rendering
* Also supports {custom.component_name} for custom blade components
* Also supports {page.content}, {page.title}, {page.excerpt} for page data
*
* @param string $html HTML content with placeholders
* @param array $data Data array for placeholders
* @param Model|null $model Optional model instance (Page, Blog, etc.) for dynamic data
*/
public static function replacePlaceholders(string $html, array $data, ?Model $model = null): string
{
// Handle special {page.content} placeholder - Sayfa/model içeriğini gösterir
if (str_contains($html, '{page.content}')) {
$pageContent = '';
if ($model) {
// Translation desteği varsa çeviriyi kullan
if (method_exists($model, 'translate')) {
$pageContent = $model->translate('content') ?: ($model->content ?? '');
} else {
$pageContent = $model->content ?? '';
}
}
$html = str_replace('{page.content}', $pageContent ?? '', $html);
}
// Handle special {page.title} placeholder - Sayfa/model başlığını gösterir
if (str_contains($html, '{page.title}')) {
$pageTitle = '';
if ($model) {
if (method_exists($model, 'translate')) {
$pageTitle = $model->translate('title') ?: ($model->title ?? '');
} else {
$pageTitle = $model->title ?? '';
}
}
$html = str_replace('{page.title}', $pageTitle ?? '', $html);
}
// Handle special {page.excerpt} placeholder - Sayfa/model özetini gösterir
if (str_contains($html, '{page.excerpt}')) {
$pageExcerpt = '';
if ($model) {
if (method_exists($model, 'translate')) {
$pageExcerpt = $model->translate('excerpt') ?: ($model->excerpt ?? '');
} else {
$pageExcerpt = $model->excerpt ?? '';
}
}
$html = str_replace('{page.excerpt}', $pageExcerpt ?? '', $html);
}
// Handle special {menu} placeholder first
if (str_contains($html, '{menu}')) {
$renderedMenu = \App\Services\MenuService::render();
$html = str_replace('{menu}', $renderedMenu, $html);
}
// Handle special {staticMenu} placeholder
if (str_contains($html, '{staticMenu}')) {
$renderedStaticMenu = view('components.static-menu')->render();
$html = str_replace('{staticMenu}', $renderedStaticMenu, $html);
}
// Handle custom blade components: {custom.component_name} or {custom.component-name}
preg_match_all('/\{custom\.([a-z][a-z_-]*)\}/i', $html, $customMatches);
if (!empty($customMatches[0])) {
foreach ($customMatches[0] as $index => $fullMatch) {
$componentName = $customMatches[1][$index] ?? null;
if ($componentName) {
// Normalize component name to lowercase for consistent file system access
$componentName = strtolower($componentName);
$viewPath = "components.custom.{$componentName}";
if (view()->exists($viewPath)) {
try {
$renderedComponent = view($viewPath)->render();
// Ensure rendered component is properly formatted
$renderedComponent = trim($renderedComponent);
$html = str_replace($fullMatch, $renderedComponent, $html);
} catch (\Exception $e) {
// Log the error for debugging
\Log::error("Custom component render error: {$viewPath}", [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
// Show error message in preview (visible in development)
$errorMessage = htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8');
$errorHtml = "<div style='padding: 10px; background: #fee; border: 1px solid #fcc; color: #c00; margin: 5px 0; border-radius: 4px;'>" .
"<strong>Custom Component Error:</strong> {$viewPath}<br>" .
"<small>{$errorMessage}</small>" .
"</div>";
$html = str_replace($fullMatch, $errorHtml, $html);
}
} else {
// Component doesn't exist - show notice in preview
$noticeHtml = "<div style='padding: 10px; background: #fff3cd; border: 1px solid #ffc107; color: #856404; margin: 5px 0; border-radius: 4px;'>" .
"<strong>Custom Component Not Found:</strong> {$viewPath}<br>" .
"<small>Create the file at: resources/views/components/custom/{$componentName}.blade.php</small>" .
"</div>";
$html = str_replace($fullMatch, $noticeHtml, $html);
}
}
}
}
// First, parse all placeholders in the format {type.field_name} or {type.field-name}
// Also supports {special.page.title} format (special.* prefix)
// Exclude custom.* from this regex to avoid double processing
// Pattern: {type.field} or {special.type.field} or {type.field.field2} etc.
preg_match_all('/\{([a-z]+(?:\.[a-z][a-z_-]*)+)\}/i', $html, $matches);
$placeholders = array_unique($matches[1] ?? []);
// Filter out custom.* placeholders as they're already handled above
$placeholders = array_filter($placeholders, function($placeholder) {
return !str_starts_with($placeholder, 'custom.');
});
// Replace each placeholder
foreach ($placeholders as $placeholder) {
// Handle special.* prefix - remove it and process as normal placeholder
// e.g., {special.page.title} -> {page.title}, {special.menu} -> {menu}
$originalPlaceholder = $placeholder;
if (str_starts_with($placeholder, 'special.')) {
$placeholder = substr($placeholder, 8); // Remove 'special.' prefix (8 characters)
}
// Extract type from placeholder
$parts = explode('.', $placeholder);
$type = $parts[0] ?? null;
$field = $parts[1] ?? null;
// Handle special placeholders that were prefixed with special.*
// These are page data placeholders: page.content, page.title, page.excerpt
if ($type === 'page') {
if ($field === 'content' && str_contains($html, '{' . $originalPlaceholder . '}')) {
$pageContent = '';
if ($page) {
if (method_exists($page, 'translate')) {
$pageContent = $page->translate('content') ?: $page->content;
} else {
$pageContent = $page->content;
}
}
$html = str_replace('{' . $originalPlaceholder . '}', $pageContent ?? '', $html);
continue;
}
if ($field === 'title' && str_contains($html, '{' . $originalPlaceholder . '}')) {
$pageTitle = '';
if ($page) {
if (method_exists($page, 'translate')) {
$pageTitle = $page->translate('title') ?: $page->title;
} else {
$pageTitle = $page->title;
}
}
$html = str_replace('{' . $originalPlaceholder . '}', $pageTitle ?? '', $html);
continue;
}
if ($field === 'excerpt' && str_contains($html, '{' . $originalPlaceholder . '}')) {
$pageExcerpt = '';
if ($page) {
if (method_exists($page, 'translate')) {
$pageExcerpt = $page->translate('excerpt') ?: $page->excerpt;
} else {
$pageExcerpt = $page->excerpt;
}
}
$html = str_replace('{' . $originalPlaceholder . '}', $pageExcerpt ?? '', $html);
continue;
}
}
// Handle special menu placeholders that were prefixed with special.*
if ($placeholder === 'menu' && str_contains($html, '{' . $originalPlaceholder . '}')) {
$renderedMenu = \App\Services\MenuService::render();
$html = str_replace('{' . $originalPlaceholder . '}', $renderedMenu, $html);
continue;
}
if ($placeholder === 'staticMenu' && str_contains($html, '{' . $originalPlaceholder . '}')) {
$renderedStaticMenu = view('components.static-menu')->render();
$html = str_replace('{' . $originalPlaceholder . '}', $renderedStaticMenu, $html);
continue;
}
// Handle setting.* placeholders - get value from Setting model
if ($type === 'setting' && $field) {
$value = setting($field, '');
// Get setting type to determine if it's a file/image
$settingModel = \App\Models\Setting::where('key', $field)->where('is_active', true)->first();
if ($settingModel && in_array($settingModel->type, ['file', 'image'])) {
// For file/image settings, use file type for formatting
$type = 'file';
}
} else {
// Try to find the value in data with various key formats
$value = null;
// 1. Try exact match (text.title)
if (isset($data[$placeholder])) {
$value = $data[$placeholder];
}
// 2. Try with section_data prefix (section_data.text.title)
elseif (isset($data["section_data.{$placeholder}"])) {
$value = $data["section_data.{$placeholder}"];
}
// 3. Try nested array access (data['section_data']['text.title'] or data['text']['title'])
else {
if (count($parts) === 2) {
[$type, $field] = $parts;
// Try nested: data['section_data'][$type][$field]
if (isset($data['section_data'][$type][$field])) {
$value = $data['section_data'][$type][$field];
}
// Try nested: data[$type][$field]
elseif (isset($data[$type][$field])) {
$value = $data[$type][$field];
}
}
}
}
// Handle different value types
if (is_array($value)) {
// Handle arrays (e.g., multiple images)
$value = array_map(function($item) use ($type) {
return self::formatFileUrl($item, $type);
}, $value);
$value = implode(', ', $value);
} elseif (is_bool($value)) {
$value = $value ? 'true' : 'false';
} elseif ($value === null) {
$value = '';
} else {
// Format file/image URLs
$value = self::formatFileUrl($value, $type);
}
// Replace both {text.title} and {{text.title}} formats
// Also replace original placeholder if it was prefixed with special.*
$replacements = [
"{{$placeholder}}",
"{{{$placeholder}}}"
];
if ($originalPlaceholder !== $placeholder) {
// Original placeholder had special.* prefix, replace it too
$replacements[] = "{{$originalPlaceholder}}";
$replacements[] = "{{{$originalPlaceholder}}}";
}
$html = str_replace($replacements, array_fill(0, count($replacements), $value), $html);
}
return $html;
}
/**
* Format file URL based on type
* Converts storage paths to public URLs
*/
protected static function formatFileUrl($value, $type): string
{
// If it's already a full URL, return as is
if (is_string($value) && (str_starts_with($value, 'http://') || str_starts_with($value, 'https://'))) {
return $value;
}
// Only process image and file types
if (!in_array($type, ['image', 'images', 'file', 'files'])) {
return (string) $value;
}
// If value is empty, return empty string
if (empty($value) || !is_string($value)) {
return '';
}
// Check if the path starts with storage/ (already formatted)
if (str_starts_with($value, 'storage/')) {
return asset($value);
}
// Remove private/ prefix if exists (old format from private disk)
if (str_starts_with($value, 'private/')) {
$value = str_replace('private/', '', $value);
}
// Check if the path is a relative path that should be in storage
// Templates are stored in templates/images or templates/files
if (str_starts_with($value, 'templates/')) {
return asset('storage/' . $value);
}
// If it's already using asset() or Storage::url(), assume it's already formatted
// Otherwise, prepend storage/ if it looks like a storage path
if (!str_starts_with($value, '/')) {
return asset('storage/' . $value);
}
return $value;
}
}