Skip to content

Commit ce3986e

Browse files
committed
Merge remote-tracking branch 'origin/3.x'
# Conflicts: # src/Database/Adapter/Pool.php # src/Database/Database.php # src/Database/Mirror.php
2 parents a0e2c7d + 6c953e5 commit ce3986e

File tree

7 files changed

+674
-13
lines changed

7 files changed

+674
-13
lines changed

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,42 @@ A list of the utopia/php concepts and their relevant equivalent using the differ
3232

3333
Attribute filters are functions that manipulate attributes before saving them to the database and after retrieving them from the database. You can add filters using the `Database::addFilter($name, $encode, $decode)` where `$name` is the name of the filter that we can add later to attribute `filters` array. `$encode` and `$decode` are the functions used to encode and decode the attribute, respectively. There are also instance-level filters that can only be defined while constructing the `Database` instance. Instance level filters override the static filters if they have the same name.
3434

35+
### Custom Document Types
36+
37+
The database library supports mapping custom document classes to specific collections, enabling a domain-driven design approach. This allows you to create collection-specific classes (like `User`, `Post`, `Product`) that extend the base `Document` class with custom methods and business logic.
38+
39+
```php
40+
// Define a custom document class
41+
class User extends Document
42+
{
43+
public function getEmail(): string
44+
{
45+
return $this->getAttribute('email', '');
46+
}
47+
48+
public function isAdmin(): bool
49+
{
50+
return $this->getAttribute('role') === 'admin';
51+
}
52+
}
53+
54+
// Register the custom type
55+
$database->setDocumentType('users', User::class);
56+
57+
// Now all documents from 'users' collection are User instances
58+
$user = $database->getDocument('users', 'user123');
59+
$email = $user->getEmail(); // Use custom methods
60+
if ($user->isAdmin()) {
61+
// Domain logic
62+
}
63+
```
64+
65+
**Benefits:**
66+
- ✅ Domain-driven design with business logic in domain objects
67+
- ✅ Type safety with IDE autocomplete for custom methods
68+
- ✅ Code organization and encapsulation
69+
- ✅ Fully backwards compatible
70+
3571
### Reserved Attributes
3672

3773
- `$id` - the document unique ID, you can set your own custom ID or a random UID will be generated by the library.

