feat(06-admin-base): Admin\ base infrastructure — Form Edit System + Support layer (Phase 6)
Phase 6 zamknięta po 2 planach. Pełny fundament dla Phase 7-13 (migracja 17 admin controllers do Admin\ namespace). 06-01 (Forms infrastructure): - Admin\ViewModels\Forms\* — 5 ViewModeli (687 L) - Admin\Validation\FormValidator (196 L) - composer.json: php >=7.4, PSR-4 paths cross-platform safe (Admin\ → autoload/admin/, Frontend\ → autoload/front/) 06-02 (Support layer): - Admin\Support\TableListRequestFactory (99 L) — parser list z $_GET - Admin\Support\Forms\FormRequestHandler (159 L) — POST + CSRF + walidacja + persist - Admin\Support\Forms\FormFieldRenderer (494 L) — renderer HTML pól Decyzje: - Brak BaseController — Phase 7+ kontrolery jako POJOs z DI (jak shopPRO) - PSR-4 filename fix: TableListRequestFactory.php (bez shopPRO 'class.' prefix) - PascalCase namespace (Admin\Support) na lowercase folder admin/ ze względu na Windows fs case-insensitivity vs legacy admin/controls/ Pliki: 8 nowych klas, 1635 L kodu PHP 7.4-kompatybilnego, zero regresji. Smoke test: walidacja e-maila zwraca PL komunikat, factory parsuje ?page=&per_page=&sort=&filter=, Domain/Shared nadal ładują się. PHPUnit: 37/37 OK. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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*
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
---
|
||||
|
||||
35
.paul/changelog/2026-04-30.md
Normal file
35
.paul/changelog/2026-04-30.md
Normal file
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
66
.paul/governance/governance_2026-04-30.jsonl
Normal file
66
.paul/governance/governance_2026-04-30.jsonl
Normal file
@@ -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"}
|
||||
266
.paul/phases/06-admin-base/06-01-PLAN.md
Normal file
266
.paul/phases/06-admin-base/06-01-PLAN.md
Normal file
@@ -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
|
||||
---
|
||||
|
||||
<objective>
|
||||
## 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`
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
<clarifications>
|
||||
- **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.
|
||||
</clarifications>
|
||||
|
||||
## 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
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## 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ą
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Skopiuj 5 Form ViewModeli z shopPRO do Admin\ViewModels\Forms\ z PascalCase namespace</name>
|
||||
<files>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</files>
|
||||
<action>
|
||||
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`)
|
||||
2. Wewnętrzne `use` (jeśli są) — zaktualizuj wszelkie odwołania `admin\ViewModels\Forms\X` → `Admin\ViewModels\Forms\X`
|
||||
3. **NIE zmieniaj** sygnatur, typed properties, JSDoców, defaultów, statycznych helperów (np. `FormAction::save()`)
|
||||
|
||||
Pliki do skopiowania (źródło → cel):
|
||||
- `C:\visual studio code\projekty\shopPRO\autoload\Admin\ViewModels\Forms\FormEditViewModel.php` → `autoload/Admin/ViewModels/Forms/FormEditViewModel.php`
|
||||
- `...\FormField.php` → `autoload/Admin/ViewModels/Forms/FormField.php`
|
||||
- `...\FormFieldType.php` → `autoload/Admin/ViewModels/Forms/FormFieldType.php`
|
||||
- `...\FormTab.php` → `autoload/Admin/ViewModels/Forms/FormTab.php`
|
||||
- `...\FormAction.php` → `autoload/Admin/ViewModels/Forms/FormAction.php`
|
||||
|
||||
Avoid:
|
||||
- Wymienianie `public string $name;` na `string $name` (PHP 7.4 wspiera typed properties, jest OK)
|
||||
- Dodawanie nowych funkcji „przy okazji"
|
||||
- Mieszania case'a w nazwach katalogów (musi być dokładnie `Admin/ViewModels/Forms/`)
|
||||
</action>
|
||||
<verify>
|
||||
`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
|
||||
</verify>
|
||||
<done>AC-1 częściowo (klasy istnieją), AC-4 częściowo (PascalCase namespace).</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Skopiuj FormValidator do Admin\Validation\ z PascalCase namespace i poprawnymi `use`</name>
|
||||
<files>autoload/Admin/Validation/FormValidator.php</files>
|
||||
<action>
|
||||
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)
|
||||
</action>
|
||||
<verify>
|
||||
`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;`
|
||||
</verify>
|
||||
<done>AC-2 satisfied: FormValidator ładuje się i zna FormField/FormFieldType.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Dodaj `php: >=7.4` do composer.json i zregeneruj autoloader</name>
|
||||
<files>composer.json</files>
|
||||
<action>
|
||||
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+)
|
||||
</action>
|
||||
<verify>
|
||||
`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)`
|
||||
</verify>
|
||||
<done>AC-1 satisfied (autoload działa dla wszystkich 6 klas), AC-4 satisfied (php>=7.4).</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: Smoke test — runtime sanity check formularza</name>
|
||||
<files>(brak modyfikacji — tylko weryfikacja runtime)</files>
|
||||
<action>
|
||||
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
|
||||
</action>
|
||||
<verify>
|
||||
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)
|
||||
</verify>
|
||||
<done>AC-3 satisfied (walidacja działa runtime), AC-5 satisfied (zero regresji — tylko nowe pliki + composer.json).</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## 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
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
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
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
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)
|
||||
</output>
|
||||
100
.paul/phases/06-admin-base/06-01-SUMMARY.md
Normal file
100
.paul/phases/06-admin-base/06-01-SUMMARY.md
Normal file
@@ -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`)
|
||||
300
.paul/phases/06-admin-base/06-02-PLAN.md
Normal file
300
.paul/phases/06-admin-base/06-02-PLAN.md
Normal file
@@ -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
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Domknąć Phase 6 (Admin Base Infrastructure) przez przeniesienie z shopPRO 3 klas warstwy Support: `TableListRequestFactory` (parser requestów listy z filtrami/sortowaniem/paginacją), `FormRequestHandler` (obsługa POST/persist/walidacji formularza), `FormFieldRenderer` (renderer HTML pól formularza). Po 06-02 cała infrastruktura Phase 7+ (kontrolery) jest gotowa.
|
||||
|
||||
## Purpose
|
||||
Phase 7-13 będą migrować 17 modułów Admin do nowych namespace'ów. Każdy z tych kontrolerów będzie potrzebował:
|
||||
- `TableListRequestFactory::fromRequest()` — uniformowy parsing list z `?page=&per_page=&sort=&filter[]=`
|
||||
- `FormRequestHandler` — uniformowy handler POST → walidacja → persist → redirect
|
||||
- `FormFieldRenderer` — generowanie HTML pól z `FormField` ViewModeli (zastępuje Shared\Html\Html dla nowych formularzy)
|
||||
|
||||
Bez tych helperów każdy kontroler musiałby duplikować logikę paginacji i obsługi POST. Pakiet 06-01 + 06-02 = pełny fundament Admin\ gotowy do Phase 7.
|
||||
|
||||
**Decyzja architektoniczna:** Brak `BaseController` — shopPRO używa POJOs z DI (np. `BannerController(BannerRepository, LanguagesRepository)`), nie dziedziczenia. Phase 7+ kontrolery będą zwykłymi klasami z Domain repos i tymi helperami w konstruktorze.
|
||||
|
||||
## Output
|
||||
- `autoload/admin/Support/TableListRequestFactory.php`
|
||||
- `autoload/admin/Support/Forms/FormRequestHandler.php`
|
||||
- `autoload/admin/Support/Forms/FormFieldRenderer.php`
|
||||
- `.paul/phases/06-admin-base/06-02-SUMMARY.md`
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
<clarifications>
|
||||
- **BaseCtrl** — Czy tworzymy `BaseController`?
|
||||
→ Odpowiedź: NIE. POJOs z DI jak shopPRO. ROADMAP zaktualizowany — wpis BaseController usunięty ze scope Phase 6.
|
||||
- **Helpers** — Które helpery z `Admin/Support/Forms/` w 06-02?
|
||||
→ Odpowiedź: Oba — `FormRequestHandler` (159 L) + `FormFieldRenderer` (494 L). Pełny pakiet, żeby Phase 7+ miało gotowe wszystko.
|
||||
- **Filename** — Konwencja nazwy pliku?
|
||||
→ Odpowiedź: PSR-4 czyste — `TableListRequestFactory.php` (bez `class.` prefiksu z shopPRO). Zgodne z resztą cmsPRO Domain/Shared (np. `ArticlesRepository.php`).
|
||||
</clarifications>
|
||||
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
@.paul/codebase/architecture.md
|
||||
|
||||
## Prior Work
|
||||
@.paul/phases/06-admin-base/06-01-SUMMARY.md
|
||||
|
||||
## Reference Source (shopPRO — wzorzec do skopiowania)
|
||||
@C:\visual studio code\projekty\shopPRO\autoload\Admin\Support\class.TableListRequestFactory.php
|
||||
@C:\visual studio code\projekty\shopPRO\autoload\Admin\Support\Forms\FormRequestHandler.php
|
||||
@C:\visual studio code\projekty\shopPRO\autoload\Admin\Support\Forms\FormFieldRenderer.php
|
||||
|
||||
## cmsPRO target paths
|
||||
- `autoload/admin/Support/TableListRequestFactory.php` (`Admin\Support\TableListRequestFactory`)
|
||||
- `autoload/admin/Support/Forms/FormRequestHandler.php` (`Admin\Support\Forms\FormRequestHandler`)
|
||||
- `autoload/admin/Support/Forms/FormFieldRenderer.php` (`Admin\Support\Forms\FormFieldRenderer`)
|
||||
|
||||
Uwaga: PSR-4 mapping w composer.json (po 06-01) to `"Admin\\": "autoload/admin/"`. Folder na disku jest lowercase `admin/` (legacy collision z PascalCase Windows fs), ale namespace pozostaje PascalCase `Admin\Support`.
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Wszystkie 3 klasy ładują się przez PSR-4
|
||||
```gherkin
|
||||
Given composer dump-autoload wykonany po stworzeniu plików
|
||||
When `php -r "require 'vendor/autoload.php'; var_dump(class_exists('Admin\\Support\\TableListRequestFactory'), class_exists('Admin\\Support\\Forms\\FormRequestHandler'), class_exists('Admin\\Support\\Forms\\FormFieldRenderer'));"`
|
||||
Then otrzymam `bool(true) bool(true) bool(true)`
|
||||
```
|
||||
|
||||
## AC-2: TableListRequestFactory parsuje request
|
||||
```gherkin
|
||||
Given klasa Admin\Support\TableListRequestFactory załadowana
|
||||
When wywołam `TableListRequestFactory::fromRequest([], ['name', 'date_add'], 'date_add')` w smoke teście (z `$_GET = ['page'=>'2','per_page'=>'25','sort'=>'name']`)
|
||||
Then otrzymam tablicę z `page=2`, `perPage=25`, `sortColumn='name'` (lub fallback do default jeśli nie jest w sortableColumns), oraz nieputą `viewFilters`/`queryFilters`
|
||||
```
|
||||
|
||||
## AC-3: FormRequestHandler i FormFieldRenderer mają poprawne `use` do 06-01
|
||||
```gherkin
|
||||
Given pliki utworzone w Admin\Support\Forms
|
||||
When przeczytam pierwsze 10 linii FormRequestHandler.php i FormFieldRenderer.php
|
||||
Then znajdę:
|
||||
- FormRequestHandler: `use Admin\ViewModels\Forms\FormEditViewModel;`, `use Admin\ViewModels\Forms\FormFieldType;`, `use Admin\Validation\FormValidator;`
|
||||
- FormFieldRenderer: `use Admin\ViewModels\Forms\FormEditViewModel;`, `use Admin\ViewModels\Forms\FormField;`, `use Admin\ViewModels\Forms\FormFieldType;`
|
||||
And żaden plik nie ma `namespace admin\` (lowercase) ani `use admin\` (lowercase)
|
||||
```
|
||||
|
||||
## AC-4: Smoke test integracyjny — handler + renderer + Form VMs współpracują
|
||||
```gherkin
|
||||
Given wszystkie 3 nowe klasy + Form VMs z 06-01
|
||||
When utworzę `FormField('email', FormFieldType::EMAIL, 'E-mail')`, opakowuję w `FormEditViewModel`, wywołuję `FormFieldRenderer::renderField($field, $vm)` w smoke teście
|
||||
Then otrzymam string HTML zawierający `<input` i `name="email"`
|
||||
And nie wystąpi fatal error o brakującej zależności
|
||||
```
|
||||
|
||||
## AC-5: Zero regresji — Phase 5 i 06-01 nadal działają
|
||||
```gherkin
|
||||
Given commit przed 06-02
|
||||
When `php -r "require 'vendor/autoload.php'; foreach (['Domain\\Articles\\ArticlesRepository','Shared\\Helpers\\Helpers','Admin\\ViewModels\\Forms\\FormField','Admin\\Validation\\FormValidator'] as \$c) { var_dump(class_exists(\$c)); }"`
|
||||
Then 4× `bool(true)` — żaden istniejący namespace nie został rozbity
|
||||
And `git diff --stat autoload/admin/controls/ autoload/admin/factory/ autoload/admin/view/` pokazuje 0 zmian (legacy nietknięte)
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Skopiuj TableListRequestFactory do Admin\Support z PSR-4 nazwą pliku</name>
|
||||
<files>autoload/admin/Support/TableListRequestFactory.php</files>
|
||||
<action>
|
||||
Utwórz katalog `autoload/admin/Support/` i skopiuj zawartość ze shopPRO:
|
||||
- Źródło: `C:\visual studio code\projekty\shopPRO\autoload\Admin\Support\class.TableListRequestFactory.php`
|
||||
- Cel: `autoload/admin/Support/TableListRequestFactory.php` (BEZ `class.` prefiksu — PSR-4 wymaga match nazwa pliku = nazwa klasy)
|
||||
|
||||
Zmiana namespace: `namespace admin\Support;` → `namespace Admin\Support;` (PascalCase pierwszy segment, jak 06-01).
|
||||
|
||||
Cała reszta klasy (stałe `DEFAULT_PER_PAGE_OPTIONS`, `DEFAULT_PER_PAGE`, metoda statyczna `fromRequest()`, logika parsowania `$_GET` przez `\Shared\Helpers\Helpers::get()`) bez zmian.
|
||||
|
||||
Avoid:
|
||||
- Pozostawienie prefiksu `class.` w nazwie pliku — psuje PSR-4
|
||||
- Zmiany sygnatur metod (Phase 7+ zakłada API z shopPRO)
|
||||
- Zmiany domyślnego sortColumn w sygnaturze
|
||||
</action>
|
||||
<verify>
|
||||
`php -l autoload/admin/Support/TableListRequestFactory.php` → "No syntax errors detected"
|
||||
`grep -c "^namespace Admin\\\\Support;" autoload/admin/Support/TableListRequestFactory.php` → 1
|
||||
Po `composer dump-autoload`: `php -r "require 'vendor/autoload.php'; var_dump(class_exists('Admin\\\\Support\\\\TableListRequestFactory'));"` → `bool(true)`
|
||||
</verify>
|
||||
<done>AC-1 częściowo (TableListRequestFactory ładuje się). AC-2 (smoke test factory) wykonywany w Task 3.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Skopiuj FormRequestHandler + FormFieldRenderer do Admin\Support\Forms\</name>
|
||||
<files>autoload/admin/Support/Forms/FormRequestHandler.php, autoload/admin/Support/Forms/FormFieldRenderer.php</files>
|
||||
<action>
|
||||
Utwórz katalog `autoload/admin/Support/Forms/` i skopiuj 2 pliki ze shopPRO:
|
||||
|
||||
**FormRequestHandler.php** (159 L):
|
||||
- Źródło: `C:\visual studio code\projekty\shopPRO\autoload\Admin\Support\Forms\FormRequestHandler.php`
|
||||
- Cel: `autoload/admin/Support/Forms/FormRequestHandler.php`
|
||||
- Zmiany namespace/use:
|
||||
- `namespace admin\Support\Forms;` → `namespace Admin\Support\Forms;`
|
||||
- `use admin\ViewModels\Forms\FormEditViewModel;` → `use Admin\ViewModels\Forms\FormEditViewModel;`
|
||||
- `use admin\ViewModels\Forms\FormFieldType;` → `use Admin\ViewModels\Forms\FormFieldType;`
|
||||
- `use admin\Validation\FormValidator;` → `use Admin\Validation\FormValidator;`
|
||||
- Cała reszta logiki (handle POST, persist do `$_SESSION`, redirect, walidacja przez `FormValidator`) bez zmian.
|
||||
|
||||
**FormFieldRenderer.php** (494 L):
|
||||
- Źródło: `C:\visual studio code\projekty\shopPRO\autoload\Admin\Support\Forms\FormFieldRenderer.php`
|
||||
- Cel: `autoload/admin/Support/Forms/FormFieldRenderer.php`
|
||||
- Zmiany namespace/use:
|
||||
- `namespace admin\Support\Forms;` → `namespace Admin\Support\Forms;`
|
||||
- `use admin\ViewModels\Forms\FormEditViewModel;` → `use Admin\ViewModels\Forms\FormEditViewModel;`
|
||||
- `use admin\ViewModels\Forms\FormField;` → `use Admin\ViewModels\Forms\FormField;`
|
||||
- `use admin\ViewModels\Forms\FormFieldType;` → `use Admin\ViewModels\Forms\FormFieldType;`
|
||||
- JEŻELI plik referuje również `FormTab` lub `FormAction` — analogicznie zaktualizuj `use`
|
||||
- Cała reszta (metody `renderField`, `renderTabs`, `renderLangSection` itp.) bez zmian.
|
||||
|
||||
Po zapisaniu obu plików: `php composer.phar dump-autoload`.
|
||||
|
||||
Avoid:
|
||||
- Refaktoringu generowanego HTML (UI templates Phase 7+ liczą na konkretny markup)
|
||||
- Pozostawienia jakichkolwiek `admin\` (lowercase) referencji w obu plikach
|
||||
- Zmiany w inkluzji innych klas spoza Admin\* (Shared\Html, Shared\Helpers)
|
||||
</action>
|
||||
<verify>
|
||||
`php -l autoload/admin/Support/Forms/FormRequestHandler.php` → "No syntax errors detected"
|
||||
`php -l autoload/admin/Support/Forms/FormFieldRenderer.php` → "No syntax errors detected"
|
||||
`grep -c "^namespace Admin\\\\Support\\\\Forms;" autoload/admin/Support/Forms/*.php` → 2
|
||||
`grep -E "^use admin\\\\" autoload/admin/Support/Forms/*.php` → brak wyników (zero lowercase use)
|
||||
Po dump-autoload: `class_exists('Admin\\\\Support\\\\Forms\\\\FormRequestHandler')` i `class_exists('Admin\\\\Support\\\\Forms\\\\FormFieldRenderer')` → `bool(true)` × 2
|
||||
</verify>
|
||||
<done>AC-1 satisfied (wszystkie 3 klasy 06-02 ładują się), AC-3 satisfied (poprawne `use` Admin\... do 06-01).</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Smoke test integracyjny — TableListRequestFactory + FormFieldRenderer z VM</name>
|
||||
<files>(brak modyfikacji — tylko runtime weryfikacja)</files>
|
||||
<action>
|
||||
Uruchom inline PHP smoke test sprawdzający 3 zachowania:
|
||||
|
||||
**A) TableListRequestFactory parsuje request:**
|
||||
```
|
||||
php -r "require 'vendor/autoload.php';
|
||||
\$_GET = ['page'=>'2','per_page'=>'25','sort'=>'name','status'=>'1'];
|
||||
\$ctx = Admin\Support\TableListRequestFactory::fromRequest(
|
||||
[['key'=>'status','label'=>'Status','type'=>'select','options'=>['1'=>'aktywny']]],
|
||||
['name','date_add'],
|
||||
'date_add'
|
||||
);
|
||||
echo 'page='.\$ctx['page'].' perPage='.\$ctx['perPage'].' sort='.\$ctx['sortColumn'].PHP_EOL;
|
||||
echo 'filters_status='.\$ctx['filters']['status'].PHP_EOL;
|
||||
echo 'OK_FACTORY'.PHP_EOL;"
|
||||
```
|
||||
Oczekuję: `page=2 perPage=25 sort=name`, `filters_status=1`, `OK_FACTORY`.
|
||||
|
||||
**B) FormFieldRenderer renderuje pole:**
|
||||
```
|
||||
php -r "require 'vendor/autoload.php';
|
||||
\$f = new Admin\ViewModels\Forms\FormField('email', Admin\ViewModels\Forms\FormFieldType::EMAIL, 'E-mail', 'a@b.c', 'main', true);
|
||||
\$vm = new Admin\ViewModels\Forms\FormEditViewModel('test_form', 'Test', [], [\$f], [new Admin\ViewModels\Forms\FormTab('main','Main')]);
|
||||
\$html = Admin\Support\Forms\FormFieldRenderer::renderField(\$f, \$vm);
|
||||
echo 'HTML_HAS_INPUT='.(strpos(\$html,'<input')!==false?'YES':'NO').PHP_EOL;
|
||||
echo 'HTML_HAS_NAME='.(strpos(\$html,'name=\"email\"')!==false?'YES':'NO').PHP_EOL;
|
||||
echo 'OK_RENDERER'.PHP_EOL;"
|
||||
```
|
||||
Oczekuję: `HTML_HAS_INPUT=YES`, `HTML_HAS_NAME=YES`, `OK_RENDERER`.
|
||||
|
||||
Uwaga: Jeśli `renderField` jest niestatyczna lub ma inną sygnaturę niż wyżej zakłada — dostosuj wywołanie wedle faktycznej implementacji shopPRO (NIE modyfikuj klasy; popraw smoke test).
|
||||
|
||||
**C) FormRequestHandler ładuje się i ma referencję do FormValidator:**
|
||||
```
|
||||
php -r "require 'vendor/autoload.php';
|
||||
\$h = new Admin\Support\Forms\FormRequestHandler();
|
||||
echo get_class(\$h).PHP_EOL;
|
||||
echo 'OK_HANDLER'.PHP_EOL;"
|
||||
```
|
||||
Oczekuję: `Admin\Support\Forms\FormRequestHandler`, `OK_HANDLER`.
|
||||
|
||||
**D) Zero regresji:**
|
||||
```
|
||||
php -r "require 'vendor/autoload.php';
|
||||
foreach (['Domain\Articles\ArticlesRepository','Shared\Helpers\Helpers','Admin\ViewModels\Forms\FormField','Admin\Validation\FormValidator'] as \$c) {
|
||||
echo \$c.': '.(class_exists(\$c)?'OK':'FAIL').PHP_EOL;
|
||||
}"
|
||||
```
|
||||
Oczekuję: 4× OK.
|
||||
|
||||
**Lint na nowych 3 plikach:**
|
||||
`for f in autoload/admin/Support/TableListRequestFactory.php autoload/admin/Support/Forms/*.php; do php -l "$f"; done`
|
||||
|
||||
Avoid:
|
||||
- Tworzenia trwałych plików testowych
|
||||
- Modyfikacji shopPRO source files (read-only)
|
||||
- Modyfikacji legacy `autoload/admin/controls/`, `factory/`, `view/`
|
||||
</action>
|
||||
<verify>
|
||||
Output zawiera: `OK_FACTORY`, `OK_RENDERER`, `OK_HANDLER`, oraz 4× `OK` dla zero-regression check
|
||||
`php -l` zielony × 3 dla nowych plików
|
||||
`git diff --stat autoload/admin/controls/ autoload/admin/factory/ autoload/admin/view/` → 0 zmian
|
||||
</verify>
|
||||
<done>AC-2, AC-4, AC-5 satisfied (factory działa, renderer produkuje HTML, zero regresji).</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- `autoload/admin/controls/`, `factory/`, `view/` (legacy 50+ plików — boundaries Phase 19)
|
||||
- `autoload/admin/ViewModels/`, `autoload/admin/Validation/` (utworzone w 06-01, stabilne)
|
||||
- `autoload/Domain/**`, `autoload/Shared/**` (Phase 1-5 closed)
|
||||
- `autoload/autoloader.php`
|
||||
- `composer.json` — TYLKO regen `vendor/composer/autoload_*` przez dump-autoload, BEZ edycji `composer.json` (pre 06-01 ustawienia są wystarczające)
|
||||
- `composer.lock` — bez `composer update`
|
||||
- shopPRO source files — read-only
|
||||
|
||||
## SCOPE LIMITS
|
||||
- BaseController — explicit out-of-scope (decyzja: POJOs, brak BaseController)
|
||||
- `Admin\App.php` (logowanie, special_actions, routing) → Phase 12 (Users + 2FA)
|
||||
- Migracja istniejących admin\controls\* na nowe wzorce — Phase 7+
|
||||
- Templates HTML wykorzystujące FormFieldRenderer — Phase 7+ (renderer już produkuje HTML, ale integracja z templates_admin/ to później)
|
||||
- Testy PHPUnit dla nowych klas — Phase 18
|
||||
- Refaktoring 494-liniowego FormFieldRenderer (np. split na mniejsze rendery per typ) — deferred
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Przed declared complete:
|
||||
- [ ] `php -l` zielony dla 3 nowych plików
|
||||
- [ ] `composer dump-autoload` zwraca exit 0 bez warningów PSR-4
|
||||
- [ ] Smoke test (Task 3) — wszystkie 4 sekcje (A/B/C/D) zwracają oczekiwane outputy
|
||||
- [ ] `grep -r "namespace admin\\\\Support" autoload/admin/Support/` → brak wyników (zero lowercase)
|
||||
- [ ] `git diff --stat` pokazuje TYLKO: 3 nowe pliki + regen vendor/composer/autoload_*
|
||||
- [ ] AC-1..AC-5 spełnione
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 3 klasy Support dostępne: `Admin\Support\TableListRequestFactory`, `Admin\Support\Forms\FormRequestHandler`, `Admin\Support\Forms\FormFieldRenderer`
|
||||
- TableListRequestFactory poprawnie parsuje `?page=&per_page=&sort=&filter[]=` z `$_GET`
|
||||
- FormFieldRenderer produkuje HTML pola formularza z `FormField` ViewModelu
|
||||
- Zero regresji: Domain, Shared, Admin\ViewModels, Admin\Validation działają jak przed 06-02
|
||||
- Phase 6 zamknięta — Phase 7+ ma cały fundament
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Po wykonaniu utworzyć `.paul/phases/06-admin-base/06-02-SUMMARY.md` z:
|
||||
- Lista skopiowanych plików (z liczbą linii)
|
||||
- Output 4 części smoke testu (literalnie)
|
||||
- Wszelkie deferred issues (np. refaktoring FormFieldRenderer 494 L)
|
||||
- Setup dla Phase 7 (jakie założenia: TableListRequestFactory + FormRequestHandler + FormFieldRenderer dostępne, brak BaseController — POJO pattern)
|
||||
- Aktualizacja `.paul/codebase/architecture.md` (deferred — robione w UNIFY)
|
||||
</output>
|
||||
158
.paul/phases/06-admin-base/06-02-SUMMARY.md
Normal file
158
.paul/phases/06-admin-base/06-02-SUMMARY.md
Normal file
@@ -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 `<input>` 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 `<input>`, 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
|
||||
494
autoload/admin/Support/Forms/FormFieldRenderer.php
Normal file
494
autoload/admin/Support/Forms/FormFieldRenderer.php
Normal file
@@ -0,0 +1,494 @@
|
||||
<?php
|
||||
namespace Admin\Support\Forms;
|
||||
|
||||
use Admin\ViewModels\Forms\FormEditViewModel;
|
||||
use Admin\ViewModels\Forms\FormField;
|
||||
use Admin\ViewModels\Forms\FormFieldType;
|
||||
|
||||
/**
|
||||
* Renderer pól formularza
|
||||
*/
|
||||
class FormFieldRenderer
|
||||
{
|
||||
private FormEditViewModel $form;
|
||||
|
||||
public function __construct(FormEditViewModel $form)
|
||||
{
|
||||
$this->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 '<input type="hidden" name="' . htmlspecialchars($field->name) . '" ' .
|
||||
'id="' . htmlspecialchars($field->id) . '" ' .
|
||||
'value="' . htmlspecialchars($value ?? '') . '">';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = '<div class="form-group row">';
|
||||
$html .= '<label class="col-lg-4 control-label">' . $label . ':</label>';
|
||||
$html .= '<div class="col-lg-8">';
|
||||
$html .= '<div style="display:flex;align-items:center;gap:8px;">';
|
||||
$html .= '<input type="color" id="' . $fieldId . '_picker" value="' . $colorValue . '" style="width:40px;height:34px;padding:2px;border:1px solid #ccc;cursor:pointer;" />';
|
||||
$html .= '<input type="text" name="' . $fieldName . '" id="' . $fieldId . '" value="' . $colorValue . '" class="form-control" style="max-width:150px;" />';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
$html .= '<script>$(function(){'
|
||||
. 'var $p=$("#' . $fieldId . '_picker"),$t=$("#' . $fieldId . '");'
|
||||
. '$p.on("input",function(){$t.val(this.value);});'
|
||||
. '$t.on("input",function(){var v=this.value;if(/^#[0-9a-fA-F]{6}$/.test(v))$p.val(v);});'
|
||||
. '});</script>';
|
||||
|
||||
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 = '<div id="languages-' . $section->name . '" class="languages-tabs">';
|
||||
|
||||
// Zakładki języków
|
||||
$out .= '<ul class="resp-tabs-list languages-tabs htabs">';
|
||||
foreach ($this->form->languages as $lang) {
|
||||
if ($lang['status']) {
|
||||
$out .= '<li>' . htmlspecialchars($lang['name']) . '</li>';
|
||||
}
|
||||
}
|
||||
$out .= '</ul>';
|
||||
|
||||
// Kontenery języków
|
||||
$out .= '<div class="resp-tabs-container languages-tabs">';
|
||||
foreach ($this->form->languages as $lang) {
|
||||
if ($lang['status']) {
|
||||
$out .= '<div>';
|
||||
foreach ($section->langFields as $field) {
|
||||
$out .= $this->renderLangField($field, $lang['id'], $section->name);
|
||||
}
|
||||
$out .= '</div>';
|
||||
}
|
||||
}
|
||||
$out .= '</div>';
|
||||
|
||||
$out .= '</div>';
|
||||
|
||||
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 '<div class="field-with-error">' . $html .
|
||||
'<span class="error-message">' . htmlspecialchars($error) . '</span></div>';
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
159
autoload/admin/Support/Forms/FormRequestHandler.php
Normal file
159
autoload/admin/Support/Forms/FormRequestHandler.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
namespace Admin\Support\Forms;
|
||||
|
||||
use Admin\ViewModels\Forms\FormEditViewModel;
|
||||
use Admin\ViewModels\Forms\FormFieldType;
|
||||
use Admin\Validation\FormValidator;
|
||||
|
||||
/**
|
||||
* Obsługa żądań formularza (POST, persist, walidacja)
|
||||
*/
|
||||
class FormRequestHandler
|
||||
{
|
||||
private FormValidator $validator;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
99
autoload/admin/Support/TableListRequestFactory.php
Normal file
99
autoload/admin/Support/TableListRequestFactory.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
namespace Admin\Support;
|
||||
|
||||
class TableListRequestFactory
|
||||
{
|
||||
public const DEFAULT_PER_PAGE_OPTIONS = [5, 10, 15, 25, 50, 100];
|
||||
public const DEFAULT_PER_PAGE = 15;
|
||||
|
||||
/**
|
||||
* Buduje kontekst listy (filtry, sortowanie, paginacja) z requestu.
|
||||
*
|
||||
* @return array{
|
||||
* page:int,
|
||||
* perPage:int,
|
||||
* perPageOptions:array<int,int>,
|
||||
* filters:array<string,string>,
|
||||
* viewFilters:array<int,array<string,mixed>>,
|
||||
* queryFilters:array<string,string>,
|
||||
* 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
196
autoload/admin/Validation/FormValidator.php
Normal file
196
autoload/admin/Validation/FormValidator.php
Normal file
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
namespace Admin\Validation;
|
||||
|
||||
use Admin\ViewModels\Forms\FormField;
|
||||
use Admin\ViewModels\Forms\FormFieldType;
|
||||
|
||||
/**
|
||||
* Walidator formularzy
|
||||
*/
|
||||
class FormValidator
|
||||
{
|
||||
private array $errors = [];
|
||||
|
||||
/**
|
||||
* Waliduje dane na podstawie definicji pól
|
||||
*
|
||||
* @param array $data Dane z POST
|
||||
* @param array $fields Definicje pól (FormField[])
|
||||
* @param array|null $languages Języki (dla walidacji pól językowych)
|
||||
* @return array Tablica błędów (pusta jeśli OK)
|
||||
*/
|
||||
public function validate(array $data, array $fields, ?array $languages = null): array
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
89
autoload/admin/ViewModels/Forms/FormAction.php
Normal file
89
autoload/admin/ViewModels/Forms/FormAction.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
namespace Admin\ViewModels\Forms;
|
||||
|
||||
/**
|
||||
* Definicja akcji formularza (przycisku)
|
||||
*/
|
||||
class FormAction
|
||||
{
|
||||
public string $name;
|
||||
public string $label;
|
||||
public string $type;
|
||||
public string $url;
|
||||
public ?string $backUrl;
|
||||
public string $cssClass;
|
||||
public array $attributes;
|
||||
|
||||
/**
|
||||
* @param string $name Nazwa akcji (save, cancel, delete)
|
||||
* @param string $label Etykieta przycisku
|
||||
* @param string $url URL akcji (dla save)
|
||||
* @param string|null $backUrl URL powrotu po zapisie
|
||||
* @param string $cssClass Klasy CSS przycisku
|
||||
* @param string $type Typ przycisku (submit, button, link)
|
||||
* @param array $attributes Dodatkowe atrybuty HTML
|
||||
*/
|
||||
public function __construct(
|
||||
string $name,
|
||||
string $label,
|
||||
string $url = '',
|
||||
?string $backUrl = null,
|
||||
string $cssClass = 'btn btn-primary',
|
||||
string $type = 'submit',
|
||||
array $attributes = []
|
||||
) {
|
||||
$this->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'
|
||||
);
|
||||
}
|
||||
}
|
||||
178
autoload/admin/ViewModels/Forms/FormEditViewModel.php
Normal file
178
autoload/admin/ViewModels/Forms/FormEditViewModel.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
namespace Admin\ViewModels\Forms;
|
||||
|
||||
/**
|
||||
* Główny model widoku formularza edycji
|
||||
*/
|
||||
class FormEditViewModel
|
||||
{
|
||||
public string $formId;
|
||||
public string $title;
|
||||
public string $method;
|
||||
public string $action;
|
||||
public ?string $backUrl;
|
||||
public array $tabs;
|
||||
public array $fields;
|
||||
public array $hiddenFields;
|
||||
public array $actions;
|
||||
public bool $persist;
|
||||
public array $data;
|
||||
public ?array $validationErrors;
|
||||
public ?array $languages;
|
||||
|
||||
/**
|
||||
* @param string $formId Unikalny identyfikator formularza
|
||||
* @param string $title Tytuł formularza
|
||||
* @param array $data Dane obiektu (np. banner)
|
||||
* @param array $fields Pola formularza
|
||||
* @param array $tabs Zakładki formularza
|
||||
* @param array $actions Akcje (przyciski)
|
||||
* @param string $method Metoda HTTP (POST, GET)
|
||||
* @param string $action URL akcji formularza
|
||||
* @param string|null $backUrl URL powrotu
|
||||
* @param bool $persist Czy zapamiętywać dane w sesji
|
||||
* @param array $hiddenFields Dodatkowe ukryte pola
|
||||
* @param array|null $languages Dostępne języki (dla sekcji językowych)
|
||||
* @param array|null $validationErrors Błędy walidacji
|
||||
*/
|
||||
public function __construct(
|
||||
string $formId,
|
||||
string $title,
|
||||
array $data = [],
|
||||
array $fields = [],
|
||||
array $tabs = [],
|
||||
array $actions = [],
|
||||
string $method = 'POST',
|
||||
string $action = '',
|
||||
?string $backUrl = null,
|
||||
bool $persist = true,
|
||||
array $hiddenFields = [],
|
||||
?array $languages = null,
|
||||
?array $validationErrors = null
|
||||
) {
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
364
autoload/admin/ViewModels/Forms/FormField.php
Normal file
364
autoload/admin/ViewModels/Forms/FormField.php
Normal file
@@ -0,0 +1,364 @@
|
||||
<?php
|
||||
namespace Admin\ViewModels\Forms;
|
||||
|
||||
/**
|
||||
* Definicja pojedynczego pola formularza
|
||||
*/
|
||||
class FormField
|
||||
{
|
||||
public string $name;
|
||||
public string $type;
|
||||
public string $label;
|
||||
public $value;
|
||||
public string $tabId;
|
||||
public bool $required;
|
||||
public array $attributes;
|
||||
public array $options;
|
||||
public ?string $helpText;
|
||||
public ?string $placeholder;
|
||||
public ?string $id;
|
||||
|
||||
// Specyficzne dla obrazów/plików
|
||||
public bool $useFilemanager;
|
||||
public ?string $filemanagerUrl;
|
||||
|
||||
// Specyficzne dla edytora
|
||||
public string $editorToolbar;
|
||||
public int $editorHeight;
|
||||
|
||||
// Specyficzne dla lang_section
|
||||
public ?array $langFields;
|
||||
public ?string $langSectionParentTab;
|
||||
public ?string $customHtml;
|
||||
|
||||
/**
|
||||
* @param string $name Nazwa pola (name)
|
||||
* @param string $type Typ pola (z FormFieldType)
|
||||
* @param string $label Etykieta pola
|
||||
* @param mixed $value Wartość domyślna
|
||||
* @param string $tabId Identyfikator zakładki
|
||||
* @param bool $required Czy pole wymagane
|
||||
* @param array $attributes Atrybuty HTML
|
||||
* @param array $options Opcje dla select
|
||||
* @param string|null $helpText Tekst pomocniczy
|
||||
* @param string|null $placeholder Placeholder
|
||||
* @param bool $useFilemanager Czy używać filemanagera
|
||||
* @param string|null $filemanagerUrl URL filemanagera
|
||||
* @param string $editorToolbar Konfiguracja toolbar CKEditor
|
||||
* @param int $editorHeight Wysokość edytora
|
||||
* @param array|null $langFields Pola w sekcji językowej
|
||||
* @param string|null $langSectionParentTab Zakładka nadrzędna dla sekcji językowej
|
||||
*/
|
||||
public function __construct(
|
||||
string $name,
|
||||
string $type = FormFieldType::TEXT,
|
||||
string $label = '',
|
||||
$value = null,
|
||||
string $tabId = 'default',
|
||||
bool $required = false,
|
||||
array $attributes = [],
|
||||
array $options = [],
|
||||
?string $helpText = null,
|
||||
?string $placeholder = null,
|
||||
bool $useFilemanager = false,
|
||||
?string $filemanagerUrl = null,
|
||||
string $editorToolbar = 'MyTool',
|
||||
int $editorHeight = 300,
|
||||
?array $langFields = null,
|
||||
?string $langSectionParentTab = null,
|
||||
?string $customHtml = null
|
||||
) {
|
||||
$this->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}";
|
||||
}
|
||||
}
|
||||
25
autoload/admin/ViewModels/Forms/FormFieldType.php
Normal file
25
autoload/admin/ViewModels/Forms/FormFieldType.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
namespace Admin\ViewModels\Forms;
|
||||
|
||||
/**
|
||||
* Dostępne typy pól formularza
|
||||
*/
|
||||
class FormFieldType
|
||||
{
|
||||
public const TEXT = 'text';
|
||||
public const NUMBER = 'number';
|
||||
public const EMAIL = 'email';
|
||||
public const PASSWORD = 'password';
|
||||
public const DATE = 'date';
|
||||
public const DATETIME = 'datetime';
|
||||
public const SWITCH = 'switch';
|
||||
public const SELECT = 'select';
|
||||
public const TEXTAREA = 'textarea';
|
||||
public const EDITOR = 'editor';
|
||||
public const IMAGE = 'image';
|
||||
public const FILE = 'file';
|
||||
public const HIDDEN = 'hidden';
|
||||
public const LANG_SECTION = 'lang_section';
|
||||
public const CUSTOM = 'custom';
|
||||
public const COLOR = 'color';
|
||||
}
|
||||
31
autoload/admin/ViewModels/Forms/FormTab.php
Normal file
31
autoload/admin/ViewModels/Forms/FormTab.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
namespace Admin\ViewModels\Forms;
|
||||
|
||||
/**
|
||||
* Definicja zakładki formularza
|
||||
*/
|
||||
class FormTab
|
||||
{
|
||||
public string $id;
|
||||
public string $label;
|
||||
public string $icon;
|
||||
public ?string $parentTabId;
|
||||
|
||||
/**
|
||||
* @param string $id Unikalny identyfikator zakładki
|
||||
* @param string $label Etykieta wyświetlana
|
||||
* @param string $icon Klasa FontAwesome (np. 'fa-wrench')
|
||||
* @param string|null $parentTabId Identyfikator zakładki nadrzędnej (dla zagnieżdżenia)
|
||||
*/
|
||||
public function __construct(
|
||||
string $id,
|
||||
string $label,
|
||||
string $icon = '',
|
||||
?string $parentTabId = null
|
||||
) {
|
||||
$this->id = $id;
|
||||
$this->label = $label;
|
||||
$this->icon = $icon;
|
||||
$this->parentTabId = $parentTabId;
|
||||
}
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
4
vendor/composer/autoload_psr4.php
vendored
4
vendor/composer/autoload_psr4.php
vendored
@@ -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'),
|
||||
);
|
||||
|
||||
2
vendor/composer/autoload_real.php
vendored
2
vendor/composer/autoload_real.php
vendored
@@ -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'));
|
||||
|
||||
29
vendor/composer/autoload_static.php
vendored
29
vendor/composer/autoload_static.php
vendored
@@ -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 (
|
||||
|
||||
25
vendor/composer/platform_check.php
vendored
Normal file
25
vendor/composer/platform_check.php
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
// platform_check.php @generated by Composer
|
||||
|
||||
$issues = array();
|
||||
|
||||
if (!(PHP_VERSION_ID >= 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)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user