Skip to content

Commit 137f786

Browse files
feat: add Identifier::includes()
1 parent d850fa8 commit 137f786

File tree

8 files changed

+159
-113
lines changed

8 files changed

+159
-113
lines changed

src/Domain/Identifiers.php

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@ final class Identifiers
1313
public function __construct(array $identifiers)
1414
{
1515
foreach ($identifiers as $key => $value) {
16-
if (!is_string($key) || !is_string($value)) {
17-
throw new InvalidArgumentException('Keys and values must be strings.');
18-
}
16+
$this->assertKeyIsString($key);
17+
$this->assertValueIsStringOrArrayOfStrings($value);
1918
}
2019
$this->identifiers = $identifiers;
2120
}
2221

23-
public function with(string $name, string $value): self
22+
public function with(string $name, string|array $value): self
2423
{
24+
$this->assertValueIsStringOrArrayOfStrings($value);
2525
$clone = clone $this;
2626
$clone->identifiers[$name] = $value;
2727
return $clone;
@@ -31,4 +31,26 @@ public function toArray(): array
3131
{
3232
return $this->identifiers;
3333
}
34+
35+
private function assertKeyIsString(mixed $key): void
36+
{
37+
if (!is_string($key)) {
38+
throw new InvalidArgumentException('Key must be a string.');
39+
}
40+
}
41+
42+
private function assertValueIsStringOrArrayOfStrings(mixed $value): void
43+
{
44+
if (is_string($value)) {
45+
return;
46+
}
47+
if (
48+
is_array($value)
49+
&& array_is_list($value)
50+
&& empty(array_filter($value, fn ($item) => !is_string($item)))
51+
) {
52+
return;
53+
}
54+
throw new InvalidArgumentException('Value must be a string or an array of strings.');
55+
}
3456
}

src/EventStore/Query/Identifier.php

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,39 @@ class Identifier implements QueryInterface
1414

1515
private bool $negative;
1616

17-
private function __construct(string $name, bool $negative, string|int ...$values)
17+
private bool $includes;
18+
19+
private function __construct(string $name, bool $negative, bool $includes, string|int ...$values)
1820
{
1921
$this->name = $name;
2022
$this->negative = $negative;
23+
$this->includes = $includes;
2124
$this->values = $values;
2225
}
2326

2427
public static function is(string $name, string|int $value): QueryInterface
2528
{
26-
return new self($name, false, $value);
29+
return new self($name, false, false, $value);
2730
}
2831

2932
public static function isNot(string $name, string|int $value): QueryInterface
3033
{
31-
return new self($name, true, $value);
34+
return new self($name, true, false, $value);
35+
}
36+
37+
public static function includes(string $name, string|int $value): QueryInterface
38+
{
39+
return new self($name, false, true, $value);
3240
}
3341

3442
public static function in(string $name, string|int ...$values): QueryInterface
3543
{
36-
return new self($name, false, ...$values);
44+
return new self($name, false, false, ...$values);
3745
}
3846

3947
public static function notIn(string $name, string|int ...$values): QueryInterface
4048
{
41-
return new self($name, true, ...$values);
49+
return new self($name, true, false, ...$values);
4250
}
4351

4452
public function getName(): string
@@ -55,4 +63,9 @@ public function isNegative(): bool
5563
{
5664
return $this->negative;
5765
}
66+
67+
public function isIncludes(): bool
68+
{
69+
return $this->includes;
70+
}
5871
}

src/PdoEventStore/Driver.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Backslash\Domain\RecordedEventStream;
99
use Backslash\EventStore\Query\QueryInterface;
1010
use Backslash\Serializer\SerializerInterface;
11+
use UnexpectedValueException;
1112

1213
enum Driver: string
1314
{
@@ -87,6 +88,17 @@ public function buildJsonExtractStatement(string $column, string $field): string
8788
};
8889
}
8990

