From a3caeb9a9a69baa262c2ddf6cd3e17200bbace74 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Thu, 30 Apr 2026 23:32:26 +0200 Subject: [PATCH] =?UTF-8?q?feat(06-admin-base):=20Admin\=20base=20infrastr?= =?UTF-8?q?ucture=20=E2=80=94=20Form=20Edit=20System=20+=20Support=20layer?= =?UTF-8?q?=20(Phase=206)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .paul/PROJECT.md | 7 +- .paul/ROADMAP.md | 22 +- .paul/STATE.md | 30 +- .paul/changelog/2026-04-30.md | 35 ++ .paul/codebase/architecture.md | 12 +- .paul/governance/governance_2026-04-30.jsonl | 66 +++ .paul/phases/06-admin-base/06-01-PLAN.md | 266 ++++++++++ .paul/phases/06-admin-base/06-01-SUMMARY.md | 100 ++++ .paul/phases/06-admin-base/06-02-PLAN.md | 300 +++++++++++ .paul/phases/06-admin-base/06-02-SUMMARY.md | 158 ++++++ .../admin/Support/Forms/FormFieldRenderer.php | 494 ++++++++++++++++++ .../Support/Forms/FormRequestHandler.php | 159 ++++++ .../admin/Support/TableListRequestFactory.php | 99 ++++ autoload/admin/Validation/FormValidator.php | 196 +++++++ .../admin/ViewModels/Forms/FormAction.php | 89 ++++ .../ViewModels/Forms/FormEditViewModel.php | 178 +++++++ autoload/admin/ViewModels/Forms/FormField.php | 364 +++++++++++++ .../admin/ViewModels/Forms/FormFieldType.php | 25 + autoload/admin/ViewModels/Forms/FormTab.php | 31 ++ composer.json | 7 +- docs/PROJECT_STRUCTURE.md | 25 +- vendor/composer/autoload_psr4.php | 4 + vendor/composer/autoload_real.php | 2 + vendor/composer/autoload_static.php | 29 + vendor/composer/platform_check.php | 25 + 25 files changed, 2694 insertions(+), 29 deletions(-) create mode 100644 .paul/changelog/2026-04-30.md create mode 100644 .paul/governance/governance_2026-04-30.jsonl create mode 100644 .paul/phases/06-admin-base/06-01-PLAN.md create mode 100644 .paul/phases/06-admin-base/06-01-SUMMARY.md create mode 100644 .paul/phases/06-admin-base/06-02-PLAN.md create mode 100644 .paul/phases/06-admin-base/06-02-SUMMARY.md create mode 100644 autoload/admin/Support/Forms/FormFieldRenderer.php create mode 100644 autoload/admin/Support/Forms/FormRequestHandler.php create mode 100644 autoload/admin/Support/TableListRequestFactory.php create mode 100644 autoload/admin/Validation/FormValidator.php create mode 100644 autoload/admin/ViewModels/Forms/FormAction.php create mode 100644 autoload/admin/ViewModels/Forms/FormEditViewModel.php create mode 100644 autoload/admin/ViewModels/Forms/FormField.php create mode 100644 autoload/admin/ViewModels/Forms/FormFieldType.php create mode 100644 autoload/admin/ViewModels/Forms/FormTab.php create mode 100644 vendor/composer/platform_check.php diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md index eae9b41..c3a5648 100644 --- a/.paul/PROJECT.md +++ b/.paul/PROJECT.md @@ -9,7 +9,7 @@ Autorski system CMS umożliwiający zarządzanie treściami i stronami interneto ## Already Completed - Domain (13 repos): Articles, Languages, Layouts, Pages, Settings, User, Scontainers, Banners, Authors, Newsletter, SeoAdditional, Cron, Releases+Update - Shared (7 modules): Cache, Helpers, Html, Image, Tpl, Email, Security -- Form Edit System: FormEditViewModel, multi-tab, validation, persistence +- **Admin\ base infrastructure (Phase 6, 2026-04-30):** 5 Form ViewModels + FormValidator + TableListRequestFactory + FormRequestHandler + FormFieldRenderer (1635 L total) — pełny fundament dla migracji 17 admin controllers w Phase 7-13 - PHPUnit base: Bootstrap, 3 test files - Wrapper delegation pattern: legacy factories delegate to Domain repositories @@ -18,7 +18,8 @@ Autorski system CMS umożliwiający zarządzanie treściami i stronami interneto ### Must Have - Centralny PSR-4 autoloader (hybrydowy z legacy) - ✓ Wszystkie Domain repositories — Phase 5 complete -- Shared\Email + Shared\Security (CsrfToken, HMAC-SHA256) +- ✓ Shared\Email + Shared\Security CsrfToken — Phase 2 complete (HMAC-SHA256 → Phase 17) +- ✓ Admin\ base infrastructure — Phase 6 complete (controllers Phase 7-13) - Admin\ namespace z DI dla wszystkich 17 modułów - Frontend\ namespace dla wszystkich front modułów - Bezpieczne cookies (HMAC-SHA256 zamiast hash w JSON) @@ -46,4 +47,4 @@ Autorski system CMS umożliwiający zarządzanie treściami i stronami interneto --- *Created: 2026-04-04* -*Last updated: 2026-04-26 after Phase 5* +*Last updated: 2026-04-30 after Phase 6* diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md index b4d092f..2033fd4 100644 --- a/.paul/ROADMAP.md +++ b/.paul/ROADMAP.md @@ -6,7 +6,7 @@ Pełna refaktoryzacja cmsPRO do architektury DDD wzorowanej na shopPRO. Wzorzec: ## Current Milestone **v0.1 Refaktoryzacja** (v0.1.0) Status: In progress -Phases: 5 of 19 complete +Phases: 6 of 19 complete ## Already Completed (before PAUL) - **Domain (6 repos):** Articles, Languages, Layouts, Pages, Settings, User @@ -24,7 +24,7 @@ Phases: 5 of 19 complete | 4 | Domain: Authors + Newsletter | 1 | Complete | 2026-04-04 | | 04h | **HOTFIX:** HTTPS update endpoint (out-of-roadmap) | 1 | Complete | 2026-04-26 | | 5 | Domain: SeoAdditional + Cron + Releases | 1 | Complete | 2026-04-26 | -| 6 | Admin: Base Infrastructure | 1 | Not started | - | +| 6 | Admin: Base Infrastructure | 2 | Complete | 2026-04-30 | | 7 | Admin: Articles + ArticlesArchive | 1 | Not started | - | | 8 | Admin: Pages + Layouts | 1 | Not started | - | | 9 | Admin: Languages + Settings | 1 | Not started | - | @@ -112,18 +112,20 @@ Phases: 5 of 19 complete ### Phase 6: Admin: Base Infrastructure -**Goal:** Bazowe klasy Admin\ — kontrolery bazowe, TableListRequestFactory, FormValidator (wzór shopPRO). -**Depends on:** Phase 1 (autoloader), Form Edit System (already done) -**Research:** Likely (analiza shopPRO Admin base classes) +**Goal:** Bazowe klasy Admin\ — Form Edit System + helpers do request handling i renderowania (wzór shopPRO). **Bez BaseController** — shopPRO używa POJOs z DI, kontrolery Phase 7+ będą zwykłymi klasami z Domain repos w konstruktorze. +**Depends on:** Phase 1 (autoloader) +**Research:** Done (06-01 SUMMARY) **Scope:** -- Admin\Base\BaseController (lub abstrakcyjna klasa bazowa) -- Admin\Support\TableListRequestFactory -- Admin\Support\FormValidator -- Integracja z istniejącym FormEditViewModel +- Admin\ViewModels\Forms\* (FormEditViewModel, FormField, FormFieldType, FormTab, FormAction) — done in 06-01 +- Admin\Validation\FormValidator — done in 06-01 +- Admin\Support\TableListRequestFactory — 06-02 +- Admin\Support\Forms\FormRequestHandler — 06-02 +- Admin\Support\Forms\FormFieldRenderer — 06-02 **Plans:** -- [ ] 06-01: Admin base infrastructure +- [x] 06-01: Forms infrastructure (Form ViewModels + FormValidator) — 2026-04-30 +- [x] 06-02: Support layer (TableListRequestFactory + FormRequestHandler + FormFieldRenderer) — 2026-04-30 ### Phase 7: Admin: Articles + ArticlesArchive diff --git a/.paul/STATE.md b/.paul/STATE.md index bc40f1e..11d4c83 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -5,32 +5,33 @@ See: .paul/PROJECT.md (updated 2026-04-26) **Core value:** Autorski system CMS umożliwiający zarządzanie treściami i stronami internetowymi. -**Current focus:** Phase 5 complete — ready for Phase 6 (Admin: Base Infrastructure) +**Current focus:** Phase 6 — Plan 06-01 (Forms infrastructure) created, awaiting approval ## Current Position Milestone: v0.1 Refaktoryzacja -Phase: 6 (Admin: Base Infrastructure) — Not started -Plan: Not started -Status: Ready to plan Phase 6 -Last activity: 2026-04-26 — Phase 5 complete, transitioned to Phase 6 +Phase: 6 of 19 (Admin: Base Infrastructure) — Complete +Plan: All plans complete (06-01, 06-02) +Status: Phase 6 closed — ready to plan Phase 7 +Last activity: 2026-04-30 — UNIFY 06-02 closed Phase 6 (Admin Base Infrastructure shipped) Progress: -- Milestone: [▓▓▓░░░░░░░] 26% (5 of 19 phases) +- Milestone: [▓▓▓▓░░░░░░] 32% (6 of 19 phases) +- Phase 6: [▓▓▓▓▓▓▓▓▓▓] 100% complete ## Loop Position Current loop state: ``` PLAN ──▶ APPLY ──▶ UNIFY - ✓ ✓ ✓ [Loop complete - ready for next PLAN] + ✓ ✓ ✓ [Phase 6 complete — ready for Phase 7 PLAN] ``` ## Performance Metrics **Velocity:** -- Total plans completed: 5 -- Total execution time: ~27min +- Total plans completed: 7 +- Total execution time: ~39min **By Phase:** @@ -42,10 +43,15 @@ PLAN ──▶ APPLY ──▶ UNIFY | 04-domain-authors-newsletter | 1/1 | ~2min | ~2min | | 04h-hotfix-https-updates | 1/1 | ~90min | ~90min | | 05-domain-seoadditional-cron-releases | 1/1 | ~5min | ~5min | +| 06-admin-base | 2/2 | ~12min | ~6min | ## Accumulated Context ### Decisions +- 2026-04-30: Phase 6/06-02 — Brak `BaseController`. Phase 7+ kontrolery będą POJOs z DI w konstruktorze (jak shopPRO BannerController). Powtórzony kod (np. `requirePrivilege()`) zostanie wyabstrahowany dopiero gdy zaobserwujemy realny pattern w 2-3 kontrolerach. +- 2026-04-30: Phase 6/06-02 — Filename `TableListRequestFactory.php` (PSR-4 czyste) zamiast shopPRO `class.TableListRequestFactory.php` (lamie PSR-4). Naprawia bug shopPRO. +- 2026-04-30: Phase 6/06-01 — composer.json PSR-4 mapuje `Admin\` → `autoload/admin/` (lowercase folder), bo Windows fs case-insensitive zlał `admin/` legacy z planowanym `Admin/`. Cross-platform safe na Linux. Bonus: `Frontend\` → `autoload/front/` (preventive dla Phase 14+). +- 2026-04-30: Phase 6/06-01 — Form VMs i FormValidator skopiowane 1:1 z shopPRO; jedyne zmiany to namespace `admin\` → `Admin\` (PascalCase per decyzja użytkownika). PROJECT.md mylnie deklarował, że Form Edit System już istnieje — w cmsPRO go nie było. - 2026-04-26: Phase 5 — UpdateRepository przyjmuje ($db, $settings) w konstruktorze — settings potrzebny do update_key i wersji. - 2026-04-26: Phase 5 — Cron helper methods (get_site_meta_*) stały się private w CronRepository — były wywoływane tylko wewnętrznie. - 2026-04-26: Phase 5 — class.Cron.php zachowuje brak namespace (klasa globalna) — cron.php używa bezpośrednio. @@ -66,9 +72,9 @@ None. ## Session Continuity -Last session: 2026-04-26 -Stopped at: Phase 5 complete, loop closed -Next action: /paul:plan dla Phase 6 (Admin: Base Infrastructure) +Last session: 2026-04-30 +Stopped at: Phase 6 complete (loop closed) +Next action: /paul:plan dla Phase 7 (Admin: Articles + ArticlesArchive) Resume file: .paul/ROADMAP.md --- diff --git a/.paul/changelog/2026-04-30.md b/.paul/changelog/2026-04-30.md new file mode 100644 index 0000000..ca34c4f --- /dev/null +++ b/.paul/changelog/2026-04-30.md @@ -0,0 +1,35 @@ +# 2026-04-30 + +## Co zrobiono + +- [Phase 6, Plan 06-01] Forms infrastructure — przeniesienie warstwy Form Edit System z shopPRO do cmsPRO pod namespace `Admin\` (PascalCase) +- Skopiowano 5 ViewModeli (FormEditViewModel, FormField, FormFieldType, FormTab, FormAction) do `Admin\ViewModels\Forms\` +- Skopiowano FormValidator do `Admin\Validation\` +- Dodano `"php": ">=7.4"` do composer.json +- Naprawiono PSR-4 mapowanie: `Admin\` → `autoload/admin/`, `Frontend\` → `autoload/front/` (cross-platform Linux safe) +- Smoke test runtime: walidacja e-maila zwraca PL komunikat ✓ +- Zero regresji: Domain\ + Shared\ ładują się dalej + +- [Phase 6, Plan 06-02] Support layer — TableListRequestFactory + FormRequestHandler + FormFieldRenderer (752 L) skopiowane z shopPRO +- Decyzja: brak BaseController — Phase 7+ kontrolery jako POJOs z DI (jak shopPRO) +- PSR-4 filename fix: `TableListRequestFactory.php` zamiast shopPRO `class.TableListRequestFactory.php` +- **Phase 6 zamknięta** — pełny fundament Admin\ namespace gotowy dla Phase 7+ + +## Zmienione pliki + +- `autoload/admin/ViewModels/Forms/FormEditViewModel.php` (utworzony) +- `autoload/admin/ViewModels/Forms/FormField.php` (utworzony) +- `autoload/admin/ViewModels/Forms/FormFieldType.php` (utworzony) +- `autoload/admin/ViewModels/Forms/FormTab.php` (utworzony) +- `autoload/admin/ViewModels/Forms/FormAction.php` (utworzony) +- `autoload/admin/Validation/FormValidator.php` (utworzony) +- `composer.json` +- `vendor/composer/autoload_*` (regen) +- `.paul/STATE.md`, `.paul/ROADMAP.md` +- `.paul/phases/06-admin-base/06-01-PLAN.md` (utworzony) +- `.paul/phases/06-admin-base/06-01-SUMMARY.md` (utworzony) +- `autoload/admin/Support/TableListRequestFactory.php` (utworzony) +- `autoload/admin/Support/Forms/FormRequestHandler.php` (utworzony) +- `autoload/admin/Support/Forms/FormFieldRenderer.php` (utworzony) +- `.paul/phases/06-admin-base/06-02-PLAN.md` (utworzony) +- `.paul/phases/06-admin-base/06-02-SUMMARY.md` (utworzony) diff --git a/.paul/codebase/architecture.md b/.paul/codebase/architecture.md index 82c17be..8c559d8 100644 --- a/.paul/codebase/architecture.md +++ b/.paul/codebase/architecture.md @@ -52,7 +52,16 @@ autoload/ │ ├── Html/Html.php HTML form element builder │ └── Image/ImageManipulator.php Image processing │ -├── admin/ +├── admin/ Mixed: legacy lowercase namespaces + new Admin\* PSR-4 +│ │ +│ │ NEW (Phase 6) — namespace Admin\* (PascalCase, PSR-4) +│ ├── ViewModels/Forms/ Admin\ViewModels\Forms\* — Form Edit System (5 VMs, 687 L) +│ ├── Validation/FormValidator.php +│ ├── Support/TableListRequestFactory.php Admin\Support — list parser +│ ├── Support/Forms/FormRequestHandler.php POST + CSRF + validate + persist +│ ├── Support/Forms/FormFieldRenderer.php HTML renderer (delegates Shared\Html\Html) +│ │ +│ │ LEGACY — namespace admin\* (lowercase, hybrid loader) │ ├── class.Site.php Admin routing + 2FA │ ├── controls/class.*.php 18 request handler classes (static methods) │ ├── factory/class.*.php 18 @deprecated wrappers @@ -88,6 +97,7 @@ plugins/ | `front\factory\` | `autoload/front/factory/class.*.php` | Legacy lowercase | | `Domain\*\` | `autoload/Domain/*/ClassName.php` | PSR-4 PascalCase | | `Shared\*\` | `autoload/Shared/*/ClassName.php` | PSR-4 PascalCase | +| `Admin\*\` | `autoload/admin/*/ClassName.php` | PSR-4 PascalCase namespace, lowercase folder (Win fs collision with legacy) | ## Key Patterns diff --git a/.paul/governance/governance_2026-04-30.jsonl b/.paul/governance/governance_2026-04-30.jsonl new file mode 100644 index 0000000..664962f --- /dev/null +++ b/.paul/governance/governance_2026-04-30.jsonl @@ -0,0 +1,66 @@ +{"ts":"2026-04-30T20:55:35Z","tool":"Bash","cmd":"for f in FormEditViewModel FormField FormFieldType FormTab FormAction; do\\n echo \"===== $f =====\"\\n head -50 \"C:/visual studio code/projekty/shopPRO/autoload/Admin/ViewModels/Forms/${f}.php\"\\","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T20:55:37Z","tool":"Bash","cmd":"wc -l \"C:/visual studio code/projekty/shopPRO/autoload/Admin/ViewModels/Forms/\"*.php \"C:/visual studio code/projekty/shopPRO/autoload/Admin/Validation/FormValidator.php\" 2>&1; echo \"---enum/match","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T20:55:45Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T20:55:46Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T20:57:14Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\phases\\\\06-admin-base\\\\06-01-PLAN.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T20:57:25Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T20:57:27Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T20:59:21Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/cmsPRO/autoload/Admin/ViewModels/Forms\" \"C:/visual studio code/projekty/cmsPRO/autoload/Admin/Validation\" && echo OK\",\"description\":\"Create Admin/ViewM","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T20:59:48Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\Admin\\\\ViewModels\\\\Forms\\\\FormEditViewModel.php","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:00:16Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\Admin\\\\ViewModels\\\\Forms\\\\FormField.php","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:00:19Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\Admin\\\\ViewModels\\\\Forms\\\\FormFieldType.php","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:00:21Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\Admin\\\\ViewModels\\\\Forms\\\\FormTab.php","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:00:29Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\Admin\\\\ViewModels\\\\Forms\\\\FormAction.php","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:00:48Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\Admin\\\\Validation\\\\FormValidator.php","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:00:56Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\composer.json","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:01:00Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && (composer dump-autoload 2>&1 || php composer.phar dump-autoload 2>&1) | tail -10\",\"description\":\"Regenerate composer autoloader\"},\"tool_response\"","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:01:06Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && for f in autoload/Admin/ViewModels/Forms/FormEditViewModel.php autoload/Admin/ViewModels/Forms/FormField.php autoload/Admin/ViewModels/Forms/FormFieldTy","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:01:11Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && php -r \"require 'vendor/autoload.php';\\nforeach (['Admin\\\\\\\\ViewModels\\\\\\\\Forms\\\\\\\\FormEditViewModel','Admin\\\\\\\\ViewModels\\\\\\\\Form","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:01:17Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && git status --short && echo \"---DOMAIN SANITY---\" && php -r \"require 'vendor/autoload.php'; var_dump(class_exists('Domain\\\\\\\\\\\\\\\\Articles\\\\\","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:01:59Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\composer.json","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:02:06Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && php composer.phar dump-autoload 2>&1 | tail -5 && echo \"---RETEST---\" && php -r \"require 'vendor/autoload.php';\\n\\\\$f = new Admin\\\\\\\\ViewModel","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:02:54Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\phases\\\\06-admin-base\\\\06-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:03:01Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:03:03Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:04:00Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:04:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:04:11Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:04:12Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:04:19Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:04:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:04:30Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\changelog\\\\2026-04-30.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:08:22Z","tool":"Bash","cmd":"wc -l \"C:/visual studio code/projekty/shopPRO/autoload/Admin/Support/class.TableListRequestFactory.php\" \"C:/visual studio code/projekty/shopPRO/autoload/Admin/Support/Forms/\"*.php 2>&1; echo \"---","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:11:10Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:11:12Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:12:48Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\phases\\\\06-admin-base\\\\06-02-PLAN.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:12:55Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:12:57Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:25:29Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/cmsPRO/autoload/admin/Support/Forms\" && echo OK\",\"description\":\"Create Support and Support/Forms directories\"},\"tool_response\":{\"stdout\":\"OK\",\"st","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:25:47Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\admin\\\\Support\\\\TableListRequestFactory.php","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:26:04Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\admin\\\\Support\\\\Forms\\\\FormRequestHandler.php","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:26:55Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\autoload\\\\admin\\\\Support\\\\Forms\\\\FormFieldRenderer.php","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:27:04Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && for f in autoload/admin/Support/TableListRequestFactory.php autoload/admin/Support/Forms/*.php; do php -l \"$f\"; done && echo \"---DUMP---\" && php com","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:27:20Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && php -r \"require 'vendor/autoload.php';\\necho '=== A) AUTOLOAD ==='.PHP_EOL;\\nforeach (['Admin\\\\\\\\Support\\\\\\\\TableListRequestFactory','Admin\\","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:27:30Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && php -r \"require 'vendor/autoload.php';\\n\\\\$f = new Admin\\\\\\\\ViewModels\\\\\\\\Forms\\\\\\\\FormField('email', Admin\\\\\\\\ViewModels\\\\\\\\For","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:28:20Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && git status --short | grep -v vendor | grep -v \"\\\\.serena\" | grep -v governance\",\"description\":\"Check git status excluding vendor/serena/governan","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:29:13Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\phases\\\\06-admin-base\\\\06-02-SUMMARY.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:29:23Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:29:26Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:29:53Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:29:54Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:30:02Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:30:03Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:30:04Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:30:11Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:30:13Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:30:20Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:30:25Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\changelog\\\\2026-04-30.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:30:28Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\changelog\\\\2026-04-30.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:30:44Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\codebase\\\\architecture.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:30:47Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\codebase\\\\architecture.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:31:00Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:31:04Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:31:05Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:31:34Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && php vendor/bin/phpunit 2>&1 | tail -30\",\"description\":\"Run PHPUnit test suite\"},\"tool_response\":{\"stdout\":\"PHPUnit 10.5.63 by Sebastian Bergma","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:32:05Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\cmsPRO\\\\docs\\\\PROJECT_STRUCTURE.md","cwd":"/c/visual studio code/projekty/cmsPRO"} +{"ts":"2026-04-30T21:32:10Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/cmsPRO\" && git log --oneline -5 && echo \"---STATUS---\" && git status --short\",\"description\":\"Check git log convention and status\"},\"tool_response\":{\"stdo","cwd":"/c/visual studio code/projekty/cmsPRO"} diff --git a/.paul/phases/06-admin-base/06-01-PLAN.md b/.paul/phases/06-admin-base/06-01-PLAN.md new file mode 100644 index 0000000..e3615ec --- /dev/null +++ b/.paul/phases/06-admin-base/06-01-PLAN.md @@ -0,0 +1,266 @@ +--- +phase: 06-admin-base +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - autoload/Admin/ViewModels/Forms/FormEditViewModel.php + - autoload/Admin/ViewModels/Forms/FormField.php + - autoload/Admin/ViewModels/Forms/FormFieldType.php + - autoload/Admin/ViewModels/Forms/FormTab.php + - autoload/Admin/ViewModels/Forms/FormAction.php + - autoload/Admin/Validation/FormValidator.php + - composer.json +autonomous: true +delegation: off +--- + + +## Goal +Założyć fundament `Admin\` namespace przez przeniesienie warstwy formularzy z shopPRO: 5 ViewModeli (FormEditViewModel, FormField, FormFieldType, FormTab, FormAction) w `Admin\ViewModels\Forms\` oraz `FormValidator` w `Admin\Validation\`. Plan kończy się działającym, autoloadowanym systemem definicji + walidacji formularzy gotowym do użycia w Phase 7+. + +## Purpose +ROADMAP Phase 6 (Admin: Base Infrastructure) wymaga bazowych klas Admin\ — Form Edit System jest pierwszym filarem (kontrolery z Phase 7-13 będą zwracać `FormEditViewModel` zamiast budować HTML inline). PROJECT.md deklarował to ukończone, ale audyt wykazał brak — Phase 6/Plan 01 to nadrabia, kopiując stabilny wzorzec z shopPRO. + +## Output +- `autoload/Admin/ViewModels/Forms/` — 5 plików (FormEditViewModel, FormField, FormFieldType, FormTab, FormAction) +- `autoload/Admin/Validation/FormValidator.php` +- `composer.json` z `"php": ">=7.4"` +- `.paul/phases/06-admin-base/06-01-SUMMARY.md` + + + + +- **Form VMs** — Czy mamy już Form ViewModele, czy kopiować z shopPRO? + → Odpowiedź: Skopiuj z shopPRO teraz (PROJECT.md deklarował, ale plików nie ma). +- **NS case** — Jaka konwencja namespace dla Admin\? + → Odpowiedź: Fully PascalCase (`Admin\Base`, `Admin\Support`, `Admin\ViewModels\Forms`, `Admin\Validation`). Nie powielać błędu shopPRO (`admin\` lowercase). +- **PHP target** — Cel PHP dla nowego kodu? + → Odpowiedź: `>=7.4` w composer.json. Docelowo musi działać też na 8.4/8.5+, więc kod ma być forward-compatible (typed properties OK, ale unikać deprecated wzorców 8.x). +- **Scope** — Zakres Phase 6? + → Odpowiedź: Splituję Phase 6 na 2 plany. 06-01 = Forms (ten plan). 06-02 = TableListRequestFactory + BaseController. + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md +@.paul/codebase/architecture.md + +## Reference Source (shopPRO — wzorzec do skopiowania) +@C:\visual studio code\projekty\shopPRO\autoload\Admin\ViewModels\Forms\FormEditViewModel.php +@C:\visual studio code\projekty\shopPRO\autoload\Admin\ViewModels\Forms\FormField.php +@C:\visual studio code\projekty\shopPRO\autoload\Admin\ViewModels\Forms\FormFieldType.php +@C:\visual studio code\projekty\shopPRO\autoload\Admin\ViewModels\Forms\FormTab.php +@C:\visual studio code\projekty\shopPRO\autoload\Admin\ViewModels\Forms\FormAction.php +@C:\visual studio code\projekty\shopPRO\autoload\Admin\Validation\FormValidator.php + +## cmsPRO target paths +- `autoload/Admin/ViewModels/Forms/` +- `autoload/Admin/Validation/` + +## Composer +@composer.json + + + + +## AC-1: Form ViewModels load via PSR-4 +```gherkin +Given composer.json mapuje "Admin\\" → "autoload/Admin/" +When wykonam `composer dump-autoload && php -r "var_dump(class_exists('Admin\\ViewModels\\Forms\\FormEditViewModel'));"` +Then otrzymam `bool(true)` dla wszystkich 5 klas (FormEditViewModel, FormField, FormFieldType, FormTab, FormAction) +``` + +## AC-2: FormValidator namespace and dependency +```gherkin +Given FormValidator zawiera `use Admin\ViewModels\Forms\FormField; use Admin\ViewModels\Forms\FormFieldType;` +When `php -r "var_dump(class_exists('Admin\\Validation\\FormValidator'));"` +Then otrzymam `bool(true)` i nie wystąpi fatal error o brakujących klasach +``` + +## AC-3: Smoke test — utworzenie i walidacja formularza +```gherkin +Given załadowane klasy z AC-1 i AC-2 +When utworzę `FormField('email', FormFieldType::EMAIL, 'E-mail', '', 'main', true)` + uruchomię `FormValidator::validate(['email' => 'not-an-email'], [$field])` +Then otrzymam tablicę `['email' => '...musi być poprawnym adresem e-mail.']` (lub równoważny komunikat) — walidacja działa +``` + +## AC-4: PHP version constraint i konwencja PascalCase +```gherkin +Given composer.json +When odczytam pole `require.php` +Then jest równe ">=7.4" +And wszystkie nowe pliki mają `namespace Admin\ViewModels\Forms;` lub `namespace Admin\Validation;` (PascalCase pierwszy segment, w przeciwieństwie do shopPRO `admin\`) +``` + +## AC-5: Zero regresji na istniejącym kodzie +```gherkin +Given commit przed planem +When wykonam `php -l` na każdym zmodyfikowanym pliku oraz `php -r "require 'autoload/autoloader.php';"` (lub equivalent inicjalizacji projektu) +Then żadnych błędów składni i żadnych warning/notice o redefinicji klas; istniejące Domain repos (np. `Domain\Articles\ArticlesRepository`) nadal się ładują +``` + + + + + + + Task 1: Skopiuj 5 Form ViewModeli z shopPRO do Admin\ViewModels\Forms\ z PascalCase namespace + autoload/Admin/ViewModels/Forms/FormEditViewModel.php, autoload/Admin/ViewModels/Forms/FormField.php, autoload/Admin/ViewModels/Forms/FormFieldType.php, autoload/Admin/ViewModels/Forms/FormTab.php, autoload/Admin/ViewModels/Forms/FormAction.php + + Utwórz katalog `autoload/Admin/ViewModels/Forms/` (PascalCase — jeśli system plików ma kolizję z istniejącym `autoload/admin/`, NIE łącz ich; `Admin/` to nowy namespace PSR-4, `admin/` to legacy lowercase i pozostaje nietknięty). + + Skopiuj 5 plików ze shopPRO 1:1 z TYLKO tymi modyfikacjami: + 1. **Zmień `namespace admin\ViewModels\Forms;` → `namespace Admin\ViewModels\Forms;`** (każdy plik, pierwsza linia po ` + + `php -l autoload/Admin/ViewModels/Forms/FormEditViewModel.php` (i pozostałe 4) → "No syntax errors detected" + `grep -c "^namespace Admin\\\\ViewModels\\\\Forms;" autoload/Admin/ViewModels/Forms/*.php` → 5 + `grep -c "^namespace admin\\\\" autoload/Admin/ViewModels/Forms/*.php` → 0 + + AC-1 częściowo (klasy istnieją), AC-4 częściowo (PascalCase namespace). + + + + Task 2: Skopiuj FormValidator do Admin\Validation\ z PascalCase namespace i poprawnymi `use` + autoload/Admin/Validation/FormValidator.php + + Utwórz katalog `autoload/Admin/Validation/` i skopiuj `FormValidator.php` ze shopPRO z modyfikacjami: + 1. `namespace admin\Validation;` → `namespace Admin\Validation;` + 2. `use admin\ViewModels\Forms\FormField;` → `use Admin\ViewModels\Forms\FormField;` + 3. `use admin\ViewModels\Forms\FormFieldType;` → `use Admin\ViewModels\Forms\FormFieldType;` + 4. Cała reszta (logika walidacji, metody prywatne `validateField`, `validateLangSection`, `isEmpty`) bez zmian. + + Avoid: + - Refaktoryzacji walidatora przy okazji (nawet jeśli widzisz coś do poprawy — zapisz w SUMMARY jako deferred issue) + - Zmiany komunikatów błędów (są używane w UI 1:1) + + + `php -l autoload/Admin/Validation/FormValidator.php` → "No syntax errors detected" + `grep -E "^namespace |^use " autoload/Admin/Validation/FormValidator.php` zwraca: + `namespace Admin\Validation;` + `use Admin\ViewModels\Forms\FormField;` + `use Admin\ViewModels\Forms\FormFieldType;` + + AC-2 satisfied: FormValidator ładuje się i zna FormField/FormFieldType. + + + + Task 3: Dodaj `php: >=7.4` do composer.json i zregeneruj autoloader + composer.json + + W `composer.json` dodaj sekcję `require` z `"php": ">=7.4"` PRZED `require-dev`. Jeśli sekcja `require` już istnieje — dopisz tylko klucz `php`. Zachowaj istniejące mapowania PSR-4 (`Domain\\`, `Shared\\`, `Admin\\`, `Frontend\\`). + + Po edycji uruchom `composer dump-autoload` (lub `php composer.phar dump-autoload` jeśli composer nie jest globalny). + + Avoid: + - Aktualizacji composer.lock przez `composer update` + - Dodawania nowych dependencies + - Usuwania `Frontend\\` mapowania (nieużywane teraz, ale zostaje dla Phase 14+) + + + `php -r "echo json_decode(file_get_contents('composer.json'), true)['require']['php'] ?? 'MISSING';"` → ">=7.4" + `php -r "require 'vendor/autoload.php'; var_dump(class_exists('Admin\\\\ViewModels\\\\Forms\\\\FormEditViewModel'));"` → `bool(true)` + `php -r "require 'vendor/autoload.php'; var_dump(class_exists('Admin\\\\Validation\\\\FormValidator'));"` → `bool(true)` + + AC-1 satisfied (autoload działa dla wszystkich 6 klas), AC-4 satisfied (php>=7.4). + + + + Task 4: Smoke test — runtime sanity check formularza + (brak modyfikacji — tylko weryfikacja runtime) + + Uruchom inline PHP smoke check: + ``` + php -r "require 'vendor/autoload.php'; + use Admin\ViewModels\Forms\FormField; + use Admin\ViewModels\Forms\FormFieldType; + use Admin\Validation\FormValidator; + + $field = new FormField('email', FormFieldType::EMAIL, 'E-mail', '', 'main', true); + $v = new FormValidator(); + \$errors = \$v->validate(['email' => 'not-an-email'], [\$field]); + echo isset(\$errors['email']) ? 'OK: '.\$errors['email'] : 'FAIL';" + ``` + + (Składnię cudzysłowów dostosuj do shell — bash vs cmd. Cel: dostać linię zaczynającą się od `OK:` z polskim komunikatem walidacji.) + + Następnie sprawdź lint na wszystkich nowych plikach: + `for f in autoload/Admin/ViewModels/Forms/*.php autoload/Admin/Validation/*.php; do php -l "$f"; done` + + Avoid: + - Tworzenia trwałego pliku testowego — to jednorazowy smoke check, wynik wkleimy do SUMMARY + - Modyfikacji jakichkolwiek plików legacy (admin/, autoload/admin/) — Phase 6 ich nie dotyczy + + + Output zawiera "OK:" + komunikat o niepoprawnym e-mailu + `php -l` na wszystkich 6 plikach → 6× "No syntax errors detected" + `git status` pokazuje TYLKO: 5 nowych plików w `autoload/Admin/ViewModels/Forms/`, 1 nowy plik w `autoload/Admin/Validation/`, zmodyfikowany `composer.json`, regenerowany `vendor/composer/autoload_*` (jeśli vendor/ jest tracked) + + AC-3 satisfied (walidacja działa runtime), AC-5 satisfied (zero regresji — tylko nowe pliki + composer.json). + + + + + + +## DO NOT CHANGE +- `autoload/admin/` (legacy, lowercase) — zostaje nietknięte do Phase 19 +- `autoload/Domain/**` — Phase 5 closed, repozytoria stabilne +- `autoload/Shared/**` — Phase 2 closed +- `autoload/autoloader.php` — Phase 1 (hybrid loader działa); composer dump zaktualizuje vendor/, autoloader.php nie wymaga edycji +- `composer.lock` — nie aktualizuj (`composer update` zakazany w tym planie) +- shopPRO source files — read-only reference + +## SCOPE LIMITS +- TableListRequestFactory → Plan 06-02 (NIE w tym planie) +- BaseController → Plan 06-02 +- Admin\App.php (logowanie, special_actions, routing) → Phase 12 (Users + 2FA) +- Refaktoring/poprawki w skopiowanych klasach — zero zmian merytorycznych, tylko namespace +- Migracja istniejących admin\controls\* na nowe Form VMs → Phase 7+ (per moduł) +- Tworzenie templates/partials renderujących `FormEditViewModel` → poza zakresem Phase 6 +- PHPUnit testy dla FormValidator → Phase 18 + + + + +Przed declared complete: +- [ ] `php -l` zielony dla wszystkich 6 nowych plików +- [ ] `composer dump-autoload` zwraca exit 0 +- [ ] Smoke test (Task 4) zwraca "OK:..." +- [ ] `grep -r "namespace admin\\\\" autoload/Admin/` → brak wyników (zero lowercase namespace w nowym Admin/) +- [ ] `git diff` pokazuje tylko: nowe pliki Admin/ + composer.json + vendor/composer/autoload_classmap.php (regen) +- [ ] Wszystkie AC (1-5) spełnione + + + +- 5 Form ViewModeli + FormValidator dostępne pod namespace `Admin\ViewModels\Forms\` i `Admin\Validation\` +- composer.json deklaruje `php: >=7.4` +- Smoke test walidacji e-maila zwraca komunikat błędu po polsku +- Zero zmian w legacy `autoload/admin/`, `Domain/`, `Shared/` +- Plan gotowy do unify → następnie Plan 06-02 + + + +Po wykonaniu utworzyć `.paul/phases/06-admin-base/06-01-SUMMARY.md` z: +- Lista skopiowanych plików (z liczbą linii) +- Output smoke testu (literalnie) +- Wszelkie deferred issues (np. coś co warto zrefaktoryzować w FormValidator/FormField w przyszłości) +- Setup dla Plan 06-02 (jakie założenia po 06-01: Admin\ namespace działa, Form VMs dostępne) + diff --git a/.paul/phases/06-admin-base/06-01-SUMMARY.md b/.paul/phases/06-admin-base/06-01-SUMMARY.md new file mode 100644 index 0000000..6041a64 --- /dev/null +++ b/.paul/phases/06-admin-base/06-01-SUMMARY.md @@ -0,0 +1,100 @@ +--- +phase: 06-admin-base +plan: 01 +status: complete +completed: 2026-04-30 +--- + +# SUMMARY: Plan 06-01 — Forms infrastructure + +## Co zrobiono + +Przeniesiono z shopPRO do cmsPRO warstwę Form Edit System pod nowy namespace `Admin\` (PascalCase). Wszystkie pliki ładują się przez Composer PSR-4, smoke test runtime potwierdza poprawne działanie walidacji. + +## Pliki utworzone (6) + +| Plik | Linii | Namespace | +|------|-------|-----------| +| `autoload/admin/ViewModels/Forms/FormEditViewModel.php` | 178 | `Admin\ViewModels\Forms` | +| `autoload/admin/ViewModels/Forms/FormField.php` | 364 | `Admin\ViewModels\Forms` | +| `autoload/admin/ViewModels/Forms/FormFieldType.php` | 25 | `Admin\ViewModels\Forms` | +| `autoload/admin/ViewModels/Forms/FormTab.php` | 31 | `Admin\ViewModels\Forms` | +| `autoload/admin/ViewModels/Forms/FormAction.php` | 89 | `Admin\ViewModels\Forms` | +| `autoload/admin/Validation/FormValidator.php` | 196 | `Admin\Validation` | + +**Total**: 883 linii kodu PHP 7.4-kompatybilnego. + +## Pliki zmodyfikowane (2) + +- `composer.json` — dodano `"require": { "php": ">=7.4" }`, mapowanie PSR-4 dla `Admin\` ustawione na realny case na disku +- `vendor/composer/autoload_*` — regenerowane przez `composer dump-autoload` + +## Smoke test (literalny output) + +``` +Admin\ViewModels\Forms\FormEditViewModel: OK +Admin\ViewModels\Forms\FormField: OK +Admin\ViewModels\Forms\FormFieldType: OK +Admin\ViewModels\Forms\FormTab: OK +Admin\ViewModels\Forms\FormAction: OK +Admin\Validation\FormValidator: OK +SMOKE: OK: Pole "E-mail" musi być poprawnym adresem e-mail. +PHP_REQ: >=7.4 +``` + +`php -l` na wszystkich 6 plikach: 6× "No syntax errors detected". + +Zero regresji — `Domain\Articles\ArticlesRepository` i `Shared\Helpers\Helpers` ładują się dalej. + +## Acceptance Criteria — status + +- [x] AC-1: Form ViewModels load via PSR-4 — wszystkie 5 klas + FormValidator +- [x] AC-2: FormValidator namespace + dependency — `use Admin\ViewModels\Forms\...` działa +- [x] AC-3: Smoke test walidacji e-maila — zwraca PL komunikat błędu +- [x] AC-4: PHP `>=7.4` w composer.json + PascalCase namespace na wszystkich nowych plikach +- [x] AC-5: Zero regresji — istniejące Domain/Shared repos działają + +## Deviation: PSR-4 path mapping w composer.json + +**Plan zakładał:** `"Admin\\": "autoload/Admin/"` (PascalCase folder) + +**Zmienione na:** `"Admin\\": "autoload/admin/"` (lowercase folder) + +**Powód:** Windows file system jest case-insensitive — folder `autoload/admin/` (legacy) i `autoload/Admin/` (planowany) to ten sam katalog na dysku. Git tracked path = `admin/` (lowercase, ustanowione przez wcześniejsze fazy z legacy `controls/`, `factory/`, `view/`). Na produkcyjnym Linux (case-sensitive) PSR-4 z mapowaniem `"Admin\\": "autoload/Admin/"` nie znalazłby plików, bo realny katalog na disku to `admin/`. + +**Konsekwencje:** +- Klasa `Admin\ViewModels\Forms\FormField` ładuje się z `autoload/admin/ViewModels/Forms/FormField.php` ✓ +- Legacy `admin\controls\Articles` (lowercase namespace) NIE matches PSR-4 prefix `Admin\` (case-sensitive) — ładuje się przez legacy autoloader (`autoload/autoloader.php`) ✓ +- Cross-platform safe: działa identycznie na Windows i Linux + +**Bonus zmiana (preventive):** `"Frontend\\": "autoload/Frontend/"` → `"Frontend\\": "autoload/front/"` z tego samego powodu — żeby Phase 14+ nie powtórzyła problemu. + +## Boundaries — przestrzegane + +- ✓ Brak zmian w `autoload/admin/controls/`, `factory/`, `view/` (50+ plików legacy nietknięte) +- ✓ Brak zmian w `autoload/Domain/`, `autoload/Shared/` +- ✓ Brak zmian w `autoload/autoloader.php` +- ✓ `composer.lock` nieruszony (`composer update` nie uruchamiany) +- ✓ Tylko zmodyfikowano composer.json + vendor/composer/autoload_* (regen) + +## Deferred issues / observations + +1. **FormValidator constants nie ma** — komunikaty błędów to literały string. Mogłyby być stałe klasy (np. `MSG_REQUIRED`, `MSG_INVALID_EMAIL`) dla i18n w przyszłości. Przeniesienie 1:1 z shopPRO (zachowanie kompatybilności). + +2. **FormField fluent setters mile by były** — obecnie 17-argumentowy konstruktor + 14 factory methods. Dla Phase 7+ kontrolerów to OK, ale builder pattern (`FormField::text('name')->label('X')->required()->placeholder('Y')`) byłby czystszy. Deferred. + +3. **`switch` jako nazwa metody** (`FormField::switch()`) — w PHP < 8.0 to słowo kluczowe i nie może być statycznym nazwiskiem metody w niektórych kontekstach. PHP 7.4 dopuszcza je jako nazwę metody (działa), ale uwaga przy edycji w niektórych edytorach. Zachowane bo shopPRO też tak ma. + +4. **FormTab/FormAction bez factory methods** — tylko konstruktor. To jest design choice z shopPRO; nie zmieniam. + +## Setup dla Plan 06-02 + +Po 06-01 założenia stabilne dla Plan 06-02 (Controller infrastructure): +- `Admin\` namespace działa via PSR-4 → mapowanie do `autoload/admin/` +- composer.json deklaruje `php: >=7.4` +- Form VMs dostępne — `BaseController` może budować/zwracać `FormEditViewModel` +- Konwencja PascalCase utrwalona — `Admin\Base\BaseController`, `Admin\Support\TableListRequestFactory` + +Plan 06-02 doda: +- `autoload/admin/Support/TableListRequestFactory.php` (`Admin\Support\TableListRequestFactory`) +- `autoload/admin/Base/BaseController.php` (`Admin\Base\BaseController`) diff --git a/.paul/phases/06-admin-base/06-02-PLAN.md b/.paul/phases/06-admin-base/06-02-PLAN.md new file mode 100644 index 0000000..b9b6005 --- /dev/null +++ b/.paul/phases/06-admin-base/06-02-PLAN.md @@ -0,0 +1,300 @@ +--- +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 +--- + + +## 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` + + + + +- **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`). + + +## 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`. + + + + +## 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 ` + + + + + Task 1: Skopiuj TableListRequestFactory do Admin\Support z PSR-4 nazwą pliku + autoload/admin/Support/TableListRequestFactory.php + + 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 + + + `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)` + + AC-1 częściowo (TableListRequestFactory ładuje się). AC-2 (smoke test factory) wykonywany w Task 3. + + + + Task 2: Skopiuj FormRequestHandler + FormFieldRenderer do Admin\Support\Forms\ + autoload/admin/Support/Forms/FormRequestHandler.php, autoload/admin/Support/Forms/FormFieldRenderer.php + + 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) + + + `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 + + AC-1 satisfied (wszystkie 3 klasy 06-02 ładują się), AC-3 satisfied (poprawne `use` Admin\... do 06-01). + + + + Task 3: Smoke test integracyjny — TableListRequestFactory + FormFieldRenderer z VM + (brak modyfikacji — tylko runtime weryfikacja) + + 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,' + + 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 + + AC-2, AC-4, AC-5 satisfied (factory działa, renderer produkuje HTML, zero regresji). + + + + + + +## 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 + + + + +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 + + + +- 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 + + + +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) + diff --git a/.paul/phases/06-admin-base/06-02-SUMMARY.md b/.paul/phases/06-admin-base/06-02-SUMMARY.md new file mode 100644 index 0000000..79cc48c --- /dev/null +++ b/.paul/phases/06-admin-base/06-02-SUMMARY.md @@ -0,0 +1,158 @@ +--- +phase: 06-admin-base +plan: 02 +status: complete +completed: 2026-04-30 +--- + +# SUMMARY: Plan 06-02 — Support layer + +## Co zrobiono + +Domknięto Phase 6 (Admin Base Infrastructure) przez dodanie 3 klas warstwy Support: `TableListRequestFactory` (parser list z `$_GET`), `FormRequestHandler` (POST handler z CSRF + walidacja + persist), `FormFieldRenderer` (renderer HTML pól formularza). Wszystkie klasy ładują się przez PSR-4, smoke test runtime potwierdza poprawne działanie factory + autoload wszystkich klas + zero regresji w Domain/Shared/Phase 06-01. + +## Pliki utworzone (3) + +| Plik | Linii | Namespace | +|------|-------|-----------| +| `autoload/admin/Support/TableListRequestFactory.php` | 99 | `Admin\Support` | +| `autoload/admin/Support/Forms/FormRequestHandler.php` | 159 | `Admin\Support\Forms` | +| `autoload/admin/Support/Forms/FormFieldRenderer.php` | 494 | `Admin\Support\Forms` | + +**Total**: 752 linii kodu PHP 7.4-kompatybilnego. + +## Pliki zmodyfikowane (1) + +- `vendor/composer/autoload_*` — regenerowane przez `composer dump-autoload` + +(composer.json bez zmian — Phase 06-01 już ustawiło `php >=7.4` i poprawne PSR-4 paths.) + +## Smoke test (literalny output) + +``` +=== A) AUTOLOAD === +Admin\Support\TableListRequestFactory: OK +Admin\Support\Forms\FormRequestHandler: OK +Admin\Support\Forms\FormFieldRenderer: OK + +=== B) FACTORY === +page=2 perPage=25 sort=name dir=ASC +filters_status=1 +OK_FACTORY + +=== C) HANDLER === +Admin\Support\Forms\FormRequestHandler +isFormSubmit returns: false +OK_HANDLER + +=== D) RENDERER === +(z cwd=admin/) len=2148, has_input=YES +OK_RENDERER + +=== E) ZERO REGRESSION === +Domain\Articles\ArticlesRepository: OK +Shared\Helpers\Helpers: OK +Admin\ViewModels\Forms\FormField: OK +Admin\Validation\FormValidator: OK +``` + +`php -l` na wszystkich 3 plikach: 3× "No syntax errors detected". + +## Acceptance Criteria — status + +| AC | Description | Status | Notes | +|----|-------------|--------|-------| +| AC-1 | Wszystkie 3 klasy ładują się przez PSR-4 | PASS | 3× class_exists() = true | +| AC-2 | TableListRequestFactory parsuje request | PASS | page=2, perPage=25, sort=name, dir=ASC, filters_status=1 | +| AC-3 | Poprawne `use` Admin\... do 06-01 | PASS | `grep "^use admin\\"` → 0 matches | +| AC-4 | Smoke test integracyjny (renderer + handler + Form VMs) | PARTIAL | Klasy ładują się, renderField() zwraca HTML 2148 znaków bez fatal error. Konkretne atrybuty `name="email"` w outpucie zależą od `admin/templates/html/input.php` (Phase 7 integration) | +| AC-5 | Zero regresji | PASS | Domain, Shared, Admin\ViewModels, Admin\Validation działają | + +## Deviation 1: AC-4 częściowo — renderer markup zależy od templates + +**Plan zakładał:** smoke test sprawdzi konkretny HTML `` z `name="email"` w outpucie `FormFieldRenderer::renderField()`. + +**Rzeczywistość:** `FormFieldRenderer` deleguje do `Shared\Html\Html::input()`, który używa `Shared\Tpl\Tpl` z relative path `templates/html/input.php`. To templace istnieje TYLKO w `admin/templates/html/input.php` — nie w root `templates/`. + +**Konsekwencje:** +- Kiedy cwd=root (CLI test): `Tpl` nie znajduje template → zwraca alert div (105 znaków) +- Kiedy cwd=admin/: template się znajduje → renderer produkuje 2148 znaków HTML z ``, ale generic markup (template nie jest jeszcze dostosowany do FormField → szablon ignoruje niektóre params) + +**Decyzja:** AC-4 zaliczone jako PARTIAL — kluczowy fakt że *renderer się ładuje, instantiate, i wywołuje renderField bez fatal error* jest spełniony. Pełna walidacja markup HTML zostanie wykonana w Phase 7 (Articles), gdy: +1. Templates `admin/templates/html/*.php` zostaną zaktualizowane do nowego API +2. Phase 7 controller będzie pierwszym faktycznym konsumentem `FormFieldRenderer` + +## Deviation 2: Drobna ostrzeżenie REQUEST_METHOD w CLI + +`FormRequestHandler::isFormSubmit()` referuje `$_SERVER['REQUEST_METHOD']` które nie jest ustawione w `php -r` CLI mode. PHP zgłasza warning "Undefined array key" ale metoda zwraca `false` (poprawnie). To nie problem produkcyjny — w admin runtime `$_SERVER['REQUEST_METHOD']` zawsze jest ustawione. Zostawiamy 1:1 z shopPRO. + +## Boundaries — przestrzegane + +- ✓ Brak zmian w `autoload/admin/controls/`, `factory/`, `view/` (legacy) +- ✓ Brak zmian w 06-01 plikach (`autoload/admin/ViewModels/`, `autoload/admin/Validation/`) +- ✓ Brak zmian w `autoload/Domain/`, `autoload/Shared/` +- ✓ `autoload/autoloader.php` nieruszony +- ✓ `composer.json` nieruszony (06-01 już to ustawiło) +- ✓ `composer.lock` nieruszony +- ✓ `git diff --stat autoload/admin/controls autoload/admin/factory autoload/admin/view` → 0 zmian + +## Deferred issues / observations + +1. **Templates dostosowanie do FormField VM** — `admin/templates/html/input.php`, `select.php`, `textarea.php` itp. były pisane dla starego API `Shared\Html\Html::input(['label'=>X, 'name'=>Y])`. Renderer 06-02 woła te samo API, ale FormField niesie więcej info (np. error state, lang section ID). Phase 7 przy migracji pierwszego kontrolera (Articles) trzeba sprawdzić czy template renderuje wszystkie params; jeśli nie — zaktualizować templates. (Nie ujmuje to z 06-02 — renderer i template są zgodne API, brakuje tylko niektórych nowych features.) + +2. **FormFieldRenderer.php 494 L — refaktor potencjał** — 17 metod `renderText`, `renderEmail`, `renderDate`, `renderImage` itp. w jednej klasie. Większość różni się tylko `'type' => 'X'`. Można uprościć przez table-driven dispatch. Zachowane 1:1 z shopPRO bo wzorzec jest stabilny i działa. + +3. **`generateFilemanagerUrl()` hardcoded path** — `/libraries/filemanager-9.14.2/dialog.php` jest zaszyte. cmsPRO może mieć inną wersję filemanagera. Phase 7 walidacja: czy ten path istnieje w cmsPRO? Jeśli inny — zaktualizować jako Phase 7 fix. + +4. **Brak BaseController** — decyzja udokumentowana. Phase 7+ kontrolery będą POJOs. Jeśli okaże się że istnieje powtarzalny kod między kontrolerami, można dodać BaseController w Phase 7+ jako odzysk. + +## Phase 6 — STATUS: COMPLETE + +Phase 6 (Admin Base Infrastructure) zamknięta po 2 planach. Pełny fundament dla Phase 7-13 gotowy: + +**Available namespaces:** +- `Admin\ViewModels\Forms\` — FormEditViewModel, FormField, FormFieldType, FormTab, FormAction +- `Admin\Validation\` — FormValidator +- `Admin\Support\` — TableListRequestFactory +- `Admin\Support\Forms\` — FormRequestHandler, FormFieldRenderer + +**Pattern dla Phase 7+ kontrolerów:** +```php +namespace Admin\Articles; + +use Domain\Articles\ArticlesRepository; +use Admin\Support\TableListRequestFactory; +use Admin\Support\Forms\FormRequestHandler; +use Admin\Support\Forms\FormFieldRenderer; +use Admin\ViewModels\Forms\FormEditViewModel; + +class ArticlesController +{ + private ArticlesRepository $repo; + private FormRequestHandler $formHandler; + + public function __construct(ArticlesRepository $repo) + { + $this->repo = $repo; + $this->formHandler = new FormRequestHandler(); + } + + public function listAction(): array + { + $listRequest = TableListRequestFactory::fromRequest($filterDefs, $sortable, 'date_add'); + return $this->repo->listForAdmin($listRequest['filters'], ...); + } +} +``` + +## Next Phase Readiness + +**Ready:** +- Cały fundament Admin\ namespace działa via PSR-4 +- Phase 7 (Articles + ArticlesArchive) może zacząć migrować legacy controls/class.Articles.php na nowy POJO controller w Admin\Articles\ + +**Concerns:** +- `admin/templates/html/*.php` mogą wymagać dostosowania (ale nie blokują — Phase 7 to wykryje) + +**Blockers:** +- None diff --git a/autoload/admin/Support/Forms/FormFieldRenderer.php b/autoload/admin/Support/Forms/FormFieldRenderer.php new file mode 100644 index 0000000..43ccb89 --- /dev/null +++ b/autoload/admin/Support/Forms/FormFieldRenderer.php @@ -0,0 +1,494 @@ +form = $form; + } + + /** + * Renderuje pojedyncze pole + */ + public function renderField(FormField $field): string + { + $method = 'render' . ucfirst($field->type); + + if (method_exists($this, $method)) { + return $this->$method($field); + } + + // Fallback dla nieznanych typów - renderuj jako text + return $this->renderText($field); + } + + /** + * Renderuje pole tekstowe + */ + public function renderText(FormField $field): string + { + $value = $this->form->getFieldValue($field); + $error = $this->form->getError($field->name); + + $params = [ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'value' => $value ?? '', + 'type' => 'text', + 'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]; + + if ($field->placeholder) { + $params['placeholder'] = $field->placeholder; + } + + if ($error) { + $params['class'] .= ' error'; + } + + return $this->wrapWithError(\Shared\Html\Html::input($params), $error); + } + + /** + * Renderuje pole number + */ + public function renderNumber(FormField $field): string + { + $value = $this->form->getFieldValue($field); + $error = $this->form->getError($field->name); + + $params = [ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'value' => $value ?? '', + 'type' => 'number', + 'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]; + + if ($error) { + $params['class'] .= ' error'; + } + + return $this->wrapWithError(\Shared\Html\Html::input($params), $error); + } + + /** + * Renderuje pole email + */ + public function renderEmail(FormField $field): string + { + $value = $this->form->getFieldValue($field); + $error = $this->form->getError($field->name); + + $params = [ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'value' => $value ?? '', + 'type' => 'email', + 'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]; + + if ($error) { + $params['class'] .= ' error'; + } + + return $this->wrapWithError(\Shared\Html\Html::input($params), $error); + } + + /** + * Renderuje pole password + */ + public function renderPassword(FormField $field): string + { + $value = $this->form->getFieldValue($field); + + return \Shared\Html\Html::input([ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'value' => $value ?? '', + 'type' => 'password', + 'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]); + } + + /** + * Renderuje pole daty + */ + public function renderDate(FormField $field): string + { + $value = $this->form->getFieldValue($field); + $error = $this->form->getError($field->name); + + $params = [ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'value' => $value ?? '', + 'type' => 'text', + 'class' => 'date ' . ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]; + + if ($error) { + $params['class'] .= ' error'; + } + + return $this->wrapWithError(\Shared\Html\Html::input($params), $error); + } + + /** + * Renderuje pole daty i czasu + */ + public function renderDatetime(FormField $field): string + { + $value = $this->form->getFieldValue($field); + + return \Shared\Html\Html::input([ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'value' => $value ?? '', + 'type' => 'text', + 'class' => 'datetime ' . ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]); + } + + /** + * Renderuje przełącznik (switch) + */ + public function renderSwitch(FormField $field): string + { + $value = $this->form->getFieldValue($field); + + // Domyślna wartość dla nowego rekordu + if ($value === null && $field->value === true) { + $checked = true; + } else { + $checked = (bool) $value; + } + + return \Shared\Html\Html::input_switch([ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'checked' => $checked, + ]); + } + + /** + * Renderuje select + */ + public function renderSelect(FormField $field): string + { + $value = $this->form->getFieldValue($field); + $error = $this->form->getError($field->name); + + $params = [ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'value' => $value ?? '', + 'values' => $field->options, + 'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]; + + if ($error) { + $params['class'] .= ' error'; + } + + return $this->wrapWithError(\Shared\Html\Html::select($params), $error); + } + + /** + * Renderuje textarea + */ + public function renderTextarea(FormField $field): string + { + $value = $this->form->getFieldValue($field); + + return \Shared\Html\Html::textarea([ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'value' => $value ?? '', + 'rows' => $field->attributes['rows'] ?? 4, + 'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]); + } + + /** + * Renderuje edytor (CKEditor) + */ + public function renderEditor(FormField $field): string + { + $value = $this->form->getFieldValue($field); + + return \Shared\Html\Html::textarea([ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'value' => $value ?? '', + 'rows' => max(10, ($field->attributes['rows'] ?? 10)), + 'class' => 'editor ' . ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]); + } + + /** + * Renderuje pole obrazu z filemanagerem + */ + public function renderImage(FormField $field): string + { + $value = $this->form->getFieldValue($field); + + $filemanagerUrl = $field->filemanagerUrl ?? $this->generateFilemanagerUrl($field->id); + + return \Shared\Html\Html::input_icon([ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'value' => $value ?? '', + 'type' => 'text', + 'icon_content' => 'przeglądaj', + 'icon_js' => "window.open('{$filemanagerUrl}', 'filemanager', 'location=1,status=1,scrollbars=1,width=1100,height=700')", + ]); + } + + /** + * Renderuje pole pliku + */ + public function renderFile(FormField $field): string + { + $value = $this->form->getFieldValue($field); + + if ($field->useFilemanager) { + $filemanagerUrl = $field->filemanagerUrl ?? $this->generateFilemanagerUrl($field->id); + + return \Shared\Html\Html::input_icon([ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'value' => $value ?? '', + 'type' => 'text', + 'icon_content' => 'przeglądaj', + 'icon_js' => "window.open('{$filemanagerUrl}', 'filemanager', 'location=1,status=1,scrollbars=1,width=1100,height=700')", + ]); + } + + return \Shared\Html\Html::input([ + 'label' => $field->label, + 'name' => $field->name, + 'id' => $field->id, + 'type' => 'file', + 'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]); + } + + /** + * Renderuje ukryte pole + */ + public function renderHidden(FormField $field): string + { + $value = $this->form->getFieldValue($field); + + return ''; + } + + /** + * Renderuje pole koloru (color picker + text input) + */ + public function renderColor(FormField $field): string + { + $value = $this->form->getFieldValue($field); + $error = $this->form->getError($field->name); + $colorValue = htmlspecialchars($value ?? '#000000', ENT_QUOTES, 'UTF-8'); + $fieldName = htmlspecialchars($field->name, ENT_QUOTES, 'UTF-8'); + $fieldId = htmlspecialchars($field->id, ENT_QUOTES, 'UTF-8'); + $label = htmlspecialchars($field->label, ENT_QUOTES, 'UTF-8'); + + $html = '
'; + $html .= ''; + $html .= '
'; + $html .= '
'; + $html .= ''; + $html .= ''; + $html .= '
'; + $html .= '
'; + $html .= '
'; + $html .= ''; + + return $this->wrapWithError($html, $error); + } + + public function renderCustom(FormField $field): string + { + return (string)($field->customHtml ?? ''); + } + + /** + * Renderuje sekcję językową + */ + public function renderLangSection(FormField $section): string + { + if ($section->langFields === null || $this->form->languages === null) { + return ''; + } + + $out = '
'; + + // Zakładki języków + $out .= '
    '; + foreach ($this->form->languages as $lang) { + if ($lang['status']) { + $out .= '
  • ' . htmlspecialchars($lang['name']) . '
  • '; + } + } + $out .= '
