Files
michaelschiemer/docs/ADMIN-FORM-FIELDS-DESIGN.md
Michael Schiemer 5050c7d73a docs: consolidate documentation into organized structure
- Move 12 markdown files from root to docs/ subdirectories
- Organize documentation by category:
  • docs/troubleshooting/ (1 file)  - Technical troubleshooting guides
  • docs/deployment/      (4 files) - Deployment and security documentation
  • docs/guides/          (3 files) - Feature-specific guides
  • docs/planning/        (4 files) - Planning and improvement proposals

Root directory cleanup:
- Reduced from 16 to 4 markdown files in root
- Only essential project files remain:
  • CLAUDE.md (AI instructions)
  • README.md (Main project readme)
  • CLEANUP_PLAN.md (Current cleanup plan)
  • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements)

This improves:
 Documentation discoverability
 Logical organization by purpose
 Clean root directory
 Better maintainability
2025-10-05 11:05:04 +02:00

441 lines
12 KiB
Markdown

# 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 .= '<option value="">' . htmlspecialchars($this->placeholderText) . '</option>';
}
foreach ($this->options as $value => $label) {
$selected = $value === $selectedValue ? ' selected' : '';
$html .= sprintf(
'<option value="%s"%s>%s</option>',
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?