ObjectQuel draws inspiration from QUEL, the pioneering query language from UC Berkeley's Ingres project. Unlike SQL's table-centric approach, ObjectQuel queries work directly with your domain entities using object-oriented syntax. Queries read naturally while leveraging powerful search capabilities including wildcards, regex, and full-text search.
ObjectQuel queries execute in this specific order. Understanding this flow is essential:
@InValuesAreFinal // 1. Query Flags (optional)
range of p is ProductEntity // 2. Range Definitions (required)
range of c is CategoryEntity via p.category // 3. Related Ranges (optional)
retrieve (p, c.name) // 4. Retrieve Clause (required)
where p.price < :max and c.active = true // 5. Where Clause (optional)
sort by c.name asc, p.price desc // 6. Sort Clause (optional)
window 0, 20 // 7. Pagination (optional)
Both range definitions and retrieve clauses are required. Every variable used in your query must be declared with a range definition before it can be used in retrieve, where, or sort clauses.
Range definitions declare which entities you're querying and are mandatory for all queries. Every variable referenced in your query must have a corresponding range definition:
// Single entity range
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p)
");
// Multiple ranges (explicit join)
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
range of c is App\\Entity\\CategoryEntity via p.category
retrieve (p, c)
");
When querying related entities, use the via keyword to explicitly traverse relationships. This makes the query intent clear and allows ObjectQuel to construct the appropriate joins.
The retrieve clause specifies what data to return. You have three options, each optimized for different use cases:
Returns complete entity objects with all properties hydrated. Use this when you need the entire entity:
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p) where p.active = true
");
foreach($results as $row) {
$product = $row['p']; // Full ProductEntity object
echo $product->getName();
echo $product->getPrice();
}
Returns only the requested property values as scalars. Use this to minimize memory usage and improve performance:
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p.name) where p.active = true
");
foreach($results as $row) {
$name = $row['p.name']; // String value, not an object
echo $name;
}
Combines full objects with specific property values. Use this when you need complete entities from one table but only specific fields from related entities:
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
range of c is App\\Entity\\CategoryEntity via p.category
retrieve (p, c.name) where p.price < 100
");
foreach($results as $row) {
$product = $row['p']; // Full ProductEntity object
$categoryName = $row['c.name']; // Category name as string
}
Always use parameter binding to prevent SQL injection and enable query plan caching:
// Single parameter
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p) where p.price < :maxPrice
", ['maxPrice' => 100.00]);
// Multiple parameters
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p) where p.price between :min and :max
", [
'min' => 10.00,
'max' => 100.00
]);
$results = $entityManager->executeQuery("
range of u is App\\Entity\\UserEntity
range of o is App\\Entity\\OrderEntity via u.orders
retrieve (u, o.total)
where u.registeredAt >= :startDate
and o.status = :status
sort by o.createdAt desc
", [
'startDate' => '2024-01-01',
'status' => 'completed'
]);
Security note: Never concatenate user input directly into queries. Always use parameter binding.
Build complex conditions using comparison operators and logical combinators:
// Range queries
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p) where p.price >= :minPrice and p.price <= :maxPrice
", ['minPrice' => 10.00, 'maxPrice' => 50.00]);
// Alternative using BETWEEN
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p) where p.price between :min and :max
", ['min' => 10.00, 'max' => 50.00]);
// Match multiple values
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p) where p.categoryId in (:categories)
", ['categories' => [1, 2, 3]]);
// Exclude multiple values
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p) where p.status not in (:excluded)
", ['excluded' => ['deleted', 'archived']]);
// IS NULL
$results = $entityManager->executeQuery("
range of u is App\\Entity\\UserEntity
retrieve (u) where u.deletedAt is null
");
// IS NOT NULL
$results = $entityManager->executeQuery("
range of u is App\\Entity\\UserEntity
retrieve (u) where u.emailVerifiedAt is not null
");
// Combine with AND/OR/NOT
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p)
where (p.featured = true or p.onSale = true)
and p.stock > 0
and not p.discontinued = true
");
ObjectQuel provides three pattern matching approaches, each optimized for different scenarios:
Use asterisks (*) for simple pattern matching. This is the most performant option for prefix/suffix/contains searches:
// Prefix matching (most efficient)
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p) where p.name = 'iPhone*'
");
// Suffix matching
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p) where p.name = '*Pro'
");
// Contains (use sparingly—less efficient)
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p) where p.name = '*wireless*'
");
// Complex patterns: starts with ABC, has 3 chars, ends with XYZ
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p) where p.sku = 'ABC*-???-*XYZ'
");
Use regex for complex pattern requirements. Enclose patterns in forward slashes:
// Email validation
$results = $entityManager->executeQuery("
range of u is App\\Entity\\UserEntity
retrieve (u) where u.email = /^[a-zA-Z0-9._%+-]+@company\.com$/
");
// Case-insensitive matching (use 'i' flag)
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p) where p.description = /smartphone/i
");
Use the search() function for natural language queries. This leverages database full-text indexing:
// Single field search with boolean operators
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p) where search(p.description, 'wireless +bluetooth -expensive')
");
// +word = must include
// -word = must not include
// word = optional (boosts ranking)
// Multi-field search
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p) where search(p.name, p.description, 'premium quality')
");
Performance tip: Use wildcards for exact patterns, regex for complex validation, and full-text search for natural language queries.
ObjectQuel uses the via keyword to traverse relationships by referencing the relationship property on your entity. The syntax is identical regardless of relationship direction:
// ManyToOne: OrderEntity has a 'user' property referencing UserEntity
$results = $entityManager->executeQuery("
range of o is App\\Entity\\OrderEntity
range of u is App\\Entity\\UserEntity via o.user
retrieve (o, u.email) where o.createdAt > '2024-01-01'
");
// OneToMany: UserEntity has an 'orders' property (collection of OrderEntity)
$results = $entityManager->executeQuery("
range of u is App\\Entity\\UserEntity
range of o is App\\Entity\\OrderEntity via u.orders
retrieve (u.name, o.total) where o.status = 'completed'
");
In both cases, you reference the property name on the source entity: via o.user uses OrderEntity's user property, while via u.orders uses UserEntity's orders property. ObjectQuel handles the underlying join logic automatically.
Chain multiple via clauses to traverse through multiple relationship levels:
$results = $entityManager->executeQuery("
range of u is App\\Entity\\UserEntity
range of o is App\\Entity\\OrderEntity via u.orders
range of i is App\\Entity\\OrderItemEntity via o.items
retrieve (u.name, i.quantity) where i.productId = 123
");
This traverses: User → Orders (via orders property) → Order Items (via items property), filtering items by product ID.
Control result ordering with the sort by clause. Multiple sort fields are evaluated left-to-right:
// Single field
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p) where p.active = true
sort by p.name asc
");
// Multiple fields (priority order: featured → price → name)
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p) where p.active = true
sort by p.featured desc, p.price asc, p.name asc
");
// Sort by related entity properties
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
range of c is App\\Entity\\CategoryEntity via p.category
retrieve (p) where p.active = true
sort by c.name asc, p.name asc
");
Handle large result sets efficiently using the window operator. The window operator calculates page boundaries rather than using direct LIMIT/OFFSET:
// Syntax: window [window_number], [items_per_window]
// Window numbers are 0-indexed (first page = window 0)
// First page, 10 items per page
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p) where p.active = true
sort by p.name asc
window 0, 10
");
// Third page, 25 items per page (window 2 = items 51-75)
$results = $entityManager->executeQuery("
range of p is App\\Entity\\ProductEntity
retrieve (p) where p.active = true
sort by p.name asc
window 2, 25
");
// Always sort before paginating for consistent results
// Sorting is not strictly required but highly recommended
How it works: ObjectQuel calculates the offset as window_number × items_per_window. So window 0, 10 retrieves items 1-10, window 1, 10 retrieves items 11-20, and window 2, 25 retrieves items 51-75.
Query flags modify execution behavior for specific optimization scenarios:
Optimizes IN queries when values are primary keys or unique identifiers. This tells ObjectQuel that results won't exceed the IN clause size:
// Optimized for IN queries on primary keys
$results = $entityManager->executeQuery("
@InValuesAreFinal
range of p is App\\Entity\\ProductEntity
retrieve (p) where p.productId in (:ids)
", ['ids' => [1, 2, 3, 4, 5]]);
// Without @InValuesAreFinal, ObjectQuel may use less efficient query plans
// Use this flag when querying by ID and you need all matching records
When to use: Enable this flag when your IN clause contains primary keys and you expect at most one result per value.