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
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
);
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.