feat(Production): Complete production deployment infrastructure

- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\Builder;
use App\Framework\Core\ValueObjects\Dimensions;
use App\Framework\Filesystem\FileStorage;
use App\Framework\Svg\ValueObjects\Geometry\ViewBox;
/**
* Static factory for creating SVG canvases
* Entry point for SVG generation
*/
final class SvgBuilder
{
/**
* Create canvas with dimensions
*/
public static function canvas(
Dimensions $dimensions,
?ViewBox $viewBox = null,
?FileStorage $fileStorage = null
): SvgCanvas {
return new SvgCanvas($dimensions, $viewBox, null, null, $fileStorage);
}
/**
* Create canvas from width and height
*/
public static function create(int $width, int $height, ?FileStorage $fileStorage = null): SvgCanvas
{
return new SvgCanvas(new Dimensions($width, $height), null, null, null, $fileStorage);
}
/**
* Create square canvas
*/
public static function square(int $size, ?FileStorage $fileStorage = null): SvgCanvas
{
return new SvgCanvas(new Dimensions($size, $size), null, null, null, $fileStorage);
}
/**
* Create canvas with responsive viewBox
*/
public static function responsive(int $width, int $height, ?FileStorage $fileStorage = null): SvgCanvas
{
$dimensions = new Dimensions($width, $height);
$viewBox = ViewBox::fromDimensions($dimensions);
return new SvgCanvas($dimensions, $viewBox, null, null, $fileStorage);
}
/**
* Common canvas sizes
*/
public static function icon(int $size = 24, ?FileStorage $fileStorage = null): SvgCanvas
{
return self::square($size, $fileStorage);
}
public static function badge(int $width = 100, int $height = 20, ?FileStorage $fileStorage = null): SvgCanvas
{
return self::create($width, $height, $fileStorage);
}
public static function card(int $width = 400, int $height = 300, ?FileStorage $fileStorage = null): SvgCanvas
{
return self::create($width, $height, $fileStorage);
}
public static function chart(int $width = 600, int $height = 400, ?FileStorage $fileStorage = null): SvgCanvas
{
return self::responsive($width, $height, $fileStorage);
}
}

View File

