# Admin Form Fields - Composition-basiertes Design ## Framework-Prinzip: No Inheritance! ❌ **NICHT**: Abstract Base Classes mit `extends` ✅ **STATTDESSEN**: Composition mit readonly Classes ## Design-Pattern: Composition über Vererbung ### Core Interface ```php interface FormField { public function render(FormBuilder $form): FormBuilder; public function getName(): string; public function getValue(): mixed; public function getLabel(): string; } ``` ### Shared Components (für Composition) ```php // Gemeinsame Funktionalität als Value Objects final readonly class FieldAttributes { public function __construct( public string $name, public string $id, public string $class = 'form-control', public bool $required = false, public ?string $placeholder = null, public array $additional = [] ) {} public function toArray(): array { $attrs = [ 'name' => $this->name, 'id' => $this->id, 'class' => $this->class, ...$this->additional ]; if ($this->required) { $attrs['required'] = 'required'; } if ($this->placeholder !== null) { $attrs['placeholder'] = $this->placeholder; } return $attrs; } } final readonly class FieldMetadata { public function __construct( public string $name, public string $label, public ?string $help = null ) {} } final readonly class FieldWrapper { public function wrap(string $content, FieldMetadata $metadata): string { $label = FormElement::create('label', ['for' => $metadata->name], $metadata->label); $html = FormElement::create('div', ['class' => 'form-group'], $label . $content); if ($metadata->help !== null) { $help = FormElement::create('small', ['class' => 'form-text text-muted'], $metadata->help); $html .= $help; } return $html; } } ``` ### Field Implementations (Keine Vererbung!) ```php // TextField - Composition statt Inheritance final readonly class TextField implements FormField { public function __construct( private FieldMetadata $metadata, private FieldAttributes $attributes, private FieldWrapper $wrapper, private mixed $value = null ) {} public static function create( string $name, string $label, mixed $value = null, bool $required = false, ?string $placeholder = null, ?string $help = null ): self { return new self( metadata: new FieldMetadata($name, $label, $help), attributes: new FieldAttributes( name: $name, id: $name, required: $required, placeholder: $placeholder ), wrapper: new FieldWrapper(), value: $value ); } public function render(FormBuilder $form): FormBuilder { $attrs = [...$this->attributes->toArray(), 'type' => 'text']; if ($this->value !== null) { $attrs['value'] = (string) $this->value; } $input = FormElement::create('input', $attrs); $wrapped = $this->wrapper->wrap($input, $this->metadata); return $form->addElement($wrapped); } public function getName(): string { return $this->metadata->name; } public function getValue(): mixed { return $this->value; } public function getLabel(): string { return $this->metadata->label; } } ``` ### SelectField mit Composition ```php final readonly class SelectField implements FormField { public function __construct( private FieldMetadata $metadata, private FieldAttributes $attributes, private FieldWrapper $wrapper, private FieldOptions $options, private mixed $value = null ) {} public static function create( string $name, string $label, array $options, mixed $value = null, bool $required = false, ?string $placeholder = null, ?string $help = null ): self { return new self( metadata: new FieldMetadata($name, $label, $help), attributes: new FieldAttributes( name: $name, id: $name, required: $required, placeholder: $placeholder ), wrapper: new FieldWrapper(), options: new FieldOptions($options, $placeholder), value: $value ); } public function render(FormBuilder $form): FormBuilder { $select = FormElement::create( 'select', $this->attributes->toArray(), $this->options->renderOptions($this->value) ); $wrapped = $this->wrapper->wrap($select, $this->metadata); return $form->addElement($wrapped); } public function getName(): string { return $this->metadata->name; } public function getValue(): mixed { return $this->value; } public function getLabel(): string { return $this->metadata->label; } } // Helper Value Object final readonly class FieldOptions { public function __construct( private array $options, private ?string $placeholderText = null ) {} public function renderOptions(mixed $selectedValue): string { $html = ''; if ($this->placeholderText !== null) { $html .= ''; } foreach ($this->options as $value => $label) { $selected = $value === $selectedValue ? ' selected' : ''; $html .= sprintf( '', htmlspecialchars((string) $value), $selected, htmlspecialchars((string) $label) ); } return $html; } } ``` ### DateTimeField mit spezifischen Komponenten ```php final readonly class DateTimeField implements FormField { public function __construct( private FieldMetadata $metadata, private FieldAttributes $attributes, private FieldWrapper $wrapper, private DateTimeFormatter $formatter, private mixed $value = null ) {} public static function create( string $name, string $label, mixed $value = null, bool $required = false, ?string $placeholder = null, ?string $help = null ): self { return new self( metadata: new FieldMetadata($name, $label, $help), attributes: new FieldAttributes( name: $name, id: $name, required: $required, placeholder: $placeholder ?? 'YYYY-MM-DD HH:MM' ), wrapper: new FieldWrapper(), formatter: new DateTimeFormatter(), value: $value ); } public function render(FormBuilder $form): FormBuilder { $attrs = [...$this->attributes->toArray(), 'type' => 'datetime-local']; if ($this->value !== null) { $attrs['value'] = $this->formatter->formatForInput($this->value); } $input = FormElement::create('input', $attrs); $wrapped = $this->wrapper->wrap($input, $this->metadata); return $form->addElement($wrapped); } public function getName(): string { return $this->metadata->name; } public function getValue(): mixed { return $this->value; } public function getLabel(): string { return $this->metadata->label; } } // Helper für DateTime-Formatierung final readonly class DateTimeFormatter { public function formatForInput(mixed $value): string { if ($value instanceof \DateTimeInterface) { return $value->format('Y-m-d\TH:i'); } if (is_string($value)) { return (new \DateTimeImmutable($value))->format('Y-m-d\TH:i'); } return ''; } } ``` ## Vorteile dieses Designs ### ✅ Framework-Konform - Keine Vererbung (`extends`) - Composition over Inheritance - Readonly Classes - Value Objects für Datenstrukturen ### ✅ Wiederverwendbare Komponenten - `FieldMetadata` - für alle Fields gleich - `FieldAttributes` - wiederverwendbar - `FieldWrapper` - zentrale Wrapper-Logik - `FieldOptions` - für Select/Radio/Checkbox ### ✅ Type-Safe & Testbar - Jede Komponente isoliert testbar - Klare Verantwortlichkeiten - Dependency Injection freundlich ### ✅ Flexibel & Erweiterbar - Neue Field-Types durch Composition - Custom Components durch Interface - Keine Vererbungshierarchien ## FormFieldFactory mit Composition ```php final readonly class FormFieldFactory { public function __construct( private FieldWrapper $wrapper ) {} public function createFromConfig(string $name, array $config, mixed $value = null): FormField { $type = $config['type']; $label = $config['label'] ?? ucfirst($name); $required = $config['required'] ?? false; $placeholder = $config['placeholder'] ?? null; $help = $config['help'] ?? null; return match ($type) { 'text' => TextField::create($name, $label, $value, $required, $placeholder, $help), 'email' => EmailField::create($name, $label, $value, $required, $placeholder, $help), 'password' => PasswordField::create($name, $label, $required, $placeholder, $help), 'textarea' => TextareaField::create($name, $label, $value, $required, $placeholder, $help), 'select' => SelectField::create( $name, $label, $config['options'] ?? [], $value, $required, $placeholder, $help ), 'checkbox' => CheckboxField::create($name, $label, (bool) $value, $help), 'date' => DateField::create($name, $label, $value, $required, $placeholder, $help), 'datetime' => DateTimeField::create($name, $label, $value, $required, $placeholder, $help), 'number' => NumberField::create( $name, $label, $value, $required, $placeholder, $help, $config['min'] ?? null, $config['max'] ?? null, $config['step'] ?? null ), 'url' => UrlField::create($name, $label, $value, $required, $placeholder, $help), 'file' => FileField::create($name, $label, $required, $help), 'hidden' => HiddenField::create($name, $value), default => throw new \InvalidArgumentException("Unknown field type: {$type}"), }; } } ``` ## Struktur ``` src/Framework/Admin/FormFields/ ├── FormField.php # Interface ├── ValueObjects/ │ ├── FieldMetadata.php # Name, Label, Help │ ├── FieldAttributes.php # HTML Attributes │ ├── FieldOptions.php # Select Options │ └── DateTimeFormatter.php # DateTime Formatting ├── Components/ │ └── FieldWrapper.php # Wrapper Logic └── Fields/ ├── TextField.php ├── TextareaField.php ├── EmailField.php ├── PasswordField.php ├── SelectField.php ├── CheckboxField.php ├── DateField.php ├── DateTimeField.php ├── NumberField.php ├── UrlField.php ├── FileField.php └── HiddenField.php ``` ## Nächste Schritte Soll ich: 1. Die Basis-Komponenten (FieldMetadata, FieldAttributes, FieldWrapper) erstellen? 2. Dann die wichtigsten Field-Klassen implementieren? 3. FormFieldFactory anpassen?