Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
459 lines
13 KiB
Markdown
459 lines
13 KiB
Markdown
# LiveComponent FormBuilder Integration
|
|
|
|
Elegante Integration zwischen LiveComponent System und bestehendem FormBuilder.
|
|
|
|
## Architektur
|
|
|
|
```
|
|
┌─────────────────────────┐
|
|
│ MultiStepFormDefinition │ ← Value Object mit Steps
|
|
└────────────┬────────────┘
|
|
│
|
|
├─► FormStepDefinition ← Step-Info + Fields
|
|
│
|
|
└─► FormFieldDefinition ← Field-Config (text, email, etc.)
|
|
│
|
|
├─► FieldType (enum)
|
|
├─► FieldCondition (conditional rendering)
|
|
└─► StepValidator (validation logic)
|
|
|
|
┌─────────────────────────┐
|
|
│ MultiStepFormComponent │ ← Generic LiveComponent
|
|
└────────────┬────────────┘
|
|
│
|
|
└─► LiveFormBuilder ← Erweitert bestehenden FormBuilder
|
|
│
|
|
└─► FormBuilder (bestehend, wiederverwendet!)
|
|
```
|
|
|
|
## Verwendungsbeispiel
|
|
|
|
### 1. Form Definition erstellen (Deklarativ, Type-Safe)
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\FormBuilder\MultiStepFormDefinition;
|
|
use App\Framework\LiveComponents\FormBuilder\FormStepDefinition;
|
|
use App\Framework\LiveComponents\FormBuilder\FormFieldDefinition;
|
|
use App\Framework\LiveComponents\FormBuilder\FieldCondition;
|
|
|
|
// Deklarative Form-Definition - kein Code-Duplikat!
|
|
$userRegistrationForm = new MultiStepFormDefinition(
|
|
steps: [
|
|
// Step 1: Personal Information
|
|
new FormStepDefinition(
|
|
title: 'Persönliche Informationen',
|
|
description: 'Bitte geben Sie Ihre persönlichen Daten ein',
|
|
fields: [
|
|
FormFieldDefinition::text(
|
|
name: 'first_name',
|
|
label: 'Vorname',
|
|
required: true
|
|
),
|
|
FormFieldDefinition::text(
|
|
name: 'last_name',
|
|
label: 'Nachname',
|
|
required: true
|
|
),
|
|
FormFieldDefinition::email(
|
|
name: 'email',
|
|
label: 'E-Mail Adresse',
|
|
required: true
|
|
)
|
|
],
|
|
validator: new PersonalInfoValidator()
|
|
),
|
|
|
|
// Step 2: Account Type
|
|
new FormStepDefinition(
|
|
title: 'Konto-Typ',
|
|
description: 'Wählen Sie Ihren Konto-Typ',
|
|
fields: [
|
|
FormFieldDefinition::radio(
|
|
name: 'account_type',
|
|
label: 'Account Type',
|
|
options: [
|
|
'personal' => 'Privatkonto',
|
|
'business' => 'Geschäftskonto'
|
|
],
|
|
required: true
|
|
),
|
|
// Conditional Field - nur bei Business
|
|
FormFieldDefinition::text(
|
|
name: 'company_name',
|
|
label: 'Firmenname',
|
|
required: true
|
|
)->showWhen(
|
|
FieldCondition::equals('account_type', 'business')
|
|
),
|
|
FormFieldDefinition::text(
|
|
name: 'vat_number',
|
|
label: 'USt-IdNr.',
|
|
placeholder: 'DE123456789'
|
|
)->showWhen(
|
|
FieldCondition::equals('account_type', 'business')
|
|
)
|
|
],
|
|
validator: new AccountTypeValidator()
|
|
),
|
|
|
|
// Step 3: Preferences
|
|
new FormStepDefinition(
|
|
title: 'Präferenzen',
|
|
description: 'Passen Sie Ihre Einstellungen an',
|
|
fields: [
|
|
FormFieldDefinition::checkbox(
|
|
name: 'newsletter',
|
|
label: 'Newsletter abonnieren'
|
|
),
|
|
FormFieldDefinition::select(
|
|
name: 'language',
|
|
label: 'Bevorzugte Sprache',
|
|
options: [
|
|
'en' => 'English',
|
|
'de' => 'Deutsch',
|
|
'fr' => 'Français'
|
|
],
|
|
defaultValue: 'de'
|
|
)
|
|
]
|
|
)
|
|
],
|
|
submitHandler: new UserRegistrationSubmitHandler()
|
|
);
|
|
```
|
|
|
|
### 2. Validator implementieren
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\FormBuilder\StepValidator;
|
|
|
|
final readonly class PersonalInfoValidator implements StepValidator
|
|
{
|
|
public function validate(array $formData): array
|
|
{
|
|
$errors = [];
|
|
|
|
if (empty($formData['first_name'] ?? '')) {
|
|
$errors['first_name'] = 'Vorname ist erforderlich';
|
|
}
|
|
|
|
if (empty($formData['last_name'] ?? '')) {
|
|
$errors['last_name'] = 'Nachname ist erforderlich';
|
|
}
|
|
|
|
if (empty($formData['email'] ?? '')) {
|
|
$errors['email'] = 'E-Mail ist erforderlich';
|
|
} elseif (!filter_var($formData['email'], FILTER_VALIDATE_EMAIL)) {
|
|
$errors['email'] = 'Ungültige E-Mail Adresse';
|
|
}
|
|
|
|
return $errors;
|
|
}
|
|
}
|
|
|
|
final readonly class AccountTypeValidator implements StepValidator
|
|
{
|
|
public function validate(array $formData): array
|
|
{
|
|
$errors = [];
|
|
|
|
if (empty($formData['account_type'] ?? '')) {
|
|
$errors['account_type'] = 'Bitte wählen Sie einen Konto-Typ';
|
|
}
|
|
|
|
// Conditional validation für Business
|
|
if (($formData['account_type'] ?? '') === 'business') {
|
|
if (empty($formData['company_name'] ?? '')) {
|
|
$errors['company_name'] = 'Firmenname ist erforderlich';
|
|
}
|
|
}
|
|
|
|
return $errors;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Submit Handler implementieren
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\FormBuilder\FormSubmitHandler;
|
|
use App\Framework\LiveComponents\FormBuilder\SubmitResult;
|
|
|
|
final readonly class UserRegistrationSubmitHandler implements FormSubmitHandler
|
|
{
|
|
public function __construct(
|
|
private UserService $userService
|
|
) {}
|
|
|
|
public function handle(array $formData): SubmitResult
|
|
{
|
|
try {
|
|
$user = $this->userService->registerUser(
|
|
firstName: $formData['first_name'],
|
|
lastName: $formData['last_name'],
|
|
email: $formData['email'],
|
|
accountType: $formData['account_type'],
|
|
companyName: $formData['company_name'] ?? null,
|
|
newsletter: ($formData['newsletter'] ?? 'no') === 'yes',
|
|
language: $formData['language'] ?? 'de'
|
|
);
|
|
|
|
return SubmitResult::success(
|
|
message: 'Registrierung erfolgreich!',
|
|
redirectUrl: '/dashboard',
|
|
data: ['user_id' => $user->id]
|
|
);
|
|
} catch (\Exception $e) {
|
|
return SubmitResult::failure(
|
|
message: 'Registrierung fehlgeschlagen: ' . $e->getMessage()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4. Controller Setup
|
|
|
|
```php
|
|
use App\Framework\Http\Attributes\Route;
|
|
use App\Framework\Http\Method;
|
|
use App\Framework\Http\ViewResult;
|
|
use App\Framework\LiveComponents\FormBuilder\MultiStepFormComponent;
|
|
use App\Framework\LiveComponents\ValueObjects\ComponentId;
|
|
|
|
final readonly class UserRegistrationController
|
|
{
|
|
#[Route('/register', method: Method::GET)]
|
|
public function showRegistrationForm(): ViewResult
|
|
{
|
|
// Form Definition (könnte auch aus Container kommen)
|
|
$formDefinition = $this->createUserRegistrationForm();
|
|
|
|
// Component erstellen
|
|
$component = new MultiStepFormComponent(
|
|
id: ComponentId::generate('user-registration'),
|
|
formDefinition: $formDefinition
|
|
);
|
|
|
|
return new ViewResult(
|
|
template: 'pages/register',
|
|
data: [
|
|
'registration_form' => $component
|
|
]
|
|
);
|
|
}
|
|
|
|
private function createUserRegistrationForm(): MultiStepFormDefinition
|
|
{
|
|
return new MultiStepFormDefinition(
|
|
steps: [
|
|
// ... (wie oben)
|
|
],
|
|
submitHandler: new UserRegistrationSubmitHandler($this->userService)
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 5. Template Usage
|
|
|
|
```html
|
|
<!-- pages/register.view.php -->
|
|
<div class="registration-page">
|
|
<h1>Benutzerregistrierung</h1>
|
|
|
|
<!-- LiveComponent einbinden -->
|
|
<livecomponent name="multi-step-form" data="registration_form" />
|
|
</div>
|
|
```
|
|
|
|
## Vorteile dieser Lösung
|
|
|
|
### ✅ Kein Code-Duplikat
|
|
- Nutzt bestehenden `FormBuilder` aus View-Modul
|
|
- `LiveFormBuilder` erweitert nur mit LiveComponent-Features
|
|
- Keine doppelte Field-Rendering-Logik
|
|
|
|
### ✅ Type-Safe & Framework-Compliant
|
|
- Alle Value Objects: `FormFieldDefinition`, `FormStepDefinition`, `MultiStepFormDefinition`
|
|
- Readonly Classes überall
|
|
- Enums für `FieldType`
|
|
|
|
### ✅ Deklarativ statt Imperativ
|
|
- Form-Definition rein deklarativ (kein Code für Rendering)
|
|
- Klare Trennung: Definition vs. Rendering vs. Validation vs. Submission
|
|
|
|
### ✅ Conditional Fields eingebaut
|
|
```php
|
|
FormFieldDefinition::text('company_name', 'Firma', required: true)
|
|
->showWhen(FieldCondition::equals('account_type', 'business'))
|
|
```
|
|
|
|
### ✅ Wiederverwendbare Validators
|
|
```php
|
|
final readonly class EmailValidator implements StepValidator
|
|
{
|
|
public function validate(array $formData): array
|
|
{
|
|
// Wiederverwendbare Validation-Logic
|
|
}
|
|
}
|
|
```
|
|
|
|
### ✅ Testbar
|
|
```php
|
|
// Unit Test für Validator
|
|
it('validates email format', function () {
|
|
$validator = new PersonalInfoValidator();
|
|
|
|
$errors = $validator->validate(['email' => 'invalid']);
|
|
|
|
expect($errors)->toHaveKey('email');
|
|
});
|
|
|
|
// Integration Test für Component
|
|
it('moves to next step after validation', function () {
|
|
$component = new MultiStepFormComponent(
|
|
id: ComponentId::generate('test'),
|
|
formDefinition: $this->testFormDef
|
|
);
|
|
|
|
$result = $component->nextStep([
|
|
'first_name' => 'John',
|
|
'last_name' => 'Doe',
|
|
'email' => 'john@example.com'
|
|
]);
|
|
|
|
expect($result->get('current_step'))->toBe(2);
|
|
});
|
|
```
|
|
|
|
## Erweiterungsmöglichkeiten
|
|
|
|
### Custom Field Types
|
|
|
|
```php
|
|
// Neuen FieldType hinzufügen
|
|
enum FieldType: string
|
|
{
|
|
case TEXT = 'text';
|
|
case EMAIL = 'email';
|
|
// ... existing types
|
|
case DATE = 'date';
|
|
case PHONE = 'phone';
|
|
case CURRENCY = 'currency';
|
|
}
|
|
|
|
// LiveFormBuilder erweitern
|
|
final readonly class LiveFormBuilder
|
|
{
|
|
public function addLiveDateInput(
|
|
string $name,
|
|
string $label,
|
|
?string $value = null
|
|
): self {
|
|
// Implementation
|
|
}
|
|
}
|
|
```
|
|
|
|
### Multi-Field Conditions
|
|
|
|
```php
|
|
final readonly class AndCondition implements FieldConditionContract
|
|
{
|
|
public function __construct(
|
|
private FieldCondition $left,
|
|
private FieldCondition $right
|
|
) {}
|
|
|
|
public function matches(array $formData): bool
|
|
{
|
|
return $this->left->matches($formData)
|
|
&& $this->right->matches($formData);
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
FormFieldDefinition::text('special_field', 'Special')
|
|
->showWhen(
|
|
new AndCondition(
|
|
FieldCondition::equals('account_type', 'business'),
|
|
FieldCondition::equals('country', 'DE')
|
|
)
|
|
);
|
|
```
|
|
|
|
### Custom Renderers
|
|
|
|
```php
|
|
interface FieldRenderer
|
|
{
|
|
public function render(FormFieldDefinition $field, mixed $value): string;
|
|
}
|
|
|
|
final readonly class CustomTextRenderer implements FieldRenderer
|
|
{
|
|
public function render(FormFieldDefinition $field, mixed $value): string
|
|
{
|
|
// Custom rendering logic
|
|
}
|
|
}
|
|
```
|
|
|
|
## Vergleich: Vorher vs. Nachher
|
|
|
|
### ❌ Vorher (Code-Duplikat)
|
|
|
|
```php
|
|
// Hardcoded Form in Component
|
|
public function getRenderData(): ComponentRenderData
|
|
{
|
|
return new ComponentRenderData(
|
|
templatePath: 'livecomponent-dynamic-form',
|
|
data: [
|
|
'form_first_name' => $formData['first_name'] ?? '',
|
|
'form_last_name' => $formData['last_name'] ?? '',
|
|
// ... 50 weitere Zeilen hardcoded mappings
|
|
]
|
|
);
|
|
}
|
|
```
|
|
|
|
### ✅ Nachher (Wiederverwendbar)
|
|
|
|
```php
|
|
// Deklarative Definition
|
|
$formDef = new MultiStepFormDefinition(
|
|
steps: [
|
|
new FormStepDefinition(
|
|
title: 'Personal Info',
|
|
fields: [
|
|
FormFieldDefinition::text('first_name', 'First Name'),
|
|
FormFieldDefinition::text('last_name', 'Last Name')
|
|
]
|
|
)
|
|
]
|
|
);
|
|
|
|
// Generic Component - keine Anpassungen nötig!
|
|
$component = new MultiStepFormComponent(
|
|
id: ComponentId::generate('my-form'),
|
|
formDefinition: $formDef
|
|
);
|
|
```
|
|
|
|
## Zusammenfassung
|
|
|
|
Diese Integration:
|
|
- ✅ Nutzt bestehenden `FormBuilder` (keine Code-Duplizierung)
|
|
- ✅ Erweitert ihn minimal für LiveComponent-Features
|
|
- ✅ Vollständig type-safe mit Value Objects
|
|
- ✅ Framework-compliant (readonly, final, composition)
|
|
- ✅ Deklarative Form-Definitionen
|
|
- ✅ Conditional Fields eingebaut
|
|
- ✅ Wiederverwendbare Validators
|
|
- ✅ Generische Component (kein Custom-Code pro Form)
|
|
- ✅ Einfach testbar
|
|
- ✅ Leicht erweiterbar
|