feat: ✨ add VO and Enums to works on Coupon/CampaignRequest

This commit is contained in:
K Muller 2026-09-21 14:24:37 +02:00 • committed by Corentin Huet
parent 32a9a83b5b
commit 0f39559ff9
7 changed files with 349 additions and 0 deletions

View file

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Domain\Enums;
use Shared\Enums\BaseEnum;
enum Country: int
{
/** @use \Shared\Enums\BaseEnum<int> */
use BaseEnum;
case FR = 1;
case BE = 2;
case LU = 3;
/**
* Optional labels definition
*
* @phpstan-ignore-next-line
*/
private const LABELS = [
self::FR->name => 'FR',
self::BE->name => 'BE',
self::LU->name => 'LU',
];
}

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Shared\Enums;
enum Currency: int
{
/** @use BaseEnum<int> */
use BaseEnum;
case EUR = 1;
/**
* Optional labels definition
*
* @phpstan-ignore-next-line
*/
private const LABELS = [
self::EUR->name => 'Euro',
];
}

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Domain\Models\ValueObjects;
use Shared\DDD\Models\Equatable;
use Shared\ValueObjects\Money;
/**
* Wraps a Money value as a requested/expected monetary amount.
*/
final readonly class MoneyAmount implements \Stringable, Equatable
{
public function __construct(
public Money $value,
) {
}
#[\Override]
public function equals(mixed $other): bool
{
return $other instanceof self && $this->value->equals($other->value);
}
public function __toString(): string
{
return (string) $this->value;
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Domain\Models\ValueObjects\RequestedCoupon;
use Shared\ValueObjects\DateTime;
final readonly class Period
{
public function __construct(
public DateTime $start,
public DateTime $end,
) {
}
}

View file

@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace Shared\Enums;
/**
* @template V of string|int
*
* @mixin \BackedEnum
*/
trait BaseEnum
{
private const LABELS_CONSTANT_NAME = 'LABELS';
/**
* @return V
*/
public function value(): string | int
{
return $this->value;
}
public function label(): string
{
return self::toLabels()[$this->name] ?? ucfirst(strtolower($this->name));
}
public function name(): string
{
return $this->name;
}
/**
* @return array<string,string>
*/
public static function toLabels(): array
{
static $cache = null;
if ($cache !== null) {
return $cache;
}
$labels = defined(self::class . '::' . self::LABELS_CONSTANT_NAME)
? constant(self::class . '::' . self::LABELS_CONSTANT_NAME)
: null;
$result = [];
foreach (self::cases() as $enum) {
if ($labels !== null && ! isset($labels[$enum->name])) {
throw new \Error('Missing definition label ' . self::class . " for '{$enum->name}'");
}
$result[$enum->name] = $labels[$enum->name] ?? ucfirst(strtolower($enum->name));
}
return $cache = $result;
}
/**
* @return array<string, V>
*/
public static function toValues(): array
{
static $cache = null;
if ($cache !== null) {
return $cache;
}
return $cache = array_column(
self::cases(),
'value',
'name'
);
}
/**
* @param self|V $value
*/
public static function make(self | int | string $value, string $field = 'value'): ?self
{
if ($value instanceof self) {
return $value;
}
if ($field === 'value') {
$tryValue = null;
try {
$tryValue = self::tryFrom(intval($value));
} catch (\ValueError) {
$tryValue = self::tryFrom(strval($value));
} finally {
if ($tryValue !== null) {
return $tryValue;
}
}
}
if ($field === 'label') {
$normalizedSearch = preg_replace('/[\W_]+/u', '', mb_strtolower((string) $value));
foreach (self::cases() as $enum) {
$normalizedLabel = preg_replace('/[\W_]+/u', '', mb_strtolower($enum->label()));
if ($normalizedLabel === $normalizedSearch) {
return $enum;
}
}
return null;
}
foreach (self::cases() as $enum) {
if ($enum->{$field} === $value) {
return $enum;
}
}
return null;
}
public function equals(mixed $other): bool
{
return $this === $other;
}
public function jsonSerialize(): mixed
{
return $this->value;
}
// * PRIVATE
/**
* @return array<string, array<string, mixed>>
*/
private static function getCustomDefinitions(): array
{
static $cache = null;
if ($cache !== null) {
return $cache;
}
$names = array_column(self::cases(), 'name', 'name');
$filteredConstants = [];
foreach ((new \ReflectionClass(self::class))->getConstants() as $key => $constant) {
if ($key === self::LABELS_CONSTANT_NAME || ! \is_array($constant)) {
continue;
}
if (! empty(array_intersect_key($constant, $names))) {
$filteredConstants[$key] = $constant;
}
}
return $cache = $filteredConstants;
}
}

View file

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Shared\ValueObjects;
use Shared\DDD\Models\Equatable;
use Shared\Helpers\DatesHelper;
/**
* Represents a valid point in time, always normalized to UTC.
*/
final readonly class DateTime implements \Stringable, \JsonSerializable, Equatable
{
public \DateTimeImmutable $value;
/**
* @param \DateTimeInterface|string $value A DateTimeInterface instance or a parsable date string.
* @throws \DomainException
*/
public function __construct(
\DateTimeInterface | string $value,
) {
$dateTime = is_string($value) ? DatesHelper::parseDateTime($value) : $value;
$this->value = \DateTimeImmutable::createFromInterface($dateTime)
->setTimezone(new \DateTimeZone('UTC'));
}
public static function now(): self
{
return new self(DatesHelper::now());
}
#[\Override]
public function equals(mixed $other): bool
{
return $other instanceof self
&& $this->value->format(\DateTimeInterface::RFC3339_EXTENDED) === $other->value->format(\DateTimeInterface::RFC3339_EXTENDED);
}
public function __toString(): string
{
return DatesHelper::toRfc3339($this->value);
}
public function jsonSerialize(): string
{
return (string) $this;
}
}

View file

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Shared\ValueObjects;
use Shared\DDD\Models\Equatable;
use Shared\Enums\Currency;
/**
* Represents a monetary amount, expressed as an integer in the currency's
* smallest unit (e.g. cents).
*/
final readonly class Money implements \Stringable, Equatable
{
/**
* @param Currency $currency Currency of the amount.
* @param int $value Amount expressed in the currency's smallest unit (e.g. cents). Must be zero or positive.
* @throws \DomainException
*/
public function __construct(
public Currency $currency,
public int $value,
) {
if ($this->value < 0) {
throw new \DomainException('Money value cannot be negative.');
}
}
#[\Override]
public function equals(mixed $other): bool
{
return $other instanceof self
&& $this->currency->equals($other->currency)
&& $this->value === $other->value;
}
public function __toString(): string
{
return sprintf('%d %s', $this->value, $this->currency->name());
}
}