From d6b8454dce19ec28ccdda78a287c2f28b42e1b1b Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 3 Nov 2025 10:32:21 +0000 Subject: [PATCH 1/9] Feat: External types --- README.md | 38 +++ src/Database/Database.php | 122 ++++++- tests/e2e/Adapter/Base.php | 2 + .../Scopes/CustomDocumentTypeTests.php | 305 ++++++++++++++++++ 4 files changed, 455 insertions(+), 12 deletions(-) create mode 100644 tests/e2e/Adapter/Scopes/CustomDocumentTypeTests.php diff --git a/README.md b/README.md index 835bee0ee..91ac65f3a 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,44 @@ A list of the utopia/php concepts and their relevant equivalent using the differ 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. +### Custom Document Types + +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. + +```php +// Define a custom document class +class User extends Document +{ + public function getEmail(): string + { + return $this->getAttribute('email', ''); + } + + public function isAdmin(): bool + { + return $this->getAttribute('role') === 'admin'; + } +} + +// Register the custom type +$database->setDocumentType('users', User::class); + +// Now all documents from 'users' collection are User instances +$user = $database->getDocument('users', 'user123'); +$email = $user->getEmail(); // Use custom methods +if ($user->isAdmin()) { + // Domain logic +} +``` + +**Benefits:** +- ✅ Domain-driven design with business logic in domain objects +- ✅ Type safety with IDE autocomplete for custom methods +- ✅ Code organization and encapsulation +- ✅ Fully backwards compatible + +For complete documentation, see [docs/custom-document-types.md](docs/custom-document-types.md). + ### Reserved Attributes - `$id` - the document unique ID, you can set your own custom ID or a random UID will be generated by the library. diff --git a/src/Database/Database.php b/src/Database/Database.php index fcb421f15..2a61eead4 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -414,6 +414,12 @@ class Database */ protected array $relationshipDeleteStack = []; + /** + * Type mapping for collections to custom document classes + * @var array> + */ + protected array $documentTypes = []; + /** * @param Adapter $adapter * @param Cache $cache @@ -1202,6 +1208,78 @@ public function enableLocks(bool $enabled): static return $this; } + /** + * Set custom document class for a collection + * + * @param string $collection Collection ID + * @param class-string $className Fully qualified class name that extends Document + * @return static + * @throws DatabaseException + */ + public function setDocumentType(string $collection, string $className): static + { + if (!\class_exists($className)) { + throw new DatabaseException("Class {$className} does not exist"); + } + + if (!\is_subclass_of($className, Document::class)) { + throw new DatabaseException("Class {$className} must extend " . Document::class); + } + + $this->documentTypes[$collection] = $className; + + return $this; + } + + /** + * Get custom document class for a collection + * + * @param string $collection Collection ID + * @return class-string|null + */ + public function getDocumentType(string $collection): ?string + { + return $this->documentTypes[$collection] ?? null; + } + + /** + * Clear document type mapping for a collection + * + * @param string $collection Collection ID + * @return static + */ + public function clearDocumentType(string $collection): static + { + unset($this->documentTypes[$collection]); + + return $this; + } + + /** + * Clear all document type mappings + * + * @return static + */ + public function clearAllDocumentTypes(): static + { + $this->documentTypes = []; + + return $this; + } + + /** + * Create a document instance of the appropriate type + * + * @param string $collection Collection ID + * @param array $data Document data + * @return Document + */ + protected function createDocumentInstance(string $collection, array $data): Document + { + $className = $this->documentTypes[$collection] ?? Document::class; + + return new $className($data); + } public function getPreserveDates(): bool { @@ -3642,11 +3720,12 @@ public function deleteIndex(string $collection, string $id): bool /** * Get Document * + * @template T of Document * @param string $collection * @param string $id * @param Query[] $queries * @param bool $forUpdate - * @return Document + * @return T|Document * @throws NotFoundException * @throws QueryException * @throws Exception @@ -3708,14 +3787,14 @@ public function getDocument(string $collection, string $id, array $queries = [], } if ($cached) { - $document = new Document($cached); + $document = $this->createDocumentInstance($collection->getId(), $cached); if ($collection->getId() !== self::METADATA) { if (!$validator->isValid([ ...$collection->getRead(), ...($documentSecurity ? $document->getRead() : []) ])) { - return new Document(); + return $this->createDocumentInstance($collection->getId(), []); } } @@ -3732,11 +3811,16 @@ public function getDocument(string $collection, string $id, array $queries = [], ); if ($document->isEmpty()) { - return $document; + return $this->createDocumentInstance($collection->getId(), []); } $document = $this->adapter->castingAfter($collection, $document); + // Convert to custom document type if mapped + if (isset($this->documentTypes[$collection->getId()])) { + $document = $this->createDocumentInstance($collection->getId(), $document->getArrayCopy()); + } + $document->setAttribute('$collection', $collection->getId()); if ($collection->getId() !== self::METADATA) { @@ -3744,7 +3828,7 @@ public function getDocument(string $collection, string $id, array $queries = [], ...$collection->getRead(), ...($documentSecurity ? $document->getRead() : []) ])) { - return new Document(); + return $this->createDocumentInstance($collection->getId(), []); } } @@ -4376,11 +4460,10 @@ private function applySelectFiltersToDocuments(array $documents, array $selectQu /** * Create Document * + * @template T of Document * @param string $collection * @param Document $document - * - * @return Document - * + * @return T|Document * @throws AuthorizationException * @throws DatabaseException * @throws StructureException @@ -4481,6 +4564,11 @@ public function createDocument(string $collection, Document $document): Document $document = $this->casting($collection, $document); $document = $this->decode($collection, $document); + // Convert to custom document type if mapped + if (isset($this->documentTypes[$collection->getId()])) { + $document = $this->createDocumentInstance($collection->getId(), $document->getArrayCopy()); + } + $this->trigger(self::EVENT_DOCUMENT_CREATE, $document); return $document; @@ -4932,11 +5020,11 @@ private function relateDocumentsById( /** * Update Document * + * @template T of Document * @param string $collection * @param string $id * @param Document $document - * @return Document - * + * @return T|Document * @throws AuthorizationException * @throws ConflictException * @throws DatabaseException @@ -5169,6 +5257,11 @@ public function updateDocument(string $collection, string $id, Document $documen $document = $this->decode($collection, $document); + // Convert to custom document type if mapped + if (isset($this->documentTypes[$collection->getId()])) { + $document = $this->createDocumentInstance($collection->getId(), $document->getArrayCopy()); + } + $this->trigger(self::EVENT_DOCUMENT_UPDATE, $document); return $document; @@ -7044,11 +7137,11 @@ public function purgeCachedDocument(string $collectionId, ?string $id): bool /** * Find Documents * + * @template T of Document * @param string $collection * @param array $queries * @param string $forPermission - * - * @return array + * @return array * @throws DatabaseException * @throws QueryException * @throws TimeoutException @@ -7184,6 +7277,11 @@ public function find(string $collection, array $queries = [], string $forPermiss $node = $this->casting($collection, $node); $node = $this->decode($collection, $node, $selections); + // Convert to custom document type if mapped + if (isset($this->documentTypes[$collection->getId()])) { + $node = $this->createDocumentInstance($collection->getId(), $node->getArrayCopy()); + } + if (!$node->isEmpty()) { $node->setAttribute('$collection', $collection->getId()); } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 441125241..3643d8d60 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\TestCase; use Tests\E2E\Adapter\Scopes\AttributeTests; use Tests\E2E\Adapter\Scopes\CollectionTests; +use Tests\E2E\Adapter\Scopes\CustomDocumentTypeTests; use Tests\E2E\Adapter\Scopes\DocumentTests; use Tests\E2E\Adapter\Scopes\GeneralTests; use Tests\E2E\Adapter\Scopes\IndexTests; @@ -22,6 +23,7 @@ abstract class Base extends TestCase { use CollectionTests; + use CustomDocumentTypeTests; use DocumentTests; use AttributeTests; use IndexTests; diff --git a/tests/e2e/Adapter/Scopes/CustomDocumentTypeTests.php b/tests/e2e/Adapter/Scopes/CustomDocumentTypeTests.php new file mode 100644 index 000000000..037494b90 --- /dev/null +++ b/tests/e2e/Adapter/Scopes/CustomDocumentTypeTests.php @@ -0,0 +1,305 @@ +getAttribute('email', ''); + } + + public function getName(): string + { + return $this->getAttribute('name', ''); + } + + public function isActive(): bool + { + return $this->getAttribute('status') === 'active'; + } +} + +class TestPost extends Document +{ + public function getTitle(): string + { + return $this->getAttribute('title', ''); + } + + public function getContent(): string + { + return $this->getAttribute('content', ''); + } +} + +trait CustomDocumentTypeTests +{ + public function testSetDocumentType(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + $database->setDocumentType('users', TestUser::class); + + $this->assertEquals( + TestUser::class, + $database->getDocumentType('users') + ); + } + + public function testGetDocumentTypeReturnsNull(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + $this->assertNull($database->getDocumentType('nonexistent_collection')); + } + + public function testSetDocumentTypeWithInvalidClass(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('does not exist'); + + $database->setDocumentType('users', 'NonExistentClass'); + } + + public function testSetDocumentTypeWithNonDocumentClass(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('must extend'); + + $database->setDocumentType('users', \stdClass::class); + } + + public function testClearDocumentType(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + $database->setDocumentType('users', TestUser::class); + $this->assertEquals(TestUser::class, $database->getDocumentType('users')); + + $database->clearDocumentType('users'); + $this->assertNull($database->getDocumentType('users')); + } + + public function testClearAllDocumentTypes(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + $database->setDocumentType('users', TestUser::class); + $database->setDocumentType('posts', TestPost::class); + + $this->assertEquals(TestUser::class, $database->getDocumentType('users')); + $this->assertEquals(TestPost::class, $database->getDocumentType('posts')); + + $database->clearAllDocumentTypes(); + + $this->assertNull($database->getDocumentType('users')); + $this->assertNull($database->getDocumentType('posts')); + } + + public function testMethodChaining(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + $result = $database->setDocumentType('users', TestUser::class); + + $this->assertInstanceOf(Database::class, $result); + + $database + ->setDocumentType('users', TestUser::class) + ->setDocumentType('posts', TestPost::class); + + $this->assertEquals(TestUser::class, $database->getDocumentType('users')); + $this->assertEquals(TestPost::class, $database->getDocumentType('posts')); + } + + public function testCustomDocumentTypeWithGetDocument(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + // Create collection + $database->createCollection('customUsers', permissions: [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ]); + + $database->createAttribute('customUsers', 'email', Database::VAR_STRING, 255, true); + $database->createAttribute('customUsers', 'name', Database::VAR_STRING, 255, true); + $database->createAttribute('customUsers', 'status', Database::VAR_STRING, 50, true); + + $database->setDocumentType('customUsers', TestUser::class); + + /** @var TestUser $created */ + $created = $database->createDocument('customUsers', new Document([ + '$id' => ID::unique(), + 'email' => 'test@example.com', + 'name' => 'Test User', + 'status' => 'active', + '$permissions' => [Permission::read(Role::any())], + ])); + + // Verify it's a TestUser instance + $this->assertInstanceOf(TestUser::class, $created); + $this->assertEquals('test@example.com', $created->getEmail()); + $this->assertEquals('Test User', $created->getName()); + $this->assertTrue($created->isActive()); + + // Get document and verify type + /** @var TestUser $fetched */ + $fetched = $database->getDocument('customUsers', $created->getId()); + $this->assertInstanceOf(TestUser::class, $fetched); + $this->assertEquals('test@example.com', $fetched->getEmail()); + $this->assertTrue($fetched->isActive()); + + // Cleanup + $database->deleteCollection('customUsers'); + $database->clearDocumentType('customUsers'); + } + + public function testCustomDocumentTypeWithFind(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + // Create collection + $database->createCollection('customPosts', permissions: [ + Permission::read(Role::any()), + Permission::create(Role::any()), + ]); + + $database->createAttribute('customPosts', 'title', Database::VAR_STRING, 255, true); + $database->createAttribute('customPosts', 'content', Database::VAR_STRING, 5000, true); + + // Register custom type + $database->setDocumentType('customPosts', TestPost::class); + + // Create multiple documents + $post1 = $database->createDocument('customPosts', new Document([ + '$id' => ID::unique(), + 'title' => 'First Post', + 'content' => 'This is the first post', + '$permissions' => [Permission::read(Role::any())], + ])); + + $post2 = $database->createDocument('customPosts', new Document([ + '$id' => ID::unique(), + 'title' => 'Second Post', + 'content' => 'This is the second post', + '$permissions' => [Permission::read(Role::any())], + ])); + + // Find documents + /** @var TestPost[] $posts */ + $posts = $database->find('customPosts', [Query::limit(10)]); + + $this->assertCount(2, $posts); + $this->assertInstanceOf(TestPost::class, $posts[0]); + $this->assertInstanceOf(TestPost::class, $posts[1]); + $this->assertEquals('First Post', $posts[0]->getTitle()); + $this->assertEquals('Second Post', $posts[1]->getTitle()); + + // Cleanup + $database->deleteCollection('customPosts'); + $database->clearDocumentType('customPosts'); + } + + public function testCustomDocumentTypeWithUpdateDocument(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + // Create collection + $database->createCollection('customUsersUpdate', permissions: [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + ]); + + $database->createAttribute('customUsersUpdate', 'email', Database::VAR_STRING, 255, true); + $database->createAttribute('customUsersUpdate', 'name', Database::VAR_STRING, 255, true); + $database->createAttribute('customUsersUpdate', 'status', Database::VAR_STRING, 50, true); + + // Register custom type + $database->setDocumentType('customUsersUpdate', TestUser::class); + + // Create document + /** @var TestUser $created */ + $created = $database->createDocument('customUsersUpdate', new Document([ + '$id' => ID::unique(), + 'email' => 'original@example.com', + 'name' => 'Original Name', + 'status' => 'active', + '$permissions' => [Permission::read(Role::any()), Permission::update(Role::any())], + ])); + + // Update document + /** @var TestUser $updated */ + $updated = $database->updateDocument('customUsersUpdate', $created->getId(), new Document([ + '$id' => $created->getId(), + 'email' => 'updated@example.com', + 'name' => 'Updated Name', + 'status' => 'inactive', + ])); + + // Verify it's still TestUser and has updated values + $this->assertInstanceOf(TestUser::class, $updated); + $this->assertEquals('updated@example.com', $updated->getEmail()); + $this->assertEquals('Updated Name', $updated->getName()); + $this->assertFalse($updated->isActive()); + + // Cleanup + $database->deleteCollection('customUsersUpdate'); + $database->clearDocumentType('customUsersUpdate'); + } + + public function testDefaultDocumentForUnmappedCollection(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + // Create collection without custom type + $database->createCollection('unmappedCollection', permissions: [ + Permission::read(Role::any()), + Permission::create(Role::any()), + ]); + + $database->createAttribute('unmappedCollection', 'data', Database::VAR_STRING, 255, true); + + // Create document + $created = $database->createDocument('unmappedCollection', new Document([ + '$id' => ID::unique(), + 'data' => 'test data', + '$permissions' => [Permission::read(Role::any())], + ])); + + // Should be regular Document, not custom type + $this->assertInstanceOf(Document::class, $created); + $this->assertNotInstanceOf(TestUser::class, $created); + + // Cleanup + $database->deleteCollection('unmappedCollection'); + } +} From 6e40fe806b24448ca9631c5aa90a046b312cd711 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 3 Nov 2025 10:38:29 +0000 Subject: [PATCH 2/9] Fix phpstan errors --- src/Database/Database.php | 12 ++++-------- tests/e2e/Adapter/Scopes/CustomDocumentTypeTests.php | 10 +++++----- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 2a61eead4..90dd93b96 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -3720,12 +3720,11 @@ public function deleteIndex(string $collection, string $id): bool /** * Get Document * - * @template T of Document * @param string $collection * @param string $id * @param Query[] $queries * @param bool $forUpdate - * @return T|Document + * @return Document * @throws NotFoundException * @throws QueryException * @throws Exception @@ -4460,10 +4459,9 @@ private function applySelectFiltersToDocuments(array $documents, array $selectQu /** * Create Document * - * @template T of Document * @param string $collection * @param Document $document - * @return T|Document + * @return Document * @throws AuthorizationException * @throws DatabaseException * @throws StructureException @@ -5020,11 +5018,10 @@ private function relateDocumentsById( /** * Update Document * - * @template T of Document * @param string $collection * @param string $id * @param Document $document - * @return T|Document + * @return Document * @throws AuthorizationException * @throws ConflictException * @throws DatabaseException @@ -7137,11 +7134,10 @@ public function purgeCachedDocument(string $collectionId, ?string $id): bool /** * Find Documents * - * @template T of Document * @param string $collection * @param array $queries * @param string $forPermission - * @return array + * @return array * @throws DatabaseException * @throws QueryException * @throws TimeoutException diff --git a/tests/e2e/Adapter/Scopes/CustomDocumentTypeTests.php b/tests/e2e/Adapter/Scopes/CustomDocumentTypeTests.php index 037494b90..48ad6027f 100644 --- a/tests/e2e/Adapter/Scopes/CustomDocumentTypeTests.php +++ b/tests/e2e/Adapter/Scopes/CustomDocumentTypeTests.php @@ -72,18 +72,18 @@ public function testSetDocumentTypeWithInvalidClass(): void $this->expectException(DatabaseException::class); $this->expectExceptionMessage('does not exist'); - + + // @phpstan-ignore-next-line - Testing with invalid class name $database->setDocumentType('users', 'NonExistentClass'); - } - - public function testSetDocumentTypeWithNonDocumentClass(): void + } public function testSetDocumentTypeWithNonDocumentClass(): void { /** @var Database $database */ $database = static::getDatabase(); $this->expectException(DatabaseException::class); $this->expectExceptionMessage('must extend'); - + + // @phpstan-ignore-next-line - Testing with non-Document class $database->setDocumentType('users', \stdClass::class); } From 1c8ef7a201d6af627f91e7c5978301a34645603c Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 3 Nov 2025 10:39:37 +0000 Subject: [PATCH 3/9] format --- tests/e2e/Adapter/Scopes/CustomDocumentTypeTests.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Adapter/Scopes/CustomDocumentTypeTests.php b/tests/e2e/Adapter/Scopes/CustomDocumentTypeTests.php index 48ad6027f..3a18c861b 100644 --- a/tests/e2e/Adapter/Scopes/CustomDocumentTypeTests.php +++ b/tests/e2e/Adapter/Scopes/CustomDocumentTypeTests.php @@ -72,7 +72,7 @@ public function testSetDocumentTypeWithInvalidClass(): void $this->expectException(DatabaseException::class); $this->expectExceptionMessage('does not exist'); - + // @phpstan-ignore-next-line - Testing with invalid class name $database->setDocumentType('users', 'NonExistentClass'); } public function testSetDocumentTypeWithNonDocumentClass(): void @@ -82,7 +82,7 @@ public function testSetDocumentTypeWithInvalidClass(): void $this->expectException(DatabaseException::class); $this->expectExceptionMessage('must extend'); - + // @phpstan-ignore-next-line - Testing with non-Document class $database->setDocumentType('users', \stdClass::class); } From fc9f335858929a0facf48a24c7e3962de5218cb4 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 3 Nov 2025 10:42:00 +0000 Subject: [PATCH 4/9] wire cleanup --- tests/e2e/Adapter/Scopes/CustomDocumentTypeTests.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/e2e/Adapter/Scopes/CustomDocumentTypeTests.php b/tests/e2e/Adapter/Scopes/CustomDocumentTypeTests.php index 3a18c861b..9953e73e2 100644 --- a/tests/e2e/Adapter/Scopes/CustomDocumentTypeTests.php +++ b/tests/e2e/Adapter/Scopes/CustomDocumentTypeTests.php @@ -55,6 +55,9 @@ public function testSetDocumentType(): void TestUser::class, $database->getDocumentType('users') ); + + // Cleanup + $database->clearDocumentType('users'); } public function testGetDocumentTypeReturnsNull(): void @@ -63,6 +66,8 @@ public function testGetDocumentTypeReturnsNull(): void $database = static::getDatabase(); $this->assertNull($database->getDocumentType('nonexistent_collection')); + + // No cleanup needed - no types were set } public function testSetDocumentTypeWithInvalidClass(): void @@ -131,6 +136,9 @@ public function testMethodChaining(): void $this->assertEquals(TestUser::class, $database->getDocumentType('users')); $this->assertEquals(TestPost::class, $database->getDocumentType('posts')); + + // Cleanup to prevent test pollution + $database->clearAllDocumentTypes(); } public function testCustomDocumentTypeWithGetDocument(): void From a0ddee09f19fb4f8fa0e13b2599957564f45ea25 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 3 Nov 2025 10:43:04 +0000 Subject: [PATCH 5/9] fix missing link --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 91ac65f3a..15ff0da75 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,6 @@ if ($user->isAdmin()) { - ✅ Code organization and encapsulation - ✅ Fully backwards compatible -For complete documentation, see [docs/custom-document-types.md](docs/custom-document-types.md). - ### Reserved Attributes - `$id` - the document unique ID, you can set your own custom ID or a random UID will be generated by the library. From d64bfe2ceadea8cd71d0cd6bfb84607ff75b99fc Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Thu, 6 Nov 2025 03:35:54 +0000 Subject: [PATCH 6/9] Fix mirror --- src/Database/Database.php | 1 + src/Database/Mirror.php | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/Database/Database.php b/src/Database/Database.php index 90dd93b96..b1de4f03e 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1208,6 +1208,7 @@ public function enableLocks(bool $enabled): static return $this; } + /** * Set custom document class for a collection * diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index dac23a063..56b1f8452 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -1099,4 +1099,16 @@ protected function logError(string $action, \Throwable $err): void $callback($action, $err); } } + + /** + * Set custom document class for a collection + * + * @param string $collection Collection ID + * @param class-string $className Fully qualified class name that extends Document + * @return static + */ + public function setDocumentType(string $collection, string $className): static + { + return $this->delegate(__FUNCTION__, \func_get_args()); + } } From 4e2b687cc16adefeb0e147e10bcc264ddb3f47d9 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Thu, 6 Nov 2025 03:43:25 +0000 Subject: [PATCH 7/9] fix return typ --- src/Database/Mirror.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index 56b1f8452..e674a3c3c 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -1109,6 +1109,7 @@ protected function logError(string $action, \Throwable $err): void */ public function setDocumentType(string $collection, string $className): static { - return $this->delegate(__FUNCTION__, \func_get_args()); + $this->delegate(__FUNCTION__, \func_get_args()); + return $this; } } From 99f56830db519ce14548fb21bd2ed3f3c1e07687 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Thu, 6 Nov 2025 05:11:08 +0000 Subject: [PATCH 8/9] mirror set types also to current instance --- src/Database/Mirror.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index e674a3c3c..82b45b0fd 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -1110,6 +1110,8 @@ protected function logError(string $action, \Throwable $err): void public function setDocumentType(string $collection, string $className): static { $this->delegate(__FUNCTION__, \func_get_args()); + $this->documentTypes[$collection] = $className; return $this; } + } From f7b17d84f1a74611d03d61acdbe42bc148fb93cd Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Thu, 6 Nov 2025 05:12:13 +0000 Subject: [PATCH 9/9] Fix mirror --- src/Database/Mirror.php | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index 82b45b0fd..d9a6d09df 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -1114,4 +1114,31 @@ public function setDocumentType(string $collection, string $className): static return $this; } + /** + * Clear document type mapping for a collection + * + * @param string $collection Collection ID + * @return static + */ + public function clearDocumentType(string $collection): static + { + $this->delegate(__FUNCTION__, \func_get_args()); + unset($this->documentTypes[$collection]); + + return $this; + } + + /** + * Clear all document type mappings + * + * @return static + */ + public function clearAllDocumentTypes(): static + { + $this->delegate(__FUNCTION__); + $this->documentTypes = []; + + return $this; + } + }