Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,42 @@ 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

### Reserved Attributes

- `$id` - the document unique ID, you can set your own custom ID or a random UID will be generated by the library.
Expand Down
111 changes: 103 additions & 8 deletions src/Database/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,12 @@ class Database
*/
protected array $relationshipDeleteStack = [];

/**
* Type mapping for collections to custom document classes
* @var array<string, class-string<Document>>
*/
protected array $documentTypes = [];

/**
* @param Adapter $adapter
* @param Cache $cache
Expand Down Expand Up @@ -1203,6 +1209,79 @@ public function enableLocks(bool $enabled): static
return $this;
}

/**
* Set custom document class for a collection
*
* @param string $collection Collection ID
* @param class-string<Document> $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<Document>|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<string, mixed> $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
{
return $this->preserveDates;
Expand Down Expand Up @@ -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(), []);
}
}

Expand All @@ -3732,19 +3811,24 @@ 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) {
if (!$validator->isValid([
...$collection->getRead(),
...($documentSecurity ? $document->getRead() : [])
])) {
return new Document();
return $this->createDocumentInstance($collection->getId(), []);
}
}

Expand Down Expand Up @@ -4378,9 +4462,7 @@ private function applySelectFiltersToDocuments(array $documents, array $selectQu
*
* @param string $collection
* @param Document $document
*
* @return Document
*
* @throws AuthorizationException
* @throws DatabaseException
* @throws StructureException
Expand Down Expand Up @@ -4481,6 +4563,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;
Expand Down Expand Up @@ -4936,7 +5023,6 @@ private function relateDocumentsById(
* @param string $id
* @param Document $document
* @return Document
*
* @throws AuthorizationException
* @throws ConflictException
* @throws DatabaseException
Expand Down Expand Up @@ -5169,6 +5255,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;
Expand Down Expand Up @@ -7047,7 +7138,6 @@ public function purgeCachedDocument(string $collectionId, ?string $id): bool
* @param string $collection
* @param array<Query> $queries
* @param string $forPermission
*
* @return array<Document>
* @throws DatabaseException
* @throws QueryException
Expand Down Expand Up @@ -7184,6 +7274,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());
}
Expand Down
42 changes: 42 additions & 0 deletions src/Database/Mirror.php
Original file line number Diff line number Diff line change
Expand Up @@ -1099,4 +1099,46 @@ 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<Document> $className Fully qualified class name that extends Document
* @return static
*/
public function setDocumentType(string $collection, string $className): static
{
$this->delegate(__FUNCTION__, \func_get_args());
$this->documentTypes[$collection] = $className;
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;
}

}
2 changes: 2 additions & 0 deletions tests/e2e/Adapter/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,6 +23,7 @@
abstract class Base extends TestCase
{
use CollectionTests;
use CustomDocumentTypeTests;
use DocumentTests;
use AttributeTests;
use IndexTests;
Expand Down
Loading