Files
shopPRO/REFACTORING_PLAN.md

22 KiB
Raw Blame History

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

// ❌ 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

// 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ć

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<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 - 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:

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

// 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

  • Single Responsibility - jedna klasa = jedna odpowiedzialność
  • Open/Closed - otwarty na rozszerzenia, zamknięty na modyfikacje
  • Liskov Substitution - podklasy mogą zastąpić nadklasy
  • Interface Segregation - wiele małych interfejsów
  • Dependency 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

// ✅ 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

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
  7. Category
  8. 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

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