# 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�w/zdj�� - 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�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`.