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

12 KiB

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

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)

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

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

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

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

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?