Skip to content

Commit f2d01b6

Browse files
authored
Merge pull request #756 from utopia-php/feat-custom-type
Feat: Custom types
2 parents 76568b8 + f7b17d8 commit f2d01b6

File tree

5 files changed

+496
-8
lines changed

5 files changed

+496
-8
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/Database.php

Lines changed: 103 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,12 @@ class Database
414414
*/
415415
protected array $relationshipDeleteStack = [];
416416

417+
/**
418+
* Type mapping for collections to custom document classes
419+
* @var array<string, class-string<Document>>
420+
*/
421+
protected array $documentTypes = [];
422+
417423
/**
418424
* @param Adapter $adapter
419425
* @param Cache $cache
@@ -1203,6 +1209,79 @@ public function enableLocks(bool $enabled): static
12031209
return $this;
12041210
}
12051211

1212+
/**
1213+
* Set custom document class for a collection
1214+
*
1215+
* @param string $collection Collection ID
1216+
* @param class-string<Document> $className Fully qualified class name that extends Document
1217+
* @return static
1218+
* @throws DatabaseException
1219+
*/
1220+
public function setDocumentType(string $collection, string $className): static
1221+
{
1222+
if (!\class_exists($className)) {
1223+
throw new DatabaseException("Class {$className} does not exist");
1224+
}
1225+
1226+
if (!\is_subclass_of($className, Document::class)) {
1227+
throw new DatabaseException("Class {$className} must extend " . Document::class);
1228+
}
1229+
1230+
$this->documentTypes[$collection] = $className;
1231+
1232+
return $this;
1233+
}
1234+
1235+
/**
1236+
* Get custom document class for a collection
1237+
*
1238+
* @param string $collection Collection ID
1239+
* @return class-string<Document>|null
1240+
*/
1241+
public function getDocumentType(string $collection): ?string
1242+
{
1243+
return $this->documentTypes[$collection] ?? null;
1244+
}
1245+
1246+
/**
1247+
* Clear document type mapping for a collection
1248+
*
1249+
* @param string $collection Collection ID
1250+
* @return static
1251+
*/
1252+
public function clearDocumentType(string $collection): static
1253+
{
1254+
unset($this->documentTypes[$collection]);
1255+
1256+
return $this;
1257+
}
1258+
1259+
/**
1260+
* Clear all document type mappings
1261+
*
1262+
* @return static
1263+
*/
1264+
public function clearAllDocumentTypes(): static
1265+
{
1266+
$this->documentTypes = [];
1267+
1268+
return $this;
1269+
}
1270+
1271+
/**
1272+
* Create a document instance of the appropriate type
1273+
*
1274+
* @param string $collection Collection ID
1275+
* @param array<string, mixed> $data Document data
1276+
* @return Document
1277+
*/
1278+
protected function createDocumentInstance(string $collection, array $data): Document
1279+
{
1280+
$className = $this->documentTypes[$collection] ?? Document::class;
1281+
1282+
return new $className($data);
1283+
}
1284+
12061285
public function getPreserveDates(): bool
12071286
{
12081287
return $this->preserveDates;
@@ -3708,14 +3787,14 @@ public function getDocument(string $collection, string $id, array $queries = [],
37083787
}
37093788

37103789
if ($cached) {
3711-
$document = new Document($cached);
3790+
$document = $this->createDocumentInstance($collection->getId(), $cached);
37123791

37133792
if ($collection->getId() !== self::METADATA) {
37143793
if (!$validator->isValid([
37153794
...$collection->getRead(),
37163795
...($documentSecurity ? $document->getRead() : [])
37173796
])) {
3718-
return new Document();
3797+
return $this->createDocumentInstance($collection->getId(), []);
37193798
}
37203799
}
37213800

@@ -3732,19 +3811,24 @@ public function getDocument(string $collection, string $id, array $queries = [],
37323811
);
37333812

37343813
if ($document->isEmpty()) {
3735-
return $document;
3814+
return $this->createDocumentInstance($collection->getId(), []);
37363815
}
37373816

37383817
$document = $this->adapter->castingAfter($collection, $document);
37393818

3819+
// Convert to custom document type if mapped
3820+
if (isset($this->documentTypes[$collection->getId()])) {
3821+
$document = $this->createDocumentInstance($collection->getId(), $document->getArrayCopy());
3822+
}
3823+
37403824
$document->setAttribute('$collection', $collection->getId());
37413825

37423826
if ($collection->getId() !== self::METADATA) {
37433827
if (!$validator->isValid([
37443828
...$collection->getRead(),
37453829
...($documentSecurity ? $document->getRead() : [])
37463830
])) {
3747-
return new Document();
3831+
return $this->createDocumentInstance($collection->getId(), []);
37483832
}
37493833
}
37503834

@@ -4378,9 +4462,7 @@ private function applySelectFiltersToDocuments(array $documents, array $selectQu
43784462
*
43794463
* @param string $collection
43804464
* @param Document $document
4381-
*
43824465
* @return Document
4383-
*
43844466
* @throws AuthorizationException
43854467
* @throws DatabaseException
43864468
* @throws StructureException
@@ -4481,6 +4563,11 @@ public function createDocument(string $collection, Document $document): Document
44814563
$document = $this->casting($collection, $document);
44824564
$document = $this->decode($collection, $document);
44834565

4566+
// Convert to custom document type if mapped
4567+
if (isset($this->documentTypes[$collection->getId()])) {
4568+
$document = $this->createDocumentInstance($collection->getId(), $document->getArrayCopy());
4569+
}
4570+
44844571
$this->trigger(self::EVENT_DOCUMENT_CREATE, $document);
44854572

44864573
return $document;
@@ -4936,7 +5023,6 @@ private function relateDocumentsById(
49365023
* @param string $id
49375024
* @param Document $document
49385025
* @return Document
4939-
*
49405026
* @throws AuthorizationException
49415027
* @throws ConflictException
49425028
* @throws DatabaseException
@@ -5169,6 +5255,11 @@ public function updateDocument(string $collection, string $id, Document $documen
51695255

51705256
$document = $this->decode($collection, $document);
51715257

5258+
// Convert to custom document type if mapped
5259+
if (isset($this->documentTypes[$collection->getId()])) {
5260+
$document = $this->createDocumentInstance($collection->getId(), $document->getArrayCopy());
5261+
}
5262+
51725263
$this->trigger(self::EVENT_DOCUMENT_UPDATE, $document);
51735264

51745265
return $document;
@@ -7047,7 +7138,6 @@ public function purgeCachedDocument(string $collectionId, ?string $id): bool
70477138
* @param string $collection
70487139
* @param array<Query> $queries
70497140
* @param string $forPermission
7050-
*
70517141
* @return array<Document>
70527142
* @throws DatabaseException
70537143
* @throws QueryException
@@ -7184,6 +7274,11 @@ public function find(string $collection, array $queries = [], string $forPermiss
71847274
$node = $this->casting($collection, $node);
71857275
$node = $this->decode($collection, $node, $selections);
71867276

7277+
// Convert to custom document type if mapped
7278+
if (isset($this->documentTypes[$collection->getId()])) {
7279+
$node = $this->createDocumentInstance($collection->getId(), $node->getArrayCopy());
7280+
}
7281+
71877282
if (!$node->isEmpty()) {
71887283
$node->setAttribute('$collection', $collection->getId());
71897284
}

src/Database/Mirror.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,4 +1099,46 @@ protected function logError(string $action, \Throwable $err): void
10991099
$callback($action, $err);
11001100
}
11011101
}
1102+
1103+
/**
1104+
* Set custom document class for a collection
1105+
*
1106+
* @param string $collection Collection ID
1107+
* @param class-string<Document> $className Fully qualified class name that extends Document
1108+
* @return static
1109+
*/
1110+
public function setDocumentType(string $collection, string $className): static
1111+
{
1112+
$this->delegate(__FUNCTION__, \func_get_args());
1113+
$this->documentTypes[$collection] = $className;
1114+
return $this;
1115+
}
1116+
1117+
/**
1118+
* Clear document type mapping for a collection
1119+
*
1120+
* @param string $collection Collection ID
1121+
* @return static
1122+
*/
1123+
public function clearDocumentType(string $collection): static
1124+
{
1125+
$this->delegate(__FUNCTION__, \func_get_args());
1126+
unset($this->documentTypes[$collection]);
1127+
1128+
return $this;
1129+
}
1130+
1131+
/**
1132+
* Clear all document type mappings
1133+
*
1134+
* @return static
1135+
*/
1136+
public function clearAllDocumentTypes(): static
1137+
{
1138+
$this->delegate(__FUNCTION__);
1139+
$this->documentTypes = [];
1140+
1141+
return $this;
1142+
}
1143+
11021144
}

tests/e2e/Adapter/Base.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PHPUnit\Framework\TestCase;
66
use Tests\E2E\Adapter\Scopes\AttributeTests;
77
use Tests\E2E\Adapter\Scopes\CollectionTests;
8+
use Tests\E2E\Adapter\Scopes\CustomDocumentTypeTests;
89
use Tests\E2E\Adapter\Scopes\DocumentTests;
910
use Tests\E2E\Adapter\Scopes\GeneralTests;
1011
use Tests\E2E\Adapter\Scopes\IndexTests;
@@ -22,6 +23,7 @@
2223
abstract class Base extends TestCase
2324
{
2425
use CollectionTests;
26+
use CustomDocumentTypeTests;
2527
use DocumentTests;
2628
use AttributeTests;
2729
use IndexTests;

0 commit comments

Comments
 (0)