Query Builder

Lumberjack has a built-in query builder which provides an expressive, fluent and explicit way of querying data in WordPress. It can be used instead of WP_Query to query posts (of any type) and means you do not have to worry about "the loop".

There are 2 ways in which you can use the query builder. Either on a Post Type:

use App\PostTypes\Project;

$projects = Project::builder()
    ->orderBy('date', 'asc')
    ->get();

Or by using the query builder directly:

use App\PostTypes\Project;
use App\PostTypes\CaseStudy;
use Rareloop\Lumberjack\QueryBuilder;

$posts = (new QueryBuilder)->wherePostType([
    Project::getPostType(),
    CaseStudy::getPostType(),
])
->orderBy('date', 'asc')
->get();

Using the query builder on a Post Type

This is the most common way to use the query builder. You can start querying posts simply by using the Rareloop\Lumberjack\Post class. To start a query, use the builder() method.

Post::builder();

This creates an instance of Rareloop\Lumberjack\ScopedQueryBuilder. This class does a couple of important things:

Returns the correct post objects

When querying a post type, you will always get the same post object back in your results rather than WP_Post objects.

For example, if you have a custom post type called Employee, when you query employees you will always get Employee objects back as results. Lets see what this looks like:

use App\PostTypes\Employee;

$employees = Employee::builder()->get();

dump($employees);

/*
    Collection {
        #items: array:2 [
            0 => App\PostTypes\Employee,
            1 => App\PostTypes\Employee,
        ]
    }
*/

The collection contains instances of the Employee class. This is extremely powerful as you now have access to all the behaviours that come with employees, as defined in your post type class. In this case, you may have a photoUrl() method on an Employee that knows (encapsulates) how to get the correct size image from the featured image:

class Employee extends Post
{
    ...

    public function photoUrl() : string
    {
        $thumbnail = $this->thumbnail();

        if (empty($thumbnail)) {
            return null;
        }

        return $thumbnail->src('large');
    }
}

And when iterating through your results, you can safely use this photoUrl() method:

use App\PostTypes\Employee;

$employees = Employee::builder()->get();

$employee = $employees->first();

// This will echo out the featured image url if there is one
echo $employee->photoUrl();

All post types extend Timber's Post object, so you get access to all of their behaviour out of the box.

The example above is making use of Timber's thumbnail() method on a Post, and src() method on an Image.

If collections are new to you, be sure the check out the documentation on them:

Query scopes

Sometimes you will need to perform the same filter on a query in multiple places within your theme.

// Get all featured posts, newest first
$featuredPostIds = [1, 2];

$posts = Post::whereIdIn($featuredPostIds)
    ->orderBy('date', 'desc')
    ->get();

...

// Get the latest 3 featured posts
$featuredPostIds = [1, 2];

$posts = Post::whereIdIn($featuredPostIds)
    ->orderBy('date', 'desc')
    ->limit(3)
    ->get();

In this example, we are scoping the query to only show featured images. However there's 2 issues with this:

  1. We doing it twice, without reusing any code

  2. It can be unclear as to what you are doing

Instead, We can encapsulate the knowledge about how to find featured posts by creating a query scope on our post type:

namespace App\PostTypes;

use Rareloop\Lumberjack\Post as LumberjackPost;

class Post extends LumberjackPost
{
    ...

    public function scopeFeatured($query)
    {
        $featuredPostIds = [1, 2];

        return $query->whereIdIn($featuredPostIds);    
    }
}

Now we have a query scope, we can refactor our previous queries, making them more declarative and easier to change:

// Get all featured posts, newest first
$posts = Post::featured()
    ->orderBy('date', 'desc')
    ->get();

...

// Get the latest 3 featured posts
$posts = Post::featured()
    ->orderBy('date', 'desc')
    ->limit(3)
    ->get();

Query scopes must start with the word scope, and must follow with the name of the method you want available to the builder. The method should also be defined in CamelCase. For example:

scopeFoo() will allow you to call foo() on a query.

scopeFooBar() will allow you to call fooBar() on a query.

You can also pass through parameters into the query scope:

namespace App\PostTypes;

use Rareloop\Lumberjack\Post as LumberjackPost;

class Post extends LumberjackPost
{
    ...

    public function scopeExclude($query, $postId)
    {        
        return $query->whereIdNotIn([$postId]);    
    }
}

...

// Get all featured posts, newest first, excluding post id 1
$posts = Post::exclude(1)
    ->orderBy('date', 'desc')
    ->get();

Available methods

All the available methods can be chained, with the exclusion of getParameters and get.

getParameters

returns: array

Get the current state of the query builder, as an array. These parameters can be directly fed into WP_Query as arguments.

For example:

$parameters = Post::builder()
    ->limit(3)
    ->orderBy('date', 'desc')
    ->getParameters();

// $parameters would look like this:
// [
//   "post_type" => "post"
//   "posts_per_page" => 3
//   "orderby" => "date"
//   "order" => "DESC"
// ]

This can be useful if you need to add your own arguments that the query builder does not support.

$parameters = Post::builder()
    ->limit(3)
    ->orderBy('date', 'desc')
    ->getParameters();

$parameters['author_name'] = 'Adam';

$query = new WP_Query($parameters);

Alternatively, you can add your own methods to the query builder using macros. See Extending the Query Builder.

wherePostType($postType)

Scope the query to a particular post type, or post types.

Populates post_type in WP_Query.

// Single
$query->wherePostType('page');

// Multiple
$query->wherePostType (['page', 'jobs']):

When using the query builder from a post type, the query is automatically scoped to the correct post type.

// This automatically sets the post type on the query to the Job post type
$jobs = Job::builder()->get();

Reference: WP_Query - Type Parameters__

whereIdIn(array $ids)

