Files
michaelschiemer/docs/ADMIN-FORM-FIELDS-USAGE.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 Usage Guide

Dokumentation für das neue composition-basierte Admin Form Field System.

Übersicht

Das Admin Form Field System wurde vollständig refactored zu einem composition-basierten Design ohne Vererbung. Alle Fields implementieren das FormField Interface und nutzen Value Objects für gemeinsame Funktionalität.

Architektur

FormField (Interface)
├── FieldMetadata (Value Object) - name, label, help
├── FieldAttributes (Value Object) - HTML attributes
├── FieldWrapper (Component) - wrapping logic
└── Field Implementations (ohne extends!)
    ├── TextField
    ├── EmailField
    ├── NumberField
    ├── TextareaField
    ├── SelectField
    ├── DateTimeField
    ├── CheckboxField
    └── HiddenField

Verfügbare Field Types

TextField

Einfaches Text Input Field.

use App\Framework\Admin\FormFields\Fields\TextField;

$field = TextField::create(
    name: 'username',
    label: 'Username',
    value: 'john_doe',
    required: true,
    placeholder: 'Enter your username',
    help: 'Choose a unique username'
);

$form = $field->render($formBuilder);

EmailField

Email Input mit Browser-Validierung.

use App\Framework\Admin\FormFields\Fields\EmailField;

$field = EmailField::create(
    name: 'email',
    label: 'Email Address',
    value: 'user@example.com',
    required: true,
    placeholder: 'you@example.com',
    help: 'We will never share your email'
);

$form = $field->render($formBuilder);

NumberField

Number Input mit min/max/step Constraints.

use App\Framework\Admin\FormFields\Fields\NumberField;

$field = NumberField::create(
    name: 'age',
    label: 'Age',
    value: 25,
    required: true,
    min: 18,
    max: 120,
    step: 1,
    help: 'Must be 18 or older'
);

$form = $field->render($formBuilder);

TextareaField

Multi-line Text Input.

use App\Framework\Admin\FormFields\Fields\TextareaField;

$field = TextareaField::create(
    name: 'description',
    label: 'Description',
    value: 'Long text content...',
    required: false,
    placeholder: 'Enter description',
    help: 'Provide a detailed description',
    rows: 10
);

$form = $field->render($formBuilder);

SelectField

Dropdown Select mit Options.

use App\Framework\Admin\FormFields\Fields\SelectField;

$field = SelectField::create(
    name: 'status',
    label: 'Status',
    options: [
        'draft' => 'Draft',
        'published' => 'Published',
        'archived' => 'Archived'
    ],
    value: 'draft',
    required: true,
    help: 'Select campaign status',
    placeholder: 'Choose status...'
);

$form = $field->render($formBuilder);

DateTimeField

DateTime Input mit verschiedenen Typen.

use App\Framework\Admin\FormFields\Fields\DateTimeField;

// DateTime Local
$field = DateTimeField::create(
    name: 'release_date',
    label: 'Release Date',
    value: new \DateTime('2024-06-15 14:30:00'),
    required: true,
    help: 'When to release the track',
    type: 'datetime-local'
);

// Date Only
$field = DateTimeField::create(
    name: 'birth_date',
    label: 'Birth Date',
    value: '1990-05-20',
    type: 'date'
);

// Time Only
$field = DateTimeField::create(
    name: 'appointment_time',
    label: 'Time',
    value: '14:30',
    type: 'time'
);

$form = $field->render($formBuilder);

CheckboxField

Checkbox mit custom checked value.

use App\Framework\Admin\FormFields\Fields\CheckboxField;

$field = CheckboxField::create(
    name: 'accept_terms',
    label: 'I accept the terms and conditions',
    value: true,
    required: true,
    help: 'You must accept to continue',
    checkedValue: '1'
);

$form = $field->render($formBuilder);

HiddenField

Hidden Input (kein Label/Wrapper).

use App\Framework\Admin\FormFields\Fields\HiddenField;

$field = HiddenField::create(
    name: 'campaign_id',
    value: 'abc123'
);

$form = $field->render($formBuilder);

FormFieldFactory Usage

Die FormFieldFactory erstellt Fields aus Array-Konfiguration:

use App\Framework\Admin\FormFields\FormFieldFactory;

$factory = new FormFieldFactory();

// Single field from config
$field = $factory->createFromConfig([
    'type' => 'text',
    'name' => 'artist_name',
    'label' => 'Artist Name',
    'value' => 'John Doe',
    'required' => true,
    'placeholder' => 'Enter artist name',
    'help' => 'The name of the performing artist'
]);

// Multiple fields from config array
$fields = $factory->createMultiple([
    [
        'type' => 'text',
        'name' => 'track_title',
        'label' => 'Track Title',
        'required' => true
    ],
    [
        'type' => 'select',
        'name' => 'genre',
        'label' => 'Genre',
        'options' => [
            'pop' => 'Pop',
            'rock' => 'Rock',
            'jazz' => 'Jazz'
        ],
        'required' => true
    ],
    [
        'type' => 'datetime',
        'name' => 'release_date',
        'label' => 'Release Date',
        'datetime_type' => 'datetime-local',
        'required' => true
    ]
]);

// Render all fields to form
foreach ($fields as $field) {
    $formBuilder = $field->render($formBuilder);
}

AdminFormFactory Integration

Die AdminFormFactory nutzt automatisch die FormFieldFactory:

