feat: implement preview configuration

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-08-29 07:53:14 -04:00
parent 30a8eddf06
commit eaea5f4c83
6 changed files with 398 additions and 0 deletions
@@ -13,6 +13,7 @@ class TenantConfiguration extends JsonSerializableObject
protected TenantSecurity $security;
protected TenantFirewall $firewall;
protected TenantStores $stores;
protected TenantPreview $preview;
public function __construct()
{
@@ -20,6 +21,7 @@ class TenantConfiguration extends JsonSerializableObject
$this->security = new TenantSecurity();
$this->firewall = new TenantFirewall();
$this->stores = new TenantStores();
$this->preview = new TenantPreview();
}
public function authentication(): TenantAuthentication {
@@ -38,4 +40,8 @@ class TenantConfiguration extends JsonSerializableObject
return $this->stores;
}
public function preview(): TenantPreview {
return $this->preview;
}
}
+83
View File
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace KTXC\Models\Tenant;
use InvalidArgumentException;
use KTXF\Json\JsonSerializableObject;
final class TenantPreview extends JsonSerializableObject
{
protected bool $enabled = true;
protected string $store = 'previews';
protected TenantPreviewGeneration $generation;
protected TenantPreviewVariants $variants;
public function __construct()
{
$this->generation = new TenantPreviewGeneration();
$this->variants = new TenantPreviewVariants();
}
public function jsonDeserialize(array|string $data): static
{
if (is_string($data)) {
$data = json_decode($data, true, flags: JSON_THROW_ON_ERROR);
}
if (array_key_exists('enabled', $data)) {
if (!is_bool($data['enabled'])) {
throw new InvalidArgumentException('Preview enabled must be a boolean');
}
$this->enabled = $data['enabled'];
}
if (array_key_exists('store', $data)) {
if (!is_string($data['store']) || preg_match('/^[a-z][a-z0-9-]*$/', $data['store']) !== 1) {
throw new InvalidArgumentException('Preview store must be a logical system-store name');
}
$this->store = $data['store'];
}
if (array_key_exists('generation', $data)) {
if (!is_array($data['generation'])) {
throw new InvalidArgumentException('Preview generation configuration must be an object');
}
$this->generation->jsonDeserialize($data['generation']);
}
if (array_key_exists('variants', $data)) {
if (!is_array($data['variants'])) {
throw new InvalidArgumentException('Preview variants configuration must be an object');
}
$this->variants->jsonDeserialize($data['variants']);
}
if ($this->enabled && $this->variants->all() === []) {
throw new InvalidArgumentException('At least one preview variant is required when previews are enabled');
}
return $this;
}
public function enabled(): bool
{
return $this->enabled;
}
public function store(): string
{
return $this->store;
}
public function generation(): TenantPreviewGeneration
{
return $this->generation;
}
public function variants(): TenantPreviewVariants
{
return $this->variants;
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace KTXC\Models\Tenant;
use InvalidArgumentException;
use KTXF\Json\JsonSerializableObject;
final class TenantPreviewGeneration extends JsonSerializableObject
{
public const MODE_LAZY = 'lazy';
public const MODE_EAGER_WITH_LAZY_FALLBACK = 'eager-with-lazy-fallback';
protected string $mode = self::MODE_EAGER_WITH_LAZY_FALLBACK;
protected int $maxSourceSize = 104857600;
protected int $timeoutSeconds = 30;
protected int $retryDelaySeconds = 3600;
public function jsonDeserialize(array|string $data): static
{
if (is_string($data)) {
$data = json_decode($data, true, flags: JSON_THROW_ON_ERROR);
}
if (array_key_exists('mode', $data)) {
if (!is_string($data['mode']) || !in_array($data['mode'], [
self::MODE_LAZY,
self::MODE_EAGER_WITH_LAZY_FALLBACK,
], true)) {
throw new InvalidArgumentException('Unsupported preview generation mode');
}
$this->mode = $data['mode'];
}
foreach ([
'maxSourceSize' => 'maximum source size',
'timeoutSeconds' => 'timeout',
'retryDelaySeconds' => 'retry delay',
] as $property => $label) {
if (!array_key_exists($property, $data)) {
continue;
}
if (!is_int($data[$property]) || $data[$property] < 1) {
throw new InvalidArgumentException("Preview {$label} must be a positive integer");
}
$this->$property = $data[$property];
}
return $this;
}
public function mode(): string
{
return $this->mode;
}
public function maxSourceSize(): int
{
return $this->maxSourceSize;
}
public function timeoutSeconds(): int
{
return $this->timeoutSeconds;
}
public function retryDelaySeconds(): int
{
return $this->retryDelaySeconds;
}
}
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace KTXC\Models\Tenant;
use InvalidArgumentException;
use KTXF\Preview\MimeType;
final readonly class TenantPreviewVariant
{
public string $format;
public function __construct(
public int $width,
public int $height,
string $format,
public int $quality,
) {
if ($this->width < 1 || $this->height < 1) {
throw new InvalidArgumentException('Preview variant dimensions must be positive');
}
if ($this->quality < 1 || $this->quality > 100) {
throw new InvalidArgumentException('Preview variant quality must be between 1 and 100');
}
$this->format = MimeType::normalize($format);
}
public static function fromArray(array $data, ?self $defaults = null): self
{
$width = $data['width'] ?? $defaults?->width;
$height = $data['height'] ?? $defaults?->height;
$format = $data['format'] ?? $defaults?->format;
$quality = $data['quality'] ?? $defaults?->quality;
if (!is_int($width) || !is_int($height) || !is_string($format) || !is_int($quality)) {
throw new InvalidArgumentException('Preview variants require integer dimensions and quality plus a MIME format');
}
return new self($width, $height, $format, $quality);
}
/** @return array{width:int,height:int,format:string,quality:int} */
public function toArray(): array
{
return [
'width' => $this->width,
'height' => $this->height,
'format' => $this->format,
'quality' => $this->quality,
];
}
}
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace KTXC\Models\Tenant;
use InvalidArgumentException;
use KTXF\Json\JsonSerializableObject;
final class TenantPreviewVariants extends JsonSerializableObject
{
/** @var array<string, TenantPreviewVariant> */
private array $entries;
public function __construct()
{
$this->entries = self::defaults();
}
public function jsonDeserialize(array|string $data): static
{
if (is_string($data)) {
$data = json_decode($data, true, flags: JSON_THROW_ON_ERROR);
}
$defaults = self::defaults();
$entries = [];
foreach ($data as $name => $variant) {
if (!is_string($name) || preg_match('/^[a-z][a-z0-9-]*$/', $name) !== 1) {
throw new InvalidArgumentException('Invalid preview variant name');
}
if (!is_array($variant)) {
throw new InvalidArgumentException('Preview variant configuration must be an object');
}
$entries[$name] = TenantPreviewVariant::fromArray($variant, $defaults[$name] ?? null);
}
$this->entries = $entries;
return $this;
}
public function variant(string $name): ?TenantPreviewVariant
{
return $this->entries[$name] ?? null;
}
/** @return array<string, TenantPreviewVariant> */
public function all(): array
{
return $this->entries;
}
public function jsonSerialize(): array
{
return array_map(
static fn(TenantPreviewVariant $variant): array => $variant->toArray(),
$this->entries,
);
}
/** @return array<string, TenantPreviewVariant> */
private static function defaults(): array
{
return [
'popover' => new TenantPreviewVariant(640, 440, 'image/webp', 80),
'thumbnail' => new TenantPreviewVariant(256, 256, 'image/webp', 75),
];
}
}