← Persistence

PERSISTENCE FEATURE

Query Builder

Compose type-safe queries with a fluent API — no raw SQL, no magic strings.

Feature Guide

A quick orientation block that answers the essential questions: what this feature does, how it works, why it matters, and the key concepts behind it.

What this does

Compose type-safe queries with a fluent API — no raw SQL, no magic strings.

Query Builder

Compiled query example

The repository composes fluent constraints with typed column references, then materializes a filtered collection.

10 Returned rows
10 Limit
$products = $orm->repository(DemoProductResource::class, DemoProduct::class)
    ->query()
    ->limit(10)
    ->fetchAllAs(DemoProduct::class, $orm->getMapperRegistry());
Name Price Status
Air Purifier $149.99 active
Domain-Driven Design $54.99 active
Ergonomic Chair $299.99 active
Foam Roller $29.99 active
Head First Design Patterns $49.99 active
ResourceModelQuery where() orderBy() limit() fetchAllAs() fetchOneAs()

Verified against Semitexa Ultimate 2026.09.19.1020

Query Builder

The Semitexa ORM exposes a fluent query API that compiles type-safe constraints against ResourceModel column references without raw SQL.

How it works

A repository opens a query with ->query() on the ORM repository instance, chains where(), orderBy(), and limit() calls using typed ResourceModel::column() references and Operator / Direction enums, then materializes results with fetchAllAs() or fetchOneAs(). The result is a concrete typed collection — no magic arrays.

Why this matters

Raw SQL strings and magic column name literals are the most common source of silent query bugs. Typed column references catch rename mismatches at boot, and the fluent API keeps the query shape visible in code review without losing flexibility for complex filtering or ordering needs.

© Brian Kernighan: "Controlling complexity is the essence of computer programming."

Catalog Repository Persistence adapter
<?phpdeclare(strict_types=1);namespace Examples\Orm\QueryBuilder;use Semitexa\Core\Attribute\InjectAsReadonly;use Semitexa\Demo\Application\Db\MySQL\Model\DemoProductResource;use Semitexa\Demo\Domain\Model\DemoProduct;use Semitexa\Orm\Attribute\AsRepository;use Semitexa\Orm\Query\Direction;use Semitexa\Orm\Query\Operator;#[AsRepository]final class ProductReadRepository{    #[InjectAsReadonly]    protected ProductQueryBuilder $queryBuilder;    public function findActivePage(int $limit = 12, int $offset = 0): array    {        return $this->queryBuilder            ->map(                $this->queryBuilder                    ->new()                    ->where(DemoProductResource::column('status'), Operator::Equals, 'active')                    ->whereNull(DemoProductResource::column('deletedAt'))                    ->orderBy(DemoProductResource::column('createdAt'), Direction::Desc)                    ->limit($limit)                    ->offset($offset),            );    }    public function searchCatalog(string $term, ?float $maxPrice = null): array    {        $query = $this->queryBuilder            ->new()            ->where(DemoProductResource::column('name'), Operator::Like, '%' . $term . '%')            ->where(DemoProductResource::column('status'), Operator::Equals, 'active')            ->orderBy(DemoProductResource::column('name'), Direction::Asc)            ->limit(20);        if ($maxPrice !== null) {            $query->where(DemoProductResource::column('price'), Operator::LessThanOrEquals, $maxPrice);        }        return $this->queryBuilder->map($query);    }    public function findOneByName(string $name): ?DemoProduct    {        return $this->queryBuilder            ->one(                $this->queryBuilder                    ->new()                    ->where(DemoProductResource::column('name'), Operator::Equals, $name)                    ->where(DemoProductResource::column('status'), Operator::Equals, 'active'),            );    }}

Support Semitexa
Built for developers who prefer control over magic. Your support helps keep it fast, open, and evolving.

Donate via PayPal