# 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) - fasada → factory │ ├── Cache/ │ │ └── CacheRepository.php # ✅ Zmigrowane (clearCache) │ ├── Order/ │ ├── Category/ │ └── ... │ ├── admin/ # Warstwa administratora (istniejący katalog!) │ ├── Controllers/ # Nowe kontrolery - namespace \admin\Controllers\ │ │ ├── BannerController.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 ### 🔄 W trakcie - **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/`) - Testy: ✅ 4 nowe testy repozytorium + 6 testów kontrolera - FIX: SQL bug w `ajax_products_list_archive()` (puste wyszukiwanie + brak `archive = 1`) - Aktualizacja: ver. 0.241 - [ ] 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 - krok pośredni) - ✅ SettingsRepository - **ZMIGROWANE** (2026-02-05) 🎉 - Nowa klasa: `Domain\Settings\SettingsRepository` (saveSettings, getSettings) - Krok pośredni: fasada nad `admin\factory\Settings` (docelowo DI z $db) - Nowy kontroler: `admin\Controllers\SettingsController` (DI, instancyjny) - Testy: ✅ 3 testy (instancja, metody) - Stary kontroler `admin\controls\Settings` zachowany jako fallback - Aktualizacja: ver. 0.240 - ✅ 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 ### 📋 Do zrobienia - Order - Category - ShopAttribute - ShopProduct (factory) ## Testowanie ### Framework: PHPUnit Instalacja: ```bash composer require --dev phpunit/phpunit ``` ### Struktura testów ``` tests/ ├── Unit/ │ ├── Domain/ │ │ ├── Product/ProductRepositoryTest.php # 15 testów │ │ ├── Banner/BannerRepositoryTest.php # 4 testy │ │ ├── Settings/SettingsRepositoryTest.php # 3 testy │ │ └── Cache/CacheRepositoryTest.php # 4 testy │ └── admin/ │ └── Controllers/ │ ├── SettingsControllerTest.php # 7 testów │ └── ProductArchiveControllerTest.php # 6 testów └── Integration/ ``` **Łącznie: 48 testów, 91 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** ✅ (migracja kontrolera - krok pośredni, ver. 0.240) 5. **ProductArchive** ✅ (migracja kontrolera + cleanup szablonów, ver. 0.241) 6. **Order** 5. **Category** 6. **ShopAttribute** - **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 (Articles, Settings, Product, Category, itd.) --- *Rozpoczęto: 2025-02-05* *Ostatnia aktualizacja: 2026-02-08* ## 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