Files
michaelschiemer/docs/livecomponents/livecomponent-formbuilder-usage.md
Michael Schiemer 36ef2a1e2c
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
fix: Gitea Traefik routing and connection pool optimization
- 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
2025-11-09 14:46:15 +01:00

13 KiB

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)

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

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

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

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

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

FormFieldDefinition::text('company_name', 'Firma', required: true)
    ->showWhen(FieldCondition::equals('account_type', 'business'))

Wiederverwendbare Validators

final readonly class EmailValidator implements StepValidator
{
    public function validate(array $formData): array
    {
        // Wiederverwendbare Validation-Logic
    }
}

Testbar

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

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

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

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)

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

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