577 lines
26 KiB
Markdown
577 lines
26 KiB
Markdown
# Plan Refaktoryzacji shopPRO - Domain-Driven Architecture
|
||
|
||
## Cel
|
||
Stopniowe przeniesienie logiki biznesowej do architektury warstwowej:
|
||
- **Domain/** - logika biznesowa (core)
|
||
- **Admin/** - warstwa administratora
|
||
- **Frontend/** - warstwa użytkownika
|
||
- **Shared/** - współdzielone narzędzia
|
||
|
||
## Docelowa struktura
|
||
|
||
```
|
||
autoload/
|
||
├── Domain/ # Logika biznesowa (CORE) - namespace \Domain\
|
||
│ ├── Product/
|
||
│ │ ├── ProductRepository.php # ✅ Zmigrowane (getQuantity, getPrice, getName, find, updateQuantity, archive, unarchive)
|
||
│ │ ├── ProductService.php # Logika biznesowa (przyszłość)
|
||
│ │ └── ProductCacheService.php # Cache produktu (przyszłość)
|
||
│ ├── Banner/
|
||
│ │ └── BannerRepository.php # ✅ Zmigrowane (find, delete, save)
|
||
│ ├── Settings/
|
||
│ │ └── SettingsRepository.php # ✅ Zmigrowane (saveSettings, getSettings) - bezposrednio DB
|
||
│ ├── Dictionaries/
|
||
│ │ └── DictionariesRepository.php # ✅ Zmigrowane (listForAdmin, find, save, delete, allUnits)
|
||
│ ├── Cache/
|
||
│ │ └── CacheRepository.php # ✅ Zmigrowane (clearCache)
|
||
│ ├── Order/
|
||
│ ├── Category/
|
||
│ └── ...
|
||
│
|
||
├── admin/ # Warstwa administratora (istniejący katalog!)
|
||
│ ├── Controllers/ # Nowe kontrolery - namespace \admin\Controllers\
|
||
│ │ ├── ArticlesController.php
|
||
│ │ ├── BannerController.php
|
||
│ │ ├── DictionariesController.php
|
||
│ │ ├── FilemanagerController.php
|
||
│ │ ├── ProductArchiveController.php
|
||
│ │ └── SettingsController.php
|
||
│ ├── controls/ # Stare kontrolery (legacy fallback)
|
||
│ ├── factory/ # Stare helpery (legacy)
|
||
│ └── view/ # Widoki (statyczne - OK bez zmian)
|
||
│
|
||
├── Frontend/ # Warstwa użytkownika (przyszłość)
|
||
│ ├── Controllers/
|
||
│ └── Services/
|
||
│
|
||
├── Shared/ # Współdzielone narzędzia
|
||
│ ├── Cache/
|
||
│ │ ├── CacheHandler.php
|
||
│ │ └── RedisConnection.php
|
||
│ └── Helpers/
|
||
│ └── S.php
|
||
│
|
||
└── [LEGACY] # Stare klasy (stopniowo deprecated)
|
||
├── shop/
|
||
├── admin/factory/
|
||
└── front/factory/
|
||
```
|
||
|
||
### WAŻNE: Konwencja namespace → katalog (Linux case-sensitive!)
|
||
- `\Domain\` → `autoload/Domain/` (duże D - nowy katalog)
|
||
- `\admin\Controllers\` → `autoload/admin/Controllers/` (małe a - istniejący katalog)
|
||
- NIE używać `\Admin\` (duże A) bo na serwerze Linux katalog to `admin/` (małe a)
|
||
|
||
## Zasady migracji
|
||
|
||
### 1. Stopniowość
|
||
- Przenosimy **jedną funkcję na raz**
|
||
- Zachowujemy kompatybilność wsteczną
|
||
- Stare klasy działają jako fasady do nowych
|
||
|
||
### 2. Dependency Injection zamiast statycznych metod
|
||
```php
|
||
// ❌ STARE - statyczne
|
||
class Product {
|
||
public static function getQuantity($id) {
|
||
global $mdb;
|
||
return $mdb->get('pp_shop_products', 'quantity', ['id' => $id]);
|
||
}
|
||
}
|
||
|
||
// ✅ NOWE - instancje z DI
|
||
class ProductRepository {
|
||
private $db;
|
||
|
||
public function __construct($db) {
|
||
$this->db = $db;
|
||
}
|
||
|
||
public function getQuantity($id) {
|
||
return $this->db->get('pp_shop_products', 'quantity', ['id' => $id]);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3. Fasady dla kompatybilności
|
||
```php
|
||
// Stara klasa wywołuje nową
|
||
namespace shop;
|
||
|
||
class Product {
|
||
public static function getQuantity($id) {
|
||
global $mdb;
|
||
$repo = new \Domain\Product\ProductRepository($mdb);
|
||
return $repo->getQuantity($id);
|
||
}
|
||
}
|
||
```
|
||
|
||
## Proces migracji funkcji
|
||
|
||
### Krok 1: Wybór funkcji
|
||
- Wybierz prostą funkcję statyczną
|
||
- Sprawdź jej zależności
|
||
- Przeanalizuj gdzie jest używana
|
||
|
||
### Krok 2: Stworzenie nowej struktury
|
||
- Utwórz folder `Domain/{Module}/`
|
||
- Stwórz odpowiednią klasę (Repository/Service/Entity)
|
||
- Przenieś logikę
|
||
|
||
### Krok 3: Znalezienie użyć
|
||
```bash
|
||
grep -r "Product::getQuantity" .
|
||
```
|
||
|
||
### Krok 4: Aktualizacja wywołań
|
||
- Opcja A: Bezpośrednie wywołanie nowej klasy
|
||
- Opcja B: Fasada w starej klasie (zalecane na początek)
|
||
|
||
### Krok 5: Testy
|
||
- Napisz test jednostkowy dla nowej funkcji
|
||
- Sprawdź czy stare wywołania działają
|
||
|
||
## Status migracji
|
||
|
||
### ✅ Zmigrowane moduły
|
||
- **Cache** (częściowo)
|
||
- ✅ CacheHandler - ma delete/deletePattern
|
||
- ✅ RedisConnection - singleton
|
||
- ✅ S::clear_product_cache() - nowa metoda
|
||
|
||
### 🔄 Status modułów
|
||
- **Product**
|
||
- ✅ get_product_quantity() - **ZMIGROWANE** (2025-02-05) 🎉
|
||
- Nowa klasa: `Domain\Product\ProductRepository::getQuantity()`
|
||
- Fasada w: `shop\Product::get_product_quantity()`
|
||
- Test: `tests/Unit/Domain/Product/ProductRepositoryTest.php`
|
||
- Testy: ✅ 5/5 przechodzą
|
||
- Aktualizacja: ver. 0.238
|
||
- Użycie DI: ✅ Konstruktor przyjmuje `$db`
|
||
- ✅ get_product_price() - **ZMIGROWANE** (2026-02-05) 🎉
|
||
- Nowa metoda: `Domain\Product\ProductRepository::getPrice()`
|
||
- Fasada w: `shop\Product::get_product_price()`
|
||
- Testy: ✅ 4 nowe testy (cena regularna, promocyjna, promo wyższa, nie znaleziono)
|
||
- Użycie: `front\factory\ShopPromotion` (linia 132)
|
||
- Aktualizacja: ver. 0.239
|
||
- ✅ get_product_name() - **ZMIGROWANE** (2026-02-05) 🎉
|
||
- Nowa metoda: `Domain\Product\ProductRepository::getName()`
|
||
- Fasada w: `shop\Product::get_product_name()`
|
||
- Testy: ✅ 2 nowe testy (nazwa znaleziona, nie znaleziona)
|
||
- Użycie: brak aktywnych wywołań (przygotowane na przyszłość)
|
||
- Aktualizacja: ver. 0.239
|
||
- ✅ archive() / unarchive() - **ZMIGROWANE** (2026-02-06) 🎉
|
||
- Nowe metody: `Domain\Product\ProductRepository::archive()`, `unarchive()`
|
||
- Nowy kontroler: `admin\Controllers\ProductArchiveController` (DI, instancyjny)
|
||
- Szablony: `admin/templates/product-archive/` (rename z `archive/`, dawniej `product_archive/`)
|
||
- Testy: ✅ 4 nowe testy repozytorium + 6 testów kontrolera
|
||
- FIX: SQL bug w `ajax_products_list_archive()` (puste wyszukiwanie + brak `archive = 1`)
|
||
- Dalsze porządki: ver. 0.252 (nowy table-list, wydzielony custom script, usunięte legacy pliki)
|
||
- Aktualizacja: ver. 0.241 / 0.252
|
||
- [ ] is_product_on_promotion() - NASTĘPNA 👉
|
||
|
||
- **Banner** (DEMO pełnej migracji kontrolera)
|
||
- ✅ BannerRepository - **ZMIGROWANE** (2026-02-05) 🎉
|
||
- Nowa klasa: `Domain\Banner\BannerRepository` (find, delete, save, saveTranslations)
|
||
- Nowy kontroler: `admin\Controllers\BannerController` (DI, instancyjny)
|
||
- Router: `admin\Site::route()` → sprawdza nowy kontroler → fallback na stary
|
||
- Testy: ✅ 4 testy (find z tłumaczeniami, not found, delete, save)
|
||
- Stary kontroler `admin\controls\Banners` działa jako niezależny fallback
|
||
- Stara factory `admin\factory\Banners` zachowana bez zmian (fallback)
|
||
- Aktualizacja: ver. 0.239
|
||
|
||
- **Articles** (migracja kontrolera - etap edit/details)
|
||
- ✅ ArticleRepository::find() - **ZMIGROWANE** (2026-02-06) 🎉
|
||
- Nowa klasa: `Domain\Article\ArticleRepository` (find: artykul + relacje)
|
||
- Nowy kontroler: `admin\Controllers\ArticlesController` (DI, instancyjny)
|
||
- Zmigrowana akcja: `article_edit` -> `edit` (mapowanie w `admin\Site::$actionMap`)
|
||
- Kompatybilność: `admin\factory\Articles::article_details()` deleguje do nowego repozytorium
|
||
- Legacy cleanup: metody przejęte przez nowe kontrolery oznaczone `@deprecated` w `admin\controls\Articles|Banners|Settings`
|
||
- Testy repozytorium rozszerzone o czyszczenie nieprzypisanych plik<69>w/zdj<64><6A>
|
||
- Aktualizacja: ver. 0.243
|
||
- ✅ ArticleRepository::save() - **ZMIGROWANE** (2026-02-06) 🎉
|
||
- Metoda `save()` z prywatnych helperow (buildArticleRow, buildLangRow, saveTranslations, savePages, assignTempFiles, assignTempImages, deleteMarkedFiles, deleteMarkedImages)
|
||
- Zmigrowana akcja: `article_save` -> `save` (mapowanie w `admin\Site::$actionMap`)
|
||
- Kompatybilnosc: `admin\factory\Articles::article_save()` deleguje do repozytorium
|
||
- Testy: 7 nowych testow save (create, update, translations, pages, marked delete)
|
||
- Aktualizacja: ver. 0.244
|
||
- ✅ ArticleRepository::archive() - **ZMIGROWANE** (2026-02-06) 🎉
|
||
- Metoda `archive()` (ustawia status = -1)
|
||
- Zmigrowana akcja: `article_delete` -> `delete` (mapowanie w `admin\Site::$actionMap`)
|
||
- Kompatybilnosc: `admin\factory\Articles::articles_set_archive()` deleguje do repozytorium
|
||
- Testy: 2 nowe testy archive (success, failure)
|
||
- Aktualizacja: ver. 0.245
|
||
- ✅ ArticlesController::browseList() - **ZMIGROWANE** (2026-02-07) 🎉
|
||
- Nowa metoda kontrolera: `browseList()` (DI, instancyjna)
|
||
- Zmigrowana akcja: `browse_list` -> `browseList` (mapowanie w `admin\Site::$actionMap`)
|
||
- Legacy cleanup: usuniety `autoload/admin/controls/class.Articles.php` (brak fallback dla modułu Articles)
|
||
- Testy: 2 nowe testy kontraktu kontrolera (method exists + return type)
|
||
- ✅ ArticlesController::galleryOrderSave() - **ZMIGROWANE** (2026-02-07) 🎉
|
||
- Nowa metoda kontrolera: `galleryOrderSave()` (AJAX)
|
||
- Zmigrowana akcja: `gallery_order_save` -> `galleryOrderSave` (mapowanie w `admin\Site::$actionMap`)
|
||
- Implementacja: używa `Domain\Article\ArticleRepository::saveGalleryOrder()`
|
||
- Testy: 2 nowe testy kontraktu kontrolera (method exists + return type)
|
||
- ✅ Usuniecie legacy kontrolera Articles - **ZMIGROWANE** (2026-02-07) 🎉
|
||
- Usuniety plik: `autoload/admin/controls/class.Articles.php`
|
||
- Wymaganie dla aktualizacji: dodac wpis do `ver_X.XXX_files.txt`
|
||
- Wpis do usuniecia: `F: ../autoload/admin/controls/class.Articles.php`
|
||
- ✅ Stabilizacja generatora `.htaccess` - **ZMIGROWANE** (2026-02-07) 🎉
|
||
- FIX: regula admin ma `QSA` (query string dla sortowania/filtrow)
|
||
- FIX: `\S::htacces()` komentuje `AddHandler|SetHandler|ForceType` dla zgodnosci z hostingiem
|
||
- UPDATE: `libraries/htaccess.conf` zaktualizowany, aby poprawki nie znikaly po regeneracji
|
||
|
||
- **Settings** (migracja kontrolera)
|
||
- ✅ SettingsRepository - **ZMIGROWANE** (2026-02-05) 🎉
|
||
- Nowa klasa: `Domain\Settings\SettingsRepository` (saveSettings, getSettings)
|
||
- Aktualny stan: bezpośredni dostęp do DB (usunięta delegacja do `admin\factory\Settings`) - ver. 0.250
|
||
- Nowy kontroler: `admin\Controllers\SettingsController` (DI, instancyjny)
|
||
- Testy: ✅ 3 testy (instancja, metody)
|
||
- Stary kontroler `admin\controls\Settings` zachowany jako fallback
|
||
- Aktualizacja: ver. 0.240 / 0.250
|
||
- ✅ CacheRepository - **ZMIGROWANE** (2026-02-05) 🎉
|
||
- Nowa klasa: `Domain\Cache\CacheRepository` (clearCache)
|
||
- Używa `\S::delete_dir()` + `\RedisConnection`
|
||
- Testy: ✅ 4 testy (z Redis, bez Redis, niedostępny, struktura)
|
||
- Aktualizacja: ver. 0.240
|
||
|
||
- **Dictionaries** (migracja kontrolera i repozytorium)
|
||
- ✅ DictionariesRepository - **ZMIGROWANE** (2026-02-09) 🎉
|
||
- Nowa klasa: `Domain\Dictionaries\DictionariesRepository` (listForAdmin, find, save, delete, allUnits)
|
||
- Nowy kontroler: `admin\Controllers\DictionariesController` (DI, instancyjny)
|
||
- Migracja na nowe komponenty: `components/table-list` + `components/form-edit`
|
||
- Legacy cleanup: usunięto klasy z `admin\controls`, `admin\factory`, `front\factory`
|
||
- Aktualizacja: ver. 0.251
|
||
|
||
- **Filemanager** (migracja routingu)
|
||
- ✅ FilemanagerController - **ZMIGROWANE** (2026-02-10) 🎉
|
||
- Nowy kontroler: `admin\Controllers\FilemanagerController`
|
||
- Naprawa błędu: `Invalid Key`
|
||
- Legacy cleanup: usunięto `autoload/admin/controls/class.Filemanager.php` i `autoload/admin/view/class.FileManager.php`
|
||
- Aktualizacja: ver. 0.252
|
||
|
||
- **Users** (migracja kontrolera i repozytorium)
|
||
- ✅ UserRepository - **ZMIGROWANE** (2026-02-12) 🎉
|
||
- Nowa klasa: `Domain\User\UserRepository` (find, getById, save, delete, checkLogin, logon, details, updateById, sendTwofaCode, verifyTwofaCode)
|
||
- Nowy kontroler: `admin\Controllers\UsersController` (DI, instancyjny: view_list, user_edit, user_save, user_delete, login_form, twofa)
|
||
- Router: `admin\Site` - factory wpis dla modulu `Users`
|
||
- Fasada: `admin\factory\Users` deleguje do repozytorium (backward compatibility dla login/2FA flow)
|
||
- AJAX: `admin/ajax/users.php` - `check_login` oparty o `UserRepository`
|
||
- Legacy cleanup: usuniety `autoload/admin/controls/class.Users.php`
|
||
- Testy: 25 testow repozytorium (CRUD, logon, 2FA, checkLogin) + 12 testow kontrolera (kontrakty + normalizeUser)
|
||
|
||
### 📋 Do zrobienia
|
||
- Order
|
||
- Category
|
||
- ShopAttribute
|
||
- ShopProduct (factory)
|
||
- Pages (`browse_list` i widoki drzewiaste nadal w legacy `admin\controls` / `admin\view`)
|
||
|
||
## Testowanie
|
||
|
||
### Framework: PHPUnit
|
||
Instalacja:
|
||
```bash
|
||
composer require --dev phpunit/phpunit
|
||
```
|
||
|
||
### Struktura testów
|
||
```
|
||
tests/
|
||
├── Unit/
|
||
│ ├── Domain/
|
||
│ │ ├── Article/ArticleRepositoryTest.php
|
||
│ │ ├── Banner/BannerRepositoryTest.php
|
||
│ │ ├── Cache/CacheRepositoryTest.php
|
||
│ │ ├── Dictionaries/DictionariesRepositoryTest.php
|
||
│ │ ├── Product/ProductRepositoryTest.php
|
||
│ │ ├── Settings/SettingsRepositoryTest.php
|
||
│ │ └── User/UserRepositoryTest.php
|
||
│ └── admin/
|
||
│ └── Controllers/
|
||
│ ├── ArticlesControllerTest.php
|
||
│ ├── DictionariesControllerTest.php
|
||
│ ├── ProductArchiveControllerTest.php
|
||
│ ├── SettingsControllerTest.php
|
||
│ └── UsersControllerTest.php
|
||
└── Integration/
|
||
```
|
||
**Łącznie: 119 testów, 256 asercji**
|
||
|
||
### Przykład testu
|
||
```php
|
||
// tests/Unit/Domain/Product/ProductRepositoryTest.php
|
||
use PHPUnit\Framework\TestCase;
|
||
use Domain\Product\ProductRepository;
|
||
|
||
class ProductRepositoryTest extends TestCase {
|
||
public function testGetQuantityReturnsCorrectValue() {
|
||
// Arrange
|
||
$mockDb = $this->createMock(\medoo::class);
|
||
$mockDb->method('get')->willReturn(10);
|
||
|
||
$repo = new ProductRepository($mockDb);
|
||
|
||
// Act
|
||
$quantity = $repo->getQuantity(123);
|
||
|
||
// Assert
|
||
$this->assertEquals(10, $quantity);
|
||
}
|
||
}
|
||
```
|
||
|
||
## Zasady kodu
|
||
|
||
### 1. SOLID Principles
|
||
- **S**ingle Responsibility - jedna klasa = jedna odpowiedzialność
|
||
- **O**pen/Closed - otwarty na rozszerzenia, zamknięty na modyfikacje
|
||
- **L**iskov Substitution - podklasy mogą zastąpić nadklasy
|
||
- **I**nterface Segregation - wiele małych interfejsów
|
||
- **D**ependency Inversion - zależności od abstrakcji
|
||
|
||
### 2. Nazewnictwo
|
||
- **Entity** - `Product.php` (reprezentuje obiekt domenowy)
|
||
- **Repository** - `ProductRepository.php` (dostęp do danych)
|
||
- **Service** - `ProductService.php` (logika biznesowa)
|
||
- **Controller** - `ProductController.php` (obsługa requestów)
|
||
|
||
### 3. Type Hinting
|
||
```php
|
||
// ✅ DOBRE
|
||
public function getQuantity(int $id): ?int {
|
||
return $this->db->get('pp_shop_products', 'quantity', ['id' => $id]);
|
||
}
|
||
|
||
// ❌ ZŁE
|
||
public function getQuantity($id) {
|
||
return $this->db->get('pp_shop_products', 'quantity', ['id' => $id]);
|
||
}
|
||
```
|
||
|
||
## Narzędzia pomocnicze
|
||
|
||
### Autoloader (produkcja)
|
||
Autoloader w 9 entry pointach obsługuje dwie konwencje:
|
||
1. `autoload/{namespace}/class.{ClassName}.php` (legacy)
|
||
2. `autoload/{namespace}/{ClassName}.php` (PSR-4, fallback)
|
||
|
||
Entry pointy: `index.php`, `ajax.php`, `api.php`, `cron.php`, `cron-turstmate.php`, `download.php`, `admin/index.php`, `admin/ajax.php`, `cron/cron-xml.php`
|
||
|
||
### Static Analysis
|
||
```bash
|
||
composer require --dev phpstan/phpstan
|
||
vendor/bin/phpstan analyse autoload/Domain
|
||
```
|
||
|
||
## Kolejność refaktoryzacji (priorytet)
|
||
|
||
1. **Cache** ✅
|
||
2. **Product** (w trakcie)
|
||
- ✅ getQuantity (ver. 0.238)
|
||
- ✅ getPrice (ver. 0.239)
|
||
- ✅ getName (ver. 0.239)
|
||
- ✅ archive / unarchive (ver. 0.241)
|
||
- [ ] is_product_on_promotion - NASTĘPNA 👉
|
||
- [ ] getFromCache
|
||
- [ ] getProductImg
|
||
3. **Banner** ✅ (pełna migracja kontrolera, ver. 0.239)
|
||
4. **Settings** ✅ (pełna migracja repo/kontrolera + cleanup legacy, ver. 0.250)
|
||
5. **Dictionaries** ✅ (repo + kontroler + form/table, ver. 0.251)
|
||
6. **ProductArchive** ✅ (migracja kontrolera + cleanup szablonów, ver. 0.252)
|
||
7. **Filemanager** ✅ (migracja routingu + fix `Invalid Key`, ver. 0.252)
|
||
8. **Users** ✅ (repo + kontroler + 2FA + legacy cleanup, ver. 0.253)
|
||
9. **Order**
|
||
10. **Category**
|
||
11. **ShopAttribute**
|
||
12. **Pages** (`browse_list` i powiązane widoki nadal legacy)
|
||
|
||
- **Form Edit System** - Nowy uniwersalny system formularzy edycji
|
||
- ✅ Klasy ViewModel: `FormFieldType`, `FormField`, `FormTab`, `FormAction`, `FormEditViewModel`
|
||
- ✅ Walidacja: `FormValidator` z obsługą reguł per pole i sekcje językowe
|
||
- ✅ Persist: `FormRequestHandler` - zapamiętywanie danych przy błędzie walidacji
|
||
- ✅ Renderer: `FormFieldRenderer` - renderowanie wszystkich typów pól
|
||
- ✅ Szablon: `admin/templates/components/form-edit.php` - uniwersalny layout
|
||
- ✅ BannerController - przerobiony na nowy system formularzy
|
||
- Wspierane typy pól: text, number, email, password, date, datetime, switch, select, textarea, editor, image, file, hidden, lang_section
|
||
- Obsługa zakładek (vertical) i sekcji językowych (horizontal)
|
||
- **Do zrobienia**: Przerobić pozostałe kontrolery/formularze (Product, Category, Pages, itd.)
|
||
|
||
---
|
||
*Rozpoczęto: 2025-02-05*
|
||
*Ostatnia aktualizacja: 2026-02-12*
|
||
|
||
|
||
## Form Edit System - Dokumentacja użycia
|
||
|
||
### Architektura
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Controller │
|
||
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||
│ │ edit() │ │ save() │ │
|
||
│ │ - buduje VM │ │ - walidacja │ │
|
||
│ │ - renderuje │ │ - zapis │ │
|
||
│ └────────┬────────┘ └─────────────────┘ │
|
||
└───────────┼─────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ FormEditViewModel │
|
||
│ - title, formId, data, fields, tabs, actions │
|
||
│ - validationErrors, persist, languages │
|
||
└───────────┬─────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ components/form-edit.php (szablon) │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ FormFieldRenderer - renderuje każde pole │ │
|
||
│ │ ├─ input, select, textarea, switch │ │
|
||
│ │ ├─ date, datetime, editor, image │ │
|
||
│ │ └─ lang_section (zagnieżdżone pola) │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Przykład użycia w kontrolerze
|
||
|
||
```php
|
||
use admin\ViewModels\Forms\FormEditViewModel;
|
||
use admin\ViewModels\Forms\FormField;
|
||
use admin\ViewModels\Forms\FormTab;
|
||
use admin\ViewModels\Forms\FormAction;
|
||
use admin\Support\Forms\FormRequestHandler;
|
||
|
||
class BannerController
|
||
{
|
||
public function edit(): string
|
||
{
|
||
$banner = $this->repository->find($id);
|
||
$languages = \admin\factory\Languages::languages_list();
|
||
|
||
$viewModel = new FormEditViewModel(
|
||
formId: 'banner-edit',
|
||
title: 'Edycja banera',
|
||
data: $banner,
|
||
tabs: [
|
||
new FormTab('settings', 'Ustawienia', 'fa-wrench'),
|
||
new FormTab('content', 'Zawartość', 'fa-file'),
|
||
],
|
||
fields: [
|
||
// Zakładka Ustawienia
|
||
FormField::text('name', [
|
||
'label' => 'Nazwa',
|
||
'tab' => 'settings',
|
||
'required' => true,
|
||
]),
|
||
FormField::switch('status', [
|
||
'label' => 'Aktywny',
|
||
'tab' => 'settings',
|
||
]),
|
||
FormField::date('date_start', [
|
||
'label' => 'Data rozpoczęcia',
|
||
'tab' => 'settings',
|
||
]),
|
||
|
||
// Sekcja językowa w zakładce Zawartość
|
||
FormField::langSection('translations', 'content', [
|
||
FormField::image('src', ['label' => 'Obraz']),
|
||
FormField::text('url', ['label' => 'Url']),
|
||
FormField::editor('text', ['label' => 'Treść']),
|
||
]),
|
||
],
|
||
actions: [
|
||
FormAction::save('/admin/banners/save', '/admin/banners'),
|
||
FormAction::cancel('/admin/banners'),
|
||
],
|
||
languages: $languages,
|
||
persist: true,
|
||
);
|
||
|
||
return \Tpl::view('components/form-edit', ['form' => $viewModel]);
|
||
}
|
||
|
||
public function save(): void
|
||
{
|
||
$formHandler = new FormRequestHandler();
|
||
$viewModel = $this->buildFormViewModel(); // jak w edit()
|
||
|
||
$result = $formHandler->handleSubmit($viewModel, $_POST);
|
||
|
||
if (!$result['success']) {
|
||
// Błędy walidacji - zapisane automatycznie do sesji
|
||
echo json_encode(['success' => false, 'errors' => $result['errors']]);
|
||
exit;
|
||
}
|
||
|
||
// Sukces - persist wyczyszczony automatycznie
|
||
$this->repository->save($result['data']);
|
||
echo json_encode(['success' => true]);
|
||
exit;
|
||
}
|
||
}
|
||
```
|
||
|
||
### Dostępne typy pól
|
||
|
||
| Typ | Metoda | Opcje |
|
||
|-----|--------|-------|
|
||
| `text` | `FormField::text(name, ['label' => '...', 'required' => true])` | placeholder, help |
|
||
| `number` | `FormField::number(name, [...])` | - |
|
||
| `email` | `FormField::email(name, [...])` | walidacja formatu |
|
||
| `password` | `FormField::password(name, [...])` | - |
|
||
| `date` | `FormField::date(name, [...])` | datetimepicker |
|
||
| `datetime` | `FormField::datetime(name, [...])` | datetimepicker z czasem |
|
||
| `switch` | `FormField::switch(name, [...])` | checked (bool) |
|
||
| `select` | `FormField::select(name, ['options' => [...]])` | options: [key => label] |
|
||
| `textarea` | `FormField::textarea(name, ['rows' => 4])` | rows |
|
||
| `editor` | `FormField::editor(name, ['toolbar' => 'MyTool'])` | CKEditor |
|
||
| `image` | `FormField::image(name, ['filemanager' => true])` | filemanager URL |
|
||
| `file` | `FormField::file(name, [...])` | filemanager |
|
||
| `hidden` | `FormField::hidden(name, value)` | - |
|
||
| `lang_section` | `FormField::langSection(name, 'tab', [fields])` | pola per język |
|
||
|
||
### Walidacja
|
||
|
||
Walidacja jest automatyczna na podstawie właściwości pól:
|
||
- `required` - pole wymagane
|
||
- `type` = `email` - walidacja formatu e-mail
|
||
- `type` = `number` - walidacja liczby
|
||
- `type` = `date` - walidacja formatu YYYY-MM-DD
|
||
|
||
Dla sekcji językowych walidacja jest powtarzana dla każdego aktywnego języka.
|
||
|
||
### Persist (zapamiętywanie danych)
|
||
|
||
Gdy `persist = true`:
|
||
1. Przy błędzie walidacji dane są zapisywane w `$_SESSION['form_persist'][$formId]`
|
||
2. Formularz automatycznie przywraca dane z sesji przy ponownym wyświetleniu
|
||
3. Po udanym zapisie sesja jest czyszczona automatycznie przez `FormRequestHandler`
|
||
|
||
### Przerabianie istniejących formularzy
|
||
|
||
1. **Kontroler** - zamień `view\Xxx::edit()` na `FormEditViewModel`
|
||
2. **Repository** - dostosuj `save()` do formatu z `FormRequestHandler` (lub dodaj wsparcie dla obu formatów)
|
||
3. **Szablon** - usuń stary szablon lub zostaw jako fallback
|
||
4. **Testy** - zaktualizuj testy jeśli zmienił się format danych
|
||
|
||
|
||
## Aktualizacja 2026-02-12 - Users
|
||
|
||
### Users (migracja kontrolera i repozytorium)
|
||
- **NOWE:** `Domain\User\UserRepository` (delete, find, save, checkLogin, logon, details, sendTwofaCode, verifyTwofaCode)
|
||
- **NOWE:** `admin\Controllers\UsersController` (view_list, user_edit, user_save, user_delete)
|
||
- **UPDATE:** Router `admin\Site` - nowy kontroler DI dla modu<64>u `Users`
|
||
- **UPDATE:** `admin\factory\Users` jako fasada delegujaca do repozytorium
|
||
- **UPDATE:** `admin/ajax/users.php` - endpoint `check_login` oparty o `UserRepository`
|
||
- Testy po zmianie: 95 tests, 204 assertions
|
||
- **UPDATE:** UsersController: `view_list` + `user_edit` migrowane na nowy system list/form (table-list + form-edit)
|
||
|
||
## Aktualizacja 2026-02-12 (finalizacja Users)
|
||
- Users: pelna migracja na nowa architekture (Domain + DI Controller), bez fallbacku do legacy kontrolera/factory/view.
|
||
- `UsersController` obsluguje: `list/view_list`, `user_edit`, `user_save`, `user_delete`, `login_form`, `twofa`.
|
||
- Dodano walidacje warunkowa: `twofa_email` wymagany gdy `twofa_enabled = 1`.
|
||
- Widoki users migrowane z `grid/gridEdit` na `table-list` i `form-edit`.
|