Skip to content

Commit 6592e72

Browse files
author
codeliner
committed
Add method to filter partial documents
Also make sure that filterDocs and filterPartialDocs return a \Traversable that provides docId as key.
1 parent f5d9caa commit 6592e72

File tree

5 files changed

+437
-21
lines changed

5 files changed

+437
-21
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.idea
22
composer.lock
3-
vendor
3+
vendor
4+
.php_cs.cache

src/DocumentStore.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,23 @@ public function getDoc(string $collectionName, string $docId): ?array;
132132
* @param int|null $skip
133133
* @param int|null $limit
134134
* @param OrderBy|null $orderBy
135-
* @return \Traversable list of docs
135+
* @return \Traversable list of docs with key being the docId and value being the stored doc
136136
* @throws UnknownCollection
137137
*/
138138
public function filterDocs(string $collectionName, Filter $filter, int $skip = null, int $limit = null, OrderBy $orderBy = null): \Traversable;
139139

140+
/**
141+
* @param string $collectionName
142+
* @param PartialSelect $partialSelect
143+
* @param Filter $filter
144+
* @param int|null $skip
145+
* @param int|null $limit
146+
* @param OrderBy|null $orderBy
147+
* @return \Traversable list of docs with key being the docId and value being the stored doc
148+
* @throws UnknownCollection
149+
*/
150+
public function filterPartialDocs(string $collectionName, PartialSelect $partialSelect, Filter $filter, int $skip = null, int $limit = null, OrderBy $orderBy = null): \Traversable;
151+
140152
/**
141153
* @param string $collectionName
142154
* @param Filter $filter

src/InMemoryDocumentStore.php

Lines changed: 113 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,18 @@
1515
use EventEngine\DocumentStore\Exception\RuntimeException;
1616
use EventEngine\DocumentStore\Exception\UnknownCollection;
1717
use EventEngine\DocumentStore\Filter\AndFilter;
18-
use EventEngine\DocumentStore\Filter\AnyFilter;
1918
use EventEngine\DocumentStore\Filter\EqFilter;
2019
use EventEngine\DocumentStore\Filter\Filter;
2120
use EventEngine\DocumentStore\OrderBy\AndOrder;
2221
use EventEngine\DocumentStore\OrderBy\Asc;
2322
use EventEngine\DocumentStore\OrderBy\Desc;
2423
use EventEngine\DocumentStore\OrderBy\OrderBy;
2524
use EventEngine\Persistence\InMemoryConnection;
25+
use function array_key_exists;
26+
use function count;
27+
use function explode;
28+
use function is_array;
29+
use function json_encode;
2630

2731
final class InMemoryDocumentStore implements DocumentStore
2832
{
@@ -304,7 +308,7 @@ public function filterDocs(
304308

305309
foreach ($this->inMemoryConnection['documents'][$collectionName] as $docId => $doc) {
306310
if ($filter->match($doc, (string)$docId)) {
307-
$filteredDocs[$docId] = $doc;
311+
$filteredDocs[$docId] = ['doc' => $doc, 'docId' => $docId];
308312
}
309313
}
310314

@@ -320,7 +324,61 @@ public function filterDocs(
320324
$filteredDocs = \array_slice($filteredDocs, 0, $limit);
321325
}
322326

323-
return new \ArrayIterator($filteredDocs);
327+
$docsMap = [];
328+
329+
foreach ($filteredDocs as $docAndId) {
330+
$docsMap[$docAndId['docId']] = $docAndId['doc'];
331+
}
332+
333+
return new \ArrayIterator($docsMap);
334+
}
335+
336+
/**
337+
* @param string $collectionName
338+
* @param PartialSelect $partialSelect
339+
* @param Filter $filter
340+
* @param int|null $skip
341+
* @param int|null $limit
342+
* @param OrderBy|null $orderBy
343+
* @return \Traversable list of docs
344+
*/
345+
public function filterPartialDocs(
346+
string $collectionName,
347+
PartialSelect $partialSelect,
348+
Filter $filter,
349+
int $skip = null,
350+
int $limit = null,
351+
OrderBy $orderBy = null): \Traversable
352+
{
353+
$this->assertHasCollection($collectionName);
354+
355+
$filteredDocs = [];
356+
357+
foreach ($this->inMemoryConnection['documents'][$collectionName] as $docId => $doc) {
358+
if ($filter->match($doc, (string)$docId)) {
359+
$filteredDocs[$docId] = ['doc' => $doc, 'docId' => $docId];
360+
}
361+
}
362+
363+
$filteredDocs = \array_values($filteredDocs);
364+
365+
if ($orderBy !== null) {
366+
$this->sort($filteredDocs, $orderBy);
367+
}
368+
369+
if ($skip !== null) {
370+
$filteredDocs = \array_slice($filteredDocs, $skip, $limit);
371+
} elseif ($limit !== null) {
372+
$filteredDocs = \array_slice($filteredDocs, 0, $limit);
373+
}
374+
375+
$docsMap = [];
376+
377+
foreach ($filteredDocs as $docAndId) {
378+
$docsMap[$docAndId['docId']] = $this->transformToPartialDoc($docAndId['doc'], $partialSelect);
379+
}
380+
381+
return new \ArrayIterator($docsMap);
324382
}
325383

