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:
2026-04-30 23:32:26 +02:00
parent 72cb5b8d1d
commit a3caeb9a9a
25 changed files with 2694 additions and 29 deletions

View 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'
);
}
}

View 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;
}
}

View 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}";
}
}

View 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';
}

View 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;
}
}