Object-Relational Mapping (ORM) Lite for WordPress Plugins
Learn to build a lightweight ORM for WordPress to abstract SQL, map database rows to PHP objects, and simplify data manipulation in your plugins.
Previously in this course, we covered High-Concurrency Data Handling in WordPress Plugins, focusing on row-level locking and transaction integrity. While those techniques ensure database reliability, writing raw SQL for every CRUD operation quickly leads to unmaintainable code. In this lesson, we’ll implement an ORM Lite layer to bridge the gap between our database schema and clean, object-oriented PHP.
Why Build an ORM Lite?
In a professional plugin like our Knowledge Base, you often deal with dozens of custom tables. Writing $wpdb queries everywhere creates tight coupling: if you rename a column, you have to hunt down every query. An ORM Lite approach uses Entities (representing a single record) and Query Builders (representing a collection) to encapsulate SQL logic.
By moving from raw SQL to an object-based API, you gain:
- Type Safety: Methods that return specific objects rather than associative arrays.
- Encapsulation: Logic for calculating fields or formatting data resides in the entity, not the controller.
- DRY Code: Shared query logic (like pagination or status filtering) can be reused across repositories.
Creating the Base Entity Class
The BaseEntity acts as a data container. It should be lightweight, handling property assignment and basic data normalization.
PHPnamespace KnowledgeBase\ORM; abstract class BaseEntity { protected array $data = []; public function __construct(array $data = []) { $this->data = $data; } public function __get($key) { return $this->data[$key] ?? null; } public function toArray(): array { return $this->data; } }
By using the magic __get method, we create a flexible interface that allows us to add custom getters later (e.g., getTitle() or getFormattedDate()) without breaking existing code.
Implementing Dynamic Field Mapping
To make this useful, we need a way to transform database rows into specific entity classes. We use a Mapper or a static factory method. Let’s extend our Knowledge Base Article entity.
PHPnamespace KnowledgeBase\Entities; use KnowledgeBase\ORM\BaseEntity; class Article extends BaseEntity { public function getExcerpt(int $length = 50): string { return wp_trim_words($this->content, $length); } }
The mapping logic resides in our repository, ensuring that every database result is cast into an Article object automatically. This is significantly safer than passing raw arrays around, as it prevents Insecure Deserialization issues by strictly defining the object structure.
Building the Query Builder
A Query Builder constructs SQL strings dynamically based on method calls. This allows us to chain filters without concatenating strings manually.
PHPnamespace KnowledgeBase\ORM; class QueryBuilder { protected string $table; protected array $wheres = []; public function __construct(string $table) { $this->table = $table; } public function where(string $column, string $value): self { $this->wheres[] = "$column = '$value'"; return $this; } public function get(): array { global $wpdb; $sql = "SELECT * FROM {$this->table}"; if (!empty($this->wheres)) { $sql .= " WHERE " . implode(' AND ', $this->wheres); } return $wpdb->get_results($sql, ARRAY_A); } }
Note: In production, always use $wpdb->prepare() within the get() method to prevent SQL injection.
Hands-on Exercise: Implement a Repository
- Create a
KnowledgeBase\Repositories\ArticleRepositoryclass. - In the constructor, inject the database table name.
- Add a
find(int $id)method that uses theQueryBuilderto fetch a single record. - Return an instance of
Articleusing the data returned from the database.
Common Pitfalls
- Over-Engineering: Don't try to build a full-featured ORM like Doctrine. Keep it "Lite." If your queries involve complex JOINs across five tables, stick to raw SQL via a repository method.
- Performance: Loading thousands of objects into memory can exhaust the PHP memory limit. Use
yieldgenerators or limit result sets when fetching large collections. - Ignoring
$wpdb->prepare: Even when using an ORM, the underlying data should never be concatenated directly into SQL strings. Always pass values through theprepare()method.
Recap
We’ve moved from raw SQL to an object-oriented approach by creating a BaseEntity for data representation and a QueryBuilder for SQL abstraction. This pattern keeps the Knowledge Base plugin modular and makes data manipulation predictable. You can now easily expand your data layer without duplicating logic or risking manual query errors.
Up next: Advanced Query Filters, where we’ll allow developers to extend our repository queries using WordPress hooks.
Work with me

Custom WordPress Plugin Development
Custom WordPress & WooCommerce plugins built to standard — by the developer behind a plugin with 5,000+ active installs and a SaaS with 10,000+ users.

Custom WordPress Theme Development
A custom WordPress theme built exactly to your design — fast, clean, and easy to manage. No bloated page builders, no compromises.