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.

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

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.categoryId = c.categoryId
            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
]);

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 oi.orderId = o.orderId
            range of p is App\\Entity\\ProductEntity via p.productId = oi.productId
            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