PHPackages                             alaca/queryable - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. alaca/queryable

ActiveLibrary[Database &amp; ORM](/categories/database)

alaca/queryable
===============

A fluent SQL query builder for WordPress

114PHP

Since May 17Pushed 1mo agoCompare

[ Source](https://github.com/alaca/queryable)[ Packagist](https://packagist.org/packages/alaca/queryable)[ RSS](/packages/alaca-queryable/feed)WikiDiscussions main Synced 2w ago

READMEChangelogDependenciesVersions (1)Used By (0)

Queryable
=========

[](#queryable)

A fluent SQL query builder for WordPress. Built on `$wpdb`, with meta table support, relations, migrations, and a typed model layer

Installation
------------

[](#installation)

```
composer require alaca/queryable
```

Two ways to use
---------------

[](#two-ways-to-use)

### 1. Query Builder (DB Facade)

[](#1-query-builder-db-facade)

Use `DB::table()` for direct queries

```
use Queryable\DB;

// Simple queries
$posts = DB::table('posts')->select('ID', 'post_title')->where('post_status', 'publish')->getAll();
$user  = DB::table('users')->find('ID', 1);

// If you want to work with meta and relations using DB class, you will have to configure the schema in DB::init()
DB::init([
    'schema' => [
        'posts' => [
            'meta' => [
                'table' => 'postmeta', // table name is auto prefixed
                'foreignKey' => 'post_id',
                'primaryKey' => 'ID',
                'aliases' => [
                    'price' => '_product_price',
                    'color' => '_product_color',
                ],
            ],
            'relations' => [
                'comments' => [
                    'table' => 'comments',
                    'foreignKey' => 'comment_post_ID',
                    'primaryKey' => 'ID',
                    'type' => 'hasMany',
                ],
            ],
        ],
    ],
]);

$products = DB::table('posts')
    ->select('ID', 'post_title')
    ->withMeta('price', 'color')
    ->where('post_type', 'product')
    ->orderBy('price', 'DESC')
    ->getAll();
```

### 2. Model (Recommended)

[](#2-model-recommended)

Define a model class per table with typed public properties

```
use Queryable\Model;
use Queryable\Schema\Table;

class Campaign extends Model
{
    protected string $table = 'campaigns';
    protected string $version = '1.0.0';

    public int $id;
    public string $name;
    public string $slug;
    public float $price;
    public int $stock;
    public ?string $created_at = null;

    protected function meta(): array
    {
        return [
            'table' => 'campaign_meta',
            'foreignKey' => 'campaign_id',
            'primaryKey' => 'id',
            'aliases' => [
                'budget' => '_campaign_budget',
                'channel' => '_campaign_channel',
                'tags' => [
                    'key' => '_campaign_tags',
                    'multiple' => true
                ],
            ],
        ];
    }

    protected function relations(): array
    {
        return [
            'entries' => [
                'table' => 'campaign_entries',
                'foreignKey' => 'campaign_id',
                'primaryKey' => 'id',
                'type' => 'hasMany',
            ],
        ];
    }
}

// Schema is defined separately
Campaign::schema(function (Table $table) {
    $table->id();
    $table->string('name');
    $table->string('slug', 100)->unique();
    $table->decimal('price', 8, 2)->default(0);
    $table->integer('stock')->default(0);
    $table->datetime('created_at')->nullable();
});

// Query results are Campaign instances
$campaign = Campaign::query()->find('slug', 'summer');
$campaign instanceof Campaign; // true
$campaign->name;
$campaign->price; // float, auto-cast from DB  - should be unsigned int in real app!

Campaign::query()->select('id', 'name')->withMeta('budget')->where('status', 'active')->getAll();
Campaign::query()->insert(['name' => 'Summer Sale', 'slug' => 'summer-sale']);
Campaign::query()->where('id', 1950)->update(['name' => 'Updated']);
Campaign::query()->where('id', 1950)->delete();
```

Table of Contents
-----------------

[](#table-of-contents)

- [Base Query](#base-query)
- [Scopes](#scopes)
- [Select](#select)
- [Save](#save)
- [Lifecycle Hooks](#lifecycle-hooks)
- [Insert](#insert)
- [Update](#update)
- [Delete](#delete)
- [Where Clauses](#where-clauses)
- [Joins](#joins)
- [Ordering, Grouping, Limit &amp; Offset](#ordering-grouping-limit--offset)
- [Having](#having)
- [Union](#union)
- [Aggregates](#aggregates)
- [Find &amp; Exists](#find--exists)
- [Pluck](#pluck)
- [Meta Tables](#meta-tables)
- [Relations](#relations)
- [Migrations](#migrations)
- [Transactions](#transactions)
- [Raw Queries](#raw-queries)
- [Conditional Clauses](#conditional-clauses)
- [Clone](#clone)
- [SQL Output](#sql-output)

Base Query
----------

[](#base-query)

Apply default conditions to every query a model makes. Useful for models that represent a subset of a shared table

```
class Product extends Model
{
    protected string $table = 'posts';
    protected string $primaryKey = 'ID';

    public int $ID;
    public string $post_title;
    public string $post_status;

    protected static function baseQuery(ModelQueryBuilder $builder): ModelQueryBuilder
    {
        return $builder->where('post_type', 'product');
    }
}

// Every query automatically includes WHERE post_type = 'product'
Product::query()->where('post_status', 'publish')->getAll();
Product::query()->find('ID', 1911);
Product::query()->count('ID');
```

Scopes
------

[](#scopes)

Define reusable query filters as static methods on your model

```
class Campaign extends Model
{
    protected string $table = 'campaigns';

    public static function active(): ModelQueryBuilder
    {
        return static::query()->where('status', 'active');
    }

    public static function p2p(): ModelQueryBuilder
    {
        return static::query()->where('campaign_type', 'p2p');
    }
}

Campaign::active()->getAll();
Campaign::active()->orderBy('name')->limit(10)->getAll();
Campaign::p2p()->getAll();
```

Save
----

[](#save)

Create or update a model instance

```
// Create new
$campaign = Campaign::make();
$campaign->name = 'Summer';
$campaign->slug = 'summer';
$campaign->price = 29.99;
$campaign->save();

// Update existing
$campaign = Campaign::query()->find('id', 1950);
$campaign->name = 'Updated';
$campaign->save();
```

The primary key defaults to `id`. Override `$primaryKey` in your model if needed:

```
class Post extends Model
{
    protected string $table = 'posts';
    protected string $primaryKey = 'ID';
}
```

Lifecycle Hooks
---------------

[](#lifecycle-hooks)

Override `onBeforeSave()` and `onSave()` to run logic before and after `save()`

```
class Campaign extends Model
{
    protected string $table = 'campaigns';

    protected function onBeforeSave(): void
    {
        if (empty($this->slug)) {
            $this->slug = sanitize_title($this->name);
        }
    }

    protected function onSave(): void
    {
        do_action('campaign_saved', $this);
    }
}
```

`onBeforeSave()` runs before the database write. Any property changes made there are included in the save

`onSave()` runs after the database write

Insert
------

[](#insert)

```
// Single row
Campaign::query()->insert(['name' => 'Summer', 'slug' => 'summer']);
// Returns: QueryResult { affectedRows: 1, insertId: 1 }

// bulk insert
Campaign::query()->insert([
    ['name' => 'Summer', 'slug' => 'summer'],
    ['name' => 'Winter', 'slug' => 'winter'],
]);

// insert with meta (meta fields are separated automatically)
Campaign::query()->withMeta('budget', 'channel')->insert([
    'name'    => 'Summer',
    'slug'    => 'summer',
    'budget'  => '1950', // campaign_meta table
    'channel' => 'email', // campaign_meta table
]);

// upsert (insert or update on duplicate key)
Campaign::query()->upsert(
    ['name' => 'Summer', 'slug' => 'summer'],
    ['slug'], // conflict columns
    ['name'], // columns to update
);
```

Update
------

[](#update)

```
Campaign::query()->where('id', 1)->update(['name' => 'Updated']);

// update meta
Campaign::query()->withMeta('budget')->where('id', 1)->update([
    'name'   => 'Updated',
    'budget' => '1911',// updates meta row
]);

// Increment / Decrement
Campaign::query()->where('id', 1)->increment('stock');
Campaign::query()->where('id', 1)->increment('stock', 5);
Campaign::query()->where('id', 1)->decrement('stock', 3);

// raw update
Campaign::query()->where('id', 1)->updateRaw("stock = stock + 1");
```

Delete
------

[](#delete)

```
Campaign::query()->where('id', 1)->delete();
// meta rows are deleted automatically if the model has meta config

// truncate (clears both the table and meta table)
Campaign::query()->truncate();
```

Select
------

[](#select)

```
// all columns
Campaign::query()->getAll();

// select specific columns
Campaign::query()->select('id', 'name', 'slug')->getAll();

// single row (LIMIT 1, returns null if not found)
Campaign::query()->select('id', 'name')->get();

// distinct
Campaign::query()->distinct()->select('status')->getAll();

// select with alias
Campaign::query()->select(['id' => 'campaign_id'])->getAll();

// raaw select
Campaign::query()->selectRaw('COUNT(*) as total')->get();

// Subquery in FROM
DB::table(function ($qb) {
    $qb->table('orders')->select('user_id')->groupBy('user_id');
},
'order_totals')->selectRaw('user_id, total')->getAll();
```

Where Clauses
-------------

[](#where-clauses)

```
// basic
Campaign::query()->where('status', 'active')->getAll();
Campaign::query()->where('status', 'active')->orWhere('status', 'pending')->getAll();

// LIKE
Campaign::query()->whereLike('name', 'summer')->getAll(); // LIKE '%summer%'
Campaign::query()->whereLike('name', 'summer%')->getAll(); // LIKE 'summer%'

// IN / NOT IN
Campaign::query()->whereIn('id', [1, 2, 3])->getAll();
Campaign::query()->whereNotIn('status', ['draft', 'trash'])->getAll();

// BETWEEN
Campaign::query()->whereBetween('price', 10, 100)->getAll();

// NULL
Campaign::query()->whereIsNull('deleted_at')->getAll();
Campaign::query()->whereIsNotNull('email')->getAll();

// column comparison (no value escaping), useful for JOINS
Campaign::query()->whereColumn('users.id', 'orders.user_id')->getAll();

// nested groups
Campaign::query()->where('status', 'active')
    ->orWhere(function ($qb) {
        $qb->where('role', 'admin')->where('verified', 1);
    })
    ->getAll();
// WHERE status = 'active' OR (role = 'admin' AND verified = 1)

// Subquery
Campaign::query()->whereIn('id', function ($qb) {
    $qb->table('orders')->select('campaign_id')->where('total', 100, '>');
})->getAll();

// EXISTS
Campaign::query()->whereExists(function ($qb) {
    $qb->table('orders')->select('id')->whereRaw('orders.campaign_id = campaigns.id');
})->getAll();

// Raw
Campaign::query()->whereRaw('created_at > NOW() - INTERVAL 30 DAY')->getAll();
```

All where methods have `or` variants: `orWhere`, `orWhereLike`, `orWhereIn`, `orWhereBetween`, `orWhereIsNull`, `orWhereExists`, `orWhereColumn`.

Joins
-----

[](#joins)

```
Campaign::query()->leftJoin('entries', 'campaigns.id', 'entries.campaign_id')->getAll();
Campaign::query()->innerJoin('entries', 'campaigns.id', 'entries.campaign_id')->getAll();
Campaign::query()->rightJoin('entries', 'campaigns.id', 'entries.campaign_id')->getAll();
Campaign::query()->crossJoin('statuses')->getAll();

// with alias
Campaign::query()->leftJoin('entries', 'campaigns.id', 'e.campaign_id', 'e')->getAll();

// raw join
Campaign::query()->joinRaw('LEFT JOIN entries e ON campaigns.id = e.campaign_id')->getAll();
```

Ordering, Grouping, Limit &amp; Offset
--------------------------------------

[](#ordering-grouping-limit--offset)

```
Campaign::query()->orderBy('name')->getAll();
Campaign::query()->orderBy('name', 'DESC')->getAll();
Campaign::query()->orderByRaw('RAND()')->getAll();

Campaign::query()->groupBy('status')->getAll();
Campaign::query()->groupBy('status', 'channel')->getAll();
Campaign::query()->groupByRaw('YEAR(created_at)')->getAll();

Campaign::query()->limit(10)->offset(20)->getAll();

// order and group by meta keys
Campaign::query()->withMeta('budget')->orderBy('budget', 'DESC')->getAll();
Campaign::query()->withMeta('channel')->groupBy('channel')->getAll();
```

Having
------

[](#having)

```
Campaign::query()->groupBy('status')->havingCount('id', '>', 5)->getAll();
Campaign::query()->groupBy('status')->havingSum('stock', '>', 100)->getAll();
Campaign::query()->groupBy('status')->havingAvg('price', '>', 50)->getAll();
Campaign::query()->groupBy('status')->havingMin('price', '>', 10)->getAll();
Campaign::query()->groupBy('status')->havingMax('price', ' 5')->getAll();

// combine with OR
Campaign::query()->groupBy('status')
    ->havingSum('stock', '>', 100)
    ->orHavingAvg('price', '>', 200)
    ->getAll();
```

Union
-----

[](#union)

```
$drafts = Campaign::query()->where('status', 'draft')->select('id', 'name');
Campaign::query()->where('status', 'active')->select('id', 'name')->union($drafts)->getAll();

// Union ALL
Campaign::query()->select('id')->unionAll($drafts)->getAll();
```

Aggregates
----------

[](#aggregates)

```
Campaign::query()->count('id'); // int
Campaign::query()->sum('price'); // float
Campaign::query()->avg('price'); // float
Campaign::query()->min('price'); // float
Campaign::query()->max('price'); // float

// with conditions
Campaign::query()->where('status', 'active')->count('id');
```

Find &amp; Exists
-----------------

[](#find--exists)

```
// find by column. returns model instance or null
Campaign::query()->find('id', 1950);
Campaign::query()->find('slug', 'summer');

// check if rows exist. returns bool
Campaign::query()->where('slug', 'summer')->exists();
```

Pluck
-----

[](#pluck)

Returns a flat array of a single column values:

```
Campaign::query()->pluck('name');
// ['Save the dolphins', 'All alcoholic beverages 20% off', 'Yeah, no']
```

Meta Tables
-----------

[](#meta-tables)

Meta tables follow the WordPress pattern: a separate table with `meta_key` and `meta_value` columns linked by a foreign key

### Configuration

[](#configuration)

Define meta config in your models `meta()` method or in `DB::init()`:

```
protected function meta(): array
{
    return [
        'table' => 'campaign_meta', // meta table name (auto-prefixed)
        'foreignKey' => 'campaign_id',  // FK in meta table
        'primaryKey' => 'id', // PK in main table
        'aliases' => [  // meta keys aliases
            'budget' => '_campaign_budget',
            'channel' => '_campaign_channel',
            'tags' => [
                'key' => '_campaign_tags',
                'multiple' => true
            ],
        ],
    ];
}
```

### Aliases

[](#aliases)

Aliases let you use friendly names instead of ugly meta key names:

```
// without aliases, you have to write
->withMeta('_campaign_budget')

// with aliases
->withMeta('budget')
```

### Single vs Multiple values

[](#single-vs-multiple-values)

Some meta keys can have multiple rows with the same key. Mark these with `'multiple' => true`:

```
'aliases' => [
    'budget' => '_campaign_budget', // single value
    'tags'   => [
        'key' => '_campaign_tags',
        'multiple' => true // multiple values
    ],
],
```

**Single values**

```
$campaign = Campaign::query()->select('id')->withMeta('budget')->get();
$campaign->budget; // 5000
```

**Multiple values** are parsed into arrays:

```
$campaign = Campaign::query()->select('id')->withMeta('tags')->groupBy('id')->get();
$campaign->tags; // ['promo', 'seasonal', 'email']
```

### Meta in Queries

[](#meta-in-queries)

`withMeta()` makes meta keys available in select, where, orderBy, groupBy

```
// get specific keys
Campaign::query()->select('id', 'name')
    ->withMeta('budget', 'channel')
    ->where('budget', 1000, '>') // WHERE meta_budget.meta_value > 1000
    ->orderBy('budget', 'DESC') // ORDER BY meta_budget.meta_value DESC
    ->groupBy('channel') // GROUP BY meta_channel.meta_value
    ->getAll();

// get campaign with all meta keys
Campaign::query()->select('id', 'name')->withMeta()->getAll();
```

### Meta in Mutations

[](#meta-in-mutations)

```
// Insert: meta fields are auto-separated from table columns
Campaign::query()->withMeta('budget', 'tags')->insert([
    'name' => 'Summer',
    'slug' => 'summer',
    'budget' => '1911', // 1 row in campaign_meta
    'tags' => ['promo', 'seasonal'], // 2 rows in campaign_meta
]);

// Update: single meta upserted, multiple meta replaced
Campaign::query()->withMeta('budget', 'tags')->where('id', 1)->update([
    'budget' => '1950', // updates existing meta row
    'tags' => ['promo', 'holiday'], // deletes old rows, inserts new
]);

// Delete: meta rows are deleted before campaign
Campaign::query()->withMeta('budget')->where('id', 1)->delete();
```

Relations
---------

[](#relations)

Relations define how tables are connected. The parent table's rows are fetched first, then related rows are loaded in a separate query and attached

### Configuration

[](#configuration-1)

```
protected function relations(): array
{
    return [
        'entries' => [
            'table' => 'campaign_entries',
            'foreignKey' => 'campaign_id',
            'primaryKey' => 'id',
            'type' => 'hasMany', // hasMany | hasOne | belongsTo
        ],
    ];
}
```

### Usage

[](#usage)

```
// eager load relations with with()
$campaign = Campaign::query()->select('id', 'name')->with('entries')->get();

// hasMany returns an array
$campaign->entries;
// [
//     ['id' => 1, 'email' => 'a@test.com'],
//     ['id' => 2, 'email' => 'b@test.com'],
// ]

// hasOne returns single row or null
$campaign->profile; // ['id' => 1, 'bio' => '...'] or null
```

Relations work with both `get()` and `getAll()`.

Migrations
----------

[](#migrations)

Each model defines its table structure via `schema()` and a `$version` string. Calling `migrate()` runs `dbDelta()`

### Defining Schema

[](#defining-schema)

Schema is defined separately from the model class using a static callback

```
class Campaign extends Model
{
    protected string $table = 'campaigns';
    protected string $version = '1.0.0';

    public int $id;
    public string $name;
    // ...
}

Campaign::schema(function (Table $table) {
    $table->id();
    $table->string('name');
    $table->string('slug', 100)->unique();
    $table->decimal('price', 8, 2)->default(0);
    $table->integer('stock')->default(0);
    $table->boolean('active')->default(true);
    $table->text('description')->nullable();
    $table->datetime('created_at')->nullable();
});
```

### Running Migrations

[](#running-migrations)

When you change the schema, bump the version

```
protected string $version = '1.1.0'; // added 'priority' column
```

Call `migrate()` on plugin activation:

```
register_activation_hook(__FILE__, function () {
    require_once ABSPATH . 'wp-admin/includes/upgrade.php';

    Campaign::migrate();
    CampaignEntry::migrate();
});
```

Force migration regardless of version

```
Campaign::migrate(true);
```

### Available Column Types

[](#available-column-types)

MethodMySQL Type`$table->id()``BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY``$table->string('name')``VARCHAR(255) NOT NULL``$table->string('code', 50)``VARCHAR(50) NOT NULL``$table->text('body')``TEXT NOT NULL``$table->longText('content')``LONGTEXT NOT NULL``$table->integer('count')``INT NOT NULL``$table->bigInteger('views')``BIGINT NOT NULL``$table->tinyInteger('priority')``TINYINT NOT NULL``$table->float('rating')``FLOAT NOT NULL``$table->decimal('price', 8, 2)``DECIMAL(8,2) NOT NULL``$table->boolean('active')``TINYINT(1) NOT NULL``$table->date('birth_date')``DATE NOT NULL``$table->datetime('published_at')``DATETIME NOT NULL``$table->timestamp('verified_at')``TIMESTAMP NOT NULL``$table->json('settings')``JSON NOT NULL``$table->enum('status', ['a', 'b'])``ENUM('a','b') NOT NULL`### JSON Columns

[](#json-columns)

Models with `array` properties on `json()` columns roundtrip transparently. Arrays are JSON-encoded on save and decoded on read.

```
class Donation extends Model
{
    protected string $table = 'donations';

    public int $id;
    public ?array $gateway_metadata = null;
    public ?array $source_attribution = null;
}

Donation::schema(function (Table $t) {
    $t->id();
    $t->json('gateway_metadata')->nullable();
    $t->json('source_attribution')->nullable();
});

$d = Donation::make();
$d->gateway_metadata = ['stripe' => 'pi_123', 'fee' => 25];
$d->save();

$reloaded = Donation::query()->find('id', $d->id);
$reloaded->gateway_metadata; // ['stripe' => 'pi_123', 'fee' => 25]
```

Notes:

- `null` stays `null` in both directions.
- If the property type is `?string`, the user supplied string is written verbatim.
- Encoding uses `JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES` so multibyte characters and URLs stay readable in the DB.
- Column metadata is derived once per class per request from the registered `schema()` callback and cached.

### Column Modifiers

[](#column-modifiers)

```
$table->string('email')->unique();
$table->integer('stock')->default(0)->unsigned();
$table->datetime('deleted_at')->nullable();
$table->boolean('featured')->default(false);
$table->bigInteger('user_id')->unsigned()->references('users', 'ID')->onDelete('CASCADE');
$table->string('country', 2)->index();
```

Available modifiers: `->nullable()`, `->unique()`, `->primary()`, `->unsigned()`, `->default($value)`, `->references($table, $column)`, `->onDelete($action)`, `->index($name = null)`

### Indexes

[](#indexes)

Single-column indexes can be declared fluently on the column:

```
$table->string('country', 2)->index();              // KEY idx_country (country)
$table->string('country', 2)->index('cc_idx');      // KEY cc_idx (country)
```

Composite indexes are declared on the table:

```
$table->index(['status', 'paid_at']);                   // KEY idx_status_paid_at (status,paid_at)
$table->index(['status', 'paid_at'], 'idx_pay_status'); // KEY idx_pay_status (status,paid_at)
```

Composite unique indexes use `Table::unique()`

```
$table->unique(['gateway', 'external_id']);             // UNIQUE KEY uk_gateway_external_id (gateway,external_id)
$table->unique(['gateway', 'external_id'], 'uk_gw_ext'); // UNIQUE KEY uk_gw_ext (gateway,external_id)
```

When no name is supplied, names are generated as `idx_` / `uk_` + the column list joined with underscores, lowercased. Names longer than 64 characters (MySQL's identifier limit) are truncated with a 6-char hash suffix to stay collision-safe.

Output is `dbDelta()`-compatible — uppercase `KEY` on its own line with tight `(col1,col2)` formatting.

### Meta Table Auto-Creation

[](#meta-table-auto-creation)

If the model has a `meta()` config, `migrate()` automatically creates the meta table

```
protected function meta(): array
{
    return [
        'table' => 'campaign_meta',
        'foreignKey' => 'campaign_id',
        'primaryKey' => 'id',
    ];
}

// Campaign::migrate() creates both tables
```

Transactions
------------

[](#transactions)

```
// using a model
Campaign::transaction(function () {
    Campaign::query()->insert(['name' => 'A', 'slug' => 'a']);
    Campaign::query()->insert(['name' => 'B', 'slug' => 'b']);
});

// using DB facade
DB::transaction(function () {
    DB::table('campaigns')->insert(['name' => 'A', 'slug' => 'a']);
    DB::table('campaigns')->insert(['name' => 'B', 'slug' => 'b']);
});

// auto-commits on success, rollbacks if an exception is thrown
```

Raw Queries
-----------

[](#raw-queries)

```
DB::raw('SELECT * FROM wp_posts WHERE post_status = %s', ['publish']);
```

Parameters use `$wpdb->prepare()` for escaping

Conditional Clauses
-------------------

[](#conditional-clauses)

Conditionally add clauses

```
$status = $_GET['status'] ?? null;

Campaign::query()->select('id', 'name')
    ->when($status, fn ($qb) => $qb->where('status', $status))
    ->getAll();
```

Clone
-----

[](#clone)

Create an independent copy to build query variants

```
$base = Campaign::query()->select('id', 'name');
$active = $base->clone()->where('status', 'active');
$drafts = $base->clone()->where('status', 'draft');
```

SQL Output
----------

[](#sql-output)

Get the generated SQL without executing:

```
Campaign::query()->select('id', 'name')->where('status', 'active')->toSQL();
// SELECT id, name FROM wp_campaigns WHERE status = 'active'
```

###  Health Score

22

—

LowBetter than 21% of packages

Maintenance61

Regular maintenance activity

Popularity10

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/4222590?v=4)[Ante Laća](/maintainers/alaca)[@alaca](https://github.com/alaca)

---

Top Contributors

[![alaca](https://avatars.githubusercontent.com/u/4222590?v=4)](https://github.com/alaca "alaca (12 commits)")

### Embed Badge

![Health badge](/badges/alaca-queryable/health.svg)

```
[![Health](https://phpackages.com/badges/alaca-queryable/health.svg)](https://phpackages.com/packages/alaca-queryable)
```

###  Alternatives

[jdorn/sql-formatter

a PHP SQL highlighting library

3.9k116.5M113](/packages/jdorn-sql-formatter)[propel/propel1

Propel is an open-source Object-Relational Mapping (ORM) for PHP5.

8351.6M87](/packages/propel-propel1)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
