Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/dynamic-consistency-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ final class GuestIsCheckedOut
}
}
```
:::tip
A tagged property can also be an array. Each element becomes its own tag, with the same prefix and hash.
For example, `#[EventTag(prefix: 'guest')] public readonly array $guestNames` creates a `guest:` tag for every guest.
:::

:::note
You can find out more about [events](events.md).
:::
Expand Down
66 changes: 43 additions & 23 deletions src/Serializer/AttributeEventTagExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use function array_keys;
use function array_map;
use function hash;
use function is_array;
use function is_int;
use function is_string;
use function strval;
Expand All @@ -37,38 +38,57 @@
$attribute = $attributes[0]->newInstance();

$value = $property->getValue($event);
$values = is_array($value) ? $value : [$value];

if ($value === null) {
continue;
}
foreach ($values as $item) {
$tag = $this->tag($event, $property->getName(), $item, $attribute->prefix, $attribute->hash);

if ($value instanceof Stringable || is_int($value)) {
$value = (string)$value;
}
if ($tag === null) {
continue;

Check warning on line 47 in src/Serializer/AttributeEventTagExtractor.php

View workflow job for this annotation

GitHub Actions / Mutation tests on diff (locked, 8.5, ubuntu-latest)

Escaped Mutant for Mutator "Continue_": @@ @@ $tag = $this->tag($event, $property->getName(), $item, $attribute->prefix, $attribute->hash); if ($tag === null) { - continue; + break; } $tags[$tag] = true;
}

if ($value instanceof Identifier) {
$value = $value->toString();
$tags[$tag] = true;

Check warning on line 50 in src/Serializer/AttributeEventTagExtractor.php

View workflow job for this annotation

GitHub Actions / Mutation tests on diff (locked, 8.5, ubuntu-latest)

Escaped Mutant for Mutator "TrueValue": @@ @@ continue; } - $tags[$tag] = true; + $tags[$tag] = false; } }
}
}

if (!is_string($value)) {
throw EventTagExtractorError::invalidValueType(
$event::class,
$property->getName(),
$value,
);
}
return array_map(strval(...), array_keys($tags));
}

if ($attribute->hash) {
$value = hash($attribute->hash, $value);
}
private function tag(
object $event,
string $property,
mixed $value,
string|null $prefix,
string|null $hash,
): string|null {
if ($value === null) {
return null;
}

if ($attribute->prefix) {
$value = $attribute->prefix . ':' . $value;
}
if ($value instanceof Stringable || is_int($value)) {
$value = (string)$value;
}

$tags[$value] = true;
if ($value instanceof Identifier) {
$value = $value->toString();
}

return array_map(strval(...), array_keys($tags));
if (!is_string($value)) {
throw EventTagExtractorError::invalidValueType(
$event::class,
$property,
$value,
);
}

if ($hash) {
$value = hash($hash, $value);
}

if ($prefix) {
$value = $prefix . ':' . $value;
}

return $value;
}
}
81 changes: 80 additions & 1 deletion tests/Unit/Serializer/AttributeEventTagExtractorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use PHPUnit\Framework\TestCase;
use Stringable;

use function hash;
use function sprintf;

#[CoversClass(AttributeEventTagExtractor::class)]
Expand Down Expand Up @@ -175,7 +176,46 @@ public function __construct(
self::assertSame(['foo'], $tags);
}

public function testExtractInvalidValueType(): void
public function testExtractArray(): void
{
$extractor = new AttributeEventTagExtractor();

$event = new class (['1', 2, new CustomId('3'), null, '1']) {
/** @param list<string|int|CustomId|null> $productIds */
public function __construct(
#[EventTag(prefix: 'product')]
public array $productIds,
) {
}
};

$tags = $extractor->extract($event);

self::assertSame(['product:1', 'product:2', 'product:3'], $tags);
}

public function testExtractArrayWithHash(): void
{
$extractor = new AttributeEventTagExtractor();

$event = new class (['foo', 'bar']) {
/** @param list<string> $emails */
public function __construct(
#[EventTag(prefix: 'email', hash: 'sha256')]
public array $emails,
) {
}
};

$tags = $extractor->extract($event);

self::assertSame(
['email:' . hash('sha256', 'foo'), 'email:' . hash('sha256', 'bar')],
$tags,
);
}

public function testExtractEmptyArray(): void
{
$extractor = new AttributeEventTagExtractor();

Expand All @@ -188,6 +228,22 @@ public function __construct(
}
};

self::assertSame([], $extractor->extract($event));
}

public function testExtractNestedArrayIsInvalid(): void
{
$extractor = new AttributeEventTagExtractor();

$event = new class ([['foo']]) {
/** @param list<list<string>> $items */
public function __construct(
#[EventTag]
public array $items,
) {
}
};

$this->expectException(EventTagExtractorError::class);
$this->expectExceptionMessage(
sprintf(
Expand All @@ -198,4 +254,27 @@ public function __construct(

$extractor->extract($event);
}

public function testExtractInvalidValueType(): void
{
$extractor = new AttributeEventTagExtractor();

$event = new class (1.5) {
public function __construct(
#[EventTag]
public float $value,
) {
}
};

$this->expectException(EventTagExtractorError::class);
$this->expectExceptionMessage(
sprintf(
'Event tag value for property "value" in class "%s" must be stringable, float given',
$event::class,
),
);

$extractor->extract($event);
}
}
Loading