DatastoreHasCounts Interface

The DatastoreHasCounts interface extends Datastore to add efficient counting operations. It provides the count() method for determining how many records match given criteria without fetching and loading all the data.

This interface is useful for pagination (knowing total pages), dashboard metrics (e.g., "23 unread messages"), and existence checks (e.g., "are there any drafts?").

Interface Definition

interface DatastoreHasCounts extends Datastore
{
    /**
     * Returns the total number of records matching the criteria.
     *
     * @param array $args Filtering criteria (same format as get())
     * @return int The count of matching records
     */
    public function count(array $args = []): int;
}

Method

count(array $args = []): int

Counts records matching the provided criteria without fetching them.

Parameters:

Returns:

When to use:

Example: total records

$totalPosts = $postDatastore->count();
echo "Total posts: {$totalPosts}";

Example: filtered count

$publishedCount = $postDatastore->count(['status' => 'published']);
$draftCount = $postDatastore->count(['status' => 'draft']);

echo "Published: {$publishedCount}, Drafts: {$draftCount}";

Example: existence check

$hasDrafts = $postDatastore->count(['status' => 'draft']) > 0;

if ($hasDrafts) {
    echo "You have unpublished drafts";
}

Why This Interface Exists

Without count(), you'd have to fetch all records and count them:

// ❌ BAD: loads all records into memory
$posts = $datastore->get(['author_id' => 123]);
$count = count($posts); // Expensive!

With count(), the operation happens at the storage layer:

// ✅ GOOD: efficient database COUNT query
$count = $datastore->count(['author_id' => 123]);

For databases, this translates to SELECT COUNT(*) FROM ..., which is far more efficient than fetching rows.


Usage Patterns

Pagination

Counting is essential for calculating total pages:

final class PostPaginationService
{
    public function __construct(
        private PostDatastore $posts
    ) {}

    public function getPaginationInfo(array $filters, int $perPage): array
    {
        $total = $this->posts->count($filters);
        $totalPages = (int) ceil($total / $perPage);

        return [
            'total' => $total,
            'per_page' => $perPage,
            'total_pages' => $totalPages,
        ];
    }

    public function getPage(array $filters, int $page, int $perPage): iterable
    {
        return $this->posts->get(array_merge($filters, [
            'limit' => $perPage,
            'offset' => ($page - 1) * $perPage,
        ]));
    }
}

Usage:

$filters = ['author_id' => 123, 'status' => 'published'];

$info = $service->getPaginationInfo($filters, perPage: 10);
// ['total' => 47, 'per_page' => 10, 'total_pages' => 5]

$posts = $service->getPage($filters, page: 2, perPage: 10);
// Returns posts 11-20

Dashboard Metrics

Counting is ideal for statistics dashboards:

final class DashboardService
{
    public function __construct(
        private PostDatastore $posts
    ) {}

    public function getStats(int $authorId): array
    {
        return [
            'total' => $this->posts->count(['author_id' => $authorId]),
            'published' => $this->posts->count([
                'author_id' => $authorId,
                'status' => 'published'
            ]),
            'drafts' => $this->posts->count([
                'author_id' => $authorId,
                'status' => 'draft'
            ]),
        ];
    }
}

Returns:

[
    'total' => 52,
    'published' => 48,
    'drafts' => 4,
]

Conditional Logic

Use count() for existence checks or thresholds:

// Check if user has any posts before allowing account deletion
$postCount = $postDatastore->count(['author_id' => $userId]);

if ($postCount > 0) {
    throw new ValidationException("Cannot delete user with existing posts");
}

// Enforce post limits
$userPostCount = $postDatastore->count(['author_id' => $userId]);

if ($userPostCount >= 100) {
    throw new LimitExceededException("Post limit reached");
}

Combining with WHERE Queries

If your datastore implements both DatastoreHasCounts and DatastoreHasWhere, you can count complex queries:

$query = $postDatastore
    ->where()
    ->equals('author_id', 123)
    ->greaterThan('published_date', '2024-01-01')
    ->lessThan('view_count', 100);

$count = $query->count(); // How many match?
$posts = $query->getResults(); // Fetch them if needed

This is more powerful than count($args) because you get the full query-builder API.


Relationship to Other Interfaces

vs. get() + count()

Method Efficiency Use Case
count($args) High (storage-layer count) Pagination, metrics, existence checks
count(get($args)) Low (loads all data) Never do this

Always use count() instead of loading and counting.

Combining with Other Extensions

interface PostDatastore extends 
    Datastore,
    DatastoreHasPrimaryKey,
    DatastoreHasWhere,
    DatastoreHasCounts
{
    // get(), save(), delete()
    // find()
    // where()
    // count()
}

This provides a complete query API.


Implementation with Decorator Traits

Use WithDatastoreCountDecorator to auto-implement:

final class PostDatastoreImpl implements DatastoreHasCounts
{
    use WithDatastoreCountDecorator;

    public function __construct(
        private DatastoreHandlerHasCounts $handler
    ) {}
}

The trait delegates count() to the handler.


Implementation Notes

When implementing this interface:


When NOT to Implement This Interface

Skip DatastoreHasCounts if:


What's Next