src/Database/Adapter/SQL.php

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3187,7 +3187,18 @@ public function count(Document $collection, array $queries = [], ?int $max = nul
31873187

31883188
$queries = array_map(fn ($query) => clone $query, $queries);
31893189

3190-
$conditions = $this->getSQLConditions($queries, $binds);
3190+
// Extract vector queries (used for ORDER BY) and keep non-vector for WHERE
3191+
$vectorQueries = [];
3192+
$otherQueries = [];
3193+
foreach ($queries as $query) {
3194+
if (in_array($query->getMethod(), Query::VECTOR_TYPES)) {
3195+
$vectorQueries[] = $query;
3196+
} else {
3197+
$otherQueries[] = $query;
3198+
}
3199+
}
3200+
3201+
$conditions = $this->getSQLConditions($otherQueries, $binds);
31913202
if (!empty($conditions)) {
31923203
$where[] = $conditions;
31933204
}
@@ -3205,12 +3216,23 @@ public function count(Document $collection, array $queries = [], ?int $max = nul
32053216
? 'WHERE ' . \implode(' AND ', $where)
32063217
: '';
32073218

3219+
// Add vector distance calculations to ORDER BY (similarity-aware LIMIT)
3220+
$vectorOrders = [];
3221+
foreach ($vectorQueries as $query) {
3222+
$vectorOrder = $this->getVectorDistanceOrder($query, $binds, $alias);
3223+
if ($vectorOrder) {
3224+
$vectorOrders[] = $vectorOrder;
3225+
}
3226+
}
3227+
$sqlOrder = !empty($vectorOrders) ? 'ORDER BY ' . implode(', ', $vectorOrders) : '';
3228+
32083229
$sql = "
32093230
SELECT COUNT(1) as sum FROM (
32103231
SELECT 1
32113232
FROM {$this->getSQLTable($name)} AS {$this->quote($alias)}
3212-
{$sqlWhere}
3213-
{$limit}
3233+
{$sqlWhere}
3234+
{$sqlOrder}
3235+
{$limit}
32143236
) table_count
32153237
";
32163238

@@ -3263,7 +3285,18 @@ public function sum(Document $collection, string $attribute, array $queries = []
32633285

32643286
$queries = array_map(fn ($query) => clone $query, $queries);
32653287

3266-
$conditions = $this->getSQLConditions($queries, $binds);
3288+
// Extract vector queries (used for ORDER BY) and keep non-vector for WHERE
3289+
$vectorQueries = [];
3290+
$otherQueries = [];
3291+
foreach ($queries as $query) {
3292+
if (in_array($query->getMethod(), Query::VECTOR_TYPES)) {
3293+
$vectorQueries[] = $query;
3294+
} else {
3295+
$otherQueries[] = $query;
3296+
}
3297+
}
3298+
3299+
$conditions = $this->getSQLConditions($otherQueries, $binds);
32673300
if (!empty($conditions)) {
32683301
$where[] = $conditions;
32693302
}
@@ -3281,11 +3314,22 @@ public function sum(Document $collection, string $attribute, array $queries = []
32813314
? 'WHERE ' . \implode(' AND ', $where)
32823315
: '';
32833316

3317+
// Add vector distance calculations to ORDER BY (similarity-aware LIMIT)
3318+
$vectorOrders = [];
3319+
foreach ($vectorQueries as $query) {
3320+
$vectorOrder = $this->getVectorDistanceOrder($query, $binds, $alias);
3321+
if ($vectorOrder) {
3322+
$vectorOrders[] = $vectorOrder;
3323+
}
3324+
}
3325+
$sqlOrder = !empty($vectorOrders) ? 'ORDER BY ' . implode(', ', $vectorOrders) : '';
3326+
32843327
$sql = "
32853328
SELECT SUM({$this->quote($attribute)}) as sum FROM (
32863329
SELECT {$this->quote($attribute)}
32873330
FROM {$this->getSQLTable($name)} AS {$this->quote($alias)}
32883331
{$sqlWhere}
3332+
{$sqlOrder}
32893333
{$limit}
32903334
) table_count
32913335
";

src/Database/Database.php

Lines changed: 103 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,12 @@ class Database
416416
*/
417417
protected array $relationshipDeleteStack = [];
418418

419+
/**
420+
* Type mapping for collections to custom document classes
421+
* @var array<string, class-string<Document>>
422+
*/
423+
protected array $documentTypes = [];
424+
419425

420426
/**
421427
* @var Authorization
@@ -1236,6 +1242,79 @@ public function enableLocks(bool $enabled): static
12361242
return $this;
12371243
}
12381244

1245+
/**
1246+
* Set custom document class for a collection
1247+
*
1248+
* @param string $collection Collection ID
1249+
* @param class-string<Document> $className Fully qualified class name that extends Document
1250+
* @return static
1251+
* @throws DatabaseException
1252+
*/
1253+
public function setDocumentType(string $collection, string $className): static
1254+
{
1255+
if (!\class_exists($className)) {
1256+
throw new DatabaseException("Class {$className} does not exist");
1257+
}
1258+
1259+
if (!\is_subclass_of($className, Document::class)) {
1260+
throw new DatabaseException("Class {$className} must extend " . Document::class);
1261+
}
1262+
1263+
$this->documentTypes[$collection] = $className;
1264+
1265+
return $this;
1266+
}
1267+
1268+
/**
1269+
* Get custom document class for a collection
1270+
*
1271+
* @param string $collection Collection ID
1272+
* @return class-string<Document>|null
1273+
*/
1274+
public function getDocumentType(string $collection): ?string
1275+
{
1276+
return $this->documentTypes[$collection] ?? null;
1277+
}
1278+
1279+
/**
1280+
* Clear document type mapping for a collection
1281+
*
1282+
* @param string $collection Collection ID
1283+
* @return static
1284+
*/
1285+
public function clearDocumentType(string $collection): static
1286+
{
1287+
unset($this->documentTypes[$collection]);
1288+
1289+
return $this;
1290+
}
1291+
1292+
/**
1293+
* Clear all document type mappings
1294+
*
1295+
* @return static
1296+
*/
1297+
public function clearAllDocumentTypes(): static
1298+
{
1299+
$this->documentTypes = [];
1300+
1301+
return $this;
1302+
}
1303+
1304+
/**
1305+
* Create a document instance of the appropriate type
1306+
*
1307+
* @param string $collection Collection ID
1308+
* @param array<string, mixed> $data Document data
1309+
* @return Document
1310+
*/
1311+
protected function createDocumentInstance(string $collection, array $data): Document
1312+
{
1313+
$className = $this->documentTypes[$collection] ?? Document::class;
1314+
1315+
return new $className($data);
1316+
}
1317+
12391318
public function getPreserveDates(): bool
12401319
{
12411320
return $this->preserveDates;
@@ -4179,15 +4258,15 @@ public function getDocument(string $collection, string $id, array $queries = [],
41794258
}
41804259

41814260
if ($cached) {
4182-
$document = new Document($cached);
4261+
$document = $this->createDocumentInstance($collection->getId(), $cached);
41834262

41844263
if ($collection->getId() !== self::METADATA) {
41854264

41864265
if (!$this->authorization->isValid(new Input(self::PERMISSION_READ, [
41874266
...$collection->getRead(),
41884267
...($documentSecurity ? $document->getRead() : [])
41894268
]))) {
4190-
return new Document();
4269+
return $this->createDocumentInstance($collection->getId(), []);
41914270
}
41924271
}
41934272

@@ -4204,19 +4283,24 @@ public function getDocument(string $collection, string $id, array $queries = [],
42044283
);
42054284

42064285
if ($document->isEmpty()) {
4207-
return $document;
4286+
return $this->createDocumentInstance($collection->getId(), []);
42084287
}
42094288

42104289
$document = $this->adapter->castingAfter($collection, $document);
42114290

4291+
// Convert to custom document type if mapped
4292+
if (isset($this->documentTypes[$collection->getId()])) {
4293+
$document = $this->createDocumentInstance($collection->getId(), $document->getArrayCopy());
4294+
}
4295+
42124296
$document->setAttribute('$collection', $collection->getId());
42134297

42144298
if ($collection->getId() !== self::METADATA) {
42154299
if (!$this->authorization->isValid(new Input(self::PERMISSION_READ, [
42164300
...$collection->getRead(),
42174301
...($documentSecurity ? $document->getRead() : [])
42184302
]))) {
4219-
return new Document();
4303+
return $this->createDocumentInstance($collection->getId(), []);
42204304
}
42214305
}
42224306

@@ -4850,9 +4934,7 @@ private function applySelectFiltersToDocuments(array $documents, array $selectQu
48504934
*
48514935
* @param string $collection
48524936
* @param Document $document
4853-
*
48544937
* @return Document
4855-
*
48564938
* @throws AuthorizationException
48574939
* @throws DatabaseException
48584940
* @throws StructureException
@@ -4953,6 +5035,11 @@ public function createDocument(string $collection, Document $document): Document
49535035
$document = $this->casting($collection, $document);
49545036
$document = $this->decode($collection, $document);
49555037

5038+
// Convert to custom document type if mapped
5039+
if (isset($this->documentTypes[$collection->getId()])) {
5040+
$document = $this->createDocumentInstance($collection->getId(), $document->getArrayCopy());
5041+
}
5042+
49565043
$this->trigger(self::EVENT_DOCUMENT_CREATE, $document);
49575044

49585045
return $document;
@@ -5407,7 +5494,6 @@ private function relateDocumentsById(
54075494
* @param string $id
54085495
* @param Document $document
54095496
* @return Document
5410-
*
54115497
* @throws AuthorizationException
54125498
* @throws ConflictException
54135499
* @throws DatabaseException
@@ -5642,6 +5728,11 @@ public function updateDocument(string $collection, string $id, Document $documen
56425728

56435729
$document = $this->decode($collection, $document);
56445730

5731+
// Convert to custom document type if mapped
5732+
if (isset($this->documentTypes[$collection->getId()])) {
5733+
$document = $this->createDocumentInstance($collection->getId(), $document->getArrayCopy());
5734+
}
5735+
56455736
$this->trigger(self::EVENT_DOCUMENT_UPDATE, $document);
56465737

56475738
return $document;
@@ -6616,7 +6707,6 @@ public function upsertDocumentsWithIncrease(
66166707

66176708
if (!$old->isEmpty()) {
66186709
$old = $this->adapter->castingAfter($collection, $old);
6619-
$old = $this->decode($collection, $old);
66206710
}
66216711

66226712
try {
@@ -7530,7 +7620,6 @@ public function purgeCachedDocument(string $collectionId, ?string $id): bool
75307620
* @param string $collection
75317621
* @param array<Query> $queries
75327622
* @param string $forPermission
7533-
*
75347623
* @return array<Document>
75357624
* @throws DatabaseException
75367625
* @throws QueryException
@@ -7667,6 +7756,11 @@ public function find(string $collection, array $queries = [], string $forPermiss
76677756
$node = $this->casting($collection, $node);
76687757
$node = $this->decode($collection, $node, $selections);
76697758

7759+
// Convert to custom document type if mapped
7760+
if (isset($this->documentTypes[$collection->getId()])) {
7761+
$node = $this->createDocumentInstance($collection->getId(), $node->getArrayCopy());
7762+
}
7763+
76707764
if (!$node->isEmpty()) {
76717765
$node->setAttribute('$collection', $collection->getId());
76727766
}

src/Database/Mirror.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,4 +1114,46 @@ public function setAuthorization(Authorization $authorization): self
11141114

11151115
return $this;
11161116
}
1117+
1118+
/**
1119+
* Set custom document class for a collection
1120+
*
1121+
* @param string $collection Collection ID
1122+
* @param class-string<Document> $className Fully qualified class name that extends Document
1123+
* @return static
1124+
*/
1125+
public function setDocumentType(string $collection, string $className): static
1126+
{
1127+
$this->delegate(__FUNCTION__, \func_get_args());
1128+
$this->documentTypes[$collection] = $className;
1129+
return $this;
1130+
}
1131+
1132+
/**
1133+
* Clear document type mapping for a collection
1134+
*
1135+
* @param string $collection Collection ID
1136+
* @return static
1137+
*/
1138+
public function clearDocumentType(string $collection): static
1139+
{
1140+
$this->delegate(__FUNCTION__, \func_get_args());
1141+
unset($this->documentTypes[$collection]);
1142+
1143+
return $this;
1144+
}
1145+
1146+
/**
1147+
* Clear all document type mappings
1148+
*
1149+
* @return static
1150+
*/
1151+
public function clearAllDocumentTypes(): static
1152+
{
1153+
$this->delegate(__FUNCTION__);
1154+
$this->documentTypes = [];
1155+
1156+
return $this;
1157+
}
1158+
11171159
}

0 commit comments

Comments
 (0)