'; + + // Kontenery języków + $out .= '
'; + foreach ($this->form->languages as $lang) { + if ($lang['status']) { + $out .= '
'; + foreach ($section->langFields as $field) { + $out .= $this->renderLangField($field, $lang['id'], $section->name); + } + $out .= '
'; + } + } + $out .= '
'; + + $out .= '
'; + + return $out; + } + + /** + * Renderuje pole w sekcji językowej + */ + private function renderLangField(FormField $field, $languageId, string $sectionName): string + { + $value = $this->form->getFieldValue($field, $languageId, $field->name); + $error = $this->form->getError($sectionName . '_' . $field->name, $languageId); + + $name = $field->getLocalizedName($languageId); + $id = $field->getLocalizedId($languageId); + + switch ($field->type) { + case FormFieldType::IMAGE: + $filemanagerUrl = $field->filemanagerUrl ?? $this->generateFilemanagerUrl($id); + return $this->wrapWithError(\Shared\Html\Html::input_icon([ + 'label' => $field->label, + 'name' => $name, + 'id' => $id, + 'value' => $value ?? '', + 'type' => 'text', + 'icon_content' => 'przeglądaj', + 'icon_js' => "window.open('{$filemanagerUrl}', 'filemanager', 'location=1,status=1,scrollbars=1,width=1100,height=700')", + ]), $error); + + case FormFieldType::TEXTAREA: + case FormFieldType::EDITOR: + return $this->wrapWithError(\Shared\Html\Html::textarea([ + 'label' => $field->label, + 'name' => $name, + 'id' => $id, + 'value' => $value ?? '', + 'rows' => $field->type === FormFieldType::EDITOR ? 10 : ($field->attributes['rows'] ?? 4), + 'class' => $field->type === FormFieldType::EDITOR ? 'editor' : '', + ]), $error); + + case FormFieldType::SWITCH: + return \Shared\Html\Html::input_switch([ + 'label' => $field->label, + 'name' => $name, + 'id' => $id, + 'checked' => (bool) $value, + ]); + + case FormFieldType::SELECT: + return $this->wrapWithError(\Shared\Html\Html::select([ + 'label' => $field->label, + 'name' => $name, + 'id' => $id, + 'value' => $value ?? '', + 'values' => $field->options, + 'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]), $error); + + default: // TEXT, URL, etc. + if (!empty($field->attributes['icon_content'])) { + $iconJs = (string)($field->attributes['icon_js'] ?? ''); + if ($iconJs !== '') { + $iconJs = str_replace('{lang}', (string)$languageId, $iconJs); + } + + return $this->wrapWithError(\Shared\Html\Html::input_icon([ + 'label' => $field->label, + 'name' => $name, + 'id' => $id, + 'value' => $value ?? '', + 'type' => $field->type === FormFieldType::EMAIL ? 'email' : 'text', + 'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + 'icon_content' => (string)$field->attributes['icon_content'], + 'icon_class' => (string)($field->attributes['icon_class'] ?? ''), + 'icon_js' => $iconJs, + ]), $error); + } + + return $this->wrapWithError(\Shared\Html\Html::input([ + 'label' => $field->label, + 'name' => $name, + 'id' => $id, + 'value' => $value ?? '', + 'type' => $field->type === FormFieldType::EMAIL ? 'email' : 'text', + 'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), + ]), $error); + } + } + + /** + * Generuje URL do filemanagera + */ + private function generateFilemanagerUrl(string $fieldId): string + { + $rfmAkey = $_SESSION['rfm_akey'] ?? bin2hex(random_bytes(16)); + $_SESSION['rfm_akey'] = $rfmAkey; + $_SESSION['rfm_akey_expires'] = time() + 20 * 60; + $_SESSION['can_use_rfm'] = true; + + $fieldIdParam = rawurlencode($fieldId); + $akeyParam = rawurlencode($rfmAkey); + return "/libraries/filemanager-9.14.2/dialog.php?type=1&popup=1&field_id={$fieldIdParam}&akey={$akeyParam}"; + } + + /** + * Opakowuje pole w kontener błędu + */ + private function wrapWithError(string $html, ?string $error): string + { + if ($error) { + return '
' . $html . + '' . htmlspecialchars($error) . '
'; + } + return $html; + } +} diff --git a/autoload/admin/Support/Forms/FormRequestHandler.php b/autoload/admin/Support/Forms/FormRequestHandler.php new file mode 100644 index 0000000..133e91b --- /dev/null +++ b/autoload/admin/Support/Forms/FormRequestHandler.php @@ -0,0 +1,159 @@ +validator = new FormValidator(); + } + + /** + * Przetwarza żądanie POST formularza + * + * @param FormEditViewModel $formViewModel + * @param array $postData Dane z $_POST + * @return array Wynik przetwarzania ['success' => bool, 'errors' => array, 'data' => array] + */ + public function handleSubmit(FormEditViewModel $formViewModel, array $postData): array + { + $result = [ + 'success' => false, + 'errors' => [], + 'data' => [] + ]; + + // Walidacja CSRF + $csrfToken = isset($postData['_csrf_token']) ? (string) $postData['_csrf_token'] : ''; + if (!\Shared\Security\CsrfToken::validate($csrfToken)) { + $result['errors'] = ['csrf' => 'Nieprawidłowy token bezpieczeństwa. Odśwież stronę i spróbuj ponownie.']; + return $result; + } + + // Walidacja + $errors = $this->validator->validate($postData, $formViewModel->fields, $formViewModel->languages); + + if (!empty($errors)) { + $result['errors'] = $errors; + // Zapisz dane do persist przy błędzie walidacji + if ($formViewModel->persist) { + $formViewModel->saveToPersist($postData); + } + return $result; + } + + // Przetwórz dane (np. konwersja typów) + $processedData = $this->processData($postData, $formViewModel->fields); + + $result['success'] = true; + $result['data'] = $processedData; + + // Wyczyść persist po sukcesie + if ($formViewModel->persist) { + $formViewModel->clearPersist(); + } + + return $result; + } + + /** + * Przetwarza dane z formularza (konwersja typów) + */ + private function processData(array $postData, array $fields): array + { + $processed = []; + + foreach ($fields as $field) { + $value = $postData[$field->name] ?? null; + + // Konwersja typów + switch ($field->type) { + case FormFieldType::SWITCH: + $processed[$field->name] = $value ? 1 : 0; + break; + + case FormFieldType::NUMBER: + $processed[$field->name] = $value !== null && $value !== '' ? (float)$value : null; + break; + + case FormFieldType::LANG_SECTION: + if ($field->langFields !== null) { + $processed[$field->name] = $this->processLangSection($postData, $field); + } + break; + + default: + $processed[$field->name] = $value; + } + } + + return $processed; + } + + /** + * Przetwarza sekcję językową + */ + private function processLangSection(array $postData, $section): array + { + $result = []; + + if ($section->langFields === null) { + return $result; + } + + foreach ($section->langFields as $field) { + $fieldName = $field->name; + $langData = $postData[$fieldName] ?? []; + + foreach ($langData as $langId => $value) { + if (!isset($result[$langId])) { + $result[$langId] = []; + } + + // Konwersja typów dla pól językowych + switch ($field->type) { + case FormFieldType::SWITCH: + $result[$langId][$fieldName] = $value ? 1 : 0; + break; + case FormFieldType::NUMBER: + $result[$langId][$fieldName] = $value !== null && $value !== '' ? (float)$value : null; + break; + default: + $result[$langId][$fieldName] = $value; + } + } + } + + return $result; + } + + /** + * Przywraca dane z persist do POST (przy błędzie walidacji) + */ + public function restoreFromPersist(FormEditViewModel $formViewModel): ?array + { + if (!$formViewModel->persist) { + return null; + } + + return $_SESSION['form_persist'][$formViewModel->formId] ?? null; + } + + /** + * Sprawdza czy żądanie jest submitowaniem formularza + */ + public function isFormSubmit(string $formId): bool + { + return $_SERVER['REQUEST_METHOD'] === 'POST' && + (isset($_POST['_form_id']) && $_POST['_form_id'] === $formId); + } +} diff --git a/autoload/admin/Support/TableListRequestFactory.php b/autoload/admin/Support/TableListRequestFactory.php new file mode 100644 index 0000000..801729c --- /dev/null +++ b/autoload/admin/Support/TableListRequestFactory.php @@ -0,0 +1,99 @@ +, + * filters:array, + * viewFilters:array>, + * queryFilters:array, + * sortColumn:string, + * sortDir:string + * } + */ + public static function fromRequest( + array $filterDefinitions, + array $sortableColumns, + string $defaultSortColumn = 'date_add', + ?array $perPageOptions = null, + ?int $defaultPerPage = null + ): array { + if ($perPageOptions === null) { + $perPageOptions = self::DEFAULT_PER_PAGE_OPTIONS; + } + + if ($defaultPerPage === null) { + $defaultPerPage = self::DEFAULT_PER_PAGE; + } + + if (!in_array($defaultPerPage, $perPageOptions, true)) { + $defaultPerPage = (int)$perPageOptions[0]; + } + + $page = max(1, (int)\Shared\Helpers\Helpers::get('page')); + $perPage = (int)\Shared\Helpers\Helpers::get('per_page'); + if (!in_array($perPage, $perPageOptions, true)) { + $perPage = $defaultPerPage; + } + + $filters = []; + $viewFilters = []; + $queryFilters = []; + + foreach ($filterDefinitions as $definition) { + $key = (string)($definition['key'] ?? ''); + if ($key === '') { + continue; + } + + $type = (string)($definition['type'] ?? 'text'); + $value = (string)\Shared\Helpers\Helpers::get($key); + + $filters[$key] = $value; + $queryFilters[$key] = $value; + + $filterConfig = [ + 'key' => $key, + 'label' => (string)($definition['label'] ?? $key), + 'type' => $type, + 'value' => $value, + ]; + + if ($type === 'select' && isset($definition['options']) && is_array($definition['options'])) { + $filterConfig['options'] = $definition['options']; + } + + $viewFilters[] = $filterConfig; + } + + $sortColumn = trim((string)\Shared\Helpers\Helpers::get('sort')); + if (!in_array($sortColumn, $sortableColumns, true)) { + $sortColumn = $defaultSortColumn; + } + + $sortDir = strtoupper(trim((string)\Shared\Helpers\Helpers::get('dir'))); + if (!in_array($sortDir, ['ASC', 'DESC'], true)) { + $sortDir = 'DESC'; + } + + return [ + 'page' => $page, + 'perPage' => $perPage, + 'perPageOptions' => $perPageOptions, + 'filters' => $filters, + 'viewFilters' => $viewFilters, + 'queryFilters' => $queryFilters, + 'sortColumn' => $sortColumn, + 'sortDir' => $sortDir, + ]; + } +} diff --git a/autoload/admin/Validation/FormValidator.php b/autoload/admin/Validation/FormValidator.php new file mode 100644 index 0000000..a2f0ff9 --- /dev/null +++ b/autoload/admin/Validation/FormValidator.php @@ -0,0 +1,196 @@ +errors = []; + + foreach ($fields as $field) { + if ($field->type === FormFieldType::LANG_SECTION) { + $this->validateLangSection($data, $field, $languages ?? []); + } else { + $this->validateField($data, $field); + } + } + + return $this->errors; + } + + /** + * Waliduje pojedyncze pole + */ + private function validateField(array $data, FormField $field): void + { + $value = $data[$field->name] ?? null; + + // Walidacja wymagalności + if ($field->required && $this->isEmpty($value)) { + $this->errors[$field->name] = "Pole \"{$field->label}\" jest wymagane."; + return; + } + + // Jeśli pole puste i nie jest wymagane - pomijamy dalszą walidację + if ($this->isEmpty($value)) { + return; + } + + // Walidacja typu + switch ($field->type) { + case FormFieldType::EMAIL: + if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { + $this->errors[$field->name] = "Pole \"{$field->label}\" musi być poprawnym adresem e-mail."; + } + break; + + case FormFieldType::NUMBER: + if (!is_numeric($value)) { + $this->errors[$field->name] = "Pole \"{$field->label}\" musi być liczbą."; + } + break; + + case FormFieldType::DATE: + if (!$this->isValidDate($value)) { + $this->errors[$field->name] = "Pole \"{$field->label}\" musi być poprawną datą (YYYY-MM-DD)."; + } + break; + + case FormFieldType::DATETIME: + if (!$this->isValidDateTime($value)) { + $this->errors[$field->name] = "Pole \"{$field->label}\" musi być poprawną datą i czasem."; + } + break; + } + + // Walidacja customowa (callback) + if (isset($field->attributes['validate_callback']) && is_callable($field->attributes['validate_callback'])) { + $result = call_user_func($field->attributes['validate_callback'], $value, $data); + if ($result !== true) { + $this->errors[$field->name] = is_string($result) ? $result : "Pole \"{$field->label}\" zawiera nieprawidłową wartość."; + } + } + } + + /** + * Waliduje sekcję językową + */ + private function validateLangSection(array $data, FormField $section, array $languages): void + { + if ($section->langFields === null) { + return; + } + + foreach ($languages as $language) { + if (!($language['status'] ?? false)) { + continue; + } + + $langId = $language['id']; + + foreach ($section->langFields as $field) { + $fieldName = $field->name; + $value = $data[$fieldName][$langId] ?? null; + + // Walidacja wymagalności + if ($field->required && $this->isEmpty($value)) { + $errorKey = "{$section->name}_{$fieldName}"; + $this->errors[$errorKey][$langId] = "Pole \"{$field->label}\" ({$language['name']}) jest wymagane."; + continue; + } + + // Walidacja typu dla pól językowych + if (!$this->isEmpty($value)) { + switch ($field->type) { + case FormFieldType::EMAIL: + if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { + $errorKey = "{$section->name}_{$fieldName}"; + $this->errors[$errorKey][$langId] = "Pole \"{$field->label}\" ({$language['name']}) musi być poprawnym e-mailem."; + } + break; + } + } + } + } + } + + /** + * Sprawdza czy wartość jest pusta + */ + private function isEmpty($value): bool + { + return $value === null || $value === '' || (is_array($value) && empty($value)); + } + + /** + * Sprawdza czy data jest poprawna (YYYY-MM-DD) + */ + private function isValidDate(string $date): bool + { + $d = \DateTime::createFromFormat('Y-m-d', $date); + return $d && $d->format('Y-m-d') === $date; + } + + /** + * Sprawdza czy data i czas są poprawne + */ + private function isValidDateTime(string $datetime): bool + { + $d = \DateTime::createFromFormat('Y-m-d H:i:s', $datetime); + if ($d && $d->format('Y-m-d H:i:s') === $datetime) { + return true; + } + + // Spróbuj bez sekund + $d = \DateTime::createFromFormat('Y-m-d H:i', $datetime); + return $d && $d->format('Y-m-d H:i') === $datetime; + } + + /** + * Sprawdza czy walidacja zakończyła się sukcesem + */ + public function isValid(): bool + { + return empty($this->errors); + } + + /** + * Zwraca wszystkie błędy + */ + public function getErrors(): array + { + return $this->errors; + } + + /** + * Zwraca pierwszy błąd + */ + public function getFirstError(): ?string + { + if (empty($this->errors)) { + return null; + } + + $first = reset($this->errors); + if (is_array($first)) { + return reset($first); + } + return $first; + } +} diff --git a/autoload/admin/ViewModels/Forms/FormAction.php b/autoload/admin/ViewModels/Forms/FormAction.php new file mode 100644 index 0000000..3cf0370 --- /dev/null +++ b/autoload/admin/ViewModels/Forms/FormAction.php @@ -0,0 +1,89 @@ +name = $name; + $this->label = $label; + $this->url = $url; + $this->backUrl = $backUrl; + $this->cssClass = $cssClass; + $this->type = $type; + $this->attributes = $attributes; + } + + /** + * Predefiniowana akcja Zapisz + */ + public static function save(string $url, string $backUrl = '', string $label = 'Zapisz'): self + { + return new self( + 'save', + $label, + $url, + $backUrl, + 'btn btn-primary', + 'submit' + ); + } + + /** + * Predefiniowana akcja Podgląd (otwiera w nowej karcie) + */ + public static function preview(string $url, string $label = 'Podgląd'): self + { + return new self( + 'preview', + $label, + $url, + null, + 'btn btn-info', + 'link', + ['target' => '_blank'] + ); + } + + /** + * Predefiniowana akcja Anuluj + */ + public static function cancel(string $backUrl, string $label = 'Anuluj'): self + { + return new self( + 'cancel', + $label, + $backUrl, + null, + 'btn btn-default', + 'link' + ); + } +} diff --git a/autoload/admin/ViewModels/Forms/FormEditViewModel.php b/autoload/admin/ViewModels/Forms/FormEditViewModel.php new file mode 100644 index 0000000..e217bd1 --- /dev/null +++ b/autoload/admin/ViewModels/Forms/FormEditViewModel.php @@ -0,0 +1,178 @@ +formId = $formId; + $this->title = $title; + $this->data = $data; + $this->fields = $fields; + $this->tabs = $tabs; + $this->actions = $actions; + $this->method = $method; + $this->action = $action; + $this->backUrl = $backUrl; + $this->persist = $persist; + $this->hiddenFields = $hiddenFields; + $this->languages = $languages; + $this->validationErrors = $validationErrors; + } + + /** + * Sprawdza czy formularz ma zakładki + */ + public function hasTabs(): bool + { + return count($this->tabs) > 0; + } + + /** + * Sprawdza czy formularz ma sekcje językowe + */ + public function hasLangSections(): bool + { + foreach ($this->fields as $field) { + if ($field->type === FormFieldType::LANG_SECTION) { + return true; + } + } + return false; + } + + /** + * Zwraca pola dla konkretnej zakładki + */ + public function getFieldsForTab(string $tabId): array + { + return array_filter($this->fields, function (FormField $field) use ($tabId) { + return $field->tabId === $tabId && $field->type !== FormFieldType::LANG_SECTION; + }); + } + + /** + * Zwraca sekcje językowe dla konkretnej zakładki + */ + public function getLangSectionsForTab(string $tabId): array + { + return array_filter($this->fields, function (FormField $field) use ($tabId) { + return $field->type === FormFieldType::LANG_SECTION && + $field->langSectionParentTab === $tabId; + }); + } + + /** + * Pobiera wartość pola z danych lub sesji (persist) + */ + public function getFieldValue(FormField $field, $languageId = null, ?string $langFieldName = null) + { + $fieldName = $field->name; + + // Dla sekcji językowych - pobierz wartość z data[lang_id][field_name] + if ($languageId !== null && $langFieldName !== null) { + $fieldName = $langFieldName; + return $this->data['languages'][$languageId][$fieldName] ?? null; + } + + // Zwykłe pole - najpierw sprawdź sesję (persist), potem dane + if ($this->persist && isset($_SESSION['form_persist'][$this->formId][$fieldName])) { + return $_SESSION['form_persist'][$this->formId][$fieldName]; + } + + return $this->data[$fieldName] ?? $field->value; + } + + /** + * Sprawdza czy pole ma błąd walidacji + */ + public function hasError(string $fieldName, $languageId = null): bool + { + if ($this->validationErrors === null) { + return false; + } + + if ($languageId !== null) { + return isset($this->validationErrors[$fieldName][$languageId]); + } + + return isset($this->validationErrors[$fieldName]); + } + + /** + * Pobiera komunikat błędu dla pola + */ + public function getError(string $fieldName, $languageId = null): ?string + { + if ($languageId !== null) { + return $this->validationErrors[$fieldName][$languageId] ?? null; + } + return $this->validationErrors[$fieldName] ?? null; + } + + /** + * Czyści dane persist z sesji + */ + public function clearPersist(): void + { + if (isset($_SESSION['form_persist'][$this->formId])) { + unset($_SESSION['form_persist'][$this->formId]); + } + } + + /** + * Zapisuje dane do sesji (persist) + */ + public function saveToPersist(array $data): void + { + if (!isset($_SESSION['form_persist'])) { + $_SESSION['form_persist'] = []; + } + $_SESSION['form_persist'][$this->formId] = $data; + } +} diff --git a/autoload/admin/ViewModels/Forms/FormField.php b/autoload/admin/ViewModels/Forms/FormField.php new file mode 100644 index 0000000..7c81cdc --- /dev/null +++ b/autoload/admin/ViewModels/Forms/FormField.php @@ -0,0 +1,364 @@ +name = $name; + $this->type = $type; + $this->label = $label; + $this->value = $value; + $this->tabId = $tabId; + $this->required = $required; + $this->attributes = $attributes; + $this->options = $options; + $this->helpText = $helpText; + $this->placeholder = $placeholder; + $this->useFilemanager = $useFilemanager; + $this->filemanagerUrl = $filemanagerUrl; + $this->editorToolbar = $editorToolbar; + $this->editorHeight = $editorHeight; + $this->langFields = $langFields; + $this->langSectionParentTab = $langSectionParentTab; + $this->customHtml = $customHtml; + $this->id = $attributes['id'] ?? $name; + } + + // Factory methods dla różnych typów pól + + public static function text(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::TEXT, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + $config['attributes'] ?? [], + [], + $config['help'] ?? null, + $config['placeholder'] ?? null + ); + } + + public static function number(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::NUMBER, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + $config['attributes'] ?? [], + [], + $config['help'] ?? null + ); + } + + public static function email(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::EMAIL, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + $config['attributes'] ?? [] + ); + } + + public static function password(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::PASSWORD, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + $config['attributes'] ?? [] + ); + } + + public static function date(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::DATE, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + array_merge(['class' => 'date'], $config['attributes'] ?? []) + ); + } + + public static function datetime(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::DATETIME, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + array_merge(['class' => 'datetime'], $config['attributes'] ?? []) + ); + } + + public static function switch(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::SWITCH, + $config['label'] ?? '', + $config['value'] ?? false, + $config['tab'] ?? 'default', + false, + $config['attributes'] ?? [] + ); + } + + public static function select(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::SELECT, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + $config['attributes'] ?? [], + $config['options'] ?? [] + ); + } + + public static function textarea(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::TEXTAREA, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + array_merge(['rows' => $config['rows'] ?? 4], $config['attributes'] ?? []) + ); + } + + public static function editor(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::EDITOR, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + $config['attributes'] ?? [], + [], + null, + null, + false, + null, + $config['toolbar'] ?? 'MyTool', + $config['height'] ?? 300 + ); + } + + public static function image(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::IMAGE, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + $config['attributes'] ?? [], + [], + null, + null, + $config['filemanager'] ?? true, + $config['filemanager_url'] ?? null + ); + } + + public static function file(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::FILE, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + $config['attributes'] ?? [], + [], + null, + null, + $config['filemanager'] ?? true + ); + } + + public static function color(string $name, array $config = []): self + { + return new self( + $name, + FormFieldType::COLOR, + $config['label'] ?? '', + $config['value'] ?? null, + $config['tab'] ?? 'default', + $config['required'] ?? false, + $config['attributes'] ?? [], + [], + $config['help'] ?? null + ); + } + + public static function hidden(string $name, $value = null): self + { + return new self( + $name, + FormFieldType::HIDDEN, + '', + $value, + 'default' + ); + } + + public static function custom(string $name, string $html, array $config = []): self + { + return new self( + $name, + FormFieldType::CUSTOM, + $config['label'] ?? '', + null, + $config['tab'] ?? 'default', + false, + $config['attributes'] ?? [], + [], + null, + null, + false, + null, + 'MyTool', + 300, + null, + null, + $html + ); + } + + /** + * Sekcja językowa - grupa pól powtarzana dla każdego języka + * + * @param string $name Nazwa sekcji (prefiks dla pól) + * @param string $parentTab Identyfikator zakładki nadrzędnej + * @param array $fields Pola w sekcji językowej (tablica FormField) + */ + public static function langSection(string $name, string $parentTab, array $fields): self + { + return new self( + $name, + FormFieldType::LANG_SECTION, + '', + null, + $parentTab, + false, + [], + [], + null, + null, + false, + null, + 'MyTool', + 300, + $fields, + $parentTab + ); + } + + /** + * Zwraca nazwę pola z sufiksem dla konkretnego języka + */ + public function getLocalizedName($languageId): string + { + return "{$this->name}[{$languageId}]"; + } + + /** + * Zwraca ID pola z sufiksem dla konkretnego języka + */ + public function getLocalizedId($languageId): string + { + return "{$this->id}_{$languageId}"; + } +} diff --git a/autoload/admin/ViewModels/Forms/FormFieldType.php b/autoload/admin/ViewModels/Forms/FormFieldType.php new file mode 100644 index 0000000..2260855 --- /dev/null +++ b/autoload/admin/ViewModels/Forms/FormFieldType.php @@ -0,0 +1,25 @@ +id = $id; + $this->label = $label; + $this->icon = $icon; + $this->parentTabId = $parentTabId; + } +} diff --git a/composer.json b/composer.json index f5a0fd4..f7ee9bf 100644 --- a/composer.json +++ b/composer.json @@ -1,4 +1,7 @@ { + "require": { + "php": ">=7.4" + }, "require-dev": { "phpunit/phpunit": "^10.5" }, @@ -6,8 +9,8 @@ "psr-4": { "Domain\\": "autoload/Domain/", "Shared\\": "autoload/Shared/", - "Admin\\": "autoload/Admin/", - "Frontend\\": "autoload/Frontend/" + "Admin\\": "autoload/admin/", + "Frontend\\": "autoload/front/" } }, "autoload-dev": { diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md index d1df427..e5a6288 100644 --- a/docs/PROJECT_STRUCTURE.md +++ b/docs/PROJECT_STRUCTURE.md @@ -80,7 +80,30 @@ autoload/Domain/ └── User/UserRepository.php ← \Domain\User\UserRepository ✓ ``` -Następne: `Admin\` namespace (Fazy 6–13), `Frontend\` namespace (Fazy 14–16). +### Faza 6 ✓ — Admin\ Base Infrastructure (`autoload/admin/{ViewModels,Validation,Support}/`) + +Skopiowane z shopPRO i przeniesione pod `Admin\` (PSR-4 PascalCase namespace, lowercase folder ze względu na case-insensitive Windows fs): + +``` +autoload/admin/ ← namespace Admin\* (nowe) + admin\* (legacy) +├── ViewModels/Forms/ ← Admin\ViewModels\Forms +│ ├── FormEditViewModel.php +│ ├── FormField.php +│ ├── FormFieldType.php +│ ├── FormTab.php +│ └── FormAction.php +├── Validation/ +│ └── FormValidator.php ← Admin\Validation\FormValidator +└── Support/ + ├── TableListRequestFactory.php ← Admin\Support — parser list (page/per_page/sort/filter) + └── Forms/ + ├── FormRequestHandler.php ← Admin\Support\Forms — POST + CSRF + validate + persist + └── FormFieldRenderer.php ← Admin\Support\Forms — HTML renderer (deleguje Shared\Html\Html) +``` + +Decyzja: **brak BaseController** — Phase 7+ kontrolery będą POJOs z DI w konstruktorze (jak shopPRO `BannerController(BannerRepository, LanguagesRepository)`). + +Następne: `Admin\` controllers (Fazy 7–13), `Frontend\` namespace (Fazy 14–16). --- ## Katalogi diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php index 151344c..02b5ea1 100644 --- a/vendor/composer/autoload_psr4.php +++ b/vendor/composer/autoload_psr4.php @@ -7,6 +7,10 @@ $baseDir = dirname($vendorDir); return array( 'Tests\\' => array($baseDir . '/tests'), + 'Shared\\' => array($baseDir . '/autoload/Shared'), 'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'), + 'Frontend\\' => array($baseDir . '/autoload/front'), + 'Domain\\' => array($baseDir . '/autoload/Domain'), 'DeepCopy\\' => array($vendorDir . '/myclabs/deep-copy/src/DeepCopy'), + 'Admin\\' => array($baseDir . '/autoload/admin'), ); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php index 85d7568..6dff997 100644 --- a/vendor/composer/autoload_real.php +++ b/vendor/composer/autoload_real.php @@ -22,6 +22,8 @@ class ComposerAutoloaderInitedf908e1f6b0e4fca8854163be177e40 return self::$loader; } + require __DIR__ . '/platform_check.php'; + spl_autoload_register(array('ComposerAutoloaderInitedf908e1f6b0e4fca8854163be177e40', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); spl_autoload_unregister(array('ComposerAutoloaderInitedf908e1f6b0e4fca8854163be177e40', 'loadClassLoader')); diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index c1a3172..801c7d5 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -16,14 +16,27 @@ class ComposerStaticInitedf908e1f6b0e4fca8854163be177e40 array ( 'Tests\\' => 6, ), + 'S' => + array ( + 'Shared\\' => 7, + ), 'P' => array ( 'PhpParser\\' => 10, ), + 'F' => + array ( + 'Frontend\\' => 9, + ), 'D' => array ( + 'Domain\\' => 7, 'DeepCopy\\' => 9, ), + 'A' => + array ( + 'Admin\\' => 6, + ), ); public static $prefixDirsPsr4 = array ( @@ -31,14 +44,30 @@ class ComposerStaticInitedf908e1f6b0e4fca8854163be177e40 array ( 0 => __DIR__ . '/../..' . '/tests', ), + 'Shared\\' => + array ( + 0 => __DIR__ . '/../..' . '/autoload/Shared', + ), 'PhpParser\\' => array ( 0 => __DIR__ . '/..' . '/nikic/php-parser/lib/PhpParser', ), + 'Frontend\\' => + array ( + 0 => __DIR__ . '/../..' . '/autoload/front', + ), + 'Domain\\' => + array ( + 0 => __DIR__ . '/../..' . '/autoload/Domain', + ), 'DeepCopy\\' => array ( 0 => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy', ), + 'Admin\\' => + array ( + 0 => __DIR__ . '/../..' . '/autoload/admin', + ), ); public static $classMap = array ( diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php new file mode 100644 index 0000000..d2225c7 --- /dev/null +++ b/vendor/composer/platform_check.php @@ -0,0 +1,25 @@ += 70400)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 7.4.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; + } + } + throw new \RuntimeException( + 'Composer detected issues in your platform: ' . implode(' ', $issues) + ); +}