feat(06-admin-base): Admin\ base infrastructure — Form Edit System + Support layer (Phase 6)
Phase 6 zamknięta po 2 planach. Pełny fundament dla Phase 7-13 (migracja 17 admin controllers do Admin\ namespace). 06-01 (Forms infrastructure): - Admin\ViewModels\Forms\* — 5 ViewModeli (687 L) - Admin\Validation\FormValidator (196 L) - composer.json: php >=7.4, PSR-4 paths cross-platform safe (Admin\ → autoload/admin/, Frontend\ → autoload/front/) 06-02 (Support layer): - Admin\Support\TableListRequestFactory (99 L) — parser list z $_GET - Admin\Support\Forms\FormRequestHandler (159 L) — POST + CSRF + walidacja + persist - Admin\Support\Forms\FormFieldRenderer (494 L) — renderer HTML pól Decyzje: - Brak BaseController — Phase 7+ kontrolery jako POJOs z DI (jak shopPRO) - PSR-4 filename fix: TableListRequestFactory.php (bez shopPRO 'class.' prefix) - PascalCase namespace (Admin\Support) na lowercase folder admin/ ze względu na Windows fs case-insensitivity vs legacy admin/controls/ Pliki: 8 nowych klas, 1635 L kodu PHP 7.4-kompatybilnego, zero regresji. Smoke test: walidacja e-maila zwraca PL komunikat, factory parsuje ?page=&per_page=&sort=&filter=, Domain/Shared nadal ładują się. PHPUnit: 37/37 OK. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
494
autoload/admin/Support/Forms/FormFieldRenderer.php
Normal file
494
autoload/admin/Support/Forms/FormFieldRenderer.php
Normal file
@@ -0,0 +1,494 @@
|
||||
<?php
|
||||
namespace Admin\Support\Forms;
|
||||
|
||||
use Admin\ViewModels\Forms\FormEditViewModel;
|
||||
use Admin\ViewModels\Forms\FormField;
|
||||
use Admin\ViewModels\Forms\FormFieldType;
|
||||
|
||||
/**
|
||||
* Renderer pól formularza
|
||||
*/
|
||||
class FormFieldRenderer
|
||||
{
|
||||
private FormEditViewModel $form;
|
||||
|
||||
public function __construct(FormEditViewModel $form)
|
||||
{
|
||||
$this->form = $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderuje pojedyncze pole
|
||||
*/
|
||||
public function renderField(FormField $field): string
|
||||
{
|
||||
$method = 'render' . ucfirst($field->type);
|
||||
|
||||
if (method_exists($this, $method)) {
|
||||
return $this->$method($field);
|
||||
}
|
||||
|
||||
// Fallback dla nieznanych typów - renderuj jako text
|
||||
return $this->renderText($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderuje pole tekstowe
|
||||
*/
|
||||
public function renderText(FormField $field): string
|
||||
{
|
||||
$value = $this->form->getFieldValue($field);
|
||||
$error = $this->form->getError($field->name);
|
||||
|
||||
$params = [
|
||||
'label' => $field->label,
|
||||
'name' => $field->name,
|
||||
'id' => $field->id,
|
||||
'value' => $value ?? '',
|
||||
'type' => 'text',
|
||||
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||
];
|
||||
|
||||
if ($field->placeholder) {
|
||||
$params['placeholder'] = $field->placeholder;
|
||||
}
|
||||
|
||||
if ($error) {
|
||||
$params['class'] .= ' error';
|
||||
}
|
||||
|
||||
return $this->wrapWithError(\Shared\Html\Html::input($params), $error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderuje pole number
|
||||
*/
|
||||
public function renderNumber(FormField $field): string
|
||||
{
|
||||
$value = $this->form->getFieldValue($field);
|
||||
$error = $this->form->getError($field->name);
|
||||
|
||||
$params = [
|
||||
'label' => $field->label,
|
||||
'name' => $field->name,
|
||||
'id' => $field->id,
|
||||
'value' => $value ?? '',
|
||||
'type' => 'number',
|
||||
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||
];
|
||||
|
||||
if ($error) {
|
||||
$params['class'] .= ' error';
|
||||
}
|
||||
|
||||
return $this->wrapWithError(\Shared\Html\Html::input($params), $error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderuje pole email
|
||||
*/
|
||||
public function renderEmail(FormField $field): string
|
||||
{
|
||||
$value = $this->form->getFieldValue($field);
|
||||
$error = $this->form->getError($field->name);
|
||||
|
||||
$params = [
|
||||
'label' => $field->label,
|
||||
'name' => $field->name,
|
||||
'id' => $field->id,
|
||||
'value' => $value ?? '',
|
||||
'type' => 'email',
|
||||
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||
];
|
||||
|
||||
if ($error) {
|
||||
$params['class'] .= ' error';
|
||||
}
|
||||
|
||||
return $this->wrapWithError(\Shared\Html\Html::input($params), $error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderuje pole password
|
||||
*/
|
||||
public function renderPassword(FormField $field): string
|
||||
{
|
||||
$value = $this->form->getFieldValue($field);
|
||||
|
||||
return \Shared\Html\Html::input([
|
||||
'label' => $field->label,
|
||||
'name' => $field->name,
|
||||
'id' => $field->id,
|
||||
'value' => $value ?? '',
|
||||
'type' => 'password',
|
||||
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderuje pole daty
|
||||
*/
|
||||
public function renderDate(FormField $field): string
|
||||
{
|
||||
$value = $this->form->getFieldValue($field);
|
||||
$error = $this->form->getError($field->name);
|
||||
|
||||
$params = [
|
||||
'label' => $field->label,
|
||||
'name' => $field->name,
|
||||
'id' => $field->id,
|
||||
'value' => $value ?? '',
|
||||
'type' => 'text',
|
||||
'class' => 'date ' . ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||
];
|
||||
|
||||
if ($error) {
|
||||
$params['class'] .= ' error';
|
||||
}
|
||||
|
||||
return $this->wrapWithError(\Shared\Html\Html::input($params), $error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderuje pole daty i czasu
|
||||
*/
|
||||
public function renderDatetime(FormField $field): string
|
||||
{
|
||||
$value = $this->form->getFieldValue($field);
|
||||
|
||||
return \Shared\Html\Html::input([
|
||||
'label' => $field->label,
|
||||
'name' => $field->name,
|
||||
'id' => $field->id,
|
||||
'value' => $value ?? '',
|
||||
'type' => 'text',
|
||||
'class' => 'datetime ' . ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderuje przełącznik (switch)
|
||||
*/
|
||||
public function renderSwitch(FormField $field): string
|
||||
{
|
||||
$value = $this->form->getFieldValue($field);
|
||||
|
||||
// Domyślna wartość dla nowego rekordu
|
||||
if ($value === null && $field->value === true) {
|
||||
$checked = true;
|
||||
} else {
|
||||
$checked = (bool) $value;
|
||||
}
|
||||
|
||||
return \Shared\Html\Html::input_switch([
|
||||
'label' => $field->label,
|
||||
'name' => $field->name,
|
||||
'id' => $field->id,
|
||||
'checked' => $checked,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderuje select
|
||||
*/
|
||||
public function renderSelect(FormField $field): string
|
||||
{
|
||||
$value = $this->form->getFieldValue($field);
|
||||
$error = $this->form->getError($field->name);
|
||||
|
||||
$params = [
|
||||
'label' => $field->label,
|
||||
'name' => $field->name,
|
||||
'id' => $field->id,
|
||||
'value' => $value ?? '',
|
||||
'values' => $field->options,
|
||||
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||
];
|
||||
|
||||
if ($error) {
|
||||
$params['class'] .= ' error';
|
||||
}
|
||||
|
||||
return $this->wrapWithError(\Shared\Html\Html::select($params), $error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderuje textarea
|
||||
*/
|
||||
public function renderTextarea(FormField $field): string
|
||||
{
|
||||
$value = $this->form->getFieldValue($field);
|
||||
|
||||
return \Shared\Html\Html::textarea([
|
||||
'label' => $field->label,
|
||||
'name' => $field->name,
|
||||
'id' => $field->id,
|
||||
'value' => $value ?? '',
|
||||
'rows' => $field->attributes['rows'] ?? 4,
|
||||
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderuje edytor (CKEditor)
|
||||
*/
|
||||
public function renderEditor(FormField $field): string
|
||||
{
|
||||
$value = $this->form->getFieldValue($field);
|
||||
|
||||
return \Shared\Html\Html::textarea([
|
||||
'label' => $field->label,
|
||||
'name' => $field->name,
|
||||
'id' => $field->id,
|
||||
'value' => $value ?? '',
|
||||
'rows' => max(10, ($field->attributes['rows'] ?? 10)),
|
||||
'class' => 'editor ' . ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderuje pole obrazu z filemanagerem
|
||||
*/
|
||||
public function renderImage(FormField $field): string
|
||||
{
|
||||
$value = $this->form->getFieldValue($field);
|
||||
|
||||
$filemanagerUrl = $field->filemanagerUrl ?? $this->generateFilemanagerUrl($field->id);
|
||||
|
||||
return \Shared\Html\Html::input_icon([
|
||||
'label' => $field->label,
|
||||
'name' => $field->name,
|
||||
'id' => $field->id,
|
||||
'value' => $value ?? '',
|
||||
'type' => 'text',
|
||||
'icon_content' => 'przeglądaj',
|
||||
'icon_js' => "window.open('{$filemanagerUrl}', 'filemanager', 'location=1,status=1,scrollbars=1,width=1100,height=700')",
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderuje pole pliku
|
||||
*/
|
||||
public function renderFile(FormField $field): string
|
||||
{
|
||||
$value = $this->form->getFieldValue($field);
|
||||
|
||||
if ($field->useFilemanager) {
|
||||
$filemanagerUrl = $field->filemanagerUrl ?? $this->generateFilemanagerUrl($field->id);
|
||||
|
||||
return \Shared\Html\Html::input_icon([
|
||||
'label' => $field->label,
|
||||
'name' => $field->name,
|
||||
'id' => $field->id,
|
||||
'value' => $value ?? '',
|
||||
'type' => 'text',
|
||||
'icon_content' => 'przeglądaj',
|
||||
'icon_js' => "window.open('{$filemanagerUrl}', 'filemanager', 'location=1,status=1,scrollbars=1,width=1100,height=700')",
|
||||
]);
|
||||
}
|
||||
|
||||
return \Shared\Html\Html::input([
|
||||
'label' => $field->label,
|
||||
'name' => $field->name,
|
||||
'id' => $field->id,
|
||||
'type' => 'file',
|
||||
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderuje ukryte pole
|
||||
*/
|
||||
public function renderHidden(FormField $field): string
|
||||
{
|
||||
$value = $this->form->getFieldValue($field);
|
||||
|
||||
return '<input type="hidden" name="' . htmlspecialchars($field->name) . '" ' .
|
||||
'id="' . htmlspecialchars($field->id) . '" ' .
|
||||
'value="' . htmlspecialchars($value ?? '') . '">';
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderuje pole koloru (color picker + text input)
|
||||
*/
|
||||
public function renderColor(FormField $field): string
|
||||
{
|
||||
$value = $this->form->getFieldValue($field);
|
||||
$error = $this->form->getError($field->name);
|
||||
$colorValue = htmlspecialchars($value ?? '#000000', ENT_QUOTES, 'UTF-8');
|
||||
$fieldName = htmlspecialchars($field->name, ENT_QUOTES, 'UTF-8');
|
||||
$fieldId = htmlspecialchars($field->id, ENT_QUOTES, 'UTF-8');
|
||||
$label = htmlspecialchars($field->label, ENT_QUOTES, 'UTF-8');
|
||||
|
||||
$html = '<div class="form-group row">';
|
||||
$html .= '<label class="col-lg-4 control-label">' . $label . ':</label>';
|
||||
$html .= '<div class="col-lg-8">';
|
||||
$html .= '<div style="display:flex;align-items:center;gap:8px;">';
|
||||
$html .= '<input type="color" id="' . $fieldId . '_picker" value="' . $colorValue . '" style="width:40px;height:34px;padding:2px;border:1px solid #ccc;cursor:pointer;" />';
|
||||
$html .= '<input type="text" name="' . $fieldName . '" id="' . $fieldId . '" value="' . $colorValue . '" class="form-control" style="max-width:150px;" />';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
$html .= '<script>$(function(){'
|
||||
. 'var $p=$("#' . $fieldId . '_picker"),$t=$("#' . $fieldId . '");'
|
||||
. '$p.on("input",function(){$t.val(this.value);});'
|
||||
. '$t.on("input",function(){var v=this.value;if(/^#[0-9a-fA-F]{6}$/.test(v))$p.val(v);});'
|
||||
. '});</script>';
|
||||
|
||||
return $this->wrapWithError($html, $error);
|
||||
}
|
||||
|
||||
public function renderCustom(FormField $field): string
|
||||
{
|
||||
return (string)($field->customHtml ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderuje sekcję językową
|
||||
*/
|
||||
public function renderLangSection(FormField $section): string
|
||||
{
|
||||
if ($section->langFields === null || $this->form->languages === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$out = '<div id="languages-' . $section->name . '" class="languages-tabs">';
|
||||
|
||||
// Zakładki języków
|
||||
$out .= '<ul class="resp-tabs-list languages-tabs htabs">';
|
||||
foreach ($this->form->languages as $lang) {
|
||||
if ($lang['status']) {
|
||||
$out .= '<li>' . htmlspecialchars($lang['name']) . '</li>';
|
||||
}
|
||||
}
|
||||
$out .= '</ul>';
|
||||
|
||||
// Kontenery języków
|
||||
$out .= '<div class="resp-tabs-container languages-tabs">';
|
||||
foreach ($this->form->languages as $lang) {
|
||||
if ($lang['status']) {
|
||||
$out .= '<div>';
|
||||
foreach ($section->langFields as $field) {
|
||||
$out .= $this->renderLangField($field, $lang['id'], $section->name);
|
||||
}
|
||||
$out .= '</div>';
|
||||
}
|
||||
}
|
||||
$out .= '</div>';
|
||||
|
||||
$out .= '</div>';
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderuje pole w sekcji językowej
|
||||
*/
|
||||
private function renderLangField(FormField $field, $languageId, string $sectionName): string
|
||||
{
|
||||
$value = $this->form->getFieldValue($field, $languageId, $field->name);
|
||||
$error = $this->form->getError($sectionName . '_' . $field->name, $languageId);
|
||||
|
||||
$name = $field->getLocalizedName($languageId);
|
||||
$id = $field->getLocalizedId($languageId);
|
||||
|
||||
switch ($field->type) {
|
||||
case FormFieldType::IMAGE:
|
||||
$filemanagerUrl = $field->filemanagerUrl ?? $this->generateFilemanagerUrl($id);
|
||||
return $this->wrapWithError(\Shared\Html\Html::input_icon([
|
||||
'label' => $field->label,
|
||||
'name' => $name,
|
||||
'id' => $id,
|
||||
'value' => $value ?? '',
|
||||
'type' => 'text',
|
||||
'icon_content' => 'przeglądaj',
|
||||
'icon_js' => "window.open('{$filemanagerUrl}', 'filemanager', 'location=1,status=1,scrollbars=1,width=1100,height=700')",
|
||||
]), $error);
|
||||
|
||||
case FormFieldType::TEXTAREA:
|
||||
case FormFieldType::EDITOR:
|
||||
return $this->wrapWithError(\Shared\Html\Html::textarea([
|
||||
'label' => $field->label,
|
||||
'name' => $name,
|
||||
'id' => $id,
|
||||
'value' => $value ?? '',
|
||||
'rows' => $field->type === FormFieldType::EDITOR ? 10 : ($field->attributes['rows'] ?? 4),
|
||||
'class' => $field->type === FormFieldType::EDITOR ? 'editor' : '',
|
||||
]), $error);
|
||||
|
||||
case FormFieldType::SWITCH:
|
||||
return \Shared\Html\Html::input_switch([
|
||||
'label' => $field->label,
|
||||
'name' => $name,
|
||||
'id' => $id,
|
||||
'checked' => (bool) $value,
|
||||
]);
|
||||
|
||||
case FormFieldType::SELECT:
|
||||
return $this->wrapWithError(\Shared\Html\Html::select([
|
||||
'label' => $field->label,
|
||||
'name' => $name,
|
||||
'id' => $id,
|
||||
'value' => $value ?? '',
|
||||
'values' => $field->options,
|
||||
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||
]), $error);
|
||||
|
||||
default: // TEXT, URL, etc.
|
||||
if (!empty($field->attributes['icon_content'])) {
|
||||
$iconJs = (string)($field->attributes['icon_js'] ?? '');
|
||||
if ($iconJs !== '') {
|
||||
$iconJs = str_replace('{lang}', (string)$languageId, $iconJs);
|
||||
}
|
||||
|
||||
return $this->wrapWithError(\Shared\Html\Html::input_icon([
|
||||
'label' => $field->label,
|
||||
'name' => $name,
|
||||
'id' => $id,
|
||||
'value' => $value ?? '',
|
||||
'type' => $field->type === FormFieldType::EMAIL ? 'email' : 'text',
|
||||
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||
'icon_content' => (string)$field->attributes['icon_content'],
|
||||
'icon_class' => (string)($field->attributes['icon_class'] ?? ''),
|
||||
'icon_js' => $iconJs,
|
||||
]), $error);
|
||||
}
|
||||
|
||||
return $this->wrapWithError(\Shared\Html\Html::input([
|
||||
'label' => $field->label,
|
||||
'name' => $name,
|
||||
'id' => $id,
|
||||
'value' => $value ?? '',
|
||||
'type' => $field->type === FormFieldType::EMAIL ? 'email' : 'text',
|
||||
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||
]), $error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generuje URL do filemanagera
|
||||
*/
|
||||
private function generateFilemanagerUrl(string $fieldId): string
|
||||
{
|
||||
$rfmAkey = $_SESSION['rfm_akey'] ?? bin2hex(random_bytes(16));
|
||||
$_SESSION['rfm_akey'] = $rfmAkey;
|
||||
$_SESSION['rfm_akey_expires'] = time() + 20 * 60;
|
||||
$_SESSION['can_use_rfm'] = true;
|
||||
|
||||
$fieldIdParam = rawurlencode($fieldId);
|
||||
$akeyParam = rawurlencode($rfmAkey);
|
||||
return "/libraries/filemanager-9.14.2/dialog.php?type=1&popup=1&field_id={$fieldIdParam}&akey={$akeyParam}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Opakowuje pole w kontener błędu
|
||||
*/
|
||||
private function wrapWithError(string $html, ?string $error): string
|
||||
{
|
||||
if ($error) {
|
||||
return '<div class="field-with-error">' . $html .
|
||||
'<span class="error-message">' . htmlspecialchars($error) . '</span></div>';
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
159
autoload/admin/Support/Forms/FormRequestHandler.php
Normal file
159
autoload/admin/Support/Forms/FormRequestHandler.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
namespace Admin\Support\Forms;
|
||||
|
||||
use Admin\ViewModels\Forms\FormEditViewModel;
|
||||
use Admin\ViewModels\Forms\FormFieldType;
|
||||
use Admin\Validation\FormValidator;
|
||||
|
||||
/**
|
||||
* Obsługa żądań formularza (POST, persist, walidacja)
|
||||
*/
|
||||
class FormRequestHandler
|
||||
{
|
||||
private FormValidator $validator;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->validator = new FormValidator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Przetwarza żądanie POST formularza
|
||||
*
|
||||
* @param FormEditViewModel $formViewModel
|
||||
* @param array $postData Dane z $_POST
|
||||
* @return array Wynik przetwarzania ['success' => bool, 'errors' => array, 'data' => array]
|
||||
*/
|
||||
public function handleSubmit(FormEditViewModel $formViewModel, array $postData): array
|
||||
{
|
||||
$result = [
|
||||
'success' => false,
|
||||
'errors' => [],
|
||||
'data' => []
|
||||
];
|
||||
|
||||
// Walidacja CSRF
|
||||
$csrfToken = isset($postData['_csrf_token']) ? (string) $postData['_csrf_token'] : '';
|
||||
if (!\Shared\Security\CsrfToken::validate($csrfToken)) {
|
||||
$result['errors'] = ['csrf' => 'Nieprawidłowy token bezpieczeństwa. Odśwież stronę i spróbuj ponownie.'];
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Walidacja
|
||||
$errors = $this->validator->validate($postData, $formViewModel->fields, $formViewModel->languages);
|
||||
|
||||
if (!empty($errors)) {
|
||||
$result['errors'] = $errors;
|
||||
// Zapisz dane do persist przy błędzie walidacji
|
||||
if ($formViewModel->persist) {
|
||||
$formViewModel->saveToPersist($postData);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Przetwórz dane (np. konwersja typów)
|
||||
$processedData = $this->processData($postData, $formViewModel->fields);
|
||||
|
||||
$result['success'] = true;
|
||||
$result['data'] = $processedData;
|
||||
|
||||
// Wyczyść persist po sukcesie
|
||||
if ($formViewModel->persist) {
|
||||
$formViewModel->clearPersist();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Przetwarza dane z formularza (konwersja typów)
|
||||
*/
|
||||
private function processData(array $postData, array $fields): array
|
||||
{
|
||||
$processed = [];
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$value = $postData[$field->name] ?? null;
|
||||
|
||||
// Konwersja typów
|
||||
switch ($field->type) {
|
||||
case FormFieldType::SWITCH:
|
||||
$processed[$field->name] = $value ? 1 : 0;
|
||||
break;
|
||||
|
||||
case FormFieldType::NUMBER:
|
||||
$processed[$field->name] = $value !== null && $value !== '' ? (float)$value : null;
|
||||
break;
|
||||
|
||||
case FormFieldType::LANG_SECTION:
|
||||
if ($field->langFields !== null) {
|
||||
$processed[$field->name] = $this->processLangSection($postData, $field);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$processed[$field->name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $processed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Przetwarza sekcję językową
|
||||
*/
|
||||
private function processLangSection(array $postData, $section): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
if ($section->langFields === null) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
foreach ($section->langFields as $field) {
|
||||
$fieldName = $field->name;
|
||||
$langData = $postData[$fieldName] ?? [];
|
||||
|
||||
foreach ($langData as $langId => $value) {
|
||||
if (!isset($result[$langId])) {
|
||||
$result[$langId] = [];
|
||||
}
|
||||
|
||||
// Konwersja typów dla pól językowych
|
||||
switch ($field->type) {
|
||||
case FormFieldType::SWITCH:
|
||||
$result[$langId][$fieldName] = $value ? 1 : 0;
|
||||
break;
|
||||
case FormFieldType::NUMBER:
|
||||
$result[$langId][$fieldName] = $value !== null && $value !== '' ? (float)$value : null;
|
||||
break;
|
||||
default:
|
||||
$result[$langId][$fieldName] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Przywraca dane z persist do POST (przy błędzie walidacji)
|
||||
*/
|
||||
public function restoreFromPersist(FormEditViewModel $formViewModel): ?array
|
||||
{
|
||||
if (!$formViewModel->persist) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $_SESSION['form_persist'][$formViewModel->formId] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sprawdza czy żądanie jest submitowaniem formularza
|
||||
*/
|
||||
public function isFormSubmit(string $formId): bool
|
||||
{
|
||||
return $_SERVER['REQUEST_METHOD'] === 'POST' &&
|
||||
(isset($_POST['_form_id']) && $_POST['_form_id'] === $formId);
|
||||
}
|
||||
}
|
||||
99
autoload/admin/Support/TableListRequestFactory.php
Normal file
99
autoload/admin/Support/TableListRequestFactory.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
namespace Admin\Support;
|
||||
|
||||
class TableListRequestFactory
|
||||
{
|
||||
public const DEFAULT_PER_PAGE_OPTIONS = [5, 10, 15, 25, 50, 100];
|
||||
public const DEFAULT_PER_PAGE = 15;
|
||||
|
||||
/**
|
||||
* Buduje kontekst listy (filtry, sortowanie, paginacja) z requestu.
|
||||
*
|
||||
* @return array{
|
||||
* page:int,
|
||||
* perPage:int,
|
||||
* perPageOptions:array<int,int>,
|
||||
* filters:array<string,string>,
|
||||
* viewFilters:array<int,array<string,mixed>>,
|
||||
* queryFilters:array<string,string>,
|
||||
* sortColumn:string,
|
||||
* sortDir:string
|
||||
* }
|
||||
*/
|
||||
public static function fromRequest(
|
||||
array $filterDefinitions,
|
||||
array $sortableColumns,
|
||||
string $defaultSortColumn = 'date_add',
|
||||
?array $perPageOptions = null,
|
||||
?int $defaultPerPage = null
|
||||
): array {
|
||||
if ($perPageOptions === null) {
|
||||
$perPageOptions = self::DEFAULT_PER_PAGE_OPTIONS;
|
||||
}
|
||||
|
||||
if ($defaultPerPage === null) {
|
||||
$defaultPerPage = self::DEFAULT_PER_PAGE;
|
||||
}
|
||||
|
||||
if (!in_array($defaultPerPage, $perPageOptions, true)) {
|
||||
$defaultPerPage = (int)$perPageOptions[0];
|
||||
}
|
||||
|
||||
$page = max(1, (int)\Shared\Helpers\Helpers::get('page'));
|
||||
$perPage = (int)\Shared\Helpers\Helpers::get('per_page');
|
||||
if (!in_array($perPage, $perPageOptions, true)) {
|
||||
$perPage = $defaultPerPage;
|
||||
}
|
||||
|
||||
$filters = [];
|
||||
$viewFilters = [];
|
||||
$queryFilters = [];
|
||||
|
||||
foreach ($filterDefinitions as $definition) {
|
||||
$key = (string)($definition['key'] ?? '');
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = (string)($definition['type'] ?? 'text');
|
||||
$value = (string)\Shared\Helpers\Helpers::get($key);
|
||||
|
||||
$filters[$key] = $value;
|
||||
$queryFilters[$key] = $value;
|
||||
|
||||
$filterConfig = [
|
||||
'key' => $key,
|
||||
'label' => (string)($definition['label'] ?? $key),
|
||||
'type' => $type,
|
||||
'value' => $value,
|
||||
];
|
||||
|
||||
if ($type === 'select' && isset($definition['options']) && is_array($definition['options'])) {
|
||||
$filterConfig['options'] = $definition['options'];
|
||||
}
|
||||
|
||||
$viewFilters[] = $filterConfig;
|
||||
}
|
||||
|
||||
$sortColumn = trim((string)\Shared\Helpers\Helpers::get('sort'));
|
||||
if (!in_array($sortColumn, $sortableColumns, true)) {
|
||||
$sortColumn = $defaultSortColumn;
|
||||
}
|
||||
|
||||
$sortDir = strtoupper(trim((string)\Shared\Helpers\Helpers::get('dir')));
|
||||
if (!in_array($sortDir, ['ASC', 'DESC'], true)) {
|
||||
$sortDir = 'DESC';
|
||||
}
|
||||
|
||||
return [
|
||||
'page' => $page,
|
||||
'perPage' => $perPage,
|
||||
'perPageOptions' => $perPageOptions,
|
||||
'filters' => $filters,
|
||||
'viewFilters' => $viewFilters,
|
||||
'queryFilters' => $queryFilters,
|
||||
'sortColumn' => $sortColumn,
|
||||
'sortDir' => $sortDir,
|
||||
];
|
||||
}
|
||||
}
|
||||
196
autoload/admin/Validation/FormValidator.php
Normal file
196
autoload/admin/Validation/FormValidator.php
Normal file
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
namespace Admin\Validation;
|
||||
|
||||
use Admin\ViewModels\Forms\FormField;
|
||||
use Admin\ViewModels\Forms\FormFieldType;
|
||||
|
||||
/**
|
||||
* Walidator formularzy
|
||||
*/
|
||||
class FormValidator
|
||||
{
|
||||
private array $errors = [];
|
||||
|
||||
/**
|
||||
* Waliduje dane na podstawie definicji pól
|
||||
*
|
||||
* @param array $data Dane z POST
|
||||
* @param array $fields Definicje pól (FormField[])
|
||||
* @param array|null $languages Języki (dla walidacji pól językowych)
|
||||
* @return array Tablica błędów (pusta jeśli OK)
|
||||
*/
|
||||
public function validate(array $data, array $fields, ?array $languages = null): array
|
||||
{
|
||||
$this->errors = [];
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if ($field->type === FormFieldType::LANG_SECTION) {
|
||||
$this->validateLangSection($data, $field, $languages ?? []);
|
||||
} else {
|
||||
$this->validateField($data, $field);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waliduje pojedyncze pole
|
||||
*/
|
||||
private function validateField(array $data, FormField $field): void
|
||||
{
|
||||
$value = $data[$field->name] ?? null;
|
||||
|
||||
// Walidacja wymagalności
|
||||
if ($field->required && $this->isEmpty($value)) {
|
||||
$this->errors[$field->name] = "Pole \"{$field->label}\" jest wymagane.";
|
||||
return;
|
||||
}
|
||||
|
||||
// Jeśli pole puste i nie jest wymagane - pomijamy dalszą walidację
|
||||
if ($this->isEmpty($value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Walidacja typu
|
||||
switch ($field->type) {
|
||||
case FormFieldType::EMAIL:
|
||||
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->errors[$field->name] = "Pole \"{$field->label}\" musi być poprawnym adresem e-mail.";
|
||||
}
|
||||
break;
|
||||
|
||||
case FormFieldType::NUMBER:
|
||||
if (!is_numeric($value)) {
|
||||
$this->errors[$field->name] = "Pole \"{$field->label}\" musi być liczbą.";
|
||||
}
|
||||
break;
|
||||
|
||||
case FormFieldType::DATE:
|
||||
if (!$this->isValidDate($value)) {
|
||||
$this->errors[$field->name] = "Pole \"{$field->label}\" musi być poprawną datą (YYYY-MM-DD).";
|
||||
}
|
||||
break;
|
||||
|
||||
case FormFieldType::DATETIME:
|
||||
if (!$this->isValidDateTime($value)) {
|
||||
$this->errors[$field->name] = "Pole \"{$field->label}\" musi być poprawną datą i czasem.";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Walidacja customowa (callback)
|
||||
if (isset($field->attributes['validate_callback']) && is_callable($field->attributes['validate_callback'])) {
|
||||
$result = call_user_func($field->attributes['validate_callback'], $value, $data);
|
||||
if ($result !== true) {
|
||||
$this->errors[$field->name] = is_string($result) ? $result : "Pole \"{$field->label}\" zawiera nieprawidłową wartość.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waliduje sekcję językową
|
||||
*/
|
||||
private function validateLangSection(array $data, FormField $section, array $languages): void
|
||||
{
|
||||
if ($section->langFields === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($languages as $language) {
|
||||
if (!($language['status'] ?? false)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$langId = $language['id'];
|
||||
|
||||
foreach ($section->langFields as $field) {
|
||||
$fieldName = $field->name;
|
||||
$value = $data[$fieldName][$langId] ?? null;
|
||||
|
||||
// Walidacja wymagalności
|
||||
if ($field->required && $this->isEmpty($value)) {
|
||||
$errorKey = "{$section->name}_{$fieldName}";
|
||||
$this->errors[$errorKey][$langId] = "Pole \"{$field->label}\" ({$language['name']}) jest wymagane.";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Walidacja typu dla pól językowych
|
||||
if (!$this->isEmpty($value)) {
|
||||
switch ($field->type) {
|
||||
case FormFieldType::EMAIL:
|
||||
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
$errorKey = "{$section->name}_{$fieldName}";
|
||||
$this->errors[$errorKey][$langId] = "Pole \"{$field->label}\" ({$language['name']}) musi być poprawnym e-mailem.";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sprawdza czy wartość jest pusta
|
||||
*/
|
||||
private function isEmpty($value): bool
|
||||
{
|
||||
return $value === null || $value === '' || (is_array($value) && empty($value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sprawdza czy data jest poprawna (YYYY-MM-DD)
|
||||
*/
|
||||
private function isValidDate(string $date): bool
|
||||
{
|
||||
$d = \DateTime::createFromFormat('Y-m-d', $date);
|
||||
return $d && $d->format('Y-m-d') === $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sprawdza czy data i czas są poprawne
|
||||
*/
|
||||
private function isValidDateTime(string $datetime): bool
|
||||
{
|
||||
$d = \DateTime::createFromFormat('Y-m-d H:i:s', $datetime);
|
||||
if ($d && $d->format('Y-m-d H:i:s') === $datetime) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Spróbuj bez sekund
|
||||
$d = \DateTime::createFromFormat('Y-m-d H:i', $datetime);
|
||||
return $d && $d->format('Y-m-d H:i') === $datetime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sprawdza czy walidacja zakończyła się sukcesem
|
||||
*/
|
||||
public function isValid(): bool
|
||||
{
|
||||
return empty($this->errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zwraca wszystkie błędy
|
||||
*/
|
||||
public function getErrors(): array
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zwraca pierwszy błąd
|
||||
*/
|
||||
public function getFirstError(): ?string
|
||||
{
|
||||
if (empty($this->errors)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$first = reset($this->errors);
|
||||
if (is_array($first)) {
|
||||
return reset($first);
|
||||
}
|
||||
return $first;
|
||||
}
|
||||
}
|
||||
89
autoload/admin/ViewModels/Forms/FormAction.php
Normal file
89
autoload/admin/ViewModels/Forms/FormAction.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
namespace Admin\ViewModels\Forms;
|
||||
|
||||
/**
|
||||
* Definicja akcji formularza (przycisku)
|
||||
*/
|
||||
class FormAction
|
||||
{
|
||||
public string $name;
|
||||
public string $label;
|
||||
public string $type;
|
||||
public string $url;
|
||||
public ?string $backUrl;
|
||||
public string $cssClass;
|
||||
public array $attributes;
|
||||
|
||||
/**
|
||||
* @param string $name Nazwa akcji (save, cancel, delete)
|
||||
* @param string $label Etykieta przycisku
|
||||
* @param string $url URL akcji (dla save)
|
||||
* @param string|null $backUrl URL powrotu po zapisie
|
||||
* @param string $cssClass Klasy CSS przycisku
|
||||
* @param string $type Typ przycisku (submit, button, link)
|
||||
* @param array $attributes Dodatkowe atrybuty HTML
|
||||
*/
|
||||
public function __construct(
|
||||
string $name,
|
||||
string $label,
|
||||
string $url = '',
|
||||
?string $backUrl = null,
|
||||
string $cssClass = 'btn btn-primary',
|
||||
string $type = 'submit',
|
||||
array $attributes = []
|
||||
) {
|
||||
$this->name = $name;
|
||||
$this->label = $label;
|
||||
$this->url = $url;
|
||||
$this->backUrl = $backUrl;
|
||||
$this->cssClass = $cssClass;
|
||||
$this->type = $type;
|
||||
$this->attributes = $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Predefiniowana akcja Zapisz
|
||||
*/
|
||||
public static function save(string $url, string $backUrl = '', string $label = 'Zapisz'): self
|
||||
{
|
||||
return new self(
|
||||
'save',
|
||||
$label,
|
||||
$url,
|
||||
$backUrl,
|
||||
'btn btn-primary',
|
||||
'submit'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Predefiniowana akcja Podgląd (otwiera w nowej karcie)
|
||||
*/
|
||||
public static function preview(string $url, string $label = 'Podgląd'): self
|
||||
{
|
||||
return new self(
|
||||
'preview',
|
||||
$label,
|
||||
$url,
|
||||
null,
|
||||
'btn btn-info',
|
||||
'link',
|
||||
['target' => '_blank']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Predefiniowana akcja Anuluj
|
||||
*/
|
||||
public static function cancel(string $backUrl, string $label = 'Anuluj'): self
|
||||
{
|
||||
return new self(
|
||||
'cancel',
|
||||
$label,
|
||||
$backUrl,
|
||||
null,
|
||||
'btn btn-default',
|
||||
'link'
|
||||
);
|
||||
}
|
||||
}
|
||||
178
autoload/admin/ViewModels/Forms/FormEditViewModel.php
Normal file
178
autoload/admin/ViewModels/Forms/FormEditViewModel.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
namespace Admin\ViewModels\Forms;
|
||||
|
||||
/**
|
||||
* Główny model widoku formularza edycji
|
||||
*/
|
||||
class FormEditViewModel
|
||||
{
|
||||
public string $formId;
|
||||
public string $title;
|
||||
public string $method;
|
||||
public string $action;
|
||||
public ?string $backUrl;
|
||||
public array $tabs;
|
||||
public array $fields;
|
||||
public array $hiddenFields;
|
||||
public array $actions;
|
||||
public bool $persist;
|
||||
public array $data;
|
||||
public ?array $validationErrors;
|
||||
public ?array $languages;
|
||||
|
||||
/**
|
||||
* @param string $formId Unikalny identyfikator formularza
|
||||
* @param string $title Tytuł formularza
|
||||
* @param array $data Dane obiektu (np. banner)
|
||||
* @param array $fields Pola formularza
|
||||
* @param array $tabs Zakładki formularza
|
||||
* @param array $actions Akcje (przyciski)
|
||||
* @param string $method Metoda HTTP (POST, GET)
|
||||
* @param string $action URL akcji formularza
|
||||
* @param string|null $backUrl URL powrotu
|
||||
* @param bool $persist Czy zapamiętywać dane w sesji
|
||||
* @param array $hiddenFields Dodatkowe ukryte pola
|
||||
* @param array|null $languages Dostępne języki (dla sekcji językowych)
|
||||
* @param array|null $validationErrors Błędy walidacji
|
||||
*/
|
||||
public function __construct(
|
||||
string $formId,
|
||||
string $title,
|
||||
array $data = [],
|
||||
array $fields = [],
|
||||
array $tabs = [],
|
||||
array $actions = [],
|
||||
string $method = 'POST',
|
||||
string $action = '',
|
||||
?string $backUrl = null,
|
||||
bool $persist = true,
|
||||
array $hiddenFields = [],
|
||||
?array $languages = null,
|
||||
?array $validationErrors = null
|
||||
) {
|
||||
$this->formId = $formId;
|
||||
$this->title = $title;
|
||||
$this->data = $data;
|
||||
$this->fields = $fields;
|
||||
$this->tabs = $tabs;
|
||||
$this->actions = $actions;
|
||||
$this->method = $method;
|
||||
$this->action = $action;
|
||||
$this->backUrl = $backUrl;
|
||||
$this->persist = $persist;
|
||||
$this->hiddenFields = $hiddenFields;
|
||||
$this->languages = $languages;
|
||||
$this->validationErrors = $validationErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sprawdza czy formularz ma zakładki
|
||||
*/
|
||||
public function hasTabs(): bool
|
||||
{
|
||||
return count($this->tabs) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sprawdza czy formularz ma sekcje językowe
|
||||
*/
|
||||
public function hasLangSections(): bool
|
||||
{
|
||||
foreach ($this->fields as $field) {
|
||||
if ($field->type === FormFieldType::LANG_SECTION) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zwraca pola dla konkretnej zakładki
|
||||
*/
|
||||
public function getFieldsForTab(string $tabId): array
|
||||
{
|
||||
return array_filter($this->fields, function (FormField $field) use ($tabId) {
|
||||
return $field->tabId === $tabId && $field->type !== FormFieldType::LANG_SECTION;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Zwraca sekcje językowe dla konkretnej zakładki
|
||||
*/
|
||||
public function getLangSectionsForTab(string $tabId): array
|
||||
{
|
||||
return array_filter($this->fields, function (FormField $field) use ($tabId) {
|
||||
return $field->type === FormFieldType::LANG_SECTION &&
|
||||
$field->langSectionParentTab === $tabId;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pobiera wartość pola z danych lub sesji (persist)
|
||||
*/
|
||||
public function getFieldValue(FormField $field, $languageId = null, ?string $langFieldName = null)
|
||||
{
|
||||
$fieldName = $field->name;
|
||||
|
||||
// Dla sekcji językowych - pobierz wartość z data[lang_id][field_name]
|
||||
if ($languageId !== null && $langFieldName !== null) {
|
||||
$fieldName = $langFieldName;
|
||||
return $this->data['languages'][$languageId][$fieldName] ?? null;
|
||||
}
|
||||
|
||||
// Zwykłe pole - najpierw sprawdź sesję (persist), potem dane
|
||||
if ($this->persist && isset($_SESSION['form_persist'][$this->formId][$fieldName])) {
|
||||
return $_SESSION['form_persist'][$this->formId][$fieldName];
|
||||
}
|
||||
|
||||
return $this->data[$fieldName] ?? $field->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sprawdza czy pole ma błąd walidacji
|
||||
*/
|
||||
public function hasError(string $fieldName, $languageId = null): bool
|
||||
{
|
||||
if ($this->validationErrors === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($languageId !== null) {
|
||||
return isset($this->validationErrors[$fieldName][$languageId]);
|
||||
}
|
||||
|
||||
return isset($this->validationErrors[$fieldName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pobiera komunikat błędu dla pola
|
||||
*/
|
||||
public function getError(string $fieldName, $languageId = null): ?string
|
||||
{
|
||||
if ($languageId !== null) {
|
||||
return $this->validationErrors[$fieldName][$languageId] ?? null;
|
||||
}
|
||||
return $this->validationErrors[$fieldName] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Czyści dane persist z sesji
|
||||
*/
|
||||
public function clearPersist(): void
|
||||
{
|
||||
if (isset($_SESSION['form_persist'][$this->formId])) {
|
||||
unset($_SESSION['form_persist'][$this->formId]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zapisuje dane do sesji (persist)
|
||||
*/
|
||||
public function saveToPersist(array $data): void
|
||||
{
|
||||
if (!isset($_SESSION['form_persist'])) {
|
||||
$_SESSION['form_persist'] = [];
|
||||
}
|
||||
$_SESSION['form_persist'][$this->formId] = $data;
|
||||
}
|
||||
}
|
||||
364
autoload/admin/ViewModels/Forms/FormField.php
Normal file
364
autoload/admin/ViewModels/Forms/FormField.php
Normal file
@@ -0,0 +1,364 @@
|
||||
<?php
|
||||
namespace Admin\ViewModels\Forms;
|
||||
|
||||
/**
|
||||
* Definicja pojedynczego pola formularza
|
||||
*/
|
||||
class FormField
|
||||
{
|
||||
public string $name;
|
||||
public string $type;
|
||||
public string $label;
|
||||
public $value;
|
||||
public string $tabId;
|
||||
public bool $required;
|
||||
public array $attributes;
|
||||
public array $options;
|
||||
public ?string $helpText;
|
||||
public ?string $placeholder;
|
||||
public ?string $id;
|
||||
|
||||
// Specyficzne dla obrazów/plików
|
||||
public bool $useFilemanager;
|
||||
public ?string $filemanagerUrl;
|
||||
|
||||
// Specyficzne dla edytora
|
||||
public string $editorToolbar;
|
||||
public int $editorHeight;
|
||||
|
||||
// Specyficzne dla lang_section
|
||||
public ?array $langFields;
|
||||
public ?string $langSectionParentTab;
|
||||
public ?string $customHtml;
|
||||
|
||||
/**
|
||||
* @param string $name Nazwa pola (name)
|
||||
* @param string $type Typ pola (z FormFieldType)
|
||||
* @param string $label Etykieta pola
|
||||
* @param mixed $value Wartość domyślna
|
||||
* @param string $tabId Identyfikator zakładki
|
||||
* @param bool $required Czy pole wymagane
|
||||
* @param array $attributes Atrybuty HTML
|
||||
* @param array $options Opcje dla select
|
||||
* @param string|null $helpText Tekst pomocniczy
|
||||
* @param string|null $placeholder Placeholder
|
||||
* @param bool $useFilemanager Czy używać filemanagera
|
||||
* @param string|null $filemanagerUrl URL filemanagera
|
||||
* @param string $editorToolbar Konfiguracja toolbar CKEditor
|
||||
* @param int $editorHeight Wysokość edytora
|
||||
* @param array|null $langFields Pola w sekcji językowej
|
||||
* @param string|null $langSectionParentTab Zakładka nadrzędna dla sekcji językowej
|
||||
*/
|
||||
public function __construct(
|
||||
string $name,
|
||||
string $type = FormFieldType::TEXT,
|
||||
string $label = '',
|
||||
$value = null,
|
||||
string $tabId = 'default',
|
||||
bool $required = false,
|
||||
array $attributes = [],
|
||||
array $options = [],
|
||||
?string $helpText = null,
|
||||
?string $placeholder = null,
|
||||
bool $useFilemanager = false,
|
||||
?string $filemanagerUrl = null,
|
||||
string $editorToolbar = 'MyTool',
|
||||
int $editorHeight = 300,
|
||||
?array $langFields = null,
|
||||
?string $langSectionParentTab = null,
|
||||
?string $customHtml = null
|
||||
) {
|
||||
$this->name = $name;
|
||||
$this->type = $type;
|
||||
$this->label = $label;
|
||||
$this->value = $value;
|
||||
$this->tabId = $tabId;
|
||||
$this->required = $required;
|
||||
$this->attributes = $attributes;
|
||||
$this->options = $options;
|
||||
$this->helpText = $helpText;
|
||||
$this->placeholder = $placeholder;
|
||||
$this->useFilemanager = $useFilemanager;
|
||||
$this->filemanagerUrl = $filemanagerUrl;
|
||||
$this->editorToolbar = $editorToolbar;
|
||||
$this->editorHeight = $editorHeight;
|
||||
$this->langFields = $langFields;
|
||||
$this->langSectionParentTab = $langSectionParentTab;
|
||||
$this->customHtml = $customHtml;
|
||||
$this->id = $attributes['id'] ?? $name;
|
||||
}
|
||||
|
||||
// Factory methods dla różnych typów pól
|
||||
|
||||
public static function text(string $name, array $config = []): self
|
||||
{
|
||||
return new self(
|
||||
$name,
|
||||
FormFieldType::TEXT,
|
||||
$config['label'] ?? '',
|
||||
$config['value'] ?? null,
|
||||
$config['tab'] ?? 'default',
|
||||
$config['required'] ?? false,
|
||||
$config['attributes'] ?? [],
|
||||
[],
|
||||
$config['help'] ?? null,
|
||||
$config['placeholder'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
public static function number(string $name, array $config = []): self
|
||||
{
|
||||
return new self(
|
||||
$name,
|
||||
FormFieldType::NUMBER,
|
||||
$config['label'] ?? '',
|
||||
$config['value'] ?? null,
|
||||
$config['tab'] ?? 'default',
|
||||
$config['required'] ?? false,
|
||||
$config['attributes'] ?? [],
|
||||
[],
|
||||
$config['help'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
public static function email(string $name, array $config = []): self
|
||||
{
|
||||
return new self(
|
||||
$name,
|
||||
FormFieldType::EMAIL,
|
||||
$config['label'] ?? '',
|
||||
$config['value'] ?? null,
|
||||
$config['tab'] ?? 'default',
|
||||
$config['required'] ?? false,
|
||||
$config['attributes'] ?? []
|
||||
);
|
||||
}
|
||||
|
||||
public static function password(string $name, array $config = []): self
|
||||
{
|
||||
return new self(
|
||||
$name,
|
||||
FormFieldType::PASSWORD,
|
||||
$config['label'] ?? '',
|
||||
$config['value'] ?? null,
|
||||
$config['tab'] ?? 'default',
|
||||
$config['required'] ?? false,
|
||||
$config['attributes'] ?? []
|
||||
);
|
||||
}
|
||||
|
||||
public static function date(string $name, array $config = []): self
|
||||
{
|
||||
return new self(
|
||||
$name,
|
||||
FormFieldType::DATE,
|
||||
$config['label'] ?? '',
|
||||
$config['value'] ?? null,
|
||||
$config['tab'] ?? 'default',
|
||||
$config['required'] ?? false,
|
||||
array_merge(['class' => 'date'], $config['attributes'] ?? [])
|
||||
);
|
||||
}
|
||||
|
||||
public static function datetime(string $name, array $config = []): self
|
||||
{
|
||||
return new self(
|
||||
$name,
|
||||
FormFieldType::DATETIME,
|
||||
$config['label'] ?? '',
|
||||
$config['value'] ?? null,
|
||||
$config['tab'] ?? 'default',
|
||||
$config['required'] ?? false,
|
||||
array_merge(['class' => 'datetime'], $config['attributes'] ?? [])
|
||||
);
|
||||
}
|
||||
|
||||
public static function switch(string $name, array $config = []): self
|
||||
{
|
||||
return new self(
|
||||
$name,
|
||||
FormFieldType::SWITCH,
|
||||
$config['label'] ?? '',
|
||||
$config['value'] ?? false,
|
||||
$config['tab'] ?? 'default',
|
||||
false,
|
||||
$config['attributes'] ?? []
|
||||
);
|
||||
}
|
||||
|
||||
public static function select(string $name, array $config = []): self
|
||||
{
|
||||
return new self(
|
||||
$name,
|
||||
FormFieldType::SELECT,
|
||||
$config['label'] ?? '',
|
||||
$config['value'] ?? null,
|
||||
$config['tab'] ?? 'default',
|
||||
$config['required'] ?? false,
|
||||
$config['attributes'] ?? [],
|
||||
$config['options'] ?? []
|
||||
);
|
||||
}
|
||||
|
||||
public static function textarea(string $name, array $config = []): self
|
||||
{
|
||||
return new self(
|
||||
$name,
|
||||
FormFieldType::TEXTAREA,
|
||||
$config['label'] ?? '',
|
||||
$config['value'] ?? null,
|
||||
$config['tab'] ?? 'default',
|
||||
$config['required'] ?? false,
|
||||
array_merge(['rows' => $config['rows'] ?? 4], $config['attributes'] ?? [])
|
||||
);
|
||||
}
|
||||
|
||||
public static function editor(string $name, array $config = []): self
|
||||
{
|
||||
return new self(
|
||||
$name,
|
||||
FormFieldType::EDITOR,
|
||||
$config['label'] ?? '',
|
||||
$config['value'] ?? null,
|
||||
$config['tab'] ?? 'default',
|
||||
$config['required'] ?? false,
|
||||
$config['attributes'] ?? [],
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
$config['toolbar'] ?? 'MyTool',
|
||||
$config['height'] ?? 300
|
||||
);
|
||||
}
|
||||
|
||||
public static function image(string $name, array $config = []): self
|
||||
{
|
||||
return new self(
|
||||
$name,
|
||||
FormFieldType::IMAGE,
|
||||
$config['label'] ?? '',
|
||||
$config['value'] ?? null,
|
||||
$config['tab'] ?? 'default',
|
||||
$config['required'] ?? false,
|
||||
$config['attributes'] ?? [],
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
$config['filemanager'] ?? true,
|
||||
$config['filemanager_url'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
public static function file(string $name, array $config = []): self
|
||||
{
|
||||
return new self(
|
||||
$name,
|
||||
FormFieldType::FILE,
|
||||
$config['label'] ?? '',
|
||||
$config['value'] ?? null,
|
||||
$config['tab'] ?? 'default',
|
||||
$config['required'] ?? false,
|
||||
$config['attributes'] ?? [],
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
$config['filemanager'] ?? true
|
||||
);
|
||||
}
|
||||
|
||||
public static function color(string $name, array $config = []): self
|
||||
{
|
||||
return new self(
|
||||
$name,
|
||||
FormFieldType::COLOR,
|
||||
$config['label'] ?? '',
|
||||
$config['value'] ?? null,
|
||||
$config['tab'] ?? 'default',
|
||||
$config['required'] ?? false,
|
||||
$config['attributes'] ?? [],
|
||||
[],
|
||||
$config['help'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
public static function hidden(string $name, $value = null): self
|
||||
{
|
||||
return new self(
|
||||
$name,
|
||||
FormFieldType::HIDDEN,
|
||||
'',
|
||||
$value,
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
public static function custom(string $name, string $html, array $config = []): self
|
||||
{
|
||||
return new self(
|
||||
$name,
|
||||
FormFieldType::CUSTOM,
|
||||
$config['label'] ?? '',
|
||||
null,
|
||||
$config['tab'] ?? 'default',
|
||||
false,
|
||||
$config['attributes'] ?? [],
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
'MyTool',
|
||||
300,
|
||||
null,
|
||||
null,
|
||||
$html
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sekcja językowa - grupa pól powtarzana dla każdego języka
|
||||
*
|
||||
* @param string $name Nazwa sekcji (prefiks dla pól)
|
||||
* @param string $parentTab Identyfikator zakładki nadrzędnej
|
||||
* @param array $fields Pola w sekcji językowej (tablica FormField)
|
||||
*/
|
||||
public static function langSection(string $name, string $parentTab, array $fields): self
|
||||
{
|
||||
return new self(
|
||||
$name,
|
||||
FormFieldType::LANG_SECTION,
|
||||
'',
|
||||
null,
|
||||
$parentTab,
|
||||
false,
|
||||
[],
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
'MyTool',
|
||||
300,
|
||||
$fields,
|
||||
$parentTab
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zwraca nazwę pola z sufiksem dla konkretnego języka
|
||||
*/
|
||||
public function getLocalizedName($languageId): string
|
||||
{
|
||||
return "{$this->name}[{$languageId}]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Zwraca ID pola z sufiksem dla konkretnego języka
|
||||
*/
|
||||
public function getLocalizedId($languageId): string
|
||||
{
|
||||
return "{$this->id}_{$languageId}";
|
||||
}
|
||||
}
|
||||
25
autoload/admin/ViewModels/Forms/FormFieldType.php
Normal file
25
autoload/admin/ViewModels/Forms/FormFieldType.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
namespace Admin\ViewModels\Forms;
|
||||
|
||||
/**
|
||||
* Dostępne typy pól formularza
|
||||
*/
|
||||
class FormFieldType
|
||||
{
|
||||
public const TEXT = 'text';
|
||||
public const NUMBER = 'number';
|
||||
public const EMAIL = 'email';
|
||||
public const PASSWORD = 'password';
|
||||
public const DATE = 'date';
|
||||
public const DATETIME = 'datetime';
|
||||
public const SWITCH = 'switch';
|
||||
public const SELECT = 'select';
|
||||
public const TEXTAREA = 'textarea';
|
||||
public const EDITOR = 'editor';
|
||||
public const IMAGE = 'image';
|
||||
public const FILE = 'file';
|
||||
public const HIDDEN = 'hidden';
|
||||
public const LANG_SECTION = 'lang_section';
|
||||
public const CUSTOM = 'custom';
|
||||
public const COLOR = 'color';
|
||||
}
|
||||
31
autoload/admin/ViewModels/Forms/FormTab.php
Normal file
31
autoload/admin/ViewModels/Forms/FormTab.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
namespace Admin\ViewModels\Forms;
|
||||
|
||||
/**
|
||||
* Definicja zakładki formularza
|
||||
*/
|
||||
class FormTab
|
||||
{
|
||||
public string $id;
|
||||
public string $label;
|
||||
public string $icon;
|
||||
public ?string $parentTabId;
|
||||
|
||||
/**
|
||||
* @param string $id Unikalny identyfikator zakładki
|
||||
* @param string $label Etykieta wyświetlana
|
||||
* @param string $icon Klasa FontAwesome (np. 'fa-wrench')
|
||||
* @param string|null $parentTabId Identyfikator zakładki nadrzędnej (dla zagnieżdżenia)
|
||||
*/
|
||||
public function __construct(
|
||||
string $id,
|
||||
string $label,
|
||||
string $icon = '',
|
||||
?string $parentTabId = null
|
||||
) {
|
||||
$this->id = $id;
|
||||
$this->label = $label;
|
||||
$this->icon = $icon;
|
||||
$this->parentTabId = $parentTabId;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user