Repositories

Repositories encapsulate query logic for specific entities, providing reusable methods for data access. They're optional - use them when query logic benefits from centralization, or work directly with EntityManager for simple cases.

explanation

What are Repositories?

A repository is a class that handles all queries for a specific entity type. Instead of writing the same ObjectQuel queries repeatedly throughout your application, you write them once in the repository:

// Without repository - query scattered everywhere
$products = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p) where p.price >= :minPrice
    sort by p.price desc
", ['minPrice' => 100.00]);

// With repository - query centralized
$products = $productRepository->findExpensiveProducts(100.00);

When to use repositories:

  • The same query is needed in multiple places
  • Query logic is complex and benefits from a descriptive method name
  • You want centralized query logic for an entity type
  • You're using dependency injection and service layers

When to use EntityManager directly:

  • Query is only used once
  • Query is simple and self-explanatory
  • You're building a small application

Creating a Repository

Scaffold one with Sculpt:

$ php bin/sculpt make:repository Product

 Repository 'ProductRepository' created successfully at: src/Repositories/ProductRepository.php

Pass the entity name as an argument, or omit it to be prompted interactively. "Product", "ProductEntity", and "ProductRepository" all resolve to the same entity, which must already exist. An existing repository file is left untouched unless --force is passed. Like the other make:* commands (see Schema Management), this registers as quel:make:repository instead when another ORM package is also installed in the project.

Repositories extend the base Repository class and add custom query methods:

use Quellabs\ObjectQuel\Repository;

class ProductRepository extends Repository {

    public function __construct(EntityManager $entityManager) {
        parent::__construct($entityManager, ProductEntity::class);
    }

    public function findExpensiveProducts(float $minPrice = 100.00): array {
        return $this->entityManager->executeQuery("
            range of p is App\\Entity\\ProductEntity
            retrieve (p) where p.price >= :minPrice
            sort by p.price desc
        ", ['minPrice' => $minPrice]);
    }

    public function findByCategory(string $categoryName): array {
        return $this->entityManager->executeQuery("
            range of p is App\\Entity\\ProductEntity
            range of c is App\\Entity\\CategoryEntity via p.category
            retrieve (p) where c.name = :category
            sort by p.name asc
        ", ['category' => $categoryName]);
    }
}

Built-in Methods

All repositories inherit these methods from the base Repository class:

// Find by primary key
$product = $productRepository->find(101);

// Find by criteria
$products = $productRepository->findBy(['featured' => true]);
$products = $productRepository->findBy([
    'status' => 'active',
    'price' => 29.99
]);

// Find the first entity matching criteria - returns null if none found
$product = $productRepository->findOneBy(['slug' => 'my-product']);
$product = $productRepository->findOneBy(
    ['status' => 'active'],
    ['createdAt' => 'DESC']  // optional sort to control which one you get
);
Note on findOneBy: Because ObjectQuel hydrates related entities from joined rows, a SQL LIMIT 1 would cut off relation data for the returned entity. Instead, findOneBy runs the full query and returns the first result in PHP. For typical use cases — matching on a unique or near-unique field — this is not a concern in practice.

Queries with Aggregation

Repository methods can return aggregated data:

class ProductRepository extends Repository {

    public function countInPriceRange(float $minPrice, float $maxPrice): int {
        $results = $this->entityManager->executeQuery("
            range of p is App\\Entity\\ProductEntity
            retrieve (count(p)) where p.price >= :min and p.price <= :max
        ", ['min' => $minPrice, 'max' => $maxPrice]);

        return $results[0]['count(p)'];
    }

    public function getAveragePrice(): float {
        $results = $this->entityManager->executeQuery("
            range of p is App\\Entity\\ProductEntity
            retrieve (avg(p.price))
        ");

        return $results[0]['avg(p.price)'] ?? 0.0;
    }
}

Queries with Relationships

Repositories handle complex entity relationships:

class OrderRepository extends Repository {

    public function findRecentByCustomer(int $customerId, int $days = 30): array {
        $since = (new DateTime())->sub(new DateInterval("P{$days}D"));

        return $this->entityManager->executeQuery("
            range of o is App\\Entity\\OrderEntity
            retrieve (o) where o.customerId = :customerId
            and o.orderDate >= :since
            sort by o.orderDate desc
        ", ['customerId' => $customerId, 'since' => $since]);
    }

    public function findLargeOrders(float $minAmount = 1000.00): array {
        return $this->entityManager->executeQuery("
            range of o is App\\Entity\\OrderEntity
            range of oi is App\\Entity\\OrderItemEntity via o.items
            range of p is App\\Entity\\ProductEntity via oi.product
            retrieve (o, oi, p) where o.totalAmount >= :minAmount
            sort by o.totalAmount desc
        ", ['minAmount' => $minAmount]);
    }
}

Repositories are Optional

Unlike some ORMs that require repositories for all data access, ObjectQuel makes them optional. You can use EntityManager directly anywhere in your code:

// Direct EntityManager usage - perfectly valid
$products = $entityManager->executeQuery("
    range of p is App\\Entity\\ProductEntity
    retrieve (p) where p.featured = true
");

// Or use repositories for better organization
$products = $productRepository->findBy(['featured' => true]);

This flexibility lets you choose the right tool for each situation. Start simple with EntityManager, add repositories when complexity demands it.

Benefits

  • Reusability - Write queries once, use them everywhere
  • Testability - Easy to mock repositories in tests
  • Organization - Query logic grouped by entity type
  • Type safety - Better IDE autocomplete and type hints
  • Maintainability - Changes to queries happen in one place

Frequently Asked Questions

Are repositories required to use ObjectQuel?

No — unlike ORMs that require a repository for all data access, ObjectQuel's EntityManager can be called directly anywhere in your code. Repositories are an optional organizational layer you add once query logic benefits from centralization.

Why doesn't findOneBy() just use a SQL LIMIT 1?

Because ObjectQuel hydrates related entities from joined rows, and a LIMIT 1 at the SQL level could cut off relation data belonging to the one row you want. findOneBy() instead runs the full query and returns the first result in PHP — for typical use, matching on a unique or near-unique field, this isn't a practical concern.

When should I write a repository method instead of calling executeQuery() directly?

When the same query is needed in multiple places, when the query is complex enough to benefit from a descriptive method name, or when you're already using dependency injection and service layers. For a query used only once, calling EntityManager directly is simpler.