PHPackages                             mjkhajeh/wporm - 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. mjkhajeh/wporm

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

mjkhajeh/wporm
==============

WPORM is a lightweight, Eloquent-inspired ORM for WordPress plugins and themes. It provides expressive, fluent query building, model relationships, schema management, attribute casting, and event hooks—while fully supporting the WordPress database API and table prefixing. WPORM makes it easy to build modern, maintainable database code in any WordPress project.

v2.10.2(2mo ago)5165↓50%1MITPHPPHP &gt;=7.4

Since Jun 7Pushed 2mo ago2 watchersCompare

[ Source](https://github.com/mjkhajeh/wporm)[ Packagist](https://packagist.org/packages/mjkhajeh/wporm)[ RSS](/packages/mjkhajeh-wporm/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (8)DependenciesVersions (51)Used By (1)

WPORM - Lightweight WordPress ORM
=================================

[](#wporm---lightweight-wordpress-orm)

WPORM is a lightweight Object-Relational Mapping (ORM) library for WordPress plugins. It provides an Eloquent-like API for defining models, querying data, and managing database schema, all while leveraging WordPress's native `$wpdb` database layer.

[![wporm](https://private-user-images.githubusercontent.com/81983167/452540239-f84f6905-4279-4ee3-9e1f-9fb9a3fd2e51.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQ0Mzg5ODAsIm5iZiI6MTc3NDQzODY4MCwicGF0aCI6Ii84MTk4MzE2Ny80NTI1NDAyMzktZjg0ZjY5MDUtNDI3OS00ZWUzLTllMWYtOWZiOWEzZmQyZTUxLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNjAzMjUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjYwMzI1VDExMzgwMFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWJkNTIyYjhmNGY0MTY5NzA1MDRkYjgxYWRiYTMyMmFkMTYzZTMwZTQzZTBjNDRhMGEzMzU5ZjU1MDg1MWU0NTYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.0mtF7dLdZKdqyGNKwvsW1BaFalvBMEAwtc2NXAcNDFo)](https://private-user-images.githubusercontent.com/81983167/452540239-f84f6905-4279-4ee3-9e1f-9fb9a3fd2e51.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQ0Mzg5ODAsIm5iZiI6MTc3NDQzODY4MCwicGF0aCI6Ii84MTk4MzE2Ny80NTI1NDAyMzktZjg0ZjY5MDUtNDI3OS00ZWUzLTllMWYtOWZiOWEzZmQyZTUxLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNjAzMjUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjYwMzI1VDExMzgwMFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWJkNTIyYjhmNGY0MTY5NzA1MDRkYjgxYWRiYTMyMmFkMTYzZTMwZTQzZTBjNDRhMGEzMzU5ZjU1MDg1MWU0NTYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.0mtF7dLdZKdqyGNKwvsW1BaFalvBMEAwtc2NXAcNDFo)

Documentation
-------------

[](#documentation)

- [Methods list and documents](./Methods.md)
- [Blueprint and column types documents](./Blueprint.md)
- [Casts types and define custom casts](./CastsType.md)
- [DB usage and raw queries](./DB.md)
- [Debugging tips](./Debugging.md)

Features
--------

[](#features)

- **Model-based data access**: Define models for your tables and interact with them using PHP objects.
- **Schema management**: Create and modify tables using a fluent schema builder.
- **Query builder**: Chainable query builder for flexible and safe SQL queries.
- **Attribute casting**: Automatic type casting for model attributes.
- **Relationships**: Define `hasOne`, `hasMany`, `belongsTo`, `belongsToMany`, and `hasManyThrough` relationships.
- **Events**: Hooks for model lifecycle events (creating, updating, deleting).
- **Global scopes**: Add global query constraints to models.

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

[](#installation)

### With Composer (Recommended)

[](#with-composer-recommended)

You can install WPORM via Composer. In your plugin or theme directory, run:

```
composer require mjkhajeh/wporm
```

Then include Composer's autoloader in your plugin bootstrap file:

```
require_once __DIR__ . '/vendor/autoload.php';
```

### Manual Installation

[](#manual-installation)

1. Place the `ORM` directory in your plugin folder.
2. Include the ORM in your plugin bootstrap:

```
require_once __DIR__ . '/ORM/Helpers.php';
require_once __DIR__ . '/ORM/Model.php';
require_once __DIR__ . '/ORM/QueryBuilder.php';
require_once __DIR__ . '/ORM/Blueprint.php';
require_once __DIR__ . '/ORM/SchemaBuilder.php';
require_once __DIR__ . '/ORM/ColumnDefinition.php';
require_once __DIR__ . '/ORM/DB.php';
require_once __DIR__ . '/ORM/Collection.php';
```

Defining a Model
----------------

[](#defining-a-model)

Create a model class extending `MJ\WPORM\Model`:

```
use MJ\WPORM\Model;
use MJ\WPORM\Blueprint;

class Parts extends Model {
    protected $table = 'parts';
    protected $fillable = ['id', 'part_id', 'qty', 'product_id'];
    protected $timestamps = false;

    public function up(Blueprint $blueprint) {
        $blueprint->id();
        $blueprint->integer('part_id');
        $blueprint->integer('product_id');
        $blueprint->integer('qty');
        $blueprint->index('product_id');
        $this->schema = $blueprint->toSql();
    }
}
```

> **Note:** When using `$table` in custom SQL queries, do **not** manually add the WordPress prefix (e.g., `$wpdb->prefix`). The ORM automatically handles table prefixing. Use `$table = (new User)->getTable();` as shown in the next, which returns the fully-prefixed table name.

Schema Management
-----------------

[](#schema-management)

Create or update tables using the model's `up` method and the `SchemaBuilder`:

```
use MJ\WPORM\SchemaBuilder;

$schema = new SchemaBuilder($wpdb);
$schema->create('parts', function($table) {
    $table->id();
    $table->integer('part_id');
    $table->integer('product_id');
    $table->integer('qty');
    $table->index('product_id');
});
```

### Unique Indexes (Eloquent-style)

[](#unique-indexes-eloquent-style)

You can add a unique index to a column using Eloquent-style chaining:

```
$table->string('email')->unique();
$table->integer('user_id')->unique('custom_index_name');
```

For multi-column unique indexes, use:

```
$table->unique(['col1', 'col2']);
```

This works for all column types and matches Eloquent's API.

Basic Usage
-----------

[](#basic-usage)

### Creating a Record

[](#creating-a-record)

```
$part = new Parts(['part_id' => 1, 'product_id' => 2, 'qty' => 10]);
$part->save();
```

### Querying Records

[](#querying-records)

```
// Get all parts
$all = Parts::all();

// Find by primary key
$part = Parts::find(1);

// Where clause
$parts = Parts::query()->where('qty', '>', 5)->orderBy('qty', 'desc')->limit(10)->get(); // Limit to 10 results

// Raw ORDER BY example
$parts = Parts::query()->where('qty', '>', 5)
    ->orderByRaw('FIELD(name, ?, ?)', ['Widget', 'Gadget'])
    ->limit(10)
    ->get();

// This allows custom SQL ordering, e.g. sorting by a specific value list. Bindings are safely passed to $wpdb->prepare.

// First result
$first = Parts::query()->where('product_id', 2)->first();
```

### Querying by a Specific Column

[](#querying-by-a-specific-column)

You can easily retrieve records by a specific column using the query builder's `where` method. For example, to get all parts with a specific `product_id`:

```
$parts = Parts::query()->where('product_id', 123)->get();
```

Or, to get the first user by email:

```
$user = User::query()->where('email', 'user@example.com')->first();
```

You can also use other comparison operators:

```
$recentUsers = User::query()->where('created_at', '>=', '2025-01-01')->get();
```

This approach works for any column in your table.

### Creating or Updating Records: updateOrCreate

[](#creating-or-updating-records-updateorcreate)

WPORM provides an `updateOrCreate` method, similar to Laravel Eloquent, for easily updating an existing record or creating a new one if it doesn't exist.

**Usage:**

```
// Update if a user with this email exists, otherwise create a new one
$user = User::updateOrCreate(
    ['email' => 'user@example.com'],
    ['name' => 'John Doe', 'country' => 'US']
);

// Disable global scopes for this call
$user = User::updateOrCreate(
    ['email' => 'user@example.com'],
    ['name' => 'John Doe', 'country' => 'US'],
    false // disables global scopes
);
```

- The first argument is an array of attributes to search for.
- The second argument is an array of values to update or set if creating.
- The optional third argument disables global scopes if set to `false` (default is `true`).
- Returns the updated or newly created model instance.

This is useful for upsert operations, such as syncing data or ensuring a record exists with certain values.

### Creating or Getting Records: firstOrCreate and firstOrNew

[](#creating-or-getting-records-firstorcreate-and-firstornew)

### Inserting Records: insertOrIgnore

[](#inserting-records-insertorignore)

WPORM provides an `insertOrIgnore` method, similar to Laravel Eloquent, for inserting one or multiple records and ignoring duplicate key errors (such as unique constraint violations).

**Usage:**

```
// Insert a single user, ignore if email already exists
$success = User::insertOrIgnore([
    'email' => 'user@example.com',
    'name' => 'Jane Doe',
    'country' => 'US'
]);

// Insert multiple users, ignore duplicates
$data = [
    ['email' => 'user1@example.com', 'name' => 'User One'],
    ['email' => 'user2@example.com', 'name' => 'User Two'],
    ['email' => 'user1@example.com', 'name' => 'User One Duplicate'], // duplicate email
];
$success = User::insertOrIgnore($data);
```

- Returns `true` if the insert(s) succeeded or were ignored due to duplicate keys.
- Returns `false` on other errors.
- Uses MySQL's `INSERT IGNORE` for safe upsert-like behavior.

This is useful for bulk imports or situations where you want to avoid errors on duplicate records.

WPORM also provides `firstOrCreate` and `firstOrNew` methods, similar to Laravel Eloquent, for convenient record retrieval or creation.

**firstOrCreate Usage:**

```
// Get the first user with this email, or create if not found
$user = User::firstOrCreate(
    ['email' => 'user@example.com'],
    ['name' => 'Jane Doe', 'country' => 'US']
);

// Disable global scopes for this call
$user = User::firstOrCreate(
    ['email' => 'user@example.com'],
    ['name' => 'Jane Doe', 'country' => 'US'],
    false // disables global scopes
);
```

- Returns the first matching record, or creates and saves a new one if none exists.
- The optional third argument disables global scopes if set to `false` (default is `true`).

**firstOrNew Usage:**

```
// Get the first user with this email, or instantiate (but do not save) if not found
$user = User::firstOrNew(
    ['email' => 'user@example.com'],
    ['name' => 'Jane Doe', 'country' => 'US']
);

// Disable global scopes for this call
$user = User::firstOrNew(
    ['email' => 'user@example.com'],
    ['name' => 'Jane Doe', 'country' => 'US'],
    false // disables global scopes
);
if (!$user->exists) {
    $user->save(); // Save if you want to persist
}
```

- Returns the first matching record, or a new (unsaved) instance if none exists.
- The optional third argument disables global scopes if set to `false` (default is `true`).

These methods are useful for ensuring a record exists, or for preparing a new record with default values if not found.

### Updating a Record

[](#updating-a-record)

```
$part = Parts::find(1);
$part->qty = 20;
$part->save();
```

### Deleting a Record

[](#deleting-a-record)

```
$part = Parts::find(1);
$part->delete();
```

### Truncating a Table

[](#truncating-a-table)

You can quickly remove all rows from a model's table using `truncate()` on the model query builder:

```
// Remove all records from the table
Parts::query()->truncate();
```

Pagination
----------

[](#pagination)

WPORM supports Eloquent-style pagination with the following methods on the query builder:

### paginate($perPage = 15, $page = null)

[](#paginateperpage--15-page--null)

Returns a paginated result array with total count and page info:

```
$result = User::query()->where('active', true)->paginate(10, 2);
// $result = [
//   'data' => Collection,
//   'total' => int,
//   'per_page' => int,
//   'current_page' => int,
//   'last_page' => int,
//   'from' => int,
//   'to' => int
// ]
```

### simplePaginate($perPage = 15, $page = null)

[](#simplepaginateperpage--15-page--null)

Returns a paginated result array without total count (more efficient for large tables):

```
$result = User::query()->where('active', true)->simplePaginate(10, 2);
// $result = [
//   'data' => Collection,
//   'per_page' => int,
//   'current_page' => int,
//   'next_page' => int|null
// ]
```

See [Methods.md](./Methods.md) for more details and options.

Attribute Casting
-----------------

[](#attribute-casting)

Add a `$casts` property to your model:

```
protected $casts = [
    'qty' => 'int',
    'meta' => 'json',
];
```

Array Conversion and Casting
----------------------------

[](#array-conversion-and-casting)

- Call `->toArray()` on a model or a collection to get an array representation with all casts applied.
- Built-in types (e.g. 'int', 'bool', 'float', 'json', etc.) are handled natively and will not be instantiated as classes.
- Custom cast classes must implement `MJ\WPORM\Casts\CastableInterface`.

Example:

```
protected $casts = [
    'user_id'    => 'int',
    'from'       => Time::class, // custom cast
    'to'         => Time::class, // custom cast
    'use_default'=> 'bool',
    'status'     => 'bool',
];

$model = Times::find(1);
$array = $model->toArray();

$collection = Times::query()->get();
$arrays = $collection->toArray();
```

- Custom cast classes will be instantiated and their `get()` method called.
- Built-in types will be cast using native PHP logic.

Relationships
-------------

[](#relationships)

WPORM supports Eloquent-style relationships. You can define them in your model using the following methods:

- **hasOne**: One-to-one ```
    public function profile() {
        return $this->hasOne(Profile::class, 'user_id');
    }
    ```
- **hasMany**: One-to-many ```
    public function posts() {
        return $this->hasMany(Post::class, 'user_id');
    }
    ```
- **belongsTo**: Inverse one-to-one or many ```
    public function user() {
        return $this->belongsTo(User::class, 'user_id');
    }
    ```
- **belongsToMany**: Many-to-many (with optional pivot table and keys) ```
    public function roles() {
        return $this->belongsToMany(Role::class, 'user_role', 'user_id', 'role_id');
    }
    ```
- **hasManyThrough**: Has-many-through ```
    public function comments() {
        return $this->hasManyThrough(Comment::class, Post::class, 'user_id', 'post_id');
    }
    ```

All relationship methods return either a model instance or a `Collection` of models. You can use them just like in Eloquent.

### Relationship Existence Filtering: whereHas, orWhereHas, has

[](#relationship-existence-filtering-wherehas-orwherehas-has)

- `whereHas('relation', function($q) { ... })`: Filter models where the relation exists and matches constraints.
- `orWhereHas('relation', function($q) { ... })`: OR version of whereHas.
- `has('relation', '>=', 2)`: Filter models with at least (or exactly, or at most) N related records. Operator and count are optional (defaults to "&gt;= 1").

**Examples:**

```
// Users with at least one post
User::query()->has('posts')->get();

// Users with at least 5 posts
User::query()->has('posts', '>=', 5)->get();

// Users with exactly 2 posts
User::query()->has('posts', '=', 2)->get();

// Users with at least one published post
User::query()->whereHas('posts', function($q) {
    $q->where('published', 1);
})->get();
```

Custom Attribute Accessors/Mutators
-----------------------------------

[](#custom-attribute-accessorsmutators)

```
public function getQtyAttribute() {
    return $this->attributes['qty'] * 2;
}

public function setQtyAttribute($value) {
    $this->attributes['qty'] = $value / 2;
}
```

Appended (Computed) Attributes
------------------------------

[](#appended-computed-attributes)

You can add computed (virtual) attributes to your model's array/JSON output using the `$appends` property, just like in Eloquent.

```
protected $appends = ['user'];

public function getUserAttribute() {
    return get_user_by('id', $this->user_id);
}
```

- Appended attributes are included in `toArray()` and JSON output.
- The value is resolved via a `get{AttributeName}Attribute()` accessor or, if not present, by a public property.
- Do **not** set appended attributes in `retrieved()`; use accessors instead.

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

[](#transactions)

```
Parts::query()->beginTransaction();
// ...
Parts::query()->commit();
// or
Parts::query()->rollBack();
```

Custom Queries
--------------

[](#custom-queries)

You can execute custom SQL queries using the underlying `$wpdb` instance or by extending the model/query builder. For example:

```
// Using the query builder for a custom select
$results = Parts::query()
    ->select(['part_id', 'SUM(qty) as total_qty'])
    ->where('product_id', 2)
    ->orderBy('total_qty', 'desc')
    ->limit(5) // Limit to top 5 parts
    ->get();

// Using $wpdb directly for full custom SQL
global $wpdb;
$table = (new Parts)->getTable();
$results = $wpdb->get_results(
    $wpdb->prepare("SELECT part_id, SUM(qty) as total_qty FROM $table WHERE product_id = %d GROUP BY part_id", 2),
    ARRAY_A
);
```

You can also add custom static methods to your model for more complex queries:

```
class Parts extends Model {
    // ...existing code...
    public static function partsWithMinQty($minQty) {
        return static::query()->where('qty', '>=', $minQty)->get();
    }
}

// Usage:
$parts = Parts::partsWithMinQty(5);
```

Raw Table Queries with DB::table()
----------------------------------

[](#raw-table-queries-with-dbtable)

WPORM now supports Eloquent-style raw table queries using the `DB` class:

```
use MJ\WPORM\DB;

// Update posts with IDs 3, 4, 5
db::table('post')
    ->whereIn('id', [3, 4, 5])
    ->update(['title' => 'Updated Title']);

// Select rows from any table
db::table('custom_table')->where('status', 'active')->get();
```

See [DB.md](./DB.md) for more details.

Complex Where Statements
------------------------

[](#complex-where-statements)

WPORM now supports complex nested where/orWhere statements using closures, similar to Eloquent:

```
$users = User::query()
    ->where(function ($query) {
        $query->where('country', 'US')
              ->where(function ($q) {
                  $q->where('age', '>=', 18)
                    ->orWhere('verified', true);
              });
    })
    ->orWhere(function ($query) {
        $query->where('country', 'CA')
              ->where('subscribed', true);
    })
    ->get();
```

You can still use multiple `where` calls for AND logic, and `orWhere` for OR logic:

```
$parts = Parts::query()
    ->where('qty', '>', 5)
    ->where('product_id', 2)
    ->orWhere('qty', 'index('column')`.
- For large datasets, use pagination and limit/offset queries to avoid memory issues: ```
    // For large datasets, use limit and offset for pagination:
    $usersPage2 = User::query()->orderBy('id')->limit(20)->offset(20)->get(); // Get users 21-40
    ```

FAQ
---

[](#faq)

**Q: Why is my table not created?**

- A: Ensure your model's `up()` method is correct and that you call the schema builder. Check for errors in your SQL or schema definition.

**Q: How do I debug a failed query?**

- A: Use `$wpdb->last_query` and `$wpdb->last_error` after running a query to inspect the last executed SQL and any errors.

**Q: Can I use this ORM outside of WordPress?**

- A: No, it is tightly coupled to WordPress's `$wpdb` and plugin environment.

Resources
---------

[](#resources)

- [WordPress Plugin Developer Handbook](https://developer.wordpress.org/plugins/)
- [Laravel Eloquent ORM Documentation](https://laravel.com/docs/eloquent)

License Details
---------------

[](#license-details)

This project is licensed under the MIT License. See the LICENSE file or [MIT License](https://opensource.org/licenses/MIT) for details.

---

###  Health Score

42

—

FairBetter than 90% of packages

Maintenance85

Actively maintained with recent releases

Popularity15

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity50

Maturing project, gaining track record

 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.

###  Release Activity

Cadence

Every ~5 days

Recently: every ~15 days

Total

50

Last Release

75d ago

Major Versions

v1.9.9 → v2.0.02025-06-26

### Community

Maintainers

![](https://www.gravatar.com/avatar/3b7e4293c9f0f03906ad75a2e557157b3c12919babd31bd1e7bf27694e753011?d=identicon)[mjkhajeh](/maintainers/mjkhajeh)

---

Top Contributors

[![mjkhajeh](https://avatars.githubusercontent.com/u/81983167?v=4)](https://github.com/mjkhajeh "mjkhajeh (130 commits)")

---

Tags

ormorm-frameworkwordpresswordpress-boilerplatewordpress-developmentwordpress-pluginwordpress-themewporm

### Embed Badge

![Health badge](/badges/mjkhajeh-wporm/health.svg)

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

###  Alternatives

[doctrine/orm

Object-Relational-Mapper for PHP

10.2k285.3M6.2k](/packages/doctrine-orm)[jdorn/sql-formatter

a PHP SQL highlighting library

3.9k115.1M102](/packages/jdorn-sql-formatter)[illuminate/database

The Illuminate Database package.

2.8k52.4M9.3k](/packages/illuminate-database)[mongodb/mongodb

MongoDB driver library

1.6k64.0M545](/packages/mongodb-mongodb)[ramsey/uuid-doctrine

Use ramsey/uuid as a Doctrine field type.

90340.3M211](/packages/ramsey-uuid-doctrine)[reliese/laravel

Reliese Components for Laravel Framework code generation.

1.7k3.4M16](/packages/reliese-laravel)

PHPackages © 2026

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