Article Overview

W.O.R.M in Action: A Deep Dive into My WordPress O.R.M Journey

In this post, I showcase my journey with W.O.R.M—my custom WordPress ORM solution that streamlines development by eliminating magic strings and simplifying complex queries. Discover how I transformed challenging client projects into efficient, scalable systems while pushing the limits of WordPress.

What is W.O.R.M?

W.O.R.M stands for WordPress Object Relational Mapping. At its core, it’s simply an O.R.M.

O.R.M stands for Object Relational Mapping. Models are created to represent database tables.

W.O.R.M does not map directly to database tables, but we will get into the details of how it works a bit later. First, let’s start off with why.

Why W.O.R.M?

I find WordPress to be an incredible platform to get things done quickly and reliably, which is why approximately 40% of the entire web is believed to be powered by WordPress.

In 2024, I decided to seriously consider using WordPress for client projects if the request fulfilled certain conditions. I was tasked with building a car booking site, and normally I would use Python and Django to build something comprehensive. However, after considering the time it would take to build everything from scratch and the expected high traffic for the small business, I decided to use WordPress.

I dove right into WordPress and enjoyed the development process until I encountered some major roadblocks that made development slow and frustrating. One major roadblock was magic strings.

1. Say no to magic strings

For context, WordPress at its core uses post types to contain and represent data. WordPress comes with two built-in post types (posts and pages), each having a title and body field. You can almost think of post types as database tables. We can even add custom fields to each post type. We could create our own post types (Shuttle, Timeline, Address), and we could establish relationships between post types (although this would be done through a plugin or custom code).

Defining, and using these custom fields requires the use of magic strings. Here is an example of how we access custom fields.

$address = get_post_meta($post->ID, 'address', true);

# Or
while (have_posts()) {
    get_field('address');
    .....
}

Address is a magic string, and for small simple websites this is of no consequence, but was I building a complex website, that had a lot of custom fields and since I was was using magic strings, I a had no help for the L.S.P.

L.S.P stands for Language Server Protocol. It is a protocol that allows for communication between a client and server

I found myself spending more time looking up the name of the custom field in order to access it. I was off by a letter during some calculation and would find myself debugging for about half an hour. This made making changes to the code with confidence more difficult. Needless to say, the more complex the site got, the more I needed a programming paradigm to bring some order to the chaos.

2. What about queries?

For simple sites, the traditional approach is manageable, but for more complex sites, it can become unwieldy. Suppose we want to get rental reviews for a specific car.

Here is what the relation “model” might look like.

Image

Here is what the process looks like without W.O.R.M.:

First – Query Rentals Related to the Car

This code fetches all rental posts associated with the current car by checking the related_car ACF field.

// Query for rentals associated with the current car
$rental_query = new WP_Query([
    'post_type'      => 'rentals',      // Custom post type for rentals
    'fields'         => 'ids',          // Retrieve only the post IDs for efficiency
    'posts_per_page' => -1,             // Fetch all matching rentals without pagination
    'meta_query'     => [
        [
            'key'     => 'related_car', // ACF field linking a rental to a car
            'value'   => get_the_ID(),  // Current car/post ID
            'compare' => 'LIKE'         // Use LIKE to handle serialized values or multiple IDs
        ]
    ]
]);

// Extract the list of rental IDs
$rental_ids = $rental_query->posts;

Second – Query Reviews Related to the Rental Agreement

This code fetches all review posts where the rental_agreement meta field contains any of the rental IDs from the previous query.

$review_query = new WP_Query([
    'post_type'      => 'reviews',      // Custom post type for reviews
    'posts_per_page' => -1,             // Fetch all matching reviews without pagination
    'meta_query'     => [
        'relation' => 'OR',           // Use OR relation to match any rental ID
        // Create a meta query clause for each rental ID using array_map and the spread operator
        ...array_map(
            function($rental_id) {
                return [
                    'key'     => 'rental_agreement', // Field linking a review to a rental
                    'value'   => $rental_id,           // Check against each rental ID
                    'compare' => 'LIKE'                // Use LIKE to handle partial or serialized values
                ];
            },
            $rental_ids // Array of rental IDs from the previous query
        )
    ]
]);

Third – Loop through reviews

Now loop through the reviews and access them through magic strings.

while ($review_query->have_posts()) {
    $review_query->the_post();
    $cleanliness = get_field('cleanliness');
    $comfort = get_field('comfort');
    // Do something with custom fields...
}

As you can see, this is rather complicated. It requires a deep understanding of WordPress and the ACF plugin to get the queries right. This code becomes hard to change, reuse, or test.

Let’s contrast this with W.O.R.M.

W.O.R.M. to the rescue

Let’s suppose we want to see all available rental cars.