91+
public function buildJsonArrayIncludesStatement(string $column, string $path): string
92+
{
93+
if (preg_match('/[^a-zA-Z0-9_]/', $path) !== 0) {
94+
throw new UnexpectedValueException('$path must contain only letters, numbers or underscores.');
95+
}
96+
return match ($this) {
97+
self::MYSQL => sprintf('JSON_SEARCH(`%s`, "one", ?, "", "$.%s") IS NOT NULL', $column, $path),
98+
self::SQLITE => sprintf('EXISTS (SELECT 1 FROM JSON_EACH(`%s`, "$.%s") WHERE `value` = ?)', $column, $path),
99+
};
100+
}
101+
90102
public function buildInsertStatementAndValues(
91103
RecordedEventStream $stream,
92104
?QueryInterface $concurrencyCheck,

src/PdoEventStore/QueryToWhereClause.php

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,22 @@ private function resolve(): void
7272
break;
7373
case (Identifier::class):
7474
/** @var Identifier $query */
75-
$statement = sprintf(
76-
'%s %s (%s)',
77-
$this->driver->buildJsonExtractStatement(
75+
if ($query->isIncludes()) {
76+
$statement = $this->driver->buildJsonArrayIncludesStatement(
7877
$this->config->getAlias('event_identifiers'),
7978
$query->getName(),
80-
),
81-
$query->isNegative() ? 'NOT IN' : 'IN',
82-
implode(', ', array_fill(0, count($query->getValues()), '?')),
83-
);
79+
);
80+
} else {
81+
$statement = sprintf(
82+
'%s %s (%s)',
83+
$this->driver->buildJsonExtractStatement(
84+
$this->config->getAlias('event_identifiers'),
85+
$query->getName(),
86+
),
87+
$query->isNegative() ? 'NOT IN' : 'IN',
88+
implode(', ', array_fill(0, count($query->getValues()), '?')),
89+
);
90+
}
8491
$this->values = array_merge($this->values, $query->getValues());
8592
break;
8693
case (Metadata::class):

tests/Domain/IdentifiersTest.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Backslash\Domain;
6+
7+
use InvalidArgumentException;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class IdentifiersTest extends TestCase
11+
{
12+
/** @test */
13+
public function key_and_value_must_have_valid_type(): void
14+
{
15+
$fixtures = [
16+
[['foo' => 'bar'], null],
17+
[['foo' => 'bar', 'baz' => ['foo', 'bar']], null],
18+
[['foo' => []], null],
19+
[['foo' => 'bar', 'baz' => ['foo' => '1', 'bar' => '2']], 'Value must be a string or an array of strings.'],
20+
[['foo' => 1], 'Value must be a string or an array of strings.'],
21+
[['foo' => 1.1], 'Value must be a string or an array of strings.'],
22+
[['foo' => true], 'Value must be a string or an array of strings.'],
23+
[['foo' => null], 'Value must be a string or an array of strings.'],
24+
];
25+
26+
foreach ($fixtures as $fixture) {
27+
try {
28+
new Identifiers($fixture[0]);
29+
if ($fixture[1]) {
30+
$this->fail('Exception was not thrown.');
31+
}
32+
} catch (InvalidArgumentException $e) {
33+
$this->assertEquals($fixture[1], $e->getMessage());
34+
}
35+
}
36+
}
37+
}

tests/PdoEventStore/PdoEventStoreTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Backslash\EventStore\Query\Metadata as MetadataQuery;
1818
use Backslash\EventStore\Query\Sequence;
1919
use Backslash\Shared\Event\StudentNameChangedEvent;
20+
use Backslash\Shared\Event\StudentPreferredColorChangedEvent;
2021
use Backslash\Shared\Event\StudentRegisteredEvent;
2122
use Backslash\Shared\PdoEventStore\InMemorySqlitePdoEventStoreFactory;
2223
use DateTimeImmutable;
@@ -45,6 +46,7 @@ public function it_stores_and_finds_stream(): void
4546
RecordedEvent::create(new StudentRegisteredEvent('1', 'John'), new Metadata(), Clock::now()),
4647
RecordedEvent::create(new StudentRegisteredEvent('2', 'Mary'), new Metadata(), Clock::now()),
4748
RecordedEvent::create(new StudentNameChangedEvent('2', 'Mary', 'Anna'), new Metadata(), Clock::now()),
49+
RecordedEvent::create(new StudentPreferredColorChangedEvent('1', ['blue', 'green']), new Metadata(), Clock::now()),
4850
),
4951
null,
5052
null,
@@ -53,6 +55,14 @@ public function it_stores_and_finds_stream(): void
5355
$events = $this->store->fetch($query);
5456
$this->assertCount(1, $events);
5557

58+
$query = Identifier::includes('colors', 'red');
59+
$events = $this->store->fetch($query);
60+
$this->assertCount(0, $events);
61+
62+
$query = Identifier::includes('colors', 'blue');
63+
$events = $this->store->fetch($query);
64+
$this->assertCount(1, $events);
65+
5666
$this->store->purge();
5767
$events = $this->store->fetch(null);
5868
$this->assertCount(0, $events);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Backslash\Shared\Event;
6+
7+
use Backslash\Domain\EventInterface;
8+
use Backslash\Domain\Identifiers;
9+
use Backslash\Domain\ToArrayTrait;
10+
11+
class StudentPreferredColorChangedEvent implements EventInterface
12+
{
13+
use ToArrayTrait;
14+
15+
private string $studentId;
16+
17+
private array $colors;
18+
19+
public function __construct(string $studentId, array $colors)
20+
{
21+
$this->studentId = $studentId;
22+
$this->colors = $colors;
23+
}
24+
25+
public function getIdentifiers(): Identifiers
26+
{
27+
return new Identifiers([
28+
'studentId' => $this->studentId,
29+
'colors' => $this->colors,
30+
]);
31+
}
32+
33+
public function getStudentId(): string
34+
{
35+
return $this->studentId;
36+
}
37+
38+
public function getColors(): array
39+
{
40+
return $this->colors;
41+
}
42+
}

tests/Shared/State/CourseSubscriptionState.php

Lines changed: 0 additions & 97 deletions
This file was deleted.

0 commit comments

Comments
 (0)