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
This commit is contained in:
2025-10-05 11:05:04 +02:00
parent 887847dde6
commit 5050c7d73a
36686 changed files with 196456 additions and 12398919 deletions

View File

@@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Components;
use App\Framework\View\Attributes\ComponentName;
use App\Framework\View\ValueObjects\HtmlAttributes;
use App\Framework\View\ValueObjects\HtmlElement;
use App\Framework\View\ValueObjects\HtmlTag;
use App\Framework\View\ValueObjects\StandardHtmlElement;
use App\Framework\View\ValueObjects\TagName;
#[ComponentName('alert')]
final readonly class Alert implements HtmlElement
{
public HtmlTag $tag;
public HtmlAttributes $attributes;
public string $content;
public function __construct(
public string $message,
public string $variant = 'info',
public ?string $title = null,
public bool $dismissible = false,
public ?string $icon = null,
public HtmlAttributes $additionalAttributes = new HtmlAttributes()
) {
$this->tag = new HtmlTag(TagName::DIV);
$classes = ['alert', "alert--{$this->variant}"];
if ($this->dismissible) {
$classes[] = 'alert--dismissible';
}
$this->attributes = HtmlAttributes::empty()
->withClass(implode(' ', $classes))
->with('role', 'alert');
foreach ($this->additionalAttributes->attributes as $name => $value) {
$this->attributes = $this->attributes->with($name, $value);
}
$this->content = $this->buildContent();
}
public static function info(string $message, ?string $title = null): self
{
return new self(message: $message, variant: 'info', title: $title);
}
public static function success(string $message, ?string $title = null): self
{
return new self(message: $message, variant: 'success', title: $title);
}
public static function warning(string $message, ?string $title = null): self
{
return new self(message: $message, variant: 'warning', title: $title);
}
public static function danger(string $message, ?string $title = null): self
{
return new self(message: $message, variant: 'danger', title: $title);
}
public function withDismissible(bool $dismissible = true): self
{
return new self(
message: $this->message,
variant: $this->variant,
title: $this->title,
dismissible: $dismissible,
icon: $this->icon,
additionalAttributes: $this->additionalAttributes
);
}
public function withIcon(string $icon): self
{
return new self(
message: $this->message,
variant: $this->variant,
title: $this->title,
dismissible: $this->dismissible,
icon: $icon,
additionalAttributes: $this->additionalAttributes
);
}
private function buildContent(): string
{
$elements = [];
// Icon
if ($this->icon !== null) {
$elements[] = StandardHtmlElement::create(
TagName::SPAN,
HtmlAttributes::empty()->withClass('alert__icon'),
$this->icon
);
}
// Content wrapper
$contentElements = [];
// Title
if ($this->title !== null) {
$contentElements[] = StandardHtmlElement::create(
TagName::DIV,
HtmlAttributes::empty()->withClass('alert__title'),
$this->title
);
}
// Message
$contentElements[] = StandardHtmlElement::create(
TagName::DIV,
HtmlAttributes::empty()->withClass('alert__message'),
$this->message
);
$elements[] = StandardHtmlElement::create(
TagName::DIV,
HtmlAttributes::empty()->withClass('alert__content'),
implode('', array_map('strval', $contentElements))
);
// Dismiss button
if ($this->dismissible) {
$dismissButton = StandardHtmlElement::create(
TagName::BUTTON,
HtmlAttributes::empty()
->withType('button')
->withClass('alert__close')
->with('aria-label', 'Schließen'),
'×'
);
$elements[] = $dismissButton;
}
return implode('', array_map('strval', $elements));
}
public function __toString(): string
{
return (string) StandardHtmlElement::create(
$this->tag->name,
$this->attributes,
$this->content
);
}
}

View File

@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Components;
use App\Framework\View\Attributes\ComponentName;
use App\Framework\View\ValueObjects\HtmlAttributes;
use App\Framework\View\ValueObjects\HtmlElement;
use App\Framework\View\ValueObjects\HtmlTag;
use App\Framework\View\ValueObjects\StandardHtmlElement;
use App\Framework\View\ValueObjects\TagName;
#[ComponentName('badge')]
final readonly class Badge implements HtmlElement
{
public HtmlTag $tag;
public HtmlAttributes $attributes;
public string $content;
public function __construct(
public string $text,
public string $variant = 'default',
public string $size = 'md',
public bool $pill = false,
public HtmlAttributes $additionalAttributes = new HtmlAttributes()
) {
$this->tag = new HtmlTag(TagName::SPAN);
$this->content = $text;
$classes = ['badge', "badge--{$this->variant}", "badge--{$this->size}"];
if ($this->pill) {
$classes[] = 'badge--pill';
}
$this->attributes = HtmlAttributes::empty()->withClass(implode(' ', $classes));
foreach ($this->additionalAttributes->attributes as $name => $value) {
$this->attributes = $this->attributes->with($name, $value);
}
}
public static function create(string $text): self
{
return new self(text: $text);
}
public static function primary(string $text): self
{
return new self(text: $text, variant: 'primary');
}
public static function success(string $text): self
{
return new self(text: $text, variant: 'success');
}
public static function warning(string $text): self
{
return new self(text: $text, variant: 'warning');
}
public static function danger(string $text): self
{
return new self(text: $text, variant: 'danger');
}
public static function info(string $text): self
{
return new self(text: $text, variant: 'info');
}
public function asPill(): self
{
return new self(
text: $this->text,
variant: $this->variant,
size: $this->size,
pill: true,
additionalAttributes: $this->additionalAttributes
);
}
public function withSize(string $size): self
{
return new self(
text: $this->text,
variant: $this->variant,
size: $size,
pill: $this->pill,
additionalAttributes: $this->additionalAttributes
);
}
public function __toString(): string
{
return (string) StandardHtmlElement::create(
TagName::SPAN,
$this->attributes,
$this->content
);
}
}

View File

