IdentifiableDatabaseDatastoreHandler

The IdentifiableDatabaseDatastoreHandler is the base class for implementing database-backed datastores in PHPNomad. It provides complete implementations of all standard handler interfaces (find, get, save, delete, where, count) with built-in caching, event broadcasting, and query building.

When you extend this class, you get a fully functional database handler with minimal code—just set a few properties in your constructor.

What It Provides

By extending IdentifiableDatabaseDatastoreHandler, your handler automatically implements:

Plus automatic:


Basic Usage

<?php

namespace App\Service\Datastores\Post;

use PHPNomad\Database\Abstracts\IdentifiableDatabaseDatastoreHandler;
use PHPNomad\Database\Providers\DatabaseServiceProvider;
use PHPNomad\Database\Services\TableSchemaService;
use PHPNomad\Database\Traits\WithDatastoreHandlerMethods;
use App\Core\Datastores\Post\Interfaces\PostDatastoreHandler;
use App\Core\Models\Adapters\PostAdapter;
use App\Core\Models\Post;

class PostHandler extends IdentifiableDatabaseDatastoreHandler implements PostDatastoreHandler
{
    use WithDatastoreHandlerMethods;

    public function __construct(
        DatabaseServiceProvider $serviceProvider,
        PostsTable $table,
        PostAdapter $adapter,
        TableSchemaService $tableSchemaService
    ) {
        $this->model = Post::class;
        $this->table = $table;
        $this->modelAdapter = $adapter;
        $this->serviceProvider = $serviceProvider;
        $this->tableSchemaService = $tableSchemaService;
    }
}

That's it! This handler now supports:


Required Properties

You must set these five properties in your constructor:

$model

The model class name this handler works with.

$this->model = Post::class;

$table

The table definition for database schema.

$this->table = $table;

$modelAdapter

The adapter for converting between models and arrays.

$this->modelAdapter = $adapter;

$serviceProvider

The database service provider with query builders, cache, events.

$this->serviceProvider = $serviceProvider;

$tableSchemaService

Service for managing table creation and updates.

$this->tableSchemaService = $tableSchemaService;

Provided Methods

find(int $id): Model

Finds a single record by primary key.

Implementation:

Throws: RecordNotFoundException if not found.

Example:

$post = $handler->find(42);

get(array $args = []): iterable

Retrieves multiple records matching criteria.

Implementation:

Example:

$posts = $handler->get(['author_id' => 123, 'status' => 'published']);

save(Model $item): Model

Creates or updates a record.

Implementation:

Example:

$newPost = new Post(null, 'Title', 'Content', 123, new DateTime());
$savedPost = $handler->save($newPost);
echo $savedPost->id;  // Now has an ID

delete(Model $item): void

Removes a record from the database.

Implementation:

Example:

$post = $handler->find(42);
$handler->delete($post);

where(): DatastoreWhereQuery

Returns a query builder for complex filtering.

Returns: Query interface with methods like equals(), greaterThan(), orderBy(), limit().

Example:

$posts = $handler
    ->where()
    ->equals('author_id', 123)
    ->greaterThan('view_count', 100)
    ->orderBy('published_date', 'DESC')
    ->limit(10)
    ->getResults();

count(array $args = []): int

Counts records matching criteria.

Implementation:

Example:

$publishedCount = $handler->count(['status' => 'published']);

Custom Methods

You can add custom business methods alongside the standard ones:

class PostHandler extends IdentifiableDatabaseDatastoreHandler
{
    // ... standard setup ...

    /**
     * Custom method: find posts by slug
     */
    public function findBySlug(string $slug): ?Post
    {
        $clause = $this->serviceProvider->clauseBuilder
            ->useTable($this->table)
            ->where('slug', '=', $slug);

        $sql = $this->serviceProvider->queryBuilder
            ->select('*')
            ->from($this->table)
            ->where($clause)
            ->build();

        $row = $this->serviceProvider->queryStrategy->querySingle($sql);
        
        return $row ? $this->modelAdapter->toModel($row) : null;
    }

    /**
     * Custom method: get top posts by view count
     */
    public function getTopPosts(int $limit = 10): iterable
    {
        $sql = $this->serviceProvider->queryBuilder
            ->select('*')
            ->from($this->table)
            ->orderBy('view_count', 'DESC')
            ->limit($limit)
            ->build();

        $rows = $this->serviceProvider->queryStrategy->query($sql);
        
        return array_map(
            fn($row) => $this->modelAdapter->toModel($row),
            $rows
        );
    }
}

Caching Behavior

All read operations are automatically cached:

Cached operations:

Cache invalidation:

Customize caching:

// Override to customize cache behavior
protected function getCacheKey(array $context): string
{
    return 'posts:' . md5(serialize($context));
}

protected function shouldCache(string $operation, array $context): bool
{
    // Don't cache queries with LIMIT > 100
    return !isset($context['limit']) || $context['limit'] <= 100;
}

Event Broadcasting

Mutations automatically broadcast events:

Events:

Customize events:

// Override to add custom events
public function save(Model $item): Model
{
    $isNew = !$item->getId();
    $result = parent::save($item);
    
    // Custom event
    if ($isNew && $item->status === 'published') {
        $this->serviceProvider->eventStrategy->broadcast(
            new PostPublished($result)
        );
    }
    
    return $result;
}

Table Schema Management

The handler ensures the table exists on first use:

Automatic table creation:

Table versioning: When you increment $table->getTableVersion(), the handler detects changes and updates the schema.


Error Handling

The base handler includes error handling:

Exceptions thrown:

Logging: All errors are logged via LoggerStrategy:

try {
    $post = $handler->find($id);
} catch (RecordNotFoundException $e) {
    // Logged automatically: "Record not found: posts:123"
}

Best Practices

Use WithDatastoreHandlerMethods Trait

// ✅ GOOD: use trait for standard implementations
class PostHandler extends IdentifiableDatabaseDatastoreHandler
{
    use WithDatastoreHandlerMethods;
}

// ❌ BAD: implement methods manually
class PostHandler extends IdentifiableDatabaseDatastoreHandler
{
    public function find($id) { /* manual implementation */ }
}

Set All Required Properties

// ✅ GOOD: all properties set
public function __construct(...)
{
    $this->model = Post::class;
    $this->table = $table;
    $this->modelAdapter = $adapter;
    $this->serviceProvider = $serviceProvider;
    $this->tableSchemaService = $tableSchemaService;
}

// ❌ BAD: missing properties
public function __construct(...)
{
    $this->table = $table;
    // Missing model, adapter, etc.
}

Keep Handlers Focused on Persistence

// ✅ GOOD: handler does storage only
public function save(Model $item): Model
{
    return parent::save($item);
}

// ❌ BAD: handler contains business logic
public function save(Model $item): Model
{
    if ($item->publishedDate < new DateTime()) {
        throw new ValidationException("Cannot publish in the past");
    }
    return parent::save($item);
}

Business logic belongs in services, not handlers.


What's Next