// Query for rentals
// Each model accepts an array of queries to facilitate complex queries
$rental_cars = CarsMetaModel::filterByQueries(
    // CB is the query builder and makes it easy to build queries.
    CB::metaQuery(
        // Relationship joins are dealt with underscores and conditional
        //  queries are dealt with hyphens(-eq, -like, etc)
        // In this case we join the cars post type with the profile post type and use
        //  the profileServiceType field to filter for rentals
        CB::join(['profiles__profileServiceType-eq' => 'rental' ]),
    )
);

To achieve this the query above the WordPress way, we would need at least two queries, and we would need to know the string names of the profiles post type, and profile service type among other things.

This also provide a pleasant development experience because the L.S.P will do a lot of the heavy lifting. For example when doing things the WordPress way we have to guess if the ‘meta_query’ field is spelled correctly. Doing the the W.O.R.M way eliminates guess work.

Image

What if we wanted to find all rental cars that have 4 doors and 4 seats

$rental_cars = CarsMetaModel::filterByQueries(
    CB::metaQuery(
        CB::join(['profiles__profileServiceType-eq' => 'rental' ]),
    ),
    // Since the seats, and door were defined as tantaxonomies, we can use the taxQuery
    CB::taxQuery(CB::and(['seats-eq' => 4, 'doors-eq' => 4]));
)

Accessing data becomes a breeze because we have no magic strings, and we have help from the LSP.

// $rental_cars = CarsMetaModel::filterByQueries(.....)
  foreach ($rental_cars as $rental) {
    // To access data we use model->property->getValue()
    // The property is lazy loaded, and getValue() loads the property
    $colour = $rental->taxonomyColour->getValue();

    // In this case, the getNumberOfDays method uses the necessary properties under the hood
    // To make a calculation, each property is lazy loaded
    // by getValue i.e., $this->startDate->getValue();
    $endDate = $rental->getNumberOfDays()

    // In this case, we have a custom method called getRelatedCar on the
    // RentalMetaModel that in turn calls getValue() under the hood.
    // The getRelatedCar method has validation to ensure only one car is returned
    $make = $rental->getRelatedCar()->taxonomyMake->geTermName();

    // We can also easily create data transformative methods
    $startDate = $rental->rentalStartDate->timezoneFormatedDate('d M Y');
  }

W.O.R.M. makes it easy to write clean, reusable code. And because we have help from the LSP, we have eliminated guesswork. I know what you are thinking right about now: “In the queries, wouldn’t profiles__profileServiceType-eq be considered a magic string?” The answer is yes, you are correct. The difference here is that each word in the string is validated. An error will occur, and guidance will be provided with the available terms. So you can get away with not having to remember each relationship.

Models allow us to use existing programming strategies

Now that our code uses an ORM, we can apply programming strategies to our code, such as testing and design patterns. This allows for more consistency and maintainability

So how does it work?

I will try to keep this brief because the code continues to change, and I am still considering better designs.

W.O.R.M treats post types as models and the custom fields on the custom post types as properties. We can prefix a property so that we can tell W.O.R.M to treat it differently. For example, otm-acf_timelines in this case, the prefix otm-acf is used to tell W.O.R.M that we want to treat the property as an ACF property and that it has a One TO Many relationship with the timelines post type. So when W.O.R.M generates the models, it will create the relationship. Here is an example of what the generated model would look like:

class ProjectMetaModel extends MetaModel
{
    /** ... more properties ... */
    public OnetoManyACFField $timelines;

    public function __construct(int $id)
    {
        // Result of otm-acf_timelines
        $this->timelines = new OnetoManyACFField($id, 'otm-acf_timelines', ExtendedTimelinesMetaModel::class);
        /** ... More code ... */
    }
    /** ... More code ... */
}

The models are auto-generated by W.O.R.M. In order to use them, we extend them. If you look at the example above, ExtendedTimelinesMetaModel is passed in the constructor instead of TimelinesMetaModel. So to use, simply extend:

namespace Jcodify\Snazzyportfolio\Wordpress\ORM\Models\Extended;

use Jcodify\Snazzyportfolio\Wordpress\ORM\Models\AutoGen\Models\ProjectMetaModel;

class ExtendedProjectMetaModel extends ProjectMetaModel
{
 // now we can do whatever we want in here with affecting the ProjectMetaModel
}

Conclusion

W.O.R.M. offers a structured approach to managing WordPress data by leveraging Object Relational Mapping principles. It simplifies complex queries, reduces reliance on magic strings, and enhances code maintainability through model-based design. By integrating with the Language Server Protocol, W.O.R.M. provides developers get real-time feedback and validation, reducing errors and improving development efficiency. While it has proven beneficial in several projects, there are still challenges to address and design decisions to refine. As W.O.R.M. continues to evolve, it promises to further streamline WordPress development, making it a valuable tool for developers seeking to build robust and scalable applications.