Saltar al contenido principal

Query Builder

EntityQueryBuilder is Weaver ORM's fluent query API. It wraps Doctrine DBAL with full entity awareness: results are automatically hydrated into entity objects, global scopes are applied, and soft-delete filters are managed transparently.


Getting a QueryBuilder​

Obtain a builder from a repository via query(), or from EntityWorkspace via createQueryBuilder():

<?php

// From a repository (recommended)
$users = $this->userRepository
->query()
->where('is_active', true)
->get();

// From the workspace
use Weaver\ORM\EntityWorkspace;
use App\Entity\Post;

$posts = $this->workspace
->createQueryBuilder(Post::class)
->where('status', 'published')
->get();

SELECT clauses​

select(...$columns)​

Replace the SELECT list. Calling it again replaces the previous selection.

<?php

$users = $this->userRepository
->query()
->select('id', 'name', 'email')
->get();

addSelect(...$columns)​

Append columns to the current SELECT list without replacing it.

<?php

$qb = $this->productRepository->query()->select('id', 'name', 'price');

if ($this->isGranted('ROLE_MANAGER')) {
$qb->addSelect('cost_price', 'supplier_id');
}

$products = $qb->get();

selectRaw($expression, $bindings = [])​

Add a raw SQL expression to the SELECT list.

<?php

$orders = $this->orderRepository
->query()
->select('id', 'customer_id', 'total')
->selectRaw('DATEDIFF(NOW(), created_at) AS age_days')
->where('status', 'pending')
->get();

WHERE clauses​

All where*() methods combine conditions with AND by default. Use orWhere*() variants to combine with OR.

where($column, $value) — equality​

<?php

$user = $this->userRepository
->query()
->where('email', 'alice@example.com')
->first();

where($column, $operator, $value) — with operator​

Supported operators: =, !=, <>, <, <=, >, >=, LIKE, NOT LIKE.

<?php

$expensive = $this->productRepository
->query()
->where('price', '>', 100)
->where('stock', '>', 0)
->get();

orWhere($column, $value)​

<?php

$results = $this->userRepository
->query()
->where('role', 'admin')
->orWhere('role', 'moderator')
->get();

whereIn($column, $values)​

<?php

$users = $this->userRepository
->query()
->whereIn('status', ['active', 'trial'])
->get();

whereNotIn($column, $values)​

<?php

$posts = $this->postRepository
->query()
->whereNotIn('status', ['draft', 'archived'])
->get();

whereNull($column) / whereNotNull($column)​

<?php

// Users who have never logged in
$neverLoggedIn = $this->userRepository
->query()
->whereNull('last_login_at')
->get();

// Users with a verified email
$verified = $this->userRepository
->query()
->whereNotNull('email_verified_at')
->get();

whereBetween($column, $min, $max)​

<?php

$orders = $this->orderRepository
->query()
->whereBetween('total', 50, 500)
->get();

whereRaw($sql, $bindings = [])​

<?php

$active = $this->subscriptionRepository
->query()
->whereRaw('expires_at > NOW() AND cancelled_at IS NULL')
->get();

// Always use bindings to avoid SQL injection
$results = $this->repository
->query()
->whereRaw('YEAR(created_at) = ?', [2024])
->get();

ORDER BY​

orderBy($column, $direction = 'ASC')​

<?php

$users = $this->userRepository
->query()
->orderBy('created_at', 'DESC')
->get();

orderByDesc($column)​

Shorthand for orderBy($column, 'DESC').

<?php

$latest = $this->postRepository
->query()
->where('status', 'published')
->orderByDesc('published_at')
->limit(10)
->get();

orderByRaw($expression)​

<?php

$products = $this->productRepository
->query()
->orderByRaw('FIELD(status, "featured", "active", "inactive")')
->get();

LIMIT and OFFSET​

limit($n) / offset($n)​

<?php

$page3 = $this->productRepository
->query()
->where('is_active', true)
->orderBy('name')
->limit(20)
->offset(40)
->get();

Eager loading​

with(...$relations)​

Load related entities in the same query (or a batched second query), avoiding N+1 problems.

<?php

$posts = $this->postRepository
->query()
->where('status', 'published')
->with('author', 'tags', 'comments')
->orderByDesc('published_at')
->get();

// Relations are already loaded — no extra queries
foreach ($posts as $post) {
echo $post->getAuthor()->getName();
}

Nested relations use dot notation:

<?php

$orders = $this->orderRepository
->query()
->with('customer', 'items.product')
->get();

Soft delete helpers​