@@ -0,0 +1,187 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Components;
use App\Framework\View\Attributes\ComponentName;
use App\Framework\View\ValueObjects\HtmlAttributes;
use App\Framework\View\ValueObjects\HtmlElement;
use App\Framework\View\ValueObjects\HtmlTag;
use App\Framework\View\ValueObjects\StandardHtmlElement;
use App\Framework\View\ValueObjects\TagName;
#[ComponentName('button')]
final readonly class Button implements HtmlElement
{
public HtmlTag $tag;
public HtmlAttributes $attributes;
public string $content;
public function __construct(
public string $text,
public string $type = 'button',
public string $variant = 'primary',
public string $size = 'md',
public ?string $href = null,
public bool $disabled = false,
public bool $fullWidth = false,
public ?string $icon = null,
public HtmlAttributes $additionalAttributes = new HtmlAttributes()
) {
// Determine tag based on href
$this->tag = $this->href !== null
? new HtmlTag(TagName::A)
: new HtmlTag(TagName::BUTTON);
// Build classes
$classes = ['btn', "btn--{$this->variant}", "btn--{$this->size}"];
if ($this->fullWidth) {
$classes[] = 'btn--full-width';
}
// Build attributes
$attributes = HtmlAttributes::empty()
->withClass(implode(' ', $classes));
if ($this->href !== null) {
$attributes = $attributes->with('href', $this->href);
if ($this->disabled) {
$attributes = $attributes
->withClass('btn--disabled')
->with('aria-disabled', 'true');
}
} else {
$attributes = $attributes->withType($this->type);
if ($this->disabled) {
$attributes = $attributes->withDisabled();
}
}
// Merge additional attributes
foreach ($this->additionalAttributes->attributes as $name => $value) {
$attributes = $attributes->with($name, $value);
}
$this->attributes = $attributes;
// Build content
$this->content = $this->icon !== null
? "<span class=\"btn__icon\">{$this->icon}</span><span class=\"btn__text\">{$this->text}</span>"
: $this->text;
}
public static function primary(string $text): self
{
return new self(text: $text, variant: 'primary');
}
public static function secondary(string $text): self
{
return new self(text: $text, variant: 'secondary');
}
public static function danger(string $text): self
{
return new self(text: $text, variant: 'danger');
}
public static function success(string $text): self
{
return new self(text: $text, variant: 'success');
}
public static function ghost(string $text): self
{
return new self(text: $text, variant: 'ghost');
}
public static function link(string $text, string $href): self
{
return new self(text: $text, href: $href);
}
public function asSubmit(): self
{
return new self(
text: $this->text,
type: 'submit',
variant: $this->variant,
size: $this->size,
href: $this->href,
disabled: $this->disabled,
fullWidth: $this->fullWidth,
icon: $this->icon,
additionalAttributes: $this->additionalAttributes
);
}
public function asReset(): self
{
return new self(
text: $this->text,
type: 'reset',
variant: $this->variant,
size: $this->size,
href: $this->href,
disabled: $this->disabled,
fullWidth: $this->fullWidth,
icon: $this->icon,
additionalAttributes: $this->additionalAttributes
);
}
public function withSize(string $size): self
{
return new self(
text: $this->text,
type: $this->type,
variant: $this->variant,
size: $size,
href: $this->href,
disabled: $this->disabled,
fullWidth: $this->fullWidth,
icon: $this->icon,
additionalAttributes: $this->additionalAttributes
);
}
public function withIcon(string $icon): self
{
return new self(
text: $this->text,
type: $this->type,
variant: $this->variant,
size: $this->size,
href: $this->href,
disabled: $this->disabled,
fullWidth: $this->fullWidth,
icon: $icon,
additionalAttributes: $this->additionalAttributes
);
}
public function fullWidth(): self
{
return new self(
text: $this->text,
type: $this->type,
variant: $this->variant,
size: $this->size,
href: $this->href,
disabled: $this->disabled,
fullWidth: true,
icon: $this->icon,
additionalAttributes: $this->additionalAttributes
);
}
public function __toString(): string
{
return (string) StandardHtmlElement::create(
$this->tag->name,
$this->attributes,
$this->content
);
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Components;
use App\Framework\View\ValueObjects\HtmlAttributes;
use App\Framework\View\ValueObjects\HtmlElement;
use App\Framework\View\ValueObjects\HtmlTag;
use App\Framework\View\ValueObjects\StandardHtmlElement;
use App\Framework\View\ValueObjects\TagName;
final readonly class ButtonGroup implements HtmlElement
{
public HtmlTag $tag;
public HtmlAttributes $attributes;
public string $content;
/**
* @param array<Button> $buttons
*/
public function __construct(
public array $buttons,
public string $size = 'md',
public bool $vertical = false,
public HtmlAttributes $additionalAttributes = new HtmlAttributes()
) {
$this->tag = new HtmlTag(TagName::DIV);
$classes = ['btn-group', "btn-group--{$this->size}"];
if ($this->vertical) {
$classes[] = 'btn-group--vertical';
}
$this->attributes = HtmlAttributes::empty()
->withClass(implode(' ', $classes))
->with('role', 'group');
foreach ($this->additionalAttributes->attributes as $name => $value) {
$this->attributes = $this->attributes->with($name, $value);
}
$buttonsHtml = array_map(
fn(Button $button) => (string) $button,
$this->buttons
);
$this->content = implode('', $buttonsHtml);
}
public static function create(Button ...$buttons): self
{
return new self(buttons: $buttons);
}
public function withSize(string $size): self
{
return new self(
buttons: $this->buttons,
size: $size,
vertical: $this->vertical,
additionalAttributes: $this->additionalAttributes
);
}
public function asVertical(): self
{
return new self(
buttons: $this->buttons,
size: $this->size,
vertical: true,
additionalAttributes: $this->additionalAttributes
);
}
public function __toString(): string
{
return (string) StandardHtmlElement::create(
$this->tag->name,
$this->attributes,
$this->content
);
}
}

View File

@@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Components;
use App\Framework\View\Attributes\ComponentName;
use App\Framework\View\ValueObjects\HtmlAttributes;
use App\Framework\View\ValueObjects\HtmlElement;
use App\Framework\View\ValueObjects\HtmlTag;
use App\Framework\View\ValueObjects\StandardHtmlElement;
use App\Framework\View\ValueObjects\TagName;
#[ComponentName('card')]
final readonly class Card implements HtmlElement
{
public HtmlTag $tag;
public HtmlAttributes $attributes;
public string $content;
public function __construct(
string $bodyContent,
public ?string $title = null,
public ?string $subtitle = null,
public ?string $footer = null,
public ?string $imageSrc = null,
public ?string $imageAlt = null,
public string $variant = 'default',
public HtmlAttributes $additionalAttributes = new HtmlAttributes()
) {
$this->tag = new HtmlTag(TagName::DIV);
$classes = ['card', "card--{$this->variant}"];
$this->attributes = HtmlAttributes::empty()
->withClass(implode(' ', $classes));
foreach ($this->additionalAttributes->attributes as $name => $value) {
$this->attributes = $this->attributes->with($name, $value);
}
$this->content = $this->buildContent($bodyContent);
}
public static function create(string $bodyContent): self
{
return new self(bodyContent: $bodyContent);
}
public static function withTitle(string $title, string $bodyContent): self
{
return new self(bodyContent: $bodyContent, title: $title);
}
public static function withImage(string $imageSrc, string $bodyContent, ?string $imageAlt = null): self
{
return new self(bodyContent: $bodyContent, imageSrc: $imageSrc, imageAlt: $imageAlt);
}
public function withSubtitle(string $subtitle): self
{
// Note: Can't access bodyContent from constructor, need to rebuild from content
return new self(
bodyContent: '', // Will be rebuilt in buildContent
title: $this->title,
subtitle: $subtitle,
footer: $this->footer,
imageSrc: $this->imageSrc,
imageAlt: $this->imageAlt,
variant: $this->variant,
additionalAttributes: $this->additionalAttributes
);
}
public function withFooter(string|HtmlElement $footer): self
{
return new self(
bodyContent: '', // Will be rebuilt in buildContent
title: $this->title,
subtitle: $this->subtitle,
footer: (string) $footer,
imageSrc: $this->imageSrc,
imageAlt: $this->imageAlt,
variant: $this->variant,
additionalAttributes: $this->additionalAttributes
);
}
public function withVariant(string $variant): self
{
return new self(
bodyContent: '', // Will be rebuilt in buildContent
title: $this->title,
subtitle: $this->subtitle,
footer: $this->footer,
imageSrc: $this->imageSrc,
imageAlt: $this->imageAlt,
variant: $variant,
additionalAttributes: $this->additionalAttributes
);
}
private function buildContent(string $bodyContent): string
{
$elements = [];
// Image
if ($this->imageSrc !== null) {
$imgAttrs = HtmlAttributes::empty()
->with('src', $this->imageSrc)
->withClass('card__image');
if ($this->imageAlt !== null) {
$imgAttrs = $imgAttrs->with('alt', $this->imageAlt);
}
$elements[] = StandardHtmlElement::create(TagName::IMG, $imgAttrs);
}
// Header (title + subtitle)
if ($this->title !== null || $this->subtitle !== null) {
$headerElements = [];
if ($this->title !== null) {
$headerElements[] = StandardHtmlElement::create(
TagName::H3,
HtmlAttributes::empty()->withClass('card__title'),
$this->title
);
}
if ($this->subtitle !== null) {
$headerElements[] = StandardHtmlElement::create(
TagName::P,
HtmlAttributes::empty()->withClass('card__subtitle'),
$this->subtitle
);
}
$elements[] = StandardHtmlElement::create(
TagName::DIV,
HtmlAttributes::empty()->withClass('card__header'),
implode('', array_map('strval', $headerElements))
);
}
// Body
$elements[] = StandardHtmlElement::create(
TagName::DIV,
HtmlAttributes::empty()->withClass('card__body'),
$bodyContent
);
// Footer
if ($this->footer !== null) {
$elements[] = StandardHtmlElement::create(
TagName::DIV,
HtmlAttributes::empty()->withClass('card__footer'),
$this->footer
);
}
return implode('', array_map('strval', $elements));
}
public function __toString(): string
{
return (string) StandardHtmlElement::create(
$this->tag->name,
$this->attributes,
$this->content
);
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Components;
use App\Framework\View\ValueObjects\HtmlAttributes;
use App\Framework\View\ValueObjects\HtmlElement;
use App\Framework\View\ValueObjects\HtmlTag;
use App\Framework\View\ValueObjects\StandardHtmlElement;
use App\Framework\View\ValueObjects\TagName;
final readonly class Container implements HtmlElement
{
public HtmlTag $tag;
public HtmlAttributes $attributes;
public function __construct(
public string $content,
public string $size = 'default',
public bool $fluid = false,
public HtmlAttributes $additionalAttributes = new HtmlAttributes()
) {
$this->tag = new HtmlTag(TagName::DIV);
$classes = ['container'];
if ($this->fluid) {
$classes[] = 'container--fluid';
} else {
$classes[] = "container--{$this->size}";
}
$this->attributes = HtmlAttributes::empty()->withClass(implode(' ', $classes));
foreach ($this->additionalAttributes->attributes as $name => $value) {
$this->attributes = $this->attributes->with($name, $value);
}
}
public static function create(string $content): self
{
return new self(content: $content);
}
public static function fluid(string $content): self
{
return new self(content: $content, fluid: true);
}
public static function small(string $content): self
{
return new self(content: $content, size: 'sm');
}
public static function large(string $content): self
{
return new self(content: $content, size: 'lg');
}
public function withSize(string $size): self
{
return new self(
content: $this->content,
size: $size,
fluid: $this->fluid,
additionalAttributes: $this->additionalAttributes
);
}
public function __toString(): string
{
return (string) StandardHtmlElement::create(
TagName::DIV,
$this->attributes,
$this->content
);
}
}

View File

@@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Components;
use App\Framework\View\ValueObjects\HtmlAttributes;
use App\Framework\View\ValueObjects\HtmlElement;
use App\Framework\View\ValueObjects\HtmlTag;
use App\Framework\View\ValueObjects\StandardHtmlElement;
use App\Framework\View\ValueObjects\TagName;
final readonly class FormCheckbox implements HtmlElement
{
public HtmlTag $tag;
public HtmlAttributes $attributes;
public string $content;
public function __construct(
public string $name,
public string $value = '1',
public string $label = '',
public bool $checked = false,
public ?string $id = null,
public bool $required = false,
public bool $disabled = false,
public ?string $errorMessage = null,
public HtmlAttributes $additionalAttributes = new HtmlAttributes()
) {
$this->tag = new HtmlTag(TagName::DIV);
$this->attributes = HtmlAttributes::empty()->withClass('form-checkbox-group');
$this->content = $this->buildContent();
}
public static function create(
string $name,
string $label,
string $value = '1'
): self {
return new self(name: $name, value: $value, label: $label);
}
public function withChecked(bool $checked = true): self
{
return new self(
name: $this->name,
value: $this->value,
label: $this->label,
checked: $checked,
id: $this->id,
required: $this->required,
disabled: $this->disabled,
errorMessage: $this->errorMessage,
additionalAttributes: $this->additionalAttributes
);
}
public function withRequired(bool $required = true): self
{
return new self(
name: $this->name,
value: $this->value,
label: $this->label,
checked: $this->checked,
id: $this->id,
required: $required,
disabled: $this->disabled,
errorMessage: $this->errorMessage,
additionalAttributes: $this->additionalAttributes
);
}
public function withError(string $errorMessage): self
{
return new self(
name: $this->name,
value: $this->value,
label: $this->label,
checked: $this->checked,
id: $this->id,
required: $this->required,
disabled: $this->disabled,
errorMessage: $errorMessage,
additionalAttributes: $this->additionalAttributes
);
}
private function buildContent(): string
{
$checkboxId = $this->id ?? "checkbox-{$this->name}";
$attributes = HtmlAttributes::empty()
->withType('checkbox')
->withName($this->name)
->withValue($this->value)
->withId($checkboxId)
->withClass('form-checkbox');
if ($this->checked) {
$attributes = $attributes->withChecked();
}
if ($this->required) {
$attributes = $attributes->withRequired();
}
if ($this->disabled) {
$attributes = $attributes->withDisabled();
}
if ($this->errorMessage !== null) {
$attributes = $attributes
->withClass('form-checkbox--error')
->with('aria-invalid', 'true')
->with('aria-describedby', "{$checkboxId}-error");
}
// Merge additional attributes
foreach ($this->additionalAttributes->attributes as $name => $value) {
$attributes = $attributes->with($name, $value);
}
$elements = [];
// Checkbox input
$elements[] = StandardHtmlElement::create(TagName::INPUT, $attributes);
// Label
$labelAttrs = HtmlAttributes::empty()
->withClass('form-checkbox-label')
->with('for', $checkboxId);
$elements[] = StandardHtmlElement::create(
TagName::LABEL,
$labelAttrs,
$this->label
);
// Error message
if ($this->errorMessage !== null) {
$elements[] = StandardHtmlElement::create(
TagName::SPAN,
HtmlAttributes::empty()
->withClass('form-error')
->withId("{$checkboxId}-error")
->with('role', 'alert'),
$this->errorMessage
);
}
return implode('', array_map('strval', $elements));
}
public function __toString(): string
{
return (string) StandardHtmlElement::create(
$this->tag->name,
$this->attributes,
$this->content
);
}
}

View File

@@ -0,0 +1,262 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Components;
use App\Framework\View\ValueObjects\HtmlAttributes;
use App\Framework\View\ValueObjects\HtmlElement;
use App\Framework\View\ValueObjects\HtmlTag;
use App\Framework\View\ValueObjects\StandardHtmlElement;
use App\Framework\View\ValueObjects\TagName;
final readonly class FormInput implements HtmlElement
{
public HtmlTag $tag;
public HtmlAttributes $attributes;
public string $content;
public function __construct(
public string $name,
public string $type = 'text',
public ?string $value = null,
public ?string $label = null,
public ?string $id = null,
public ?string $placeholder = null,
public bool $required = false,
public bool $disabled = false,
public bool $readonly = false,
public ?string $pattern = null,
public ?int $minLength = null,
public ?int $maxLength = null,
public ?string $errorMessage = null,
public HtmlAttributes $additionalAttributes = new HtmlAttributes()
) {
$this->tag = new HtmlTag(TagName::DIV);
$this->attributes = HtmlAttributes::empty()->withClass('form-group');
$this->content = $this->buildContent();
}
public static function text(
string $name,
?string $label = null,
?string $value = null
): self {
return new self(name: $name, type: 'text', label: $label, value: $value);
}
public static function email(
string $name,
?string $label = null,
?string $value = null
): self {
return new self(name: $name, type: 'email', label: $label, value: $value);
}
public static function password(
string $name,
?string $label = null
): self {
return new self(name: $name, type: 'password', label: $label);
}
public static function number(
string $name,
?string $label = null,
?string $value = null
): self {
return new self(name: $name, type: 'number', label: $label, value: $value);
}
public static function tel(
string $name,
?string $label = null,
?string $value = null
): self {
return new self(name: $name, type: 'tel', label: $label, value: $value);
}
public static function url(
string $name,
?string $label = null,
?string $value = null
): self {
return new self(name: $name, type: 'url', label: $label, value: $value);
}
public static function date(
string $name,
?string $label = null,
?string $value = null
): self {
return new self(name: $name, type: 'date', label: $label, value: $value);
}
public static function search(
string $name,
?string $label = null,
?string $placeholder = null
): self {
return new self(name: $name, type: 'search', label: $label, placeholder: $placeholder);
}
public function withRequired(bool $required = true): self
{
return new self(
name: $this->name,
type: $this->type,
value: $this->value,
label: $this->label,
id: $this->id,
placeholder: $this->placeholder,
required: $required,
disabled: $this->disabled,
readonly: $this->readonly,
pattern: $this->pattern,
minLength: $this->minLength,
maxLength: $this->maxLength,
errorMessage: $this->errorMessage,
additionalAttributes: $this->additionalAttributes
);
}
public function withPattern(string $pattern): self
{
return new self(
name: $this->name,
type: $this->type,
value: $this->value,
label: $this->label,
id: $this->id,
placeholder: $this->placeholder,
required: $this->required,
disabled: $this->disabled,
readonly: $this->readonly,
pattern: $pattern,
minLength: $this->minLength,
maxLength: $this->maxLength,
errorMessage: $this->errorMessage,
additionalAttributes: $this->additionalAttributes
);
}
public function withError(string $errorMessage): self
{
return new self(
name: $this->name,
type: $this->type,
value: $this->value,
label: $this->label,
id: $this->id,
placeholder: $this->placeholder,
required: $this->required,
disabled: $this->disabled,
readonly: $this->readonly,
pattern: $this->pattern,
minLength: $this->minLength,
maxLength: $this->maxLength,
errorMessage: $errorMessage,
additionalAttributes: $this->additionalAttributes
);
}
private function buildContent(): string
{
$inputId = $this->id ?? "input-{$this->name}";
$attributes = HtmlAttributes::empty()
->withType($this->type)
->withName($this->name)
->withId($inputId)
->withClass('form-input');
if ($this->value !== null) {
$attributes = $attributes->withValue($this->value);
}
if ($this->placeholder !== null) {
$attributes = $attributes->with('placeholder', $this->placeholder);
}
if ($this->required) {
$attributes = $attributes->withRequired();
}
if ($this->disabled) {
$attributes = $attributes->withDisabled();
}
if ($this->readonly) {
$attributes = $attributes->withReadonly();
}
if ($this->pattern !== null) {
$attributes = $attributes->with('pattern', $this->pattern);
}
if ($this->minLength !== null) {
$attributes = $attributes->with('minlength', (string) $this->minLength);
}
if ($this->maxLength !== null) {
$attributes = $attributes->with('maxlength', (string) $this->maxLength);
}
if ($this->errorMessage !== null) {
$attributes = $attributes
->withClass('form-input--error')
->with('aria-invalid', 'true')
->with('aria-describedby', "{$inputId}-error");
}
// Merge additional attributes
foreach ($this->additionalAttributes->attributes as $name => $value) {
$attributes = $attributes->with($name, $value);
}
$elements = [];
// Label
if ($this->label !== null) {
$labelAttrs = HtmlAttributes::empty()
->withClass('form-label')
->with('for', $inputId);
if ($this->required) {
$labelAttrs = $labelAttrs->withClass('form-label--required');
}
$elements[] = StandardHtmlElement::create(
TagName::LABEL,
$labelAttrs,
$this->label
);
}
// Input
$elements[] = StandardHtmlElement::create(TagName::INPUT, $attributes);
// Error message
if ($this->errorMessage !== null) {
$elements[] = StandardHtmlElement::create(
TagName::SPAN,
HtmlAttributes::empty()
->withClass('form-error')
->withId("{$inputId}-error")
->with('role', 'alert'),
$this->errorMessage
);
}
return implode('', array_map('strval', $elements));
}
public function __toString(): string
{
return (string) StandardHtmlElement::create(
$this->tag->name,
$this->attributes,
$this->content
);
}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Components;
use App\Framework\View\ValueObjects\HtmlAttributes;
use App\Framework\View\ValueObjects\HtmlElement;
use App\Framework\View\ValueObjects\HtmlTag;
use App\Framework\View\ValueObjects\StandardHtmlElement;
use App\Framework\View\ValueObjects\TagName;
final readonly class FormRadio implements HtmlElement
{
public HtmlTag $tag;
public HtmlAttributes $attributes;
public string $content;
public function __construct(
public string $name,
public string $value,
public string $label = '',
public bool $checked = false,
public ?string $id = null,
public bool $required = false,
public bool $disabled = false,
public HtmlAttributes $additionalAttributes = new HtmlAttributes()
) {
$this->tag = new HtmlTag(TagName::DIV);
$this->attributes = HtmlAttributes::empty()->withClass('form-radio-item');
$this->content = $this->buildContent();
}
private function buildContent(): string
{
$radioId = $this->id ?? "radio-{$this->name}-{$this->value}";
$elements = [];
// Radio input
$radioAttrs = HtmlAttributes::empty()
->withType('radio')
->withName($this->name)
->withValue($this->value)
->withId($radioId)
->withClass('form-radio');
if ($this->checked) {
$radioAttrs = $radioAttrs->withChecked();
}
if ($this->required) {
$radioAttrs = $radioAttrs->withRequired();
}
if ($this->disabled) {
$radioAttrs = $radioAttrs->withDisabled();
}
foreach ($this->additionalAttributes->attributes as $name => $value) {
$radioAttrs = $radioAttrs->with($name, $value);
}
$elements[] = StandardHtmlElement::create(TagName::INPUT, $radioAttrs);
// Label
$elements[] = StandardHtmlElement::create(
TagName::LABEL,
HtmlAttributes::empty()->withClass('form-radio-label')->with('for', $radioId),
$this->label
);
return implode('', array_map('strval', $elements));
}
public static function create(
string $name,
string $value,
string $label
): self {
return new self(name: $name, value: $value, label: $label);
}
public function withChecked(bool $checked = true): self
{
return new self(
name: $this->name,
value: $this->value,
label: $this->label,
checked: $checked,
id: $this->id,
required: $this->required,
disabled: $this->disabled,
additionalAttributes: $this->additionalAttributes
);
}
public function withRequired(bool $required = true): self
{
return new self(
name: $this->name,
value: $this->value,
label: $this->label,
checked: $this->checked,
id: $this->id,
required: $required,
disabled: $this->disabled,
additionalAttributes: $this->additionalAttributes
);
}
public function __toString(): string
{
return (string) StandardHtmlElement::create(
$this->tag->name,
$this->attributes,
$this->content
);
}
}

View File

@@ -0,0 +1,228 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Components;
use App\Framework\View\ValueObjects\HtmlAttributes;
use App\Framework\View\ValueObjects\HtmlElement;
use App\Framework\View\ValueObjects\HtmlTag;
use App\Framework\View\ValueObjects\StandardHtmlElement;
use App\Framework\View\ValueObjects\TagName;
final readonly class FormSelect implements HtmlElement
{
public HtmlTag $tag;
public HtmlAttributes $attributes;
public string $content;
/**
* @param array<string, string> $options Key-value pairs for options
*/
public function __construct(
public string $name,
public array $options,
public ?string $selected = null,
public ?string $label = null,
public ?string $id = null,
public ?string $placeholder = null,
public bool $required = false,
public bool $disabled = false,
public bool $multiple = false,
public ?string $errorMessage = null,
public HtmlAttributes $additionalAttributes = new HtmlAttributes()
) {
$this->tag = new HtmlTag(TagName::DIV);
$this->attributes = HtmlAttributes::empty()->withClass('form-group');
$this->content = $this->buildContent();
}
public static function create(
string $name,
array $options,
?string $label = null
): self {
return new self(name: $name, options: $options, label: $label);
}
public function withSelected(string $selected): self
{
return new self(
name: $this->name,
options: $this->options,
selected: $selected,
label: $this->label,
id: $this->id,
placeholder: $this->placeholder,
required: $this->required,
disabled: $this->disabled,
multiple: $this->multiple,
errorMessage: $this->errorMessage,
additionalAttributes: $this->additionalAttributes
);
}
public function withRequired(bool $required = true): self
{
return new self(
name: $this->name,
options: $this->options,
selected: $this->selected,
label: $this->label,
id: $this->id,
placeholder: $this->placeholder,
required: $required,
disabled: $this->disabled,
multiple: $this->multiple,
errorMessage: $this->errorMessage,
additionalAttributes: $this->additionalAttributes
);
}
public function withMultiple(bool $multiple = true): self
{
return new self(
name: $this->name,
options: $this->options,
selected: $this->selected,
label: $this->label,
id: $this->id,
placeholder: $this->placeholder,
required: $this->required,
disabled: $this->disabled,
multiple: $multiple,
errorMessage: $this->errorMessage,
additionalAttributes: $this->additionalAttributes
);
}
public function withError(string $errorMessage): self
{
return new self(
name: $this->name,
options: $this->options,
selected: $this->selected,
label: $this->label,
id: $this->id,
placeholder: $this->placeholder,
required: $this->required,
disabled: $this->disabled,
multiple: $this->multiple,
errorMessage: $errorMessage,
additionalAttributes: $this->additionalAttributes
);
}
private function buildContent(): string
{
$selectId = $this->id ?? "select-{$this->name}";
$attributes = HtmlAttributes::empty()
->withName($this->name)
->withId($selectId)
->withClass('form-select');
if ($this->required) {
$attributes = $attributes->withRequired();
}
if ($this->disabled) {
$attributes = $attributes->withDisabled();
}
if ($this->multiple) {
$attributes = $attributes->with('multiple', 'multiple');
}
if ($this->errorMessage !== null) {
$attributes = $attributes
->withClass('form-select--error')
->with('aria-invalid', 'true')
->with('aria-describedby', "{$selectId}-error");
}
// Merge additional attributes
foreach ($this->additionalAttributes->attributes as $name => $value) {
$attributes = $attributes->with($name, $value);
}
// Build options
$optionElements = [];
if ($this->placeholder !== null) {
$optionElements[] = StandardHtmlElement::create(
TagName::OPTION,
HtmlAttributes::empty()
->withValue('')
->withDisabled()
->withSelected(),
$this->placeholder
);
}
foreach ($this->options as $value => $text) {
$optionAttrs = HtmlAttributes::empty()->withValue($value);
if ($this->selected !== null && $value === $this->selected) {
$optionAttrs = $optionAttrs->withSelected();
}
$optionElements[] = StandardHtmlElement::create(
TagName::OPTION,
$optionAttrs,
$text
);
}
$selectContent = implode('', array_map('strval', $optionElements));
$elements = [];
// Label
if ($this->label !== null) {
$labelAttrs = HtmlAttributes::empty()
->withClass('form-label')
->with('for', $selectId);
if ($this->required) {
$labelAttrs = $labelAttrs->withClass('form-label--required');
}
$elements[] = StandardHtmlElement::create(
TagName::LABEL,
$labelAttrs,
$this->label
);
}
// Select
$elements[] = StandardHtmlElement::create(
TagName::SELECT,
$attributes,
$selectContent
);
// Error message
if ($this->errorMessage !== null) {
$elements[] = StandardHtmlElement::create(
TagName::SPAN,
HtmlAttributes::empty()
->withClass('form-error')
->withId("{$selectId}-error")
->with('role', 'alert'),
$this->errorMessage
);
}
return implode('', array_map('strval', $elements));
}
public function __toString(): string
{
return (string) StandardHtmlElement::create(
$this->tag->name,
$this->attributes,
$this->content
);
}
}

View File

@@ -0,0 +1,187 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Components;
use App\Framework\View\ValueObjects\HtmlAttributes;
use App\Framework\View\ValueObjects\HtmlElement;
use App\Framework\View\ValueObjects\HtmlTag;
use App\Framework\View\ValueObjects\StandardHtmlElement;
use App\Framework\View\ValueObjects\TagName;
final readonly class FormTextarea implements HtmlElement
{
public HtmlTag $tag;
public HtmlAttributes $attributes;
public string $content;
public function __construct(
public string $name,
public ?string $value = null,
public ?string $label = null,
public ?string $id = null,
public ?string $placeholder = null,
public int $rows = 4,
public ?int $cols = null,
public bool $required = false,
public bool $disabled = false,
public bool $readonly = false,
public ?int $minLength = null,
public ?int $maxLength = null,
public ?string $errorMessage = null,
public HtmlAttributes $additionalAttributes = new HtmlAttributes()
) {
$this->tag = new HtmlTag(TagName::DIV);
$this->attributes = HtmlAttributes::empty()->withClass('form-group');
$this->content = $this->buildContent();
}
private function buildContent(): string
{
$textareaId = $this->id ?? "textarea-{$this->name}";
$elements = [];
// Label
if ($this->label !== null) {
$labelAttrs = HtmlAttributes::empty()
->withClass('form-label')
->with('for', $textareaId);
if ($this->required) {
$labelAttrs = $labelAttrs->withClass('form-label--required');
}
$elements[] = StandardHtmlElement::create(TagName::LABEL, $labelAttrs, $this->label);
}
// Textarea
$textareaAttrs = HtmlAttributes::empty()
->withName($this->name)
->withId($textareaId)
->withClass('form-textarea')
->with('rows', (string) $this->rows);
if ($this->cols !== null) {
$textareaAttrs = $textareaAttrs->with('cols', (string) $this->cols);
}
if ($this->placeholder !== null) {
$textareaAttrs = $textareaAttrs->with('placeholder', $this->placeholder);
}
if ($this->required) {
$textareaAttrs = $textareaAttrs->withRequired();
}
if ($this->disabled) {
$textareaAttrs = $textareaAttrs->withDisabled();
}
if ($this->readonly) {
$textareaAttrs = $textareaAttrs->withReadonly();
}
if ($this->minLength !== null) {
$textareaAttrs = $textareaAttrs->with('minlength', (string) $this->minLength);
}
if ($this->maxLength !== null) {
$textareaAttrs = $textareaAttrs->with('maxlength', (string) $this->maxLength);
}
if ($this->errorMessage !== null) {
$textareaAttrs = $textareaAttrs->withClass('form-textarea--error')
->with('aria-invalid', 'true')
->with('aria-describedby', "{$textareaId}-error");
}
foreach ($this->additionalAttributes->attributes as $name => $value) {
$textareaAttrs = $textareaAttrs->with($name, $value);
}
$elements[] = StandardHtmlElement::create(TagName::TEXTAREA, $textareaAttrs, $this->value ?? '');
// Error message
if ($this->errorMessage !== null) {
$elements[] = StandardHtmlElement::create(
TagName::SPAN,
HtmlAttributes::empty()
->withClass('form-error')
->withId("{$textareaId}-error")
->with('role', 'alert'),
$this->errorMessage
);
}
return implode('', array_map('strval', $elements));
}
public static function create(
string $name,
?string $label = null,
?string $value = null
): self {
return new self(name: $name, label: $label, value: $value);
}
public function withRows(int $rows): self
{
return new self(
name: $this->name,
value: $this->value,
label: $this->label,
id: $this->id,
placeholder: $this->placeholder,
rows: $rows,
cols: $this->cols,
required: $this->required,
disabled: $this->disabled,
readonly: $this->readonly,
minLength: $this->minLength,
maxLength: $this->maxLength,
errorMessage: $this->errorMessage,
additionalAttributes: $this->additionalAttributes
);
}
public function withRequired(bool $required = true): self
{
return new self(
name: $this->name,
value: $this->value,
label: $this->label,
id: $this->id,
placeholder: $this->placeholder,
rows: $this->rows,
cols: $this->cols,
required: $required,
disabled: $this->disabled,
readonly: $this->readonly,
minLength: $this->minLength,
maxLength: $this->maxLength,
errorMessage: $this->errorMessage,
additionalAttributes: $this->additionalAttributes
);
}
public function withError(string $errorMessage): self
{
return new self(
name: $this->name,
value: $this->value,
label: $this->label,
id: $this->id,
placeholder: $this->placeholder,
rows: $this->rows,
cols: $this->cols,
required: $this->required,
disabled: $this->disabled,
readonly: $this->readonly,
minLength: $this->minLength,
maxLength: $this->maxLength,
errorMessage: $errorMessage,
additionalAttributes: $this->additionalAttributes
);
}
public function __toString(): string
{
return (string) StandardHtmlElement::create(
$this->tag->name,
$this->attributes,
$this->content
);
}
}

View File

@@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Components;
use App\Framework\View\ValueObjects\HtmlAttributes;
use App\Framework\View\ValueObjects\HtmlElement;
use App\Framework\View\ValueObjects\HtmlTag;
use App\Framework\View\ValueObjects\StandardHtmlElement;
use App\Framework\View\ValueObjects\TagName;
final readonly class Image implements HtmlElement
{
public HtmlTag $tag;
public HtmlAttributes $attributes;
public string $content;
public function __construct(
public string $src,
public string $alt,
public ?int $width = null,
public ?int $height = null,
public string $loading = 'lazy',
public string $decoding = 'async',
public ?string $srcset = null,
public ?string $sizes = null,
public HtmlAttributes $additionalAttributes = new HtmlAttributes()
) {
$this->tag = new HtmlTag(TagName::IMG);
$this->attributes = HtmlAttributes::empty()
->with('src', $this->src)
->with('alt', $this->alt)
->with('loading', $this->loading)
->with('decoding', $this->decoding);
if ($this->width !== null) {
$this->attributes = $this->attributes->with('width', (string) $this->width);
}
if ($this->height !== null) {
$this->attributes = $this->attributes->with('height', (string) $this->height);
}
if ($this->srcset !== null) {
$this->attributes = $this->attributes->with('srcset', $this->srcset);
}
if ($this->sizes !== null) {
$this->attributes = $this->attributes->with('sizes', $this->sizes);
}
// Merge additional attributes
foreach ($this->additionalAttributes->attributes as $name => $value) {
$this->attributes = $this->attributes->with($name, $value);
}
$this->content = '';
}
public static function create(string $src, string $alt): self
{
return new self(src: $src, alt: $alt);
}
public static function responsive(
string $src,
string $alt,
string $srcset,
string $sizes = '100vw'
): self {
return new self(
src: $src,
alt: $alt,
srcset: $srcset,
sizes: $sizes
);
}
public function withDimensions(int $width, int $height): self
{
return new self(
src: $this->src,
alt: $this->alt,
width: $width,
height: $height,
loading: $this->loading,
decoding: $this->decoding,
srcset: $this->srcset,
sizes: $this->sizes,
additionalAttributes: $this->additionalAttributes
);
}
public function eager(): self
{
return new self(
src: $this->src,
alt: $this->alt,
width: $this->width,
height: $this->height,
loading: 'eager',
decoding: $this->decoding,
srcset: $this->srcset,
sizes: $this->sizes,
additionalAttributes: $this->additionalAttributes
);
}
public function syncDecoding(): self
{
return new self(
src: $this->src,
alt: $this->alt,
width: $this->width,
height: $this->height,
loading: $this->loading,
decoding: 'sync',
srcset: $this->srcset,
sizes: $this->sizes,
additionalAttributes: $this->additionalAttributes
);
}
public function __toString(): string
{
return (string) StandardHtmlElement::create(
$this->tag->name,
$this->attributes,
$this->content
);
}
}

View File

@@ -0,0 +1,152 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Components;
use App\Framework\View\ValueObjects\HtmlAttributes;
use App\Framework\View\ValueObjects\HtmlElement;
use App\Framework\View\ValueObjects\HtmlTag;
use App\Framework\View\ValueObjects\StandardHtmlElement;
use App\Framework\View\ValueObjects\TagName;
final readonly class Picture implements HtmlElement
{
public HtmlTag $tag;
public HtmlAttributes $attributes;
public string $content;
/**
* @param array<PictureSource> $sources
*/
public function __construct(
public string $src,
public string $alt,
public array $sources = [],
public ?int $width = null,
public ?int $height = null,
public string $loading = 'lazy',
public HtmlAttributes $additionalAttributes = new HtmlAttributes()
) {
$this->tag = new HtmlTag(TagName::PICTURE);
$this->attributes = HtmlAttributes::empty();
// Merge additional attributes
foreach ($this->additionalAttributes->attributes as $name => $value) {
$this->attributes = $this->attributes->with($name, $value);
}
$this->content = $this->buildContent();
}
private function buildContent(): string
{
$elements = [];
// Add all source elements
foreach ($this->sources as $source) {
$elements[] = (string) $source;
}
// Add fallback img element
$imgAttrs = HtmlAttributes::empty()
->with('src', $this->src)
->with('alt', $this->alt)
->with('loading', $this->loading);
if ($this->width !== null) {
$imgAttrs = $imgAttrs->with('width', (string) $this->width);
}
if ($this->height !== null) {
$imgAttrs = $imgAttrs->with('height', (string) $this->height);
}
$elements[] = StandardHtmlElement::create(TagName::IMG, $imgAttrs);
return implode('', array_map('strval', $elements));
}
public static function create(string $src, string $alt): self
{
return new self(src: $src, alt: $alt);
}
public static function withWebP(
string $webpSrc,
string $fallbackSrc,
string $alt
): self {
return new self(
src: $fallbackSrc,
alt: $alt,
sources: [
PictureSource::webp($webpSrc),
]
);
}
public static function withAvif(
string $avifSrc,
string $webpSrc,
string $fallbackSrc,
string $alt
): self {
return new self(
src: $fallbackSrc,
alt: $alt,
sources: [
PictureSource::avif($avifSrc),
PictureSource::webp($webpSrc),
]
);
}
public function withSource(PictureSource $source): self
{
return new self(
src: $this->src,
alt: $this->alt,
sources: [...$this->sources, $source],
width: $this->width,
height: $this->height,
loading: $this->loading,
additionalAttributes: $this->additionalAttributes
);
}
public function withDimensions(int $width, int $height): self
{
return new self(
src: $this->src,
alt: $this->alt,
sources: $this->sources,
width: $width,
height: $height,
loading: $this->loading,
additionalAttributes: $this->additionalAttributes
);
}
public function eager(): self
{
return new self(
src: $this->src,
alt: $this->alt,
sources: $this->sources,
width: $this->width,
height: $this->height,
loading: 'eager',
additionalAttributes: $this->additionalAttributes
);
}
public function __toString(): string
{
return (string) StandardHtmlElement::create(
$this->tag->name,
$this->attributes,
$this->content
);
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace App\Framework\View\Components;
use App\Framework\View\ValueObjects\HtmlAttributes;
use App\Framework\View\ValueObjects\StandardHtmlElement;
use App\Framework\View\ValueObjects\TagName;
final readonly class PictureSource
{
public function __construct(
public string $srcset,
public ?string $type = null,
public ?string $media = null,
public ?string $sizes = null
) {}
public static function webp(string $srcset, ?string $media = null): self
{
return new self(srcset: $srcset, type: 'image/webp', media: $media);
}
public static function avif(string $srcset, ?string $media = null): self
{
return new self(srcset: $srcset, type: 'image/avif', media: $media);
}
public static function jpeg(string $srcset, ?string $media = null): self
{
return new self(srcset: $srcset, type: 'image/jpeg', media: $media);
}
public static function png(string $srcset, ?string $media = null): self
{
return new self(srcset: $srcset, type: 'image/png', media: $media);
}
public static function responsive(
string $srcset,
string $media,
?string $type = null
): self {
return new self(srcset: $srcset, type: $type, media: $media);
}
public function __toString(): string
{
$attributes = HtmlAttributes::empty()
->with('srcset', $this->srcset);
if ($this->type !== null) {
$attributes = $attributes->with('type', $this->type);
}
if ($this->media !== null) {
$attributes = $attributes->with('media', $this->media);
}
if ($this->sizes !== null) {
$attributes = $attributes->with('sizes', $this->sizes);
}
return (string) StandardHtmlElement::create(TagName::SOURCE, $attributes);
}
}