- 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
441 lines
12 KiB
Markdown
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?
|