-
-
Notifications
You must be signed in to change notification settings - Fork 0
Sub Queries
subQuery() builds a SELECT inside a closure that receives a fresh
builder. The result is returned as a RawQuery, which you can drop into
any context that accepts one — WHERE IN, FROM, JOIN, or standalone.
public function subQuery(
Closure $closure,
?string $alias = null,
bool $isIntervalQuery = true
): RawQuery-
$closure— receives a freshQueryBuilder(cloned from the parent, with structure reset). Use the same fluent API to build the inner SELECT. -
$alias— optional alias. Only valid when$isIntervalQueryistrue(otherwise the call throws). -
$isIntervalQuery— whentrue(default), the emitted SQL is wrapped in parentheses. Set tofalsefor a stand-alone fragment.
The most common use:
$qb->select('u.name')
->from('users AS u')
->whereIn('u.id', $qb->subQuery(function (QueryBuilder $sub) {
$sub->select('id')
->from('roles')
->where('name', 'admin');
}));
echo $qb->generateSelectQuery();SELECT `u`.`name`
FROM `users` AS `u`
WHERE `u`.`id` IN (SELECT `id` FROM `roles` WHERE `name` = :name)$derived = $qb->subQuery(function (QueryBuilder $sub) {
$sub->select('id', 'title', 'user_id')
->from('posts')
->where('user_id', 5);
}, 'p');
$qb->select('u.name', 'p.title')
->from('users AS u')
->join($derived, 'p.user_id = u.id', '');
echo $qb->generateSelectQuery();SELECT `u`.`name`, `p`.`title`
FROM `users` AS `u`
JOIN (SELECT `id`, `title`, `user_id` FROM `posts` WHERE `user_id` = 5) AS `p`
ON `p`.`user_id` = `u`.`id`
WHERE 1The alias goes on the sub-query call ('p'); the JOIN keyword can be left
empty ('') for an unqualified JOIN, or set to 'INNER', 'LEFT', etc.
When you want the raw inner SELECT without the wrapping parentheses, pass
$isIntervalQuery = false:
$raw = $qb->subQuery(function (QueryBuilder $sub) {
$sub->select('id')->from('users')->where('active', 1);
}, null, false);
echo (string) $raw;
// SELECT `id` FROM `users` WHERE `active` = 1Useful when you want to compose a larger SQL fragment by hand and slot a generated SELECT into it.
⚠️ Passing an alias and$isIntervalQuery = falseraises To define alias to a subquery, it must be an inner query. Aliases only make sense on parenthesized fragments.
Any operator that accepts a RawQuery works:
$qb->from('users')
->where('id', '=', $qb->subQuery(function (QueryBuilder $sub) {
$sub->select('user_id')
->from('latest_login')
->orderBy('logged_at', 'DESC')
->limit(1);
}));
// WHERE `id` = (SELECT `user_id` FROM `latest_login` WHERE 1
// ORDER BY `logged_at` DESC LIMIT 1)The closure receives a clone of the outer builder. The clone has its own structure and its own parameter bag, so sub-query parameters do not collide with the outer query's parameters.
$qb->select('u.name')
->from('users AS u')
->whereIn('u.role_id', $qb->subQuery(function (QueryBuilder $sub) {
$sub->select('id')
->from('roles')
->whereIn('name', ['admin', 'moderator']);
}));SELECT `u`.`name`
FROM `users` AS `u`
WHERE `u`.`role_id` IN (
SELECT `id` FROM `roles` WHERE `name` IN (:name, :name_1)
)The :name / :name_1 placeholders live in the inner builder's bag. The
resulting SQL is captured as a RawQuery and embedded into the outer
query, so the placeholders are already part of the final SQL string — but
when you execute the outer query, those placeholders need to come from
the outer parameter bag. The cleanest pattern: hoist sub-query values
out as outer parameters when they're dynamic.
$qb->setParameter('admin_role', 'admin');
$qb->select('u.name')
->from('users AS u')
->whereIn('u.role_id', $qb->subQuery(function (QueryBuilder $sub) {
$sub->select('id')->from('roles')->where('name', $sub->raw(':admin_role'));
}));
// The placeholder :admin_role lives in the OUTER bag.$qb->select('u.name')
->from('users AS u')
->whereIn('u.id', $qb->subQuery(fn (QueryBuilder $sub) =>
$sub->select('user_id')->from('orders')->where('status', 'paid')
))
->andWhereNotIn('u.id', $qb->subQuery(fn (QueryBuilder $sub) =>
$sub->select('user_id')->from('bans')
));RawQuery is the right tool when the sub-query is static:
$qb->whereIn('id', $qb->raw('(SELECT user_id FROM bans)'));That skips the closure indirection. Reserve subQuery() for cases where
the inner SELECT is itself dynamic. See Raw Queries for details.
Next: Grouped Conditions
InitORM QueryBuilder — MIT licensed · authored by Muhammet ŞAFAK · part of the InitORM family · report an issue · security disclosure