Files
cmsPRO/.paul/phases/06-admin-base/06-02-PLAN.md
Jacek Pyziak a3caeb9a9a 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>
2026-04-30 23:32:26 +02:00

301 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 06-admin-base
plan: 02
type: execute
wave: 1
depends_on: ["06-01"]
files_modified:
- autoload/admin/Support/TableListRequestFactory.php
- autoload/admin/Support/Forms/FormRequestHandler.php
- autoload/admin/Support/Forms/FormFieldRenderer.php
autonomous: true
delegation: off
---
<objective>
## Goal
Domknąć Phase 6 (Admin Base Infrastructure) przez przeniesienie z shopPRO 3 klas warstwy Support: `TableListRequestFactory` (parser requestów listy z filtrami/sortowaniem/paginacją), `FormRequestHandler` (obsługa POST/persist/walidacji formularza), `FormFieldRenderer` (renderer HTML pól formularza). Po 06-02 cała infrastruktura Phase 7+ (kontrolery) jest gotowa.
## Purpose
Phase 7-13 będą migrować 17 modułów Admin do nowych namespace'ów. Każdy z tych kontrolerów będzie potrzebował:
- `TableListRequestFactory::fromRequest()` — uniformowy parsing list z `?page=&per_page=&sort=&filter[]=`
- `FormRequestHandler` — uniformowy handler POST → walidacja → persist → redirect
- `FormFieldRenderer` — generowanie HTML pól z `FormField` ViewModeli (zastępuje Shared\Html\Html dla nowych formularzy)
Bez tych helperów każdy kontroler musiałby duplikować logikę paginacji i obsługi POST. Pakiet 06-01 + 06-02 = pełny fundament Admin\ gotowy do Phase 7.
**Decyzja architektoniczna:** Brak `BaseController` — shopPRO używa POJOs z DI (np. `BannerController(BannerRepository, LanguagesRepository)`), nie dziedziczenia. Phase 7+ kontrolery będą zwykłymi klasami z Domain repos i tymi helperami w konstruktorze.
## Output
- `autoload/admin/Support/TableListRequestFactory.php`
- `autoload/admin/Support/Forms/FormRequestHandler.php`
- `autoload/admin/Support/Forms/FormFieldRenderer.php`
- `.paul/phases/06-admin-base/06-02-SUMMARY.md`
</objective>
<context>
<clarifications>
- **BaseCtrl** — Czy tworzymy `BaseController`?
→ Odpowiedź: NIE. POJOs z DI jak shopPRO. ROADMAP zaktualizowany — wpis BaseController usunięty ze scope Phase 6.
- **Helpers** — Które helpery z `Admin/Support/Forms/` w 06-02?
→ Odpowiedź: Oba — `FormRequestHandler` (159 L) + `FormFieldRenderer` (494 L). Pełny pakiet, żeby Phase 7+ miało gotowe wszystko.
- **Filename** — Konwencja nazwy pliku?
→ Odpowiedź: PSR-4 czyste — `TableListRequestFactory.php` (bez `class.` prefiksu z shopPRO). Zgodne z resztą cmsPRO Domain/Shared (np. `ArticlesRepository.php`).
</clarifications>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
@.paul/codebase/architecture.md
## Prior Work
@.paul/phases/06-admin-base/06-01-SUMMARY.md
## Reference Source (shopPRO — wzorzec do skopiowania)
@C:\visual studio code\projekty\shopPRO\autoload\Admin\Support\class.TableListRequestFactory.php
@C:\visual studio code\projekty\shopPRO\autoload\Admin\Support\Forms\FormRequestHandler.php
@C:\visual studio code\projekty\shopPRO\autoload\Admin\Support\Forms\FormFieldRenderer.php
## cmsPRO target paths
- `autoload/admin/Support/TableListRequestFactory.php` (`Admin\Support\TableListRequestFactory`)
- `autoload/admin/Support/Forms/FormRequestHandler.php` (`Admin\Support\Forms\FormRequestHandler`)
- `autoload/admin/Support/Forms/FormFieldRenderer.php` (`Admin\Support\Forms\FormFieldRenderer`)
Uwaga: PSR-4 mapping w composer.json (po 06-01) to `"Admin\\": "autoload/admin/"`. Folder na disku jest lowercase `admin/` (legacy collision z PascalCase Windows fs), ale namespace pozostaje PascalCase `Admin\Support`.
</context>
<acceptance_criteria>
## AC-1: Wszystkie 3 klasy ładują się przez PSR-4
```gherkin
Given composer dump-autoload wykonany po stworzeniu plików
When `php -r "require 'vendor/autoload.php'; var_dump(class_exists('Admin\\Support\\TableListRequestFactory'), class_exists('Admin\\Support\\Forms\\FormRequestHandler'), class_exists('Admin\\Support\\Forms\\FormFieldRenderer'));"`
Then otrzymam `bool(true) bool(true) bool(true)`
```
## AC-2: TableListRequestFactory parsuje request
```gherkin
Given klasa Admin\Support\TableListRequestFactory załadowana
When wywołam `TableListRequestFactory::fromRequest([], ['name', 'date_add'], 'date_add')` w smoke teście (z `$_GET = ['page'=>'2','per_page'=>'25','sort'=>'name']`)
Then otrzymam tablicę z `page=2`, `perPage=25`, `sortColumn='name'` (lub fallback do default jeśli nie jest w sortableColumns), oraz nieputą `viewFilters`/`queryFilters`
```
## AC-3: FormRequestHandler i FormFieldRenderer mają poprawne `use` do 06-01
```gherkin
Given pliki utworzone w Admin\Support\Forms
When przeczytam pierwsze 10 linii FormRequestHandler.php i FormFieldRenderer.php
Then znajdę:
- FormRequestHandler: `use Admin\ViewModels\Forms\FormEditViewModel;`, `use Admin\ViewModels\Forms\FormFieldType;`, `use Admin\Validation\FormValidator;`
- FormFieldRenderer: `use Admin\ViewModels\Forms\FormEditViewModel;`, `use Admin\ViewModels\Forms\FormField;`, `use Admin\ViewModels\Forms\FormFieldType;`
And żaden plik nie ma `namespace admin\` (lowercase) ani `use admin\` (lowercase)
```
## AC-4: Smoke test integracyjny — handler + renderer + Form VMs współpracują
```gherkin
Given wszystkie 3 nowe klasy + Form VMs z 06-01
When utworzę `FormField('email', FormFieldType::EMAIL, 'E-mail')`, opakowuję w `FormEditViewModel`, wywołuję `FormFieldRenderer::renderField($field, $vm)` w smoke teście
Then otrzymam string HTML zawierający `<input` i `name="email"`
And nie wystąpi fatal error o brakującej zależności
```
## AC-5: Zero regresji — Phase 5 i 06-01 nadal działają
```gherkin
Given commit przed 06-02
When `php -r "require 'vendor/autoload.php'; foreach (['Domain\\Articles\\ArticlesRepository','Shared\\Helpers\\Helpers','Admin\\ViewModels\\Forms\\FormField','Admin\\Validation\\FormValidator'] as \$c) { var_dump(class_exists(\$c)); }"`
Then 4× `bool(true)` żaden istniejący namespace nie został rozbity
And `git diff --stat autoload/admin/controls/ autoload/admin/factory/ autoload/admin/view/` pokazuje 0 zmian (legacy nietknięte)
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Skopiuj TableListRequestFactory do Admin\Support z PSR-4 nazwą pliku</name>
<files>autoload/admin/Support/TableListRequestFactory.php</files>
<action>
Utwórz katalog `autoload/admin/Support/` i skopiuj zawartość ze shopPRO:
- Źródło: `C:\visual studio code\projekty\shopPRO\autoload\Admin\Support\class.TableListRequestFactory.php`
- Cel: `autoload/admin/Support/TableListRequestFactory.php` (BEZ `class.` prefiksu — PSR-4 wymaga match nazwa pliku = nazwa klasy)
Zmiana namespace: `namespace admin\Support;``namespace Admin\Support;` (PascalCase pierwszy segment, jak 06-01).
Cała reszta klasy (stałe `DEFAULT_PER_PAGE_OPTIONS`, `DEFAULT_PER_PAGE`, metoda statyczna `fromRequest()`, logika parsowania `$_GET` przez `\Shared\Helpers\Helpers::get()`) bez zmian.
Avoid:
- Pozostawienie prefiksu `class.` w nazwie pliku — psuje PSR-4
- Zmiany sygnatur metod (Phase 7+ zakłada API z shopPRO)
- Zmiany domyślnego sortColumn w sygnaturze
</action>
<verify>
`php -l autoload/admin/Support/TableListRequestFactory.php` → "No syntax errors detected"
`grep -c "^namespace Admin\\\\Support;" autoload/admin/Support/TableListRequestFactory.php` → 1
Po `composer dump-autoload`: `php -r "require 'vendor/autoload.php'; var_dump(class_exists('Admin\\\\Support\\\\TableListRequestFactory'));"``bool(true)`
</verify>
<done>AC-1 częściowo (TableListRequestFactory ładuje się). AC-2 (smoke test factory) wykonywany w Task 3.</done>
</task>
<task type="auto">
<name>Task 2: Skopiuj FormRequestHandler + FormFieldRenderer do Admin\Support\Forms\</name>
<files>autoload/admin/Support/Forms/FormRequestHandler.php, autoload/admin/Support/Forms/FormFieldRenderer.php</files>
<action>
Utwórz katalog `autoload/admin/Support/Forms/` i skopiuj 2 pliki ze shopPRO:
**FormRequestHandler.php** (159 L):
- Źródło: `C:\visual studio code\projekty\shopPRO\autoload\Admin\Support\Forms\FormRequestHandler.php`
- Cel: `autoload/admin/Support/Forms/FormRequestHandler.php`
- Zmiany namespace/use:
- `namespace admin\Support\Forms;``namespace Admin\Support\Forms;`
- `use admin\ViewModels\Forms\FormEditViewModel;``use Admin\ViewModels\Forms\FormEditViewModel;`
- `use admin\ViewModels\Forms\FormFieldType;``use Admin\ViewModels\Forms\FormFieldType;`
- `use admin\Validation\FormValidator;``use Admin\Validation\FormValidator;`
- Cała reszta logiki (handle POST, persist do `$_SESSION`, redirect, walidacja przez `FormValidator`) bez zmian.
**FormFieldRenderer.php** (494 L):
- Źródło: `C:\visual studio code\projekty\shopPRO\autoload\Admin\Support\Forms\FormFieldRenderer.php`
- Cel: `autoload/admin/Support/Forms/FormFieldRenderer.php`
- Zmiany namespace/use:
- `namespace admin\Support\Forms;``namespace Admin\Support\Forms;`
- `use admin\ViewModels\Forms\FormEditViewModel;``use Admin\ViewModels\Forms\FormEditViewModel;`
- `use admin\ViewModels\Forms\FormField;``use Admin\ViewModels\Forms\FormField;`
- `use admin\ViewModels\Forms\FormFieldType;``use Admin\ViewModels\Forms\FormFieldType;`
- JEŻELI plik referuje również `FormTab` lub `FormAction` — analogicznie zaktualizuj `use`
- Cała reszta (metody `renderField`, `renderTabs`, `renderLangSection` itp.) bez zmian.
Po zapisaniu obu plików: `php composer.phar dump-autoload`.
Avoid:
- Refaktoringu generowanego HTML (UI templates Phase 7+ liczą na konkretny markup)
- Pozostawienia jakichkolwiek `admin\` (lowercase) referencji w obu plikach
- Zmiany w inkluzji innych klas spoza Admin\* (Shared\Html, Shared\Helpers)
</action>
<verify>
`php -l autoload/admin/Support/Forms/FormRequestHandler.php` → "No syntax errors detected"
`php -l autoload/admin/Support/Forms/FormFieldRenderer.php` → "No syntax errors detected"
`grep -c "^namespace Admin\\\\Support\\\\Forms;" autoload/admin/Support/Forms/*.php` → 2
`grep -E "^use admin\\\\" autoload/admin/Support/Forms/*.php` → brak wyników (zero lowercase use)
Po dump-autoload: `class_exists('Admin\\\\Support\\\\Forms\\\\FormRequestHandler')` i `class_exists('Admin\\\\Support\\\\Forms\\\\FormFieldRenderer')``bool(true)` × 2
</verify>
<done>AC-1 satisfied (wszystkie 3 klasy 06-02 ładują się), AC-3 satisfied (poprawne `use` Admin\... do 06-01).</done>
</task>
<task type="auto">
<name>Task 3: Smoke test integracyjny — TableListRequestFactory + FormFieldRenderer z VM</name>
<files>(brak modyfikacji — tylko runtime weryfikacja)</files>
<action>
Uruchom inline PHP smoke test sprawdzający 3 zachowania:
**A) TableListRequestFactory parsuje request:**
```
php -r "require 'vendor/autoload.php';
\$_GET = ['page'=>'2','per_page'=>'25','sort'=>'name','status'=>'1'];
\$ctx = Admin\Support\TableListRequestFactory::fromRequest(
[['key'=>'status','label'=>'Status','type'=>'select','options'=>['1'=>'aktywny']]],
['name','date_add'],
'date_add'
);
echo 'page='.\$ctx['page'].' perPage='.\$ctx['perPage'].' sort='.\$ctx['sortColumn'].PHP_EOL;
echo 'filters_status='.\$ctx['filters']['status'].PHP_EOL;
echo 'OK_FACTORY'.PHP_EOL;"
```
Oczekuję: `page=2 perPage=25 sort=name`, `filters_status=1`, `OK_FACTORY`.
**B) FormFieldRenderer renderuje pole:**
```
php -r "require 'vendor/autoload.php';
\$f = new Admin\ViewModels\Forms\FormField('email', Admin\ViewModels\Forms\FormFieldType::EMAIL, 'E-mail', 'a@b.c', 'main', true);
\$vm = new Admin\ViewModels\Forms\FormEditViewModel('test_form', 'Test', [], [\$f], [new Admin\ViewModels\Forms\FormTab('main','Main')]);
\$html = Admin\Support\Forms\FormFieldRenderer::renderField(\$f, \$vm);
echo 'HTML_HAS_INPUT='.(strpos(\$html,'<input')!==false?'YES':'NO').PHP_EOL;
echo 'HTML_HAS_NAME='.(strpos(\$html,'name=\"email\"')!==false?'YES':'NO').PHP_EOL;
echo 'OK_RENDERER'.PHP_EOL;"
```
Oczekuję: `HTML_HAS_INPUT=YES`, `HTML_HAS_NAME=YES`, `OK_RENDERER`.
Uwaga: Jeśli `renderField` jest niestatyczna lub ma inną sygnaturę niż wyżej zakłada — dostosuj wywołanie wedle faktycznej implementacji shopPRO (NIE modyfikuj klasy; popraw smoke test).
**C) FormRequestHandler ładuje się i ma referencję do FormValidator:**
```
php -r "require 'vendor/autoload.php';
\$h = new Admin\Support\Forms\FormRequestHandler();
echo get_class(\$h).PHP_EOL;
echo 'OK_HANDLER'.PHP_EOL;"
```
Oczekuję: `Admin\Support\Forms\FormRequestHandler`, `OK_HANDLER`.
**D) Zero regresji:**
```
php -r "require 'vendor/autoload.php';
foreach (['Domain\Articles\ArticlesRepository','Shared\Helpers\Helpers','Admin\ViewModels\Forms\FormField','Admin\Validation\FormValidator'] as \$c) {
echo \$c.': '.(class_exists(\$c)?'OK':'FAIL').PHP_EOL;
}"
```
Oczekuję: 4× OK.
**Lint na nowych 3 plikach:**
`for f in autoload/admin/Support/TableListRequestFactory.php autoload/admin/Support/Forms/*.php; do php -l "$f"; done`
Avoid:
- Tworzenia trwałych plików testowych
- Modyfikacji shopPRO source files (read-only)
- Modyfikacji legacy `autoload/admin/controls/`, `factory/`, `view/`
</action>
<verify>
Output zawiera: `OK_FACTORY`, `OK_RENDERER`, `OK_HANDLER`, oraz 4× `OK` dla zero-regression check
`php -l` zielony × 3 dla nowych plików
`git diff --stat autoload/admin/controls/ autoload/admin/factory/ autoload/admin/view/` → 0 zmian
</verify>
<done>AC-2, AC-4, AC-5 satisfied (factory działa, renderer produkuje HTML, zero regresji).</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- `autoload/admin/controls/`, `factory/`, `view/` (legacy 50+ plików — boundaries Phase 19)
- `autoload/admin/ViewModels/`, `autoload/admin/Validation/` (utworzone w 06-01, stabilne)
- `autoload/Domain/**`, `autoload/Shared/**` (Phase 1-5 closed)
- `autoload/autoloader.php`
- `composer.json` — TYLKO regen `vendor/composer/autoload_*` przez dump-autoload, BEZ edycji `composer.json` (pre 06-01 ustawienia są wystarczające)
- `composer.lock` — bez `composer update`
- shopPRO source files — read-only
## SCOPE LIMITS
- BaseController — explicit out-of-scope (decyzja: POJOs, brak BaseController)
- `Admin\App.php` (logowanie, special_actions, routing) → Phase 12 (Users + 2FA)
- Migracja istniejących admin\controls\* na nowe wzorce — Phase 7+
- Templates HTML wykorzystujące FormFieldRenderer — Phase 7+ (renderer już produkuje HTML, ale integracja z templates_admin/ to później)
- Testy PHPUnit dla nowych klas — Phase 18
- Refaktoring 494-liniowego FormFieldRenderer (np. split na mniejsze rendery per typ) — deferred
</boundaries>
<verification>
Przed declared complete:
- [ ] `php -l` zielony dla 3 nowych plików
- [ ] `composer dump-autoload` zwraca exit 0 bez warningów PSR-4
- [ ] Smoke test (Task 3) — wszystkie 4 sekcje (A/B/C/D) zwracają oczekiwane outputy
- [ ] `grep -r "namespace admin\\\\Support" autoload/admin/Support/` → brak wyników (zero lowercase)
- [ ] `git diff --stat` pokazuje TYLKO: 3 nowe pliki + regen vendor/composer/autoload_*
- [ ] AC-1..AC-5 spełnione
</verification>
<success_criteria>
- 3 klasy Support dostępne: `Admin\Support\TableListRequestFactory`, `Admin\Support\Forms\FormRequestHandler`, `Admin\Support\Forms\FormFieldRenderer`
- TableListRequestFactory poprawnie parsuje `?page=&per_page=&sort=&filter[]=` z `$_GET`
- FormFieldRenderer produkuje HTML pola formularza z `FormField` ViewModelu
- Zero regresji: Domain, Shared, Admin\ViewModels, Admin\Validation działają jak przed 06-02
- Phase 6 zamknięta — Phase 7+ ma cały fundament
</success_criteria>
<output>
Po wykonaniu utworzyć `.paul/phases/06-admin-base/06-02-SUMMARY.md` z:
- Lista skopiowanych plików (z liczbą linii)
- Output 4 części smoke testu (literalnie)
- Wszelkie deferred issues (np. refaktoring FormFieldRenderer 494 L)
- Setup dla Phase 7 (jakie założenia: TableListRequestFactory + FormRequestHandler + FormFieldRenderer dostępne, brak BaseController — POJO pattern)
- Aktualizacja `.paul/codebase/architecture.md` (deferred — robione w UNIFY)
</output>