@@ -0,0 +1,285 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\Builder;
use App\Framework\Core\ValueObjects\Dimensions;
use App\Framework\Filesystem\FileStorage;
use App\Framework\Svg\Elements\CircleElement;
use App\Framework\Svg\Elements\GroupElement;
use App\Framework\Svg\Elements\LineElement;
use App\Framework\Svg\Elements\RectElement;
use App\Framework\Svg\Elements\SvgElement;
use App\Framework\Svg\Elements\TextElement;
use App\Framework\Svg\ValueObjects\Geometry\Position;
use App\Framework\Svg\ValueObjects\Geometry\Radius;
use App\Framework\Svg\ValueObjects\Geometry\ViewBox;
use App\Framework\Svg\ValueObjects\Styling\Fill;
use App\Framework\Svg\ValueObjects\Styling\Stroke;
use App\Framework\Svg\ValueObjects\Text\TextContent;
use App\Framework\Svg\ValueObjects\Text\TextStyle;
use App\Framework\Svg\ValueObjects\Transform\Transform;
/**
* Mutable builder for creating SVG documents
* Uses fluent interface for convenient API
*/
final class SvgCanvas
{
/** @var array<SvgElement> */
private array $elements = [];
public function __construct(
private readonly Dimensions $dimensions,
private readonly ?ViewBox $viewBox = null,
private ?string $title = null,
private ?string $description = null,
private readonly ?FileStorage $fileStorage = null
) {
}
/**
* Add rectangle to canvas
*/
public function rect(
Position $position,
Dimensions $dimensions,
Fill $fill,
?Stroke $stroke = null,
?Radius $rx = null,
?Radius $ry = null,
?Transform $transform = null,
?string $id = null,
?string $class = null
): self {
$this->elements[] = new RectElement(
$position,
$dimensions,
$fill,
$stroke,
$rx,
$ry,
$transform,
$id,
$class
);
return $this;
}
/**
* Add circle to canvas
*/
public function circle(
Position $center,
Radius $radius,
Fill $fill,
?Stroke $stroke = null,
?Transform $transform = null,
?string $id = null,
?string $class = null
): self {
$this->elements[] = new CircleElement(
$center,
$radius,
$fill,
$stroke,
$transform,
$id,
$class
);
return $this;
}
/**
* Add line to canvas
*/
public function line(
Position $start,
Position $end,
Stroke $stroke,
?Transform $transform = null,
?string $id = null,
?string $class = null
): self {
$this->elements[] = new LineElement(
$start,
$end,
$stroke,
$transform,
$id,
$class
);
return $this;
}
/**
* Add text to canvas
*/
public function text(
string $content,
Position $position,
TextStyle $style,
?Transform $transform = null,
?string $id = null,
?string $class = null
): self {
$this->elements[] = new TextElement(
new TextContent($content),
$position,
$style,
$transform,
$id,
$class
);
return $this;
}
/**
* Add group to canvas
*/
public function group(
array $children,
?Transform $transform = null,
?string $id = null,
?string $class = null
): self {
$this->elements[] = new GroupElement(
$children,
$transform,
$id,
$class
);
return $this;
}
/**
* Add custom element to canvas
*/
public function element(SvgElement $element): self
{
$this->elements[] = $element;
return $this;
}
/**
* Set title for accessibility
*/
public function withTitle(string $title): self
{
$this->title = $title;
return $this;
}
/**
* Set description for accessibility
*/
public function withDescription(string $description): self
{
$this->description = $description;
return $this;
}
/**
* Get all elements
*/
public function getElements(): array
{
return $this->elements;
}
/**
* Clear all elements
*/
public function clear(): self
{
$this->elements = [];
return $this;
}
/**
* Get element count
*/
public function count(): int
{
return count($this->elements);
}
/**
* Render complete SVG document
*/
public function toSvg(): string
{
$svgAttributes = [
'width' => (string) $this->dimensions->width,
'height' => (string) $this->dimensions->height,
'xmlns' => 'http://www.w3.org/2000/svg',
];
if ($this->viewBox !== null) {
$svgAttributes['viewBox'] = $this->viewBox->toSvgAttribute();
}
$attributeString = $this->buildAttributeString($svgAttributes);
$svg = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
$svg .= "<svg {$attributeString}>\n";
// Add accessibility elements
if ($this->title !== null) {
$svg .= " <title>" . htmlspecialchars($this->title, ENT_XML1, 'UTF-8') . "</title>\n";
}
if ($this->description !== null) {
$svg .= " <desc>" . htmlspecialchars($this->description, ENT_XML1, 'UTF-8') . "</desc>\n";
}
// Add all elements
foreach ($this->elements as $element) {
$svg .= " " . $element->toSvg() . "\n";
}
$svg .= "</svg>";
return $svg;
}
/**
* Render to inline SVG (without XML declaration)
*/
public function toInlineSvg(): string
{
$svg = $this->toSvg();
// Remove XML declaration
return preg_replace('/<\?xml[^?]*\?>\s*/', '', $svg) ?? $svg;
}
/**
* Export to file using Framework's FileStorage
*/
public function toFile(string $filepath): void
{
$storage = $this->fileStorage ?? new FileStorage();
$storage->put($filepath, $this->toSvg());
}
private function buildAttributeString(array $attributes): string
{
$parts = [];
foreach ($attributes as $key => $value) {
$parts[] = sprintf('%s="%s"', $key, htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'));
}
return implode(' ', $parts);
}
}

View File

@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\Charts;
use App\Framework\Core\ValueObjects\Dimensions;
use App\Framework\Svg\Builder\SvgBuilder;
use App\Framework\Svg\Charts\ValueObjects\ChartConfig;
use App\Framework\Svg\Charts\ValueObjects\ChartData;
use App\Framework\Svg\ValueObjects\Geometry\Position;
use App\Framework\Svg\ValueObjects\Geometry\Radius;
use App\Framework\Svg\ValueObjects\Styling\Fill;
use App\Framework\Svg\ValueObjects\Styling\Stroke;
use App\Framework\Svg\ValueObjects\Styling\StrokeWidth;
use App\Framework\Svg\ValueObjects\Text\TextStyle;
/**
* Bar Chart Generator
*/
final readonly class BarChart
{
public function __construct(
public ChartData $data,
public ChartConfig $config = new ChartConfig(new Dimensions(600, 400))
) {
}
/**
* Create from simple array
*/
public static function fromArray(array $data, ?ChartConfig $config = null): self
{
return new self(
ChartData::fromArray($data),
$config ?? ChartConfig::default()
);
}
/**
* Render bar chart to SVG string
*/
public function render(): string
{
$canvas = SvgBuilder::canvas($this->config->dimensions);
// Background
$canvas->rect(
Position::zero(),
$this->config->dimensions,
Fill::solid($this->config->backgroundColor)
);
$chartArea = $this->config->getChartArea();
$barCount = $this->data->count();
$maxValue = $this->data->getMaxValue();
// Calculate bar dimensions
$barSpacing = 10;
$totalSpacing = $barSpacing * ($barCount + 1);
$availableWidth = $chartArea->width - $totalSpacing;
$barWidth = $availableWidth / $barCount;
// Draw bars
$x = $this->config->padding + $barSpacing;
foreach ($this->data->points as $point) {
// Calculate bar height (normalized to chart area)
$barHeight = ($point->value / $maxValue) * $chartArea->height;
$barY = $this->config->padding + ($chartArea->height - $barHeight);
// Draw bar with rounded corners
$canvas->rect(
new Position($x, $barY),
new Dimensions((int) $barWidth, (int) $barHeight),
Fill::solid($this->config->primaryColor),
Stroke::solid($this->config->primaryColor, 1),
new Radius(4)
);
// Draw label if enabled
if ($this->config->showLabels) {
$labelY = $this->config->dimensions->height - $this->config->padding + 20;
$canvas->text(
$point->label,
new Position($x + ($barWidth / 2), $labelY),
TextStyle::default()
->centered()
->withFill(Fill::solid($this->config->textColor))
);
}
// Draw value if enabled
if ($this->config->showValues) {
$canvas->text(
number_format($point->value, 1),
new Position($x + ($barWidth / 2), $barY - 5),
TextStyle::default()
->centered()
->withFill(Fill::solid($this->config->textColor))
);
}
$x += $barWidth + $barSpacing;
}
// Draw grid if enabled
if ($this->config->showGrid) {
$this->drawGrid($canvas, $chartArea, $maxValue);
}
return $canvas->toSvg();
}
private function drawGrid($canvas, Dimensions $chartArea, float $maxValue): void
{
$gridColor = $this->config->textColor;
$gridStroke = new Stroke(
$gridColor,
new StrokeWidth(1),
new \App\Framework\Svg\ValueObjects\Styling\Opacity(0.1)
);
// Horizontal grid lines (5 lines)
$gridLines = 5;
for ($i = 0; $i <= $gridLines; $i++) {
$y = $this->config->padding + ($chartArea->height * ($i / $gridLines));
$canvas->line(
new Position($this->config->padding, $y),
new Position($this->config->padding + $chartArea->width, $y),
$gridStroke
);
// Grid value label
$value = $maxValue * (1 - ($i / $gridLines));
$canvas->text(
number_format($value, 0),
new Position($this->config->padding - 10, $y + 4),
TextStyle::default()
->withTextAnchor(\App\Framework\Svg\ValueObjects\Text\TextAnchor::END)
->withFill(new Fill($gridColor, new \App\Framework\Svg\ValueObjects\Styling\Opacity(0.6)))
);
}
}
}

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\Charts\ValueObjects;
use App\Framework\Core\ValueObjects\Dimensions;
use App\Framework\Svg\ValueObjects\Styling\SvgColor;
use App\Framework\Svg\ValueObjects\Text\FontSize;
/**
* Value Object representing chart configuration
*/
final readonly class ChartConfig
{
public function __construct(
public Dimensions $dimensions,
public SvgColor $primaryColor = new SvgColor(new \App\Framework\Core\ValueObjects\RGBColor(59, 130, 246)), // Blue
public SvgColor $backgroundColor = new SvgColor(new \App\Framework\Core\ValueObjects\RGBColor(255, 255, 255)),
public SvgColor $textColor = new SvgColor(new \App\Framework\Core\ValueObjects\RGBColor(0, 0, 0)),
public FontSize $fontSize = new FontSize(12.0),
public int $padding = 40,
public bool $showGrid = true,
public bool $showLabels = true,
public bool $showValues = false
) {
}
/**
* Create default configuration
*/
public static function default(int $width = 600, int $height = 400): self
{
return new self(new Dimensions($width, $height));
}
/**
* Get chart area dimensions (excluding padding)
*/
public function getChartArea(): Dimensions
{
return new Dimensions(
$this->dimensions->width - ($this->padding * 2),
$this->dimensions->height - ($this->padding * 2)
);
}
/**
* Create variant with different color
*/
public function withColor(SvgColor $color): self
{
return new self(
$this->dimensions,
$color,
$this->backgroundColor,
$this->textColor,
$this->fontSize,
$this->padding,
$this->showGrid,
$this->showLabels,
$this->showValues
);
}
/**
* Create dark theme variant
*/
public function darkTheme(): self
{
return new self(
$this->dimensions,
new SvgColor(new \App\Framework\Core\ValueObjects\RGBColor(96, 165, 250)), // Lighter blue
new SvgColor(new \App\Framework\Core\ValueObjects\RGBColor(17, 24, 39)), // Dark gray
new SvgColor(new \App\Framework\Core\ValueObjects\RGBColor(229, 231, 235)), // Light gray
$this->fontSize,
$this->padding,
$this->showGrid,
$this->showLabels,
$this->showValues
);
}
public function toArray(): array
{
return [
'dimensions' => $this->dimensions->toArray(),
'primaryColor' => $this->primaryColor->toSvgValue(),
'backgroundColor' => $this->backgroundColor->toSvgValue(),
'textColor' => $this->textColor->toSvgValue(),
'fontSize' => $this->fontSize->toSvgValue(),
'padding' => $this->padding,
'showGrid' => $this->showGrid,
'showLabels' => $this->showLabels,
'showValues' => $this->showValues,
];
}
}

View File

@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\Charts\ValueObjects;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
/**
* Value Object representing chart data
*/
final readonly class ChartData
{
/**
* @param array<DataPoint> $points
*/
public function __construct(
public array $points
) {
if (empty($points)) {
throw FrameworkException::create(
ErrorCode::VAL_INVALID_FORMAT,
'Chart data cannot be empty'
);
}
// Validate all points are DataPoint instances
foreach ($points as $point) {
if (!$point instanceof DataPoint) {
throw FrameworkException::create(
ErrorCode::VAL_INVALID_FORMAT,
'All chart data points must be DataPoint instances'
);
}
}
}
/**
* Create from array of values with labels
*/
public static function fromArray(array $data): self
{
$points = [];
foreach ($data as $label => $value) {
$points[] = new DataPoint((string) $label, (float) $value);
}
return new self($points);
}
/**
* Get point count
*/
public function count(): int
{
return count($this->points);
}
/**
* Get minimum value
*/
public function getMinValue(): float
{
return min(array_map(fn (DataPoint $p) => $p->value, $this->points));
}
/**
* Get maximum value
*/
public function getMaxValue(): float
{
return max(array_map(fn (DataPoint $p) => $p->value, $this->points));
}
/**
* Get sum of all values
*/
public function getSum(): float
{
return array_sum(array_map(fn (DataPoint $p) => $p->value, $this->points));
}
/**
* Get average value
*/
public function getAverage(): float
{
return $this->getSum() / $this->count();
}
/**
* Normalize values to 0-1 range
*/
public function normalize(): self
{
$min = $this->getMinValue();
$max = $this->getMaxValue();
$range = $max - $min;
if ($range === 0.0) {
return $this;
}
$normalized = array_map(
fn (DataPoint $p) => new DataPoint(
$p->label,
($p->value - $min) / $range
),
$this->points
);
return new self($normalized);
}
public function toArray(): array
{
return array_map(fn (DataPoint $p) => $p->toArray(), $this->points);
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\Charts\ValueObjects;
/**
* Value Object representing a single data point in a chart
*/
final readonly class DataPoint
{
public function __construct(
public string $label,
public float $value
) {
}
/**
* Check if value is positive
*/
public function isPositive(): bool
{
return $this->value > 0;
}
/**
* Check if value is negative
*/
public function isNegative(): bool
{
return $this->value < 0;
}
/**
* Check if value is zero
*/
public function isZero(): bool
{
return $this->value === 0.0;
}
/**
* Scale value by factor
*/
public function scale(float $factor): self
{
return new self($this->label, $this->value * $factor);
}
public function toArray(): array
{
return [
'label' => $this->label,
'value' => $this->value,
];
}
}

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\Elements;
use App\Framework\Svg\ValueObjects\Geometry\Position;
use App\Framework\Svg\ValueObjects\Geometry\Radius;
use App\Framework\Svg\ValueObjects\Styling\Fill;
use App\Framework\Svg\ValueObjects\Styling\Stroke;
use App\Framework\Svg\ValueObjects\Transform\Transform;
/**
* Value Object representing SVG circle element
*/
final readonly class CircleElement implements SvgElement
{
public function __construct(
public Position $center,
public Radius $radius,
public Fill $fill,
public ?Stroke $stroke = null,
public ?Transform $transform = null,
public ?string $id = null,
public ?string $class = null
) {
}
public function getElementType(): string
{
return 'circle';
}
public function getAttributes(): array
{
$attributes = [
'cx' => sprintf('%.2f', $this->center->x),
'cy' => sprintf('%.2f', $this->center->y),
'r' => $this->radius->toSvgValue(),
...$this->fill->toSvgAttributes(),
];
if ($this->stroke !== null && $this->stroke->isVisible()) {
$attributes = [...$attributes, ...$this->stroke->toSvgAttributes()];
}
if ($this->transform !== null && $this->transform->hasTransformations()) {
$attributes['transform'] = $this->transform->toSvgValue();
}
if ($this->id !== null) {
$attributes['id'] = $this->id;
}
if ($this->class !== null) {
$attributes['class'] = $this->class;
}
return $attributes;
}
public function hasTransform(): bool
{
return $this->transform !== null && $this->transform->hasTransformations();
}
public function getTransform(): ?Transform
{
return $this->transform;
}
public function toSvg(): string
{
$attributeString = $this->buildAttributeString($this->getAttributes());
return "<circle {$attributeString} />";
}
private function buildAttributeString(array $attributes): string
{
$parts = [];
foreach ($attributes as $key => $value) {
$parts[] = sprintf('%s="%s"', $key, htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'));
}
return implode(' ', $parts);
}
}

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\Elements;
use App\Framework\Svg\ValueObjects\Transform\Transform;
/**
* Value Object representing SVG g (group) element
* Groups multiple elements together for transformation and styling
*/
final readonly class GroupElement implements SvgElement
{
/**
* @param array<SvgElement> $children
*/
public function __construct(
public array $children,
public ?Transform $transform = null,
public ?string $id = null,
public ?string $class = null
) {
}
public function getElementType(): string
{
return 'g';
}
public function getAttributes(): array
{
$attributes = [];
if ($this->transform !== null && $this->transform->hasTransformations()) {
$attributes['transform'] = $this->transform->toSvgValue();
}
if ($this->id !== null) {
$attributes['id'] = $this->id;
}
if ($this->class !== null) {
$attributes['class'] = $this->class;
}
return $attributes;
}
public function hasTransform(): bool
{
return $this->transform !== null && $this->transform->hasTransformations();
}
public function getTransform(): ?Transform
{
return $this->transform;
}
public function toSvg(): string
{
$attributes = $this->getAttributes();
$attributeString = empty($attributes)
? ''
: ' ' . $this->buildAttributeString($attributes);
$childrenSvg = implode("\n ", array_map(
fn (SvgElement $child) => $child->toSvg(),
$this->children
));
if (empty($childrenSvg)) {
return "<g{$attributeString} />";
}
return "<g{$attributeString}>\n {$childrenSvg}\n</g>";
}
private function buildAttributeString(array $attributes): string
{
$parts = [];
foreach ($attributes as $key => $value) {
$parts[] = sprintf('%s="%s"', $key, htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'));
}
return implode(' ', $parts);
}
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\Elements;
use App\Framework\Svg\ValueObjects\Geometry\Position;
use App\Framework\Svg\ValueObjects\Styling\Stroke;
use App\Framework\Svg\ValueObjects\Transform\Transform;
/**
* Value Object representing SVG line element
*/
final readonly class LineElement implements SvgElement
{
public function __construct(
public Position $start,
public Position $end,
public Stroke $stroke,
public ?Transform $transform = null,
public ?string $id = null,
public ?string $class = null
) {
}
public function getElementType(): string
{
return 'line';
}
public function getAttributes(): array
{
$attributes = [
'x1' => sprintf('%.2f', $this->start->x),
'y1' => sprintf('%.2f', $this->start->y),
'x2' => sprintf('%.2f', $this->end->x),
'y2' => sprintf('%.2f', $this->end->y),
...$this->stroke->toSvgAttributes(),
];
if ($this->transform !== null && $this->transform->hasTransformations()) {
$attributes['transform'] = $this->transform->toSvgValue();
}
if ($this->id !== null) {
$attributes['id'] = $this->id;
}
if ($this->class !== null) {
$attributes['class'] = $this->class;
}
return $attributes;
}
public function hasTransform(): bool
{
return $this->transform !== null && $this->transform->hasTransformations();
}
public function getTransform(): ?Transform
{
return $this->transform;
}
public function toSvg(): string
{
$attributeString = $this->buildAttributeString($this->getAttributes());
return "<line {$attributeString} />";
}
private function buildAttributeString(array $attributes): string
{
$parts = [];
foreach ($attributes as $key => $value) {
$parts[] = sprintf('%s="%s"', $key, htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'));
}
return implode(' ', $parts);
}
}

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\Elements;
use App\Framework\Core\ValueObjects\Dimensions;
use App\Framework\Svg\ValueObjects\Geometry\Position;
use App\Framework\Svg\ValueObjects\Geometry\Radius;
use App\Framework\Svg\ValueObjects\Styling\Fill;
use App\Framework\Svg\ValueObjects\Styling\Stroke;
use App\Framework\Svg\ValueObjects\Transform\Transform;
/**
* Value Object representing SVG rect element
*/
final readonly class RectElement implements SvgElement
{
public function __construct(
public Position $position,
public Dimensions $dimensions,
public Fill $fill,
public ?Stroke $stroke = null,
public ?Radius $rx = null, // Horizontal border radius
public ?Radius $ry = null, // Vertical border radius
public ?Transform $transform = null,
public ?string $id = null,
public ?string $class = null
) {
}
public function getElementType(): string
{
return 'rect';
}
public function getAttributes(): array
{
$attributes = [
'x' => sprintf('%.2f', $this->position->x),
'y' => sprintf('%.2f', $this->position->y),
'width' => (string) $this->dimensions->width,
'height' => (string) $this->dimensions->height,
...$this->fill->toSvgAttributes(),
];
if ($this->stroke !== null && $this->stroke->isVisible()) {
$attributes = [...$attributes, ...$this->stroke->toSvgAttributes()];
}
if ($this->rx !== null) {
$attributes['rx'] = $this->rx->toSvgValue();
}
if ($this->ry !== null) {
$attributes['ry'] = $this->ry->toSvgValue();
}
if ($this->transform !== null && $this->transform->hasTransformations()) {
$attributes['transform'] = $this->transform->toSvgValue();
}
if ($this->id !== null) {
$attributes['id'] = $this->id;
}
if ($this->class !== null) {
$attributes['class'] = $this->class;
}
return $attributes;
}
public function hasTransform(): bool
{
return $this->transform !== null && $this->transform->hasTransformations();
}
public function getTransform(): ?Transform
{
return $this->transform;
}
public function toSvg(): string
{
$attributeString = $this->buildAttributeString($this->getAttributes());
return "<rect {$attributeString} />";
}
private function buildAttributeString(array $attributes): string
{
$parts = [];
foreach ($attributes as $key => $value) {
$parts[] = sprintf('%s="%s"', $key, htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'));
}
return implode(' ', $parts);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\Elements;
use App\Framework\Svg\ValueObjects\Transform\Transform;
/**
* Base interface for all SVG elements
*/
interface SvgElement
{
/**
* Render element to SVG string
*/
public function toSvg(): string;
/**
* Get element type (rect, circle, text, etc.)
*/
public function getElementType(): string;
/**
* Get all attributes as associative array
*/
public function getAttributes(): array;
/**
* Check if element has transform
*/
public function hasTransform(): bool;
/**
* Get transform if present
*/
public function getTransform(): ?Transform;
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\Elements;
use App\Framework\Svg\ValueObjects\Geometry\Position;
use App\Framework\Svg\ValueObjects\Text\TextContent;
use App\Framework\Svg\ValueObjects\Text\TextStyle;
use App\Framework\Svg\ValueObjects\Transform\Transform;
/**
* Value Object representing SVG text element
*/
final readonly class TextElement implements SvgElement
{
public function __construct(
public TextContent $content,
public Position $position,
public TextStyle $style,
public ?Transform $transform = null,
public ?string $id = null,
public ?string $class = null
) {
}
public function getElementType(): string
{
return 'text';
}
public function getAttributes(): array
{
$attributes = [
'x' => sprintf('%.2f', $this->position->x),
'y' => sprintf('%.2f', $this->position->y),
...$this->style->toSvgAttributes(),
];
if ($this->transform !== null && $this->transform->hasTransformations()) {
$attributes['transform'] = $this->transform->toSvgValue();
}
if ($this->id !== null) {
$attributes['id'] = $this->id;
}
if ($this->class !== null) {
$attributes['class'] = $this->class;
}
return $attributes;
}
public function hasTransform(): bool
{
return $this->transform !== null && $this->transform->hasTransformations();
}
public function getTransform(): ?Transform
{
return $this->transform;
}
public function toSvg(): string
{
$attributeString = $this->buildAttributeString($this->getAttributes());
$textContent = $this->content->toSvgSafeString();
return "<text {$attributeString}>{$textContent}</text>";
}
private function buildAttributeString(array $attributes): string
{
$parts = [];
foreach ($attributes as $key => $value) {
$parts[] = sprintf('%s="%s"', $key, htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'));
}
return implode(' ', $parts);
}
}

View File

@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Geometry;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
/**
* Value Object representing a 2D position in SVG coordinate space
*/
final readonly class Position
{
public function __construct(
public float $x,
public float $y
) {
}
public static function zero(): self
{
return new self(0.0, 0.0);
}
public static function fromArray(array $data): self
{
if (count($data) !== 2) {
throw FrameworkException::create(
ErrorCode::VAL_INVALID_FORMAT,
'Position array must contain exactly 2 elements [x, y]'
)->withData(['data' => $data]);
}
return new self((float) $data[0], (float) $data[1]);
}
/**
* Translate position by offset
*/
public function translate(float $dx, float $dy): self
{
return new self(
$this->x + $dx,
$this->y + $dy
);
}
/**
* Calculate distance to another position
*/
public function distanceTo(self $other): float
{
$dx = $other->x - $this->x;
$dy = $other->y - $this->y;
return sqrt($dx * $dx + $dy * $dy);
}
/**
* Scale position by factor
*/
public function scale(float $factor): self
{
return new self(
$this->x * $factor,
$this->y * $factor
);
}
/**
* Check if positions are equal (with tolerance for float comparison)
*/
public function equals(self $other, float $tolerance = 0.001): bool
{
return abs($this->x - $other->x) < $tolerance
&& abs($this->y - $other->y) < $tolerance;
}
/**
* Convert to SVG coordinate string
*/
public function toSvgString(): string
{
return sprintf('%.2f,%.2f', $this->x, $this->y);
}
public function toArray(): array
{
return ['x' => $this->x, 'y' => $this->y];
}
public function __toString(): string
{
return sprintf('(%.2f, %.2f)', $this->x, $this->y);
}
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Geometry;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
/**
* Value Object representing a radius value for circles and rounded corners
*/
final readonly class Radius
{
public function __construct(
public float $value
) {
if ($value < 0) {
throw FrameworkException::create(
ErrorCode::VAL_INVALID_FORMAT,
'Radius cannot be negative'
)->withData(['value' => $value]);
}
}
public static function zero(): self
{
return new self(0.0);
}
/**
* Get diameter (2 * radius)
*/
public function getDiameter(): float
{
return $this->value * 2;
}
/**
* Get circumference (2 * π * radius)
*/
public function getCircumference(): float
{
return 2 * M_PI * $this->value;
}
/**
* Get area (π * radius²)
*/
public function getArea(): float
{
return M_PI * $this->value * $this->value;
}
/**
* Scale radius by factor
*/
public function scale(float $factor): self
{
if ($factor < 0) {
throw FrameworkException::create(
ErrorCode::VAL_INVALID_FORMAT,
'Scale factor cannot be negative'
)->withData(['factor' => $factor]);
}
return new self($this->value * $factor);
}
/**
* Convert to SVG attribute value
*/
public function toSvgValue(): string
{
return sprintf('%.2f', $this->value);
}
public function __toString(): string
{
return $this->toSvgValue();
}
}

View File

@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Geometry;
use App\Framework\Core\ValueObjects\Dimensions;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
/**
* Value Object representing SVG viewBox attribute
* Defines the position and dimensions of the viewport
*/
final readonly class ViewBox
{
public function __construct(
public float $minX,
public float $minY,
public float $width,
public float $height
) {
if ($width <= 0) {
throw FrameworkException::create(
ErrorCode::VAL_INVALID_FORMAT,
'ViewBox width must be greater than 0'
)->withData(['width' => $width]);
}
if ($height <= 0) {
throw FrameworkException::create(
ErrorCode::VAL_INVALID_FORMAT,
'ViewBox height must be greater than 0'
)->withData(['height' => $height]);
}
}
/**
* Create ViewBox from origin (0,0)
*/
public static function fromDimensions(Dimensions $dimensions): self
{
return new self(0, 0, $dimensions->width, $dimensions->height);
}
/**
* Create ViewBox from position and dimensions
*/
public static function fromPositionAndDimensions(Position $position, Dimensions $dimensions): self
{
return new self(
$position->x,
$position->y,
$dimensions->width,
$dimensions->height
);
}
/**
* Get aspect ratio
*/
public function getAspectRatio(): float
{
return $this->width / $this->height;
}
/**
* Convert to SVG viewBox attribute value
*/
public function toSvgAttribute(): string
{
return sprintf('%.2f %.2f %.2f %.2f', $this->minX, $this->minY, $this->width, $this->height);
}
/**
* Get center position
*/
public function getCenter(): Position
{
return new Position(
$this->minX + ($this->width / 2),
$this->minY + ($this->height / 2)
);
}
/**
* Scale viewBox
*/
public function scale(float $factor): self
{
if ($factor <= 0) {
throw FrameworkException::create(
ErrorCode::VAL_INVALID_FORMAT,
'Scale factor must be greater than 0'
)->withData(['factor' => $factor]);
}
return new self(
$this->minX * $factor,
$this->minY * $factor,
$this->width * $factor,
$this->height * $factor
);
}
public function toArray(): array
{
return [
'minX' => $this->minX,
'minY' => $this->minY,
'width' => $this->width,
'height' => $this->height,
'aspectRatio' => $this->getAspectRatio(),
];
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Styling;
/**
* Value Object representing SVG fill styling
*/
final readonly class Fill
{
public function __construct(
public SvgColor $color,
public Opacity $opacity = new Opacity(1.0)
) {
}
public static function none(): self
{
return new self(SvgColor::transparent(), new Opacity(0.0));
}
public static function solid(SvgColor $color): self
{
return new self($color, new Opacity(1.0));
}
public static function withOpacity(SvgColor $color, float $opacity): self
{
return new self($color, new Opacity($opacity));
}
/**
* Check if fill is visible
*/
public function isVisible(): bool
{
return $this->opacity->value > 0 && !$this->color->isTransparent();
}
/**
* Convert to SVG attributes
*/
public function toSvgAttributes(): array
{
$attributes = ['fill' => $this->color->toSvgValue()];
if ($this->opacity->value < 1.0) {
$attributes['fill-opacity'] = $this->opacity->toSvgValue();
}
return $attributes;
}
public function toArray(): array
{
return [
'color' => $this->color->toSvgValue(),
'opacity' => $this->opacity->value,
'visible' => $this->isVisible(),
];
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Styling;
use App\Domain\Common\ValueObject\RGBColor;
/**
* Enum for SVG named colors with RGB values
* @see https://www.w3.org/TR/SVG11/types.html#ColorKeywords
*/
enum NamedColor: string
{
case BLACK = 'black';
case WHITE = 'white';
case RED = 'red';
case GREEN = 'green';
case BLUE = 'blue';
case YELLOW = 'yellow';
case CYAN = 'cyan';
case MAGENTA = 'magenta';
case GRAY = 'gray';
case GREY = 'grey';
case SILVER = 'silver';
case MAROON = 'maroon';
case PURPLE = 'purple';
case FUCHSIA = 'fuchsia';
case LIME = 'lime';
case OLIVE = 'olive';
case NAVY = 'navy';
case TEAL = 'teal';
case AQUA = 'aqua';
case ORANGE = 'orange';
case TRANSPARENT = 'transparent';
/**
* Get RGB representation of named color
*/
public function toRgb(): RGBColor
{
return match ($this) {
self::BLACK => RGBColor::fromRgb(0, 0, 0),
self::WHITE => RGBColor::fromRgb(255, 255, 255),
self::RED => RGBColor::fromRgb(255, 0, 0),
self::GREEN => RGBColor::fromRgb(0, 128, 0),
self::BLUE => RGBColor::fromRgb(0, 0, 255),
self::YELLOW => RGBColor::fromRgb(255, 255, 0),
self::CYAN => RGBColor::fromRgb(0, 255, 255),
self::MAGENTA => RGBColor::fromRgb(255, 0, 255),
self::GRAY, self::GREY => RGBColor::fromRgb(128, 128, 128),
self::SILVER => RGBColor::fromRgb(192, 192, 192),
self::MAROON => RGBColor::fromRgb(128, 0, 0),
self::PURPLE => RGBColor::fromRgb(128, 0, 128),
self::FUCHSIA => RGBColor::fromRgb(255, 0, 255),
self::LIME => RGBColor::fromRgb(0, 255, 0),
self::OLIVE => RGBColor::fromRgb(128, 128, 0),
self::NAVY => RGBColor::fromRgb(0, 0, 128),
self::TEAL => RGBColor::fromRgb(0, 128, 128),
self::AQUA => RGBColor::fromRgb(0, 255, 255),
self::ORANGE => RGBColor::fromRgb(255, 165, 0),
self::TRANSPARENT => RGBColor::fromRgb(0, 0, 0), // Transparent is handled via opacity
};
}
/**
* Check if color is transparent
*/
public function isTransparent(): bool
{
return $this === self::TRANSPARENT;
}
/**
* Get SVG color name
*/
public function toSvgValue(): string
{
return $this->value;
}
/**
* Get hex representation
*/
public function toHex(): string
{
return $this->toRgb()->toHex();
}
}

View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Styling;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
/**
* Value Object representing opacity (0.0 to 1.0)
*/
final readonly class Opacity
{
public function __construct(
public float $value
) {
if ($value < 0.0 || $value > 1.0) {
throw FrameworkException::create(
ErrorCode::VAL_OUT_OF_RANGE,
'Opacity must be between 0.0 and 1.0'
)->withData(['value' => $value]);
}
}
public static function transparent(): self
{
return new self(0.0);
}
public static function opaque(): self
{
return new self(1.0);
}
public static function fromPercentage(int $percentage): self
{
if ($percentage < 0 || $percentage > 100) {
throw FrameworkException::create(
ErrorCode::VAL_OUT_OF_RANGE,
'Percentage must be between 0 and 100'
)->withData(['percentage' => $percentage]);
}
return new self($percentage / 100);
}
/**
* Get as percentage (0-100)
*/
public function toPercentage(): int
{
return (int) round($this->value * 100);
}
/**
* Check if fully transparent
*/
public function isTransparent(): bool
{
return $this->value === 0.0;
}
/**
* Check if fully opaque
*/
public function isOpaque(): bool
{
return $this->value === 1.0;
}
/**
* Convert to SVG attribute value
*/
public function toSvgValue(): string
{
return sprintf('%.2f', $this->value);
}
public function __toString(): string
{
return $this->toSvgValue();
}
}

View File

@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Styling;
/**
* Value Object representing SVG stroke styling
*/
final readonly class Stroke
{
public function __construct(
public SvgColor $color,
public StrokeWidth $width,
public Opacity $opacity = new Opacity(1.0),
public ?StrokeLinecap $linecap = null,
public ?StrokeLinejoin $linejoin = null,
public ?string $dasharray = null // e.g., "5,5" for dashed line
) {
}
public static function none(): self
{
return new self(
SvgColor::transparent(),
StrokeWidth::none(),
new Opacity(0.0)
);
}
public static function solid(SvgColor $color, float $width): self
{
return new self($color, new StrokeWidth($width), new Opacity(1.0));
}
public static function dashed(SvgColor $color, float $width, string $dasharray): self
{
return new self(
$color,
new StrokeWidth($width),
new Opacity(1.0),
null,
null,
$dasharray
);
}
/**
* Create stroke with rounded ends and joins
*/
public static function rounded(SvgColor $color, float $width): self
{
return new self(
$color,
new StrokeWidth($width),
new Opacity(1.0),
StrokeLinecap::ROUND,
StrokeLinejoin::ROUND
);
}
/**
* Check if stroke is visible
*/
public function isVisible(): bool
{
return $this->opacity->value > 0
&& $this->width->value > 0
&& !$this->color->isTransparent();
}
/**
* Convert to SVG attributes
*/
public function toSvgAttributes(): array
{
$attributes = [
'stroke' => $this->color->toSvgValue(),
'stroke-width' => $this->width->toSvgValue(),
];
if ($this->opacity->value < 1.0) {
$attributes['stroke-opacity'] = $this->opacity->toSvgValue();
}
if ($this->linecap !== null) {
$attributes['stroke-linecap'] = $this->linecap->value;
}
if ($this->linejoin !== null) {
$attributes['stroke-linejoin'] = $this->linejoin->value;
}
if ($this->dasharray !== null) {
$attributes['stroke-dasharray'] = $this->dasharray;
}
return $attributes;
}
public function toArray(): array
{
return [
'color' => $this->color->toSvgValue(),
'width' => $this->width->value,
'opacity' => $this->opacity->value,
'linecap' => $this->linecap?->value,
'linejoin' => $this->linejoin?->value,
'dasharray' => $this->dasharray,
'visible' => $this->isVisible(),
];
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Styling;
/**
* Enum for SVG stroke-linecap values
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-linecap
*/
enum StrokeLinecap: string
{
case BUTT = 'butt'; // Default - flat edge
case ROUND = 'round'; // Rounded edge
case SQUARE = 'square'; // Square edge that extends beyond the path
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Styling;
/**
* Enum for SVG stroke-linejoin values
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-linejoin
*/
enum StrokeLinejoin: string
{
case MITER = 'miter'; // Default - sharp corner
case ROUND = 'round'; // Rounded corner
case BEVEL = 'bevel'; // Beveled corner
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Styling;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
/**
* Value Object representing stroke width
*/
final readonly class StrokeWidth
{
public function __construct(
public float $value
) {
if ($value < 0) {
throw FrameworkException::create(
ErrorCode::VAL_INVALID_FORMAT,
'Stroke width cannot be negative'
)->withData(['value' => $value]);
}
}
public static function none(): self
{
return new self(0.0);
}
public static function thin(): self
{
return new self(1.0);
}
public static function medium(): self
{
return new self(2.0);
}
public static function thick(): self
{
return new self(4.0);
}
/**
* Scale width by factor
*/
public function scale(float $factor): self
{
if ($factor < 0) {
throw FrameworkException::create(
ErrorCode::VAL_INVALID_FORMAT,
'Scale factor cannot be negative'
)->withData(['factor' => $factor]);
}
return new self($this->value * $factor);
}
/**
* Convert to SVG attribute value
*/
public function toSvgValue(): string
{
return sprintf('%.2f', $this->value);
}
public function __toString(): string
{
return $this->toSvgValue();
}
}

View File

@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Styling;
use App\Domain\Common\ValueObject\RGBColor;
/**
* Value Object representing color in SVG context
* Supports RGB, hex, and named colors
*/
final readonly class SvgColor
{
private function __construct(
public RGBColor $rgbColor,
public ?NamedColor $namedColor = null
) {
}
public static function fromRgb(int $r, int $g, int $b): self
{
return new self(RGBColor::fromRgb($r, $g, $b));
}
public static function fromRgbColor(RGBColor $color): self
{
return new self($color);
}
public static function fromHex(string $hex): self
{
return new self(RGBColor::fromHex($hex));
}
public static function fromNamed(NamedColor $color): self
{
return new self($color->toRgb(), $color);
}
/**
* Common SVG colors as static factory methods
*/
public static function black(): self
{
return self::fromNamed(NamedColor::BLACK);
}
public static function white(): self
{
return self::fromNamed(NamedColor::WHITE);
}
public static function transparent(): self
{
return self::fromNamed(NamedColor::TRANSPARENT);
}
public static function red(): self
{
return self::fromNamed(NamedColor::RED);
}
public static function green(): self
{
return self::fromNamed(NamedColor::GREEN);
}
public static function blue(): self
{
return self::fromNamed(NamedColor::BLUE);
}
public static function yellow(): self
{
return self::fromNamed(NamedColor::YELLOW);
}
public static function orange(): self
{
return self::fromNamed(NamedColor::ORANGE);
}
public static function gray(): self
{
return self::fromNamed(NamedColor::GRAY);
}
/**
* Convert to SVG color attribute value
* Prefers named color if available, falls back to hex
*/
public function toSvgValue(): string
{
return $this->namedColor?->toSvgValue() ?? $this->rgbColor->toHex();
}
/**
* Always returns hex representation
*/
public function toHex(): string
{
return $this->rgbColor->toHex();
}
/**
* Returns RGB string representation
*/
public function toRgbString(): string
{
return sprintf(
'rgb(%d, %d, %d)',
$this->rgbColor->red,
$this->rgbColor->green,
$this->rgbColor->blue
);
}
/**
* Check if this is a transparent color
*/
public function isTransparent(): bool
{
return $this->namedColor?->isTransparent() ?? false;
}
public function toArray(): array
{
return [
'rgb' => [
'r' => $this->rgbColor->red,
'g' => $this->rgbColor->green,
'b' => $this->rgbColor->blue,
],
'hex' => $this->rgbColor->toHex(),
'named' => $this->namedColor?->value,
];
}
public function __toString(): string
{
return $this->toSvgValue();
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Text;
/**
* Enum for SVG dominant-baseline values
* Defines vertical text alignment
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/dominant-baseline
*/
enum DominantBaseline: string
{
case AUTO = 'auto'; // Default
case MIDDLE = 'middle'; // Centered vertically
case HANGING = 'hanging'; // Top-aligned
case MATHEMATICAL = 'mathematical'; // Math baseline
case TEXT_BOTTOM = 'text-bottom'; // Bottom-aligned
case TEXT_TOP = 'text-top'; // Top of text
case CENTRAL = 'central'; // Geometric center
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Text;
/**
* Value Object representing font family
*/
final readonly class FontFamily
{
public function __construct(
public string $value
) {
}
/**
* Common system font stacks
*/
public static function system(): self
{
return new self('system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif');
}
public static function sansSerif(): self
{
return new self('Arial, Helvetica, sans-serif');
}
public static function serif(): self
{
return new self('Georgia, "Times New Roman", serif');
}
public static function monospace(): self
{
return new self('"Courier New", Courier, monospace');
}
/**
* Web-safe fonts
*/
public static function arial(): self
{
return new self('Arial, sans-serif');
}
public static function helvetica(): self
{
return new self('Helvetica, Arial, sans-serif');
}
public static function verdana(): self
{
return new self('Verdana, Geneva, sans-serif');
}
public static function georgia(): self
{
return new self('Georgia, serif');
}
public static function timesNewRoman(): self
{
return new self('"Times New Roman", Times, serif');
}
/**
* Convert to SVG attribute value
*/
public function toSvgValue(): string
{
return $this->value;
}
public function __toString(): string
{
return $this->value;
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Text;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
/**
* Value Object representing font size
*/
final readonly class FontSize
{
public function __construct(
public float $value,
public FontSizeUnit $unit = FontSizeUnit::PX
) {
if ($value <= 0) {
throw FrameworkException::create(
ErrorCode::VAL_INVALID_FORMAT,
'Font size must be greater than 0'
)->withData(['value' => $value]);
}
}
/**
* Named font sizes
*/
public static function tiny(): self
{
return new self(10.0);
}
public static function small(): self
{
return new self(12.0);
}
public static function medium(): self
{
return new self(14.0);
}
public static function large(): self
{
return new self(18.0);
}
public static function extraLarge(): self
{
return new self(24.0);
}
public static function huge(): self
{
return new self(32.0);
}
/**
* Scale font size by factor
*/
public function scale(float $factor): self
{
if ($factor <= 0) {
throw FrameworkException::create(
ErrorCode::VAL_INVALID_FORMAT,
'Scale factor must be greater than 0'
)->withData(['factor' => $factor]);
}
return new self($this->value * $factor, $this->unit);
}
/**
* Convert to SVG attribute value
*/
public function toSvgValue(): string
{
return sprintf('%.2f%s', $this->value, $this->unit->value);
}
public function __toString(): string
{
return $this->toSvgValue();
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Text;
/**
* Enum for font size units
*/
enum FontSizeUnit: string
{
case PX = 'px'; // Pixels
case PT = 'pt'; // Points
case EM = 'em'; // Relative to parent
case REM = 'rem'; // Relative to root
case PERCENT = '%'; // Percentage
}

View File

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Text;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
/**
* Value Object representing font weight
* Supports both numeric (100-900) and named weights
*/
final readonly class FontWeight
{
public function __construct(
public int $value
) {
if ($value < 100 || $value > 900 || $value % 100 !== 0) {
throw FrameworkException::create(
ErrorCode::VAL_OUT_OF_RANGE,
'Font weight must be between 100 and 900 in steps of 100'
)->withData(['value' => $value]);
}
}
/**
* Named font weights
*/
public static function thin(): self
{
return new self(100);
}
public static function extraLight(): self
{
return new self(200);
}
public static function light(): self
{
return new self(300);
}
public static function normal(): self
{
return new self(400);
}
public static function medium(): self
{
return new self(500);
}
public static function semiBold(): self
{
return new self(600);
}
public static function bold(): self
{
return new self(700);
}
public static function extraBold(): self
{
return new self(800);
}
public static function black(): self
{
return new self(900);
}
/**
* Get named weight if applicable
*/
public function getNamedWeight(): ?string
{
return match ($this->value) {
100 => 'thin',
200 => 'extra-light',
300 => 'light',
400 => 'normal',
500 => 'medium',
600 => 'semi-bold',
700 => 'bold',
800 => 'extra-bold',
900 => 'black',
default => null,
};
}
/**
* Check if weight is bold or heavier
*/
public function isBold(): bool
{
return $this->value >= 700;
}
/**
* Convert to SVG attribute value
*/
public function toSvgValue(): string
{
return (string) $this->value;
}
public function __toString(): string
{
return $this->getNamedWeight() ?? $this->toSvgValue();
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Text;
/**
* Enum for SVG text-anchor values
* Defines horizontal text alignment
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/text-anchor
*/
enum TextAnchor: string
{
case START = 'start'; // Left-aligned (default)
case MIDDLE = 'middle'; // Center-aligned
case END = 'end'; // Right-aligned
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Text;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
/**
* Value Object representing text content in SVG
* Handles escaping and validation
*/
final readonly class TextContent
{
public function __construct(
public string $value
) {
if ($value === '') {
throw FrameworkException::create(
ErrorCode::VAL_INVALID_FORMAT,
'Text content cannot be empty'
);
}
}
public static function fromString(string $text): self
{
return new self($text);
}
/**
* Get escaped text for SVG output
*/
public function toSvgSafeString(): string
{
return htmlspecialchars($this->value, ENT_XML1 | ENT_QUOTES, 'UTF-8');
}
/**
* Get length of text
*/
public function getLength(): int
{
return mb_strlen($this->value);
}
/**
* Truncate text to maximum length
*/
public function truncate(int $maxLength, string $suffix = '...'): self
{
if ($this->getLength() <= $maxLength) {
return $this;
}
$truncated = mb_substr($this->value, 0, $maxLength - mb_strlen($suffix));
return new self($truncated . $suffix);
}
/**
* Check if text is empty
*/
public function isEmpty(): bool
{
return trim($this->value) === '';
}
public function __toString(): string
{
return $this->value;
}
}

View File

@@ -0,0 +1,196 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Text;
use App\Framework\Svg\ValueObjects\Styling\Fill;
use App\Framework\Svg\ValueObjects\Styling\Stroke;
/**
* Value Object combining all text styling properties
*/
final readonly class TextStyle
{
public function __construct(
public FontFamily $fontFamily,
public FontSize $fontSize,
public FontWeight $fontWeight = new FontWeight(400),
public Fill $fill = new Fill(new \App\Framework\Svg\ValueObjects\Styling\SvgColor(new \App\Framework\Core\ValueObjects\RGBColor(0, 0, 0))),
public ?Stroke $stroke = null,
public TextAnchor $textAnchor = TextAnchor::START,
public DominantBaseline $dominantBaseline = DominantBaseline::AUTO,
public ?float $letterSpacing = null,
public ?float $wordSpacing = null,
public bool $italic = false
) {
}
/**
* Create default text style
*/
public static function default(): self
{
return new self(
FontFamily::system(),
FontSize::medium(),
FontWeight::normal()
);
}
/**
* Create bold text style
*/
public static function bold(): self
{
return new self(
FontFamily::system(),
FontSize::medium(),
FontWeight::bold()
);
}
/**
* Create heading style
*/
public static function heading(int $level = 1): self
{
$fontSize = match ($level) {
1 => FontSize::huge(),
2 => FontSize::extraLarge(),
3 => FontSize::large(),
default => FontSize::medium(),
};
return new self(
FontFamily::system(),
$fontSize,
FontWeight::bold()
);
}
/**
* Create centered text style
*/
public function centered(): self
{
return new self(
$this->fontFamily,
$this->fontSize,
$this->fontWeight,
$this->fill,
$this->stroke,
TextAnchor::MIDDLE,
$this->dominantBaseline,
$this->letterSpacing,
$this->wordSpacing,
$this->italic
);
}
/**
* Create italic variant
*/
public function italic(): self
{
return new self(
$this->fontFamily,
$this->fontSize,
$this->fontWeight,
$this->fill,
$this->stroke,
$this->textAnchor,
$this->dominantBaseline,
$this->letterSpacing,
$this->wordSpacing,
true
);
}
/**
* With custom fill
*/
public function withFill(Fill $fill): self
{
return new self(
$this->fontFamily,
$this->fontSize,
$this->fontWeight,
$fill,
$this->stroke,
$this->textAnchor,
$this->dominantBaseline,
$this->letterSpacing,
$this->wordSpacing,
$this->italic
);
}
/**
* With custom text anchor
*/
public function withTextAnchor(TextAnchor $textAnchor): self
{
return new self(
$this->fontFamily,
$this->fontSize,
$this->fontWeight,
$this->fill,
$this->stroke,
$textAnchor,
$this->dominantBaseline,
$this->letterSpacing,
$this->wordSpacing,
$this->italic
);
}
/**
* Convert to SVG attributes
*/
public function toSvgAttributes(): array
{
$attributes = [
'font-family' => $this->fontFamily->toSvgValue(),
'font-size' => $this->fontSize->toSvgValue(),
'font-weight' => $this->fontWeight->toSvgValue(),
'text-anchor' => $this->textAnchor->value,
'dominant-baseline' => $this->dominantBaseline->value,
...$this->fill->toSvgAttributes(),
];
if ($this->stroke !== null && $this->stroke->isVisible()) {
$attributes = [...$attributes, ...$this->stroke->toSvgAttributes()];
}
if ($this->letterSpacing !== null) {
$attributes['letter-spacing'] = sprintf('%.2f', $this->letterSpacing);
}
if ($this->wordSpacing !== null) {
$attributes['word-spacing'] = sprintf('%.2f', $this->wordSpacing);
}
if ($this->italic) {
$attributes['font-style'] = 'italic';
}
return $attributes;
}
public function toArray(): array
{
return [
'fontFamily' => $this->fontFamily->value,
'fontSize' => $this->fontSize->toSvgValue(),
'fontWeight' => $this->fontWeight->value,
'fill' => $this->fill->toArray(),
'stroke' => $this->stroke?->toArray(),
'textAnchor' => $this->textAnchor->value,
'dominantBaseline' => $this->dominantBaseline->value,
'letterSpacing' => $this->letterSpacing,
'wordSpacing' => $this->wordSpacing,
'italic' => $this->italic,
];
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Transform;
/**
* Value Object representing SVG rotate transformation
*/
final readonly class Rotation
{
public function __construct(
public float $degrees,
public ?float $centerX = null,
public ?float $centerY = null
) {
}
/**
* Convert to SVG transform function
*/
public function toSvgString(): string
{
if ($this->centerX === null || $this->centerY === null) {
return sprintf('rotate(%.2f)', $this->degrees);
}
return sprintf('rotate(%.2f %.2f %.2f)', $this->degrees, $this->centerX, $this->centerY);
}
/**
* Normalize degrees to 0-360 range
*/
public function normalized(): self
{
$normalized = fmod($this->degrees, 360);
if ($normalized < 0) {
$normalized += 360;
}
return new self($normalized, $this->centerX, $this->centerY);
}
public function toArray(): array
{
return [
'type' => 'rotate',
'degrees' => $this->degrees,
'centerX' => $this->centerX,
'centerY' => $this->centerY,
];
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Transform;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
/**
* Value Object representing SVG scale transformation
*/
final readonly class Scale
{
public function __construct(
public float $x,
public ?float $y = null
) {
if ($x === 0.0) {
throw FrameworkException::create(
ErrorCode::VAL_INVALID_FORMAT,
'Scale X cannot be zero'
)->withData(['x' => $x]);
}
if ($y !== null && $y === 0.0) {
throw FrameworkException::create(
ErrorCode::VAL_INVALID_FORMAT,
'Scale Y cannot be zero'
)->withData(['y' => $y]);
}
}
/**
* Create uniform scale (same in X and Y)
*/
public static function uniform(float $factor): self
{
return new self($factor, $factor);
}
/**
* Convert to SVG transform function
*/
public function toSvgString(): string
{
if ($this->y === null || $this->y === $this->x) {
return sprintf('scale(%.2f)', $this->x);
}
return sprintf('scale(%.2f %.2f)', $this->x, $this->y);
}
/**
* Check if scale is uniform
*/
public function isUniform(): bool
{
return $this->y === null || $this->y === $this->x;
}
public function toArray(): array
{
return [
'type' => 'scale',
'x' => $this->x,
'y' => $this->y ?? $this->x,
'uniform' => $this->isUniform(),
];
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Transform;
/**
* Value Object representing SVG skew transformation
*/
final readonly class Skew
{
public function __construct(
public float $xDegrees = 0.0,
public float $yDegrees = 0.0
) {
}
public static function x(float $degrees): self
{
return new self($degrees, 0.0);
}
public static function y(float $degrees): self
{
return new self(0.0, $degrees);
}
/**
* Convert to SVG transform function
*/
public function toSvgString(): string
{
if ($this->xDegrees !== 0.0 && $this->yDegrees !== 0.0) {
return sprintf('skewX(%.2f) skewY(%.2f)', $this->xDegrees, $this->yDegrees);
}
if ($this->xDegrees !== 0.0) {
return sprintf('skewX(%.2f)', $this->xDegrees);
}
return sprintf('skewY(%.2f)', $this->yDegrees);
}
public function toArray(): array
{
return [
'type' => 'skew',
'xDegrees' => $this->xDegrees,
'yDegrees' => $this->yDegrees,
];
}
}

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Transform;
/**
* Value Object representing SVG transform attribute
* Supports multiple transformations in sequence
*/
final readonly class Transform
{
/**
* @param array<Translation|Rotation|Scale|Skew> $transformations
*/
public function __construct(
public array $transformations = []
) {
}
public static function none(): self
{
return new self([]);
}
public static function translate(float $x, float $y): self
{
return new self([new Translation($x, $y)]);
}
public static function rotate(float $degrees, ?float $centerX = null, ?float $centerY = null): self
{
return new self([new Rotation($degrees, $centerX, $centerY)]);
}
public static function scale(float $x, ?float $y = null): self
{
return new self([new Scale($x, $y)]);
}
/**
* Add translation to transform chain
*/
public function withTranslation(float $x, float $y): self
{
return new self([...$this->transformations, new Translation($x, $y)]);
}
/**
* Add rotation to transform chain
*/
public function withRotation(float $degrees, ?float $centerX = null, ?float $centerY = null): self
{
return new self([...$this->transformations, new Rotation($degrees, $centerX, $centerY)]);
}
/**
* Add scale to transform chain
*/
public function withScale(float $x, ?float $y = null): self
{
return new self([...$this->transformations, new Scale($x, $y)]);
}
/**
* Check if transform has any transformations
*/
public function hasTransformations(): bool
{
return count($this->transformations) > 0;
}
/**
* Convert to SVG transform attribute value
*/
public function toSvgValue(): string
{
if (!$this->hasTransformations()) {
return '';
}
return implode(' ', array_map(
fn ($t) => $t->toSvgString(),
$this->transformations
));
}
public function toArray(): array
{
return array_map(
fn ($t) => $t->toArray(),
$this->transformations
);
}
public function __toString(): string
{
return $this->toSvgValue();
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Framework\Svg\ValueObjects\Transform;
/**
* Value Object representing SVG translate transformation
*/
final readonly class Translation
{
public function __construct(
public float $x,
public float $y = 0.0
) {
}
/**
* Convert to SVG transform function
*/
public function toSvgString(): string
{
if ($this->y === 0.0) {
return sprintf('translate(%.2f)', $this->x);
}
return sprintf('translate(%.2f %.2f)', $this->x, $this->y);
}
public function toArray(): array
{
return [
'type' => 'translate',
'x' => $this->x,
'y' => $this->y,
];
}
}