326384
/**
@@ -344,6 +402,23 @@ public function filterDocIds(
344402
return $docIds;
345403
}
346404

405+
/**
406+
* @inheritDoc
407+
*/
408+
public function countDocs(string $collectionName, Filter $filter) : int
409+
{
410+
$this->assertHasCollection($collectionName);
411+
412+
$counter = 0;
413+
foreach ($this->inMemoryConnection['documents'][$collectionName] as $docId => $doc) {
414+
if ($filter->match($doc, $docId)) {
415+
$counter++;
416+
}
417+
}
418+
419+
return $counter;
420+
}
421+
347422
private function hasDoc(string $collectionName, string $docId): bool
348423
{
349424
if (! $this->hasCollection($collectionName)) {
@@ -489,12 +564,12 @@ private function sort(&$docs, OrderBy $orderBy)
489564
if ($orderBy instanceof Asc || $orderBy instanceof Desc) {
490565
$field = $orderBy->prop();
491566

492-
return (new ArrayReader($doc))->mixedValue($field);
567+
return (new ArrayReader($doc['doc']))->mixedValue($field);
493568
}
494569

495570
throw new \RuntimeException(\sprintf(
496571
'Unable to get field from doc: %s. Given OrderBy is neither an instance of %s nor %s',
497-
\json_encode($doc),
572+
\json_encode($doc['doc']),
498573
Asc::class,
499574
Desc::class
500575
));
@@ -573,20 +648,43 @@ private function isSequentialArray(array $array): bool
573648
return \array_keys($array) === \range(0, \count($array) - 1);
574649
}
575650

576-
/**
577-
* @inheritDoc
578-
*/
579-
public function countDocs(string $collectionName, Filter $filter) : int
651+
private function transformToPartialDoc(array $doc, PartialSelect $partialSelect): array
580652
{
581-
$this->assertHasCollection($collectionName);
653+
$partialDoc = [];
654+
$reader = new ArrayReader($doc);
582655

583-
$counter = 0;
584-
foreach ($this->inMemoryConnection['documents'][$collectionName] as $docId => $doc) {
585-
if ($filter->match($doc, $docId)) {
586-
$counter++;
656+
foreach ($partialSelect->fieldAliasMap() as ['field' => $field, 'alias' => $alias]) {
657+
$value = $reader->mixedValue($field);
658+
659+
if($alias === PartialSelect::MERGE_ALIAS) {
660+
if(null === $value) {
661+
continue;
662+
}
663+
664+
if(!is_array($value)) {
665+
throw new RuntimeException('Merge not possible. $merge alias was specified for field: ' . $field . ' but field value is not an array: ' . json_encode($value));
666+
}
667+
668+
foreach ($value as $k => $v) {
669+
$partialDoc[$k] = $v;
670+
}
671+
672+
continue;
587673
}
674+
675+
$keys = explode('.', $alias);
676+
677+
$ref = &$partialDoc;
678+
foreach ($keys as $i => $key) {
679+
if(!array_key_exists($key, $ref)) {
680+
$ref[$key] = [];
681+
}
682+
$ref = &$ref[$key];
683+
}
684+
$ref = $value;
685+
unset($ref);
588686
}
589687

590-
return $counter;
688+
return $partialDoc;
591689
}
592690
}

src/PartialSelect.php

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace EventEngine\DocumentStore;
5+
6+
use EventEngine\DocumentStore\Exception\RuntimeException;
7+
use function get_class;
8+
use function gettype;
9+
use function is_int;
10+
use function is_object;
11+
use function is_string;
12+
13+
/**
14+
* Class PartialSelect
15+
*
16+
* You can pass a list of fields to PartialSelect which should be included in a partial document loaded from the
17+
* document store. The list can contain a mapping from alias to field or only the field.
18+
*
19+
* You can access nested fields using dot notation. Same applies for aliases.
20+
*
21+
* @example
22+
*
23+
* $partialSelect = new PartialSelect([
24+
* 'topLevelField',
25+
* 'aliasName' => 'anotherField',
26+
* 'nested.alias' => 'nested.field',
27+
* ]);
28+
*
29+
* Resulting partial document:
30+
*
31+
* [
32+
* 'topLevelField' => 'some value',
33+
* 'aliasName' => 'another value',
34+
* 'nested' => [
35+
* 'alias' => 'nested value'
36+
* ]
37+
* ]
38+
*
39+
* In case a field does not exist in the document, it is set to NULL in the resulting partial document.
40+
*
41+
* A special "$merge" alias allows to merge all nested fields from the original field into top level partial document.
42+
*
43+
* @example
44+
*
45+
* Original document:
46+
*
47+
* [
48+
* 'topLevelField' => 'some value',
49+
* 'nested' => [
50+
* 'subField' => 'nested value'
51+
* ]
52+
* ]
53+
*
54+
* $partialSelect = new PartialSelect([
55+
* '$merge' => 'nested',
56+
* 'topLevelField'
57+
* ]);
58+
*
59+
* Resulting partial document:
60+
*
61+
* [
62+
* 'subField' => 'nested value'
63+
* 'topLevelField' => 'some value',
64+
* ]
65+
*
66+
* @package EventEngine\DocumentStore
67+
*/
68+
final class PartialSelect
69+
{
70+
public const MERGE_ALIAS = '$merge';
71+
72+
/**
73+
* @var array<array-key, array{field: string, alias: string}>
74+
*/
75+
private $fields;
76+
77+
public function __construct(array $fieldList)
78+
{
79+
$this->populateFieldList($fieldList);
80+
}
81+
82+
public function withField(string $field): PartialSelect
83+
{
84+
$clone = clone $this;
85+
$clone->fields[] = [
86+
'field' => $field,
87+
'alias' => $field,
88+
];
89+
return $clone;
90+
}
91+
92+
public function withFieldAlias(string $field, string $alias): PartialSelect
93+
{
94+
$clone = clone $this;
95+
$clone->fields[] = [
96+
'field' => $field,
97+
'alias' => $alias,
98+
];
99+
return $clone;
100+
}
101+
102+
public function withMergedField(string $field): PartialSelect
103+
{
104+
$clone = clone $this;
105+
$clone->fields[] = [
106+
'field' => $field,
107+
'alias' => self::MERGE_ALIAS,
108+
];
109+
return $clone;
110+
}
111+
112+
/**
113+
* @return array<array-key, array{field: string, alias: string}>
114+
*/
115+
public function fieldAliasMap(): array
116+
{
117+
return $this->fields;
118+
}
119+
120+
private function populateFieldList(array $fieldList): void
121+
{
122+
foreach ($fieldList as $aliasOrIndex => $field) {
123+
if(is_int($aliasOrIndex)) {
124+
$aliasOrIndex = $field;
125+
}
126+
127+
if(!is_string($aliasOrIndex)) {
128+
throw new RuntimeException("Expected field definition to be a string. Got " . (is_object($aliasOrIndex) ? get_class($aliasOrIndex) : gettype($aliasOrIndex)));
129+
}
130+
131+
if(!is_string($field)) {
132+
throw new RuntimeException("Expected field definition to be a string. Got " . (is_object($field) ? get_class($field) : gettype($field)));
133+
}
134+
135+
$this->fields[] = [
136+
'field' => $field,
137+
'alias' => $aliasOrIndex
138+
];
139+
}
140+
}
141+
}

0 commit comments

Comments
 (0)