Scope the query to only look for specific post IDs.

Sets the post__in argument in WP_Query.

Reference: WP_Query - Post & Page Parameters__

whereIdNotIn(array $ids)

Scope the query to exclude specific post IDs.

Sets the post__not_in argument in WP_Query.

Reference: WP_Query - Post & Page Parameters__

whereStatus()

This method can either take an array of statuses, or multiple parameters for each status:

Array of statuses:

$query->whereStatus(['publish', 'draft']);

Multiple parameters:

$query->whereStatus('publish', 'draft');

Scope the query to only include posts with the given status. By default WordPress will only look for published posts, so you only need to use this method if you need to get posts with other statuses.

Sets the post_status argument in WP_Query.

Reference: WP_Query - Status Parameters__

whereMeta($key, $value, $compare = '=', $type = null)

Scope posts that have the specified custom meta fields.

Adds an array of meta query arguments to the array of meta_query arguments on WP_Query.

'meta_query' => [
    [
        'key' => 'lead',
        'value' => 'Lorem ipsum %',
        'compare' = 'LIKE',
    ]
]

Note: meta_query takes an array of meta query arguments arrays (it takes an array of arrays)

The above example can be written like so:

$query->whereMeta('lead', 'Lorem ipsum %', 'LIKE');

You can also add multiple meta queries.

$query->whereMeta('lead', 'Lorem ipsum %', 'LIKE')
    ->whereMeta('price', [20, 100], 'BETWEEN', 'numeric');

This will yield the following parameters:

'meta_query' => [
    [
        'key' => 'lead',
        'value' => 'Lorem ipsum %',
        'compare' = 'LIKE',
    ],
    [
        'key' => 'price',
        'value' => [20, 100],
        'compare' = 'BETWEEN',
        'type' => 'numeric',
    ]
]

Reference: WP_Query - Custom Field Parameters__

whereMetaRelationshipIs(string $relation)

Sets the `relation` field for your meta queries, for `WP_Query`.

$query->whereMeta('lead', 'Lorem ipsum %', 'LIKE')
    ->whereMeta('price', [20, 100], 'BETWEEN', 'numeric')
    ->whereMetaRelationshipIs('or');

This will yield the following parameters, adding the 'relation' => 'or' to the meta query.

'meta_query' => [
    'relation' => 'or',
    [
        'key' => 'lead',
        'value' => 'Lorem ipsum %',
        'compare' = 'LIKE',
    ],
    [
        'key' => 'price',
        'value' => [20, 100],
        'compare' = 'BETWEEN',
        'type' => 'numeric',
    ]
]

Reference: WP_Query - Custom Field Parameters__

limit($limit)

Set the number of results to get back from the query.

Sets the posts_per_page argument in WP_Query.

Reference: WP_Query - Pagination Parameters__

offset($offset)

Set the number of results to displace or pass over.

Sets the offset argument in WP_Query.

Reference: WP_Query - Pagination Parameters__

orderBy($orderBy, $order = 'asc')

Sort retrieved posts by parameter, e.g. date, title, menu_order.

Sets the orderby and order arguments in WP_Query.

$query->orderBy('title', 'asc');

Reference: WP_Query - Order & Orderby Parameters__

orderByMeta($metaKey, $order = 'asc', $type = null)

Sets the `orderby` argument for `WP_Query` to `meta_value` when ordering strings, and `meta_value_num` when ordering numbers.

Reference: WP_Query - Order & Orderby Parameters__

as($postClass)

When using WP_Query, you get an array of WP_Post objects back. The query builder will instead return an array of Rareloop\Lumberjack\Post objects back.

You can use this method to change what object is returned from the query builder.

use App\PostTypes\Event;

// Get an array of Event objects back
$query->wherePostType('event')
    ->as(Event::class)
    ->get();

Note: When using the query builder on a post type, this conversion is done automatically. For example:

// Get an array of Event object
$events = Event::get();

Reference: Timber - get_posts()__

get()

Execute the query and return a Collection of post objects.

$posts = $query->whereMeta('price', 100, '>', 'numeric')
    ->get();

first()

Execute the query and return the first post object. Returns null if there are no results.

$post = $query->whereMeta('price', 100, '>', 'numeric')
    ->first();

clone()

Duplicates a new instance of the query builder with all the current parameters. You can modify this query builder instance separately to the original.

This can be useful if you have similar queries with subtle differences, as it saves you duplicating the commonalities between them.

$baseQuery = (new QueryBuilder)
    ->wherePostType(Announcement::getPostType())
    ->forCurrentUser(); // Some custom Query Scope, for example

$counts = [
    'all' => $baseQuery->clone()->whereStatus('publish', 'draft', 'pending')->get()->count(),
    'publish' => $baseQuery->clone()->whereStatus('publish')->get()->count(),
    'draft' => $baseQuery->clone()->whereStatus('draft')->get()->count(),
    'pending' => $baseQuery->clone()->whereStatus('pending')->get()->count(),
];

Extending the query builder

The Lumberjack QueryBuilder class can be extended with custom functionality at runtime (the class is "macroable"). The following example adds a search method to the QueryBuilder class that can be used to filter results based on a keyword search:

use Rareloop\Lumberjack\QueryBuilder;

// Add custom function
QueryBuilder::macro('search', function ($term) {
    $this->params['s'] = $term;
    
    return $this;
});

QueryBuilder::macro('writtenByCurrentUser', function () {
    $this->params['author'] = get_current_user_id();
    
    return $this;
});

// Use the functionality
$posts = (new QueryBuilder())
    ->search('Elephant');
    ->writtenByCurrentUser()
    ->get();
    
// Or on a post type
$posts = Post::builder()
    ->search('Elephant')
    ->writtenByCurrentUser()
    ->get();

Last updated