Datastore Interfaces

Datastore interfaces define the public API for data access in PHPNomad. They describe what operations consumers can perform without tying them to any specific storage implementation. This separation is what makes datastores portable: the same interface works whether your data lives in a database, a REST API, in-memory cache, or a flat file.

At the core, every datastore interface extends from Datastore, which provides basic operations like get(), save(), and delete(). From there, you can layer on additional capabilities through extension interfaces that add primary key lookups, querying, counting, and more.

Why Interfaces Matter

In PHPNomad, interfaces are contracts that your application code depends on. By coding against Datastore or DatastoreHasPrimaryKey, you're expressing what you need without caring how it's implemented.

This matters because:

When you write a service or controller that depends on a datastore, you inject the interface, not a concrete class. The DI container handles the rest.

The Base Interface: Datastore

The Datastore interface is the minimal contract every datastore must implement. It provides three core operations:

interface Datastore
{
    /**
     * Retrieves a collection of models based on the provided criteria.
     */
    public function get(array $args = []): iterable;

    /**
     * Persists a model to storage.
     */
    public function save(Model $item): Model;

    /**
     * Removes a model from storage.
     */
    public function delete(Model $item): void;
}

What this enables

This is enough to build most CRUD operations. When you need more specific operations (like fetching by ID or running WHERE clauses), you extend this base with additional interfaces.

Extension Interfaces

PHPNomad provides several extension interfaces that add specific capabilities to the base Datastore contract. Each one is focused on a single concern, and you compose them as needed.

DatastoreHasPrimaryKey

Adds the ability to fetch by primary key — a common pattern for single-record lookups.

interface DatastoreHasPrimaryKey extends Datastore
{
    /**
     * Finds a single record by its primary key.
     * 
     * @throws RecordNotFoundException if not found
     */
    public function find(int $id): Model;
}

When to use: Your datastore has a single integer primary key (e.g., id), and you need fast lookups by ID.

Example:

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

DatastoreHasWhere

Adds query-builder-style filtering with a where() method that returns a scoped query interface.

interface DatastoreHasWhere extends Datastore
{
    /**
     * Returns a query interface for building WHERE clauses.
     */
    public function where(): DatastoreWhereQuery;
}

When to use: You need to filter records by multiple criteria, and get($args) isn't expressive enough.

Example:

$posts = $postDatastore
    ->where()
    ->equals('authorId', 123)
    ->greaterThan('publishedDate', '2024-01-01')
    ->getResults();

DatastoreHasCounts

Adds counting operations without fetching all records.

interface DatastoreHasCounts extends Datastore
{
    /**
     * Returns the total number of records matching the criteria.
     */
    public function count(array $args = []): int;
}

When to use: You need to know how many records exist without loading them all into memory (e.g., pagination totals).

Example:

$totalPosts = $postDatastore->count(['authorId' => 123]);

Composing Interfaces

In practice, most datastores implement multiple interfaces to provide a rich API. For example:

interface PostDatastore extends 
    Datastore, 
    DatastoreHasPrimaryKey, 
    DatastoreHasWhere, 
    DatastoreHasCounts
{
    // Custom business methods can also be added here
    public function findPublishedPosts(int $authorId): iterable;
}

This gives consumers:

The Minimal API Approach

Not every datastore needs all these capabilities. If your storage layer doesn't support primary keys (e.g., a log aggregator or event stream), you might only implement Datastore.

Example: minimal datastore

interface AuditLogDatastore extends Datastore
{
    // Only needs get(), save(), delete()
    // No primary keys, no WHERE queries
}

This is valid and often preferable. Only add interfaces when you actually need the capability, not because other datastores have them.

Working with DatastoreHandlers

While datastore interfaces define the public API, DatastoreHandler interfaces define the implementation contract for storage backends.

For example:

The Core implementation sits between these two, delegating calls from the public interface to the handler. This separation keeps business logic independent of storage details.

See DatastoreHandler interfaces for the storage-side contracts.

Best Practices

When designing or using datastore interfaces:

What's Next

To understand how these interfaces are implemented, see: