Declarative Query Engine • Data Mapper • PHP

Stay in Your Domain Model.
Even When Queries Get Complex.

A PHP ORM and query language inspired by QUEL. ObjectQuel lets you express complex data access through entities and relationships — not tables, joins, or database-specific SQL. Write database-independent domain queries; ObjectQuel generates optimized SQL for your database.

Mascotte
// Declare data ranges
range of p is Product
range of c is Category via p.categories

// Describe what you want
retrieve (p, category_name=c.name)
where p.price < 100
  and c.active = true
Describe Data. Not Execution.

Why Ranges Beat Joins

You declare data ranges and relationships — like via p.categories — instead of specifying joins, conditions, and execution order. ObjectQuel's planner figures out the rest.

Doctrine QueryBuilder
Procedural
$qb = $em->createQueryBuilder();
$qb->select('p', 'c.name as category_name')
   ->from(Product::class, 'p')
   ->leftJoin('p.categories', 'c')
   ->where('p.price < :maxPrice')
   ->andWhere('c.active = :active')
   ->setParameter('maxPrice', 100)
   ->setParameter('active', true);

$results = $qb->getQuery()->getResult();

You explicitly define joins, chain conditions, and control execution details.

ObjectQuel
Declarative
$results = $em->executeQuery("
    range of p is Product
    range of c is Category via p.categories
    retrieve (p, category_name=c.name)
    where p.price < :maxPrice
      and c.active = true
", [
    'maxPrice' => 100
]);
    

You declare data ranges and constraints. ObjectQuel analyzes the query and determines execution.

Navigation, Not Configuration

Relationships are paths: via p.categories expresses traversal, not SQL join mechanics.

Smart Query Decomposition

ObjectQuel analyzes ranges and constraints, then generates optimized execution steps automatically.

Composable by Design

Based on tuple relational calculus — QUEL’s declarative model for expressing relational intent.

When Other ORMs Run Out of Abstraction

Simple queries are easy in almost every ORM. The difference shows up when the query gets harder — not longer, harder.

Doctrine
Drops to native SQL
$sql = "
    SELECT u.name, o.total,
           RANK() OVER (
               PARTITION BY u.id ORDER BY o.total DESC
           ) AS rnk
    FROM users u
    JOIN orders o ON o.user_id = u.id
    JOIN products p ON p.id = o.product_id
    JOIN categories c ON c.id = p.category_id
    WHERE o.status = 'completed' AND
          c.name = 'Electronics'
";

$rows = $em->getConnection()
    ->executeQuery($sql)
    ->fetchAllAssociative();

The entity manager steps aside. Results come back as arrays, not entities — the abstraction ends here.

ObjectQuel
Still QUEL
$results = $em->executeQuery("
    range of u is User
    range of o is Order via u.orders
    range of p is Product via o.products
    range of c is Category via p.category
    retrieve (
        u.name, o.total,
        rnk = rank(by u.id sort by o.total desc)
    )
    where o.status = 'completed'
      and c.name = 'Electronics'
");

Still entities, still ranges, still hydrated objects. The window function is just another retrieve expression.

Try It Live

Write an ObjectQuel query against a live database and see the generated SQL instantly.

Examples
ObjectQuel Ctrl + Enter to run
{{error}}
Generated SQL
Run a query to see the generated SQL
{{currentSql}}
Results
Run a query to see results
{{json}}
Customer
idint
namestring
emailstring
Address
idint
streetstring
houseNumberstring
customerIdint (FK)

One Abstraction, All the Way Down

Regex, full-text search, hybrid sources — these are the cases that normally punch a hole through an ORM's abstraction and send you to raw SQL. ObjectQuel keeps them inside the same domain-level language instead.

Regex & Wildcard Filters

Write where p.name = /^widget/i or p.sku = "ABC*XYZ" natively. No whereRaw, no database-specific functions.

Full-Text Search

search(p.description, "banana +pear -apple") compiles to the correct full-text implementation for your database — no raw SQL required.

Hybrid SQL + JSON Queries

Join a SQL table and a JSON file in a single query. The engine handles cross-source matching automatically.

Relationship Traversal

via p.category is all you write. No join conditions, no foreign key configuration in the query itself.

Automatic Query Decomposition

Complex queries are split into optimized subtasks automatically. You describe the data; the engine determines execution.

Gradual Adoption

Works alongside existing database code. Introduce ObjectQuel queries one at a time — no big-bang rewrite required.

How ObjectQuel Works

From query to hydrated entities

Query Processing Pipeline
  • Parse: QUEL syntax into abstract syntax tree
  • Decompose: Break complex queries into optimized subtasks
  • Execute: Fetch from SQL databases and JSON sources
  • Hydrate: Join results and create entity objects

ObjectQuel handles optimization automatically, so you focus on describing data, not execution mechanics.

ObjectQuel Query Flow
QUEL Query range of p is... Parse AST Abstract Syntax Tree Decompose Query Decomposer Optimize & Plan SQL JSON SQL Queries Database Access JSON Data External Sources Join Results Merge Data Hydrate Entity Objects Ready to Use

ObjectQuel handles SQL and JSON sources, then hydrates results into objects

Why ObjectQuel Exists

After 25 years writing database queries, I got tired of fighting ORMs that force you to think in SQL joins or drown in method chaining. ObjectQuel brings QUEL’s declarative approach to PHP — declare data ranges and constraints, and let the engine determine execution.

— Floris, Quellabs

🚀

Get Started in Minutes

Install ObjectQuel via Composer and start querying with objects immediately

# Install via Composer
composer require quellabs/objectquel
Quick Start Example
use Cake\Database\Connection;

// Configure ObjectQuel
$config = new Configuration();
$config->setEntityPath(__DIR__ . '/Entities/');
$config->setProxyDir(__DIR__ . '/cache/proxies/');
$config->setMetadataCachePath(__DIR__ . '/cache/metadata/');

// Create CakePHP database connection
$connection = new Connection([
    'driver' => 'Cake\Database\Driver\Mysql',
    'host' => 'localhost',
    'username' => 'user',
    'password' => 'pass',
    'database' => 'db'
]);

// Initialize EntityManager
$entityManager = new EntityManager($config, $connection);

// Query immediately
$user = $entityManager->find(User::class, 10);
View on GitHub
Repository
View on Packagist
Package
Documentation
Read Docs

Open source and MIT licensed

The Quellabs Ecosystem

A family of open-source tools built around the same philosophy.

Ready to Modernize Your Data Layer?

Join developers who've already simplified their database code with ObjectQuel

GitHub Page Read Docs Sponsor on GitHub