When an entity uses the #[SoftDelete] attribute, a global scope automatically filters out soft-deleted rows. Use these methods to override the default behaviour.

withTrashed() — include soft-deleted rows​

<?php

$allPosts = $this->postRepository
->query()
->withTrashed()
->get();

onlyTrashed() — return only soft-deleted rows​

<?php

$deleted = $this->postRepository
->query()
->onlyTrashed()
->orderByDesc('deleted_at')
->get();

withoutTrashed() — explicitly exclude soft-deleted rows​

Useful inside a withTrashed() sub-scope to restore default filtering:

<?php

$active = $this->postRepository
->query()
->withoutTrashed()
->get();

SQL comments​

comment($text)​

Appends a SQL comment to the generated query. Useful for identifying slow queries in the database slow-query log.

<?php

$results = $this->reportRepository
->query()
->comment('monthly-revenue-report')
->where('month', $month)
->get();
// Generates: SELECT * FROM reports WHERE month = ? /* monthly-revenue-report */

Scopes​

Scopes are reusable query constraints. Weaver ORM supports global scopes (applied to every query) and local scopes (applied on demand).

Global scopes​

Implement ScopeInterface and register it on the mapper. The scope is applied to every query for that entity automatically.

<?php

namespace App\Scope;

use Weaver\ORM\Mapping\MapperInterface;
use Weaver\ORM\Query\EntityQueryBuilder;
use Weaver\ORM\Scope\ScopeInterface;

final class ActiveScope implements ScopeInterface
{
public function apply(EntityQueryBuilder $query, MapperInterface $mapper): void
{
$query->where('is_active', true);
}
}

Register on the mapper:

<?php

use App\Scope\ActiveScope;
use Weaver\ORM\Mapping\AbstractMapper;
use Weaver\ORM\Mapping\Attributes\GlobalScope;

#[GlobalScope(ActiveScope::class)]
class UserMapper extends AbstractMapper
{
// ...
}

Remove a global scope for a specific query:

<?php

$allUsers = $this->userRepository
->query()
->withoutScope(ActiveScope::class)
->get();

Local scopes​

Define local scope methods in the repository by prefixing the method name with scope:

<?php

namespace App\Repository;

use Weaver\ORM\Repository\EntityRepository;
use Weaver\ORM\Query\EntityQueryBuilder;

class OrderRepository extends EntityRepository
{
public function scopePending(EntityQueryBuilder $query): EntityQueryBuilder
{
return $query->where('status', 'pending');
}

public function scopeHighValue(EntityQueryBuilder $query, int $threshold = 1000): EntityQueryBuilder
{
return $query->where('total', '>=', $threshold);
}
}

// Usage:
$pendingHighValue = $this->orderRepository
->query()
->pending()
->highValue(500)
->get();

TenantScope example​

A common pattern for multi-tenant applications:

<?php

namespace App\Scope;

use App\Service\TenantContext;
use Weaver\ORM\Mapping\MapperInterface;
use Weaver\ORM\Query\EntityQueryBuilder;
use Weaver\ORM\Scope\ScopeInterface;

final class TenantScope implements ScopeInterface
{
public function __construct(
private readonly TenantContext $context,
) {}

public function apply(EntityQueryBuilder $query, MapperInterface $mapper): void
{
$query->where('tenant_id', $this->context->currentTenantId());
}
}

Result retrieval​

get() — all results as EntityCollection​

<?php

$products = $this->productRepository
->query()
->where('is_active', true)
->get(); // EntityCollection<Product>

first() — first result or null​

<?php

$latest = $this->postRepository
->query()
->orderByDesc('created_at')
->first(); // ?Post

firstOrFail() — first result or exception​

<?php

$post = $this->postRepository
->query()
->where('slug', $slug)
->firstOrFail(); // throws EntityNotFoundException if not found

count() — row count​

<?php

$total = $this->userRepository
->query()
->where('status', 'active')
->count(); // int

exists() — check existence without loading entities​

<?php

$alreadyRegistered = $this->userRepository
->query()
->where('email', $email)
->exists(); // bool

Pagination​

paginate($page, $perPage) — returns a Page object​

<?php

$page = $this->productRepository
->query()
->where('is_active', true)
->orderBy('name')
->paginate(page: 2, perPage: 20);

// $page->items() — EntityCollection for the current page
// $page->total() — total matching rows
// $page->currentPage() — current page number
// $page->lastPage() — last page number
// $page->hasMore() — whether a next page exists

See the Pagination page for the full Page API and cursor-based pagination.