ForgeSqlOrm

A component that gives your app an object-style layer over your SQL database: models map rows to classes, and a fluent query builder handles reads, writes, relations, soft deletes, and pagination. It builds on ForgeDatabaseSQL for the underlying connection.

Overview

This is another component your app chooses to use — not a layer you inherit. If you like working with your data as PHP objects and a fluent query interface instead of writing raw SQL, this capability gives you that. You still decide which models exist, how they're shaped, and what your app does with them.

What it gives you

  • Attribute-driven models mapped to tables
  • A fluent query builder for your app's data
  • Relations, casts, and soft deletes
  • Pagination helpers and views
  • Optional query result caching
  • UUID or auto-increment primary keys
  • Automatic timestamps when columns exist

Core capability: ForgeSqlOrm ships as a core capability (type: core, loaded after ForgeDatabaseSQL at order: 1). It requires a database connection (DatabaseConnectionInterface) and provides the QueryBuilderInterface to your app.

When to Reach for It

  • Your app models its data as objects and relations.
  • You want type-safe, casted columns (dates, enums, JSON).
  • You want soft deletes and pagination without reinventing them.
  • You'd rather query fluently than hand-write SQL.

If you prefer raw SQL, or your app only needs a couple of hand-written queries, you can skip this and use the connection from ForgeDatabaseSQL directly. It's your call — these are components.

Installation

A core capability, so usually present. If your project needs it, install it like any capability:

php forge.php package:install-module --module=ForgeSqlOrm

It requires ForgeDatabaseSQL to be installed and configured first.

Models

A model is a class that extends the base Model, declares its table, and maps columns to typed properties. You add these to your app wherever you keep your domain code.

use Modules\ForgeSqlOrm\ORM\Model;
use Modules\ForgeSqlOrm\ORM\Attributes\Table;
use Modules\ForgeSqlOrm\ORM\Attributes\Column;
use Modules\ForgeSqlOrm\ORM\Attributes\Hidden;
use Modules\ForgeSqlOrm\ORM\Values\Cast;

#[Table('users')]
class User extends Model
{
    #[Column(primary: true)]
    public int $id;

    #[Column]
    public string $email;

    #[Column(cast: Cast::JSON)]
    public ?array $preferences;

    #[Column]
    #[Hidden]
    public string $password;
}

The base handles the mapping for you: a string primary key is treated as a UUID (auto-generated on insert), and created_at / updated_at are maintained automatically when those properties exist. Columns marked #[Hidden] are dropped from toArray() serialization.

Queries

Use the static entry points on your model to build queries:

$user = User::query()->where('email', $email)->first();

$recent = User::query()
    ->where('active', 1)
    ->orderBy('created_at', 'DESC')
    ->limit(10)
    ->get();

$count = User::query()->count();

$byIds = User::query()->whereIn('id', [1, 2, 3])->get();

There are convenience shortcuts on the model:

$newest = User::latest()->first();   // newest first
$oldest = User::oldest()->first();   // oldest first

Saving & Deleting

New instances save with save(), which inserts or updates based on whether the row already exists. Existing rows update only their changed columns.

$user = new User();
$user->email = 'ada@example.com';
$user->save();               // INSERT

$user->preferences = ['theme' => 'dark'];
$user->save();               // UPDATE (only changed columns)

$user->delete();             // delete, or soft-delete if enabled

You can also operate without loading an instance, using insert(), insertMany(), update(), and forceDelete() on the query builder.

Casts & Relationships

Casts

Mark a column with #[Column(cast: ...)] to have the ORM convert values in and out of the database. Supported casts: INT, FLOAT, BOOL, STRING, JSON, DATE, DATETIME, TIMESTAMP, and ENUM.

Relationships

Eager-load relations with with() and access them on the model. You define how relations are resolved in your model; this loads them without N+1 queries:

$posts = Post::with('author', 'comments')->whereIn('id', $ids)->get();

foreach ($posts as $post) {
    echo $post->author->name; // already loaded, no extra query
}

Soft Deletes

Give a model a deleted_at property and deletes become soft: rows are flagged, not removed. The SoftDeletes trait provides the column for you:

use Modules\ForgeSqlOrm\ORM\Model;
use Modules\ForgeSqlOrm\ORM\Traits\SoftDeletes;

#[Table('posts')]
class Post extends Model
{
    use SoftDeletes; // adds $deleted_at column
}

Queries exclude soft-deleted rows by default:

Post::query()->withTrashed()->get();   // include deleted
Post::query()->onlyTrashed()->get();   // only deleted
$post->forceDelete();                  // remove for real

Repositories & Caching

Repositories

For app code that prefers not to call the model directly, the capability ships a small repository interface with the usual create, update, delete, find, findBy, findAll, and query operations.

Query caching

A QueryCache is available in the container (default TTL 3600s) with get, set, forget, and invalidate operations so you can cache query results when it makes sense for your app.

Pagination

Paginate a model directly and render the links with the included helpers:

$users = User::paginate(
    page: 2,
    perPage: 20,
    column: 'created_at',
    direction: 'DESC'
);
// In a view: render the pager and summary
<?= paginate($users) ?>
<?= pagination_info($users) ?>

The pagination(), paginate(), and pagination_info() helpers are provided by this capability.