メインコンテンツまでスキップ

クエリビルダー

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.