use App\Framework\Admin\Factories\AdminFormFactory;
use App\Framework\Admin\ValueObjects\AdminFormConfig;
use App\Framework\Http\Method;

$formFactory = $container->get(AdminFormFactory::class);

$formConfig = new AdminFormConfig(
    resource: 'campaigns',
    action: '/admin/campaigns/store',
    method: Method::POST,
    fields: [
        'track_title' => [
            'type' => 'text',
            'label' => 'Track Title',
            'required' => true,
            'placeholder' => 'Enter track title'
        ],
        'artist_name' => [
            'type' => 'text',
            'label' => 'Artist Name',
            'required' => true
        ],
        'release_date' => [
            'type' => 'datetime',
            'label' => 'Release Date',
            'datetime_type' => 'datetime-local',
            'required' => true,
            'help' => 'When to release the track'
        ],
        'genre' => [
            'type' => 'select',
            'label' => 'Genre',
            'options' => [
                'pop' => 'Pop',
                'rock' => 'Rock',
                'electronic' => 'Electronic',
                'jazz' => 'Jazz'
            ],
            'required' => true
        ],
        'description' => [
            'type' => 'textarea',
            'label' => 'Description',
            'rows' => 5,
            'placeholder' => 'Track description...'
        ],
        'is_featured' => [
            'type' => 'checkbox',
            'label' => 'Feature this track',
            'checked_value' => '1'
        ]
    ],
    data: [
        'track_title' => 'Summer Vibes',
        'artist_name' => 'DJ Cool',
        'is_featured' => true
    ]
);

$form = $formFactory->create($formConfig);

Framework Compliance

Das neue System folgt allen Framework-Prinzipien:

No Inheritance

  • Alle Fields sind final readonly classes
  • Keine extends oder abstract classes
  • Pure composition über Value Objects und Components

Immutability

  • Alle classes readonly
  • Value Objects für gemeinsame Daten
  • withAdditional() für Attribute-Erweiterung

Explicit Dependencies

  • Constructor Injection für alle Dependencies
  • Keine globalen States oder Service Locators
  • Klare Dependency Chain

Type Safety

  • Strikte Typisierung überall
  • Value Objects statt Arrays
  • Interface-basiertes Design

Extension Pattern

Neue Field Types hinzufügen:

namespace App\Framework\Admin\FormFields\Fields;

use App\Framework\Admin\FormFields\FormField;
use App\Framework\Admin\FormFields\ValueObjects\FieldAttributes;
use App\Framework\Admin\FormFields\ValueObjects\FieldMetadata;
use App\Framework\Admin\FormFields\Components\FieldWrapper;
use App\Framework\View\FormBuilder;
use App\Framework\View\ValueObjects\FormElement;

final readonly class ColorPickerField 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 $help = null
    ): self {
        return new self(
            metadata: new FieldMetadata($name, $label, $help),
            attributes: new FieldAttributes(
                name: $name,
                id: $name,
                required: $required
            ),
            wrapper: new FieldWrapper(),
            value: $value
        );
    }

    public function render(FormBuilder $form): FormBuilder
    {
        $attrs = $this->attributes->withAdditional(['type' => 'color']);
        $attrArray = $attrs->toArray();

        if ($this->value !== null) {
            $attrArray['value'] = (string) $this->value;
        }

        $input = FormElement::create('input', $attrArray);
        $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;
    }
}

Dann in FormFieldFactory registrieren:

// In createFromConfig() method
return match ($type) {
    // ... existing types
    'color' => ColorPickerField::create(
        name: $name,
        label: $config['label'] ?? ucfirst($name),
        value: $config['value'] ?? null,
        required: $config['required'] ?? false,
        help: $config['help'] ?? null
    ),
    // ...
};

Best Practices

  1. Verwende Factory Methods: Nutze ::create() statt direkter Konstruktor-Aufrufe
  2. Type-spezifische Validierung: Jeder Field Type validiert seine Inputs
  3. Wiederverwendbare Komponenten: FieldWrapper für konsistentes HTML
  4. Value Objects: FieldMetadata, FieldAttributes, FieldOptions für gemeinsame Daten
  5. Interface-basiert: Alle Fields implementieren FormField Interface
  6. Composition: Komponiere Funktionalität statt Vererbung zu nutzen

Migration von alter zu neuer API

Alt (direkter FormBuilder):

$form->addTextInput('username', $value, 'Username');
$form->addEmailInput('email', $value, 'Email');

Neu (composition-basierte Fields):

$usernameField = TextField::create('username', 'Username', value: $value);
$emailField = EmailField::create('email', 'Email', value: $value);

$form = $usernameField->render($form);
$form = $emailField->render($form);

Oder via Factory:

$fields = $fieldFactory->createMultiple([
    ['type' => 'text', 'name' => 'username', 'label' => 'Username', 'value' => $value],
    ['type' => 'email', 'name' => 'email', 'label' => 'Email', 'value' => $value]
]);

foreach ($fields as $field) {
    $form = $field->render($form);
}

Testing

describe('TextField', function () {
    it('renders text input with value', function () {
        $field = TextField::create(
            name: 'username',
            label: 'Username',
            value: 'john_doe',
            required: true
        );

        $formBuilder = FormBuilder::create('/test', 'POST', $this->formIdGenerator);
        $result = $field->render($formBuilder);

        $html = $result->build();

        expect($html)->toContain('type="text"');
        expect($html)->toContain('name="username"');
        expect($html)->toContain('value="john_doe"');
        expect($html)->toContain('required="required"');
    });
});