wptechnix/wp-models

Type-safe Active Record ORM models and entities for WordPress plugin development

Maintainers

Package info

github.com/WPTechnix/wp-models

Homepage

pkg:composer/wptechnix/wp-models

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.0 2026-03-19 07:06 UTC

This package is auto-updated.

Last update: 2026-03-19 07:19:18 UTC


README

CI Latest Version PHP Version License

Type-safe Active Record ORM for WordPress plugin development.

WP Models replaces scattered $wpdb queries with a clean data-access layer. Database rows become typed PHP objects with automatic casting, UTC-aware datetime handling, safe SQL compilation, built-in object caching, and pagination — all without pulling in a full framework.

Features

  • Typed entities — declare column types once (int, decimal, bool, datetime, json, enum:ClassName); the library casts on read and serialises on write, no manual conversion needed
  • Full query APIfind, findWhere, paginate, create, update, delete, firstOrCreate, updateOrCreate, bulk variants, chunk, pluck, and more
  • Safe SQL by design — conditions use a column allow-list and wpdb::prepare() placeholders; SQL injection is structurally prevented, not just filtered
  • Automatic caching — two-level WordPress object cache (query result IDs + entity rows); reads hit cache first, writes stay consistent automatically
  • Paginationpaginate() returns an immutable result object with navigation helpers, page-number generation for UI controls, and JsonSerializable output for REST API responses
  • UTC datetime handling — dates stored as UTC, returned in the WordPress site timezone, written back as UTC — no manual timezone conversion required
  • PHP 8.1 Backed Enums — first-class enum casting in entity definitions
  • Dirty tracking — know exactly which attributes changed before calling save()

Requirements

Requirement Version / Notes
PHP ^8.1
WordPress Loaded by your plugin — provides $wpdb, ABSPATH, and timezone functions

Installation

composer require wptechnix/wp-models

Load Composer's autoloader in your plugin file if you haven't already:

// my-plugin/my-plugin.php
require_once __DIR__ . '/vendor/autoload.php';

Quick Start

Define an Entity and a Model

use WPTechnix\WPModels\AbstractEntity;
use WPTechnix\WPModels\AbstractModel;

// One entity class represents one database row
class OrderEntity extends AbstractEntity
{
    protected static string $primaryKey = 'id';

    protected static array $casts = [
        'id'         => 'int',
        'user_id'    => 'int',
        'total'      => 'decimal',   // stored and returned as string — safe for money
        'status'     => 'string',
        'is_paid'    => 'bool',
        'meta'       => 'json',      // auto-encoded/decoded
        'created_at' => 'datetime',  // stored UTC, returned in WP timezone
    ];
}

// One model class manages one database table
class OrderModel extends AbstractModel
{
    protected string $table       = 'orders';           // without WP prefix
    protected string $entityClass = OrderEntity::class;
    protected string $primaryKey  = 'id';

    // Security allow-list: only these columns may appear in WHERE / ORDER BY
    protected array $queryableColumns = [
        'id', 'user_id', 'status', 'is_paid', 'total', 'created_at',
    ];
}

Create, Read, Update, Delete

$orders = OrderModel::instance();

// Create
$id = $orders->create([
    'user_id' => get_current_user_id(),
    'total'   => '49.99',
    'status'  => 'pending',
    'meta'    => ['source' => 'checkout'],
]);

// Read — properties are typed, no casting needed
$order = $orders->find($id);
echo $order->total;                         // string '49.99'
echo $order->is_paid;                       // bool false
echo $order->created_at->format('d M Y');   // DateTimeImmutable in WP timezone
echo $order->meta['source'];               // 'checkout'

// Update
$orders->update($id, ['status' => 'complete', 'is_paid' => true]);

// Delete
$orders->delete($id);

Query and Paginate

// Filter with conditions
$pending = $orders->findWhere([
    ['column' => 'status',  'value' => 'pending'],
    ['column' => 'is_paid', 'value' => false],
], orderBy: ['created_at' => 'DESC']);

// Paginate
$page = $orders->paginate(
    page: absint($_GET['paged'] ?? 1),
    perPage: 20,
    conditions: [['column' => 'user_id', 'value' => get_current_user_id()]],
);

foreach ($page->items as $order) {
    echo $order->total;
}

echo "Showing {$page->getFromNumber()}{$page->getToNumber()} of {$page->total}";

Documentation

Guide Description
Getting Started Installation, table setup, first entity and model, full usage walkthrough
Entity Reference Cast types, datetime and timezone handling, enum support, dirty tracking, save and delete
Model Reference Complete query API — CRUD, bulk operations, pagination, chunking, aggregates
Query Conditions Conditions format, all operators, OR groups, building conditions dynamically
PaginatedResult Reference Navigation helpers, page number generation, REST API serialisation
Caching How caching works, what clears it, and how to handle edge cases

Changelog

See CHANGELOG.md.

Contributing

See CONTRIBUTING.md.

License

MIT © WPTechnix — see LICENSE.