Query Builder
Models support a fluent query builder that will return an easy-to-use Collection of models.
The Mantle Framework is not required to use the query builder. You can require the package in your application with Composer and use the models and query builder in your application.
composer require mantle-framework/database
Retrieving Models
Once a model is defined you are ready to start retrieving data. Each model acts as a powerful query builder that allows you to fluently query the underlying data that each model represents.
Querying a Model by ID
If you know a model's primary key, you can retrieve it using the find()
method.
use App\Models\Post;
$post = Post::find( 1 );
Queuing Collections of Models
The all()
method will return all the results for a model.
use App\Models\Post;
$posts = Post::all();
foreach ( $posts as $post ) {
echo $post->title;
}
For post models, the all
method will retrieve only published posts by default. You
can easily include all post statuses by calling anyStatus()
on the model.
use App\Models\Post;
Post::where( ... )->anyStatus()->all();
Retrieving the results of the current query can be done by using the get()
method.
use App\Models\Post;
$posts = Post::where( 'post_status', 'publish' )->get();
You can use the found_rows()
method to retrieve the total number of results
from the collection of models returned by the query.
use App\Models\Post;
$posts = Post::where( 'post_status', 'publish' )->get();
$total = $posts->found_rows();
Querying a Single Model
You can also use the first()
method to retrieve just a single model from
either the model or the model's query builder.
use App\Models\Post;
$post = Post::first();
// Retrieve the first post with the title "example".
$post = Post::where( 'title', 'example' )->first();
Counting the Number of Results
You can count the number of results for a query by using the count()
method.
use App\Models\Post;
$count = Post::where( 'post_status', 'publish' )->count();
Querying Models
Queries can be started by calling the query()
method on the model or using any
query method directly on the model statically.
use App\Models\Post;
// Using the static magic method.
Post::where( 'post_status', 'publish' )->get();
// Using the query method.
Post::query()->where( 'post_status', 'publish' )->get();
Querying Model Fields
Model fields can be queried against using the where()
method or using
magic-methods in the format of where{Field}()
to fluently build queries.
Example_Post::where( 'slug', 'slug-to-find' )->first();
Example_Post::whereSlug( 'slug-to-find' )->first();
You can use the field name or any field alias to
query against. Mantle will automatically convert the field name to the correct
argument for the underlying WP_Query
or WP_Tax_Query
class.
Chaining Queries
Queries can be chained together to build more complex queries.
Example_Post::where( 'slug', 'slug-to-find' )
->where( 'post_status', 'publish' )
->first();
Querying Model in a List of IDs
You can also use whereIn()
and whereNotIn()
to query against a list of
values to retrieve models who are in or not in the list of IDs.
// Posts in a list of IDs.
Example_Post::whereIn( 'id', [ 1, 2, 3 ] )->get();
// Posts not a list of IDs.
Example_Post::whereNotIn( 'id', [ 1, 2, 3 ] )->get();
Querying Posts with Terms
Post queries can be built to query against terms.
// Get the first 10 posts in the 'term-slug' tag.
Example_Post::whereTerm( 'term-slug', 'post_tag' )
->take( 10 )
->get();
// Get the first 10 posts in the 'term-slug' or 'other-tag' tags.
Example_Post::whereTerm( 'term-slug', 'post_tag' )
->orWhereTerm( 'other-tag', 'post_tag' )
->take( 10 )
->get();
// Get the first 10 posts in the taxonomy term controlled by `Example_Term`.
$term = Example_Term::first();
Example_Post::whereTerm( $term )->take( 10 )->get();
Querying with Meta
Normal concerns for querying against a model's meta still should be observed.
The Post
and Term
models support querying with meta.
// Instance of Example_Post if found.
Example_Post::whereMeta( 'meta-key', 'meta-value' )->first();
// Multiple meta keys to match.
Example_Post::whereMeta( 'meta-key', 'meta-value' )
->andWhereMeta( 'another-meta-key', 'another-value' )
->first();
Example_Post::whereMeta( 'meta-key', 'meta-value' )
->orWhereMeta( 'another-meta-key', 'another-value' )
->first();
Querying By Date
Mantle provides a few methods to query against dates to circumvent the
limitations of WP_Query
.
All date arguments support a date string, unix timestamp, Carbon instance, or
DateTime/DateTimeInterface instance. All comparison operators support =
, !=
,
>
, >=
, <
, <=
.
Where possible, the query will be converted to a date_query
argument for
WP_Query
to use. If the query cannot be converted to a date_query
argument,
Mantle will use a raw SQL query to query against the database column directly.
This applies when trying to query a specific
date which WordPress does not
support.
whereDate
Find posts where the date matches a specific value. Supports a date string, unix
timestamp, Carbon instance, or DateTime instance. The default column to query
against is post_date
.
use App\Models\Post;
$posts = Post::query()
->whereDate( '2021-01-01', '>' )
->get();
$posts = Post::query()
->whereDate( 1612137600, '>' )
->get();
$posts = Post::query()
->whereDate( new DateTime( '2021-01-01' ), '>', 'post_modified' )
->get();
whereUtcDate
Find posts where the post_date_gmt
matches a specific value. Supports a date
string, unix timestamp, Carbon instance, or DateTime instance.
use App\Models\Post;
$posts = Post::query()
->whereUtcDate( '2021-01-01' )
->get();
$posts = Post::query()
->whereUtcDate( 1612137600, '>' )
->get();
whereModifiedDate
Find posts where the post_modified
matches a specific value.
use App\Models\Post;
$posts = Post::query()
->whereModifiedDate( '2021-01-01', '>' )
->get();
whereModifiedUtcDate
Find posts where the post_modified_gmt
matches a specific value.
use App\Models\Post;
$posts = Post::query()
->whereModifiedUtcDate( '2021-01-01', '>' )
->get();
olderThan
Find posts where the date is older than a specific value.
use App\Models\Post;
$posts = Post::query()
->olderThan( '2021-01-01' )
->get();
olderThanOrEqualTo
Find posts where the date is older than or equal to a specific value.
use App\Models\Post;
$posts = Post::query()
->olderThanOrEqualTo( '2021-01-01' )
->get();
newerThan
Find posts where the date is newer than a specific value.
use App\Models\Post;
$posts = Post::query()
->newerThan( '2021-01-01' )
->get();
newerThanOrEqualTo
Find posts where the date is newer than or equal to a specific value.
use App\Models\Post;
$posts = Post::query()
->newerThanOrEqualTo( '2021-01-01' )
->get();
Limit Results
You can limit the number of results returned by using the take()
method.
// Get the first 10 posts.
Example_Post::where( 'title', 'example' )->take( 10 )->get();
Pages
You can paginate results by using the page()
method.
// Get the second page of results.
Example_Post::where( 'title', 'example' )->page( 2 )->get();
For more information on advanced pagination, see the Pagination section.
Ordering Results
Query results can be ordered by using the orderBy()
/order_by()
methods.
Example_Post::query()->orderBy( 'name', 'asc' )->get();
You can also order by the value passed to whereIn()
:
Example_Post::query()
->whereIn( 'id', [ 1, 2, 3 ] )
->orderByWhereIn()
->get();
You can pass multiple fields and directions to the orderBy()
method by calling
it multiple times.
Example_Post::query()
->orderBy( 'name', 'asc' )
->orderBy( 'date', 'desc' )
->get();
To remove all ordering from a query, you can use the removeOrder()
method.
Example_Post::query()->removeOrder()->get();
Conditional Clauses
Sometimes you may want certain clauses to be applied only if a condition is
met. You can use the when()
method to conditionally add clauses to a query.
use App\Models\Post;
use Mantle\Database\Query\Post_Query_Builder;
Post::query()
->when(
$request->has( 'name' ),
fn ( Post_Query_Builder $query ) => $query->where( 'name', $request->get( 'name' ) ),
)
->get();
The closure will only be executed if the condition is met. You can pass another
callback to the third argument of the when()
method to add a clause when the
condition is not met.
use App\Models\Post;
use Mantle\Database\Query\Post_Query_Builder;
Post::query()
->when(
$request->has( 'name' ),
// Applied when the condition is met.
fn ( Post_Query_Builder $query ) => $query->where( 'name', $request->get( 'name' ) ),
// Applied when the condition is not met.
fn ( Post_Query_Builder $query ) => $query->where( 'name', 'default-name' ),
)
->get();
Chunking Results
If you need to work with thousands of models, you can use the chunk()
method
to process the results in chunks.
use App\Models\Post;
// Chunk the results in groups of 100.
Post::chunk( 100, function ( $posts ) {
foreach ( $posts as $post ) {
// Do something with the post.
}
} );
You can also use the chunk_by_id()
method to chunk results by the model's ID.
This is useful if you need to process results in batches and potentially
delete/modify them in a way that can break the sorting of the query.
use App\Models\Post;
// Chunk the results in groups of 100.
Post::chunk_by_id( 100, function ( $posts ) {
foreach ( $posts as $post ) {
// Do something with the post.
}
} );
Each/Each By ID
If you need to process each model in a query, you can use the
each()
/each_by_id()
methods. These methods will process each model in the
query performantly without loading all of the models into memory. each_by_id()
is
useful if you need to process results in batches and potentially delete/modify
them in a way that can break the sorting of the query.
use App\Models\Post;
// Process each post using normal pagination chunking.
Post::where( 'status', 'publish' )->each( function ( Post $post ) {
// Do something with the post.
} );
// Process each post using ID-based pagination chunking.
Post::where( 'status', 'publish' )->each_by_id( function ( Post $post ) {
// Do something with the post.
} );
Map/Map By ID
If you need to map the results of a query, you can use the map()
/map_by_id()
methods. These methods will map the results of the query performantly without
loading all of the models into memory. map_by_id()
is useful if you need to
process results in batches and potentially delete/modify them in a way that can
break the sorting of the query.
use App\Models\Post;
// Map the results using normal pagination chunking.
$posts = Post::where( 'status', 'publish' )->map( function ( Post $post ) {
// Perform some additional processing here.
return [ $post->id => $post->title ];
} );
// Map the results using ID-based pagination chunking.
$posts = Post::where( 'status', 'publish' )->map_by_id( function ( Post $post ) {
// Perform some additional processing here.
return [ $post->id => $post->title ];
} );
Debugging Queries
The query arguments that are passed to the underlying WP_Query
/WP_Tax_Query
/etc.
can be dumped by using the dump()
/dd()
methods.
use App\Models\Post;
// Dump the query arguments.
Post::query()->where( 'name', 'example' )->dump()->get();
// Dump the query arguments and end the script.
Post::query()->where( 'name', 'example' )->dd();
You can dump the raw SQL query by using the dump_sql()
/dd_sql()
methods.
use App\Models\Post;
// Dump the raw SQL query.
Post::query()->where( 'name', 'example' )->dump_sql();
// Dump the raw SQL query and end the script.
Post::query()->where( 'name', 'example' )->dd_sql();
Advanced: Querying Against Database Columns
You can query against the database columns directly by using the
whereRaw()
/orWhereRaw()
methods. This allows you to write raw SQL queries to
query against the database columns to circumvent the limitations of WP_Query
and WP_Tax_Query
.
use App\Models\Post;
// Get the first 10 posts where the comment count is greater than 10.
$posts = Post::query()->whereRaw( 'comment_count', '>', 10 )->get();
// Get a post by ID using a raw SQL query.
$post = Post::query()->whereRaw( 'ID', 1 )->first();
// Partial match the post title using a raw SQL query.
$posts = Post::query()
->whereRaw( 'post_title', 'LIKE', 'example prefix:%' )
->get();
// Attach multiple conditions together.
$posts = Post::query()
->whereRaw( 'comment_count', '>', 10 )
->orWhereRaw( 'comment_count', '<', 5 )
->get();
These methods are useful for more complex queries that cannot be expressed using
WP_Query
or WP_Tax_Query
via the fluent query builder. For example, Mantle
uses this internally to chunk models by ID (which is not supported by
WP_Query
). The variables passed to whereRaw()
/orWhereRaw()
are
automatically escaped with $wpdb->prepare()
to prevent SQL injection.
Advanced: Modifying Query Clauses
Apart of the above raw SQL queries, you can also modify the query clauses
directly by using the add_clause()
method on post/term queries. This allows
you to modify post/term query clauses that are exposed via the
posts_clauses
/terms_clauses
filters.
The callback passed to add_clause()
will receive the current query clauses and
should return the modified query clauses. See the relevant WordPress core
documentation for the posts_clauses
and terms_clauses
filters for more
information:
use App\Models\Post;
// Add a custom clause to the post query.
$posts = Post::query()->add_clause( function ( array $clauses ) {
$clauses['where'] .= ' AND post_date > "2021-01-01"';
return $clauses;
} )->get();
Mantle recommends to use the
whereRaw()
/orWhereRaw()
methods over
add_clause()
as it is more expressive and easier to understand. The
add_clause()
method is useful to make it simpler than WordPress core makes it
to quickly modify the query clause directly.
Retrieving or Creating Models
Models can be retrieved or created if they do not exist using the following helper methods:
first_or_new
The first_or_new()
method will retrieve the first model matching the query or
create a new instance of the model if no matching model exists. It will not save
the model. The second argument is an array of attributes to set on the model if
it is created.
use App\Models\Post;
// Retrieve the first post with the title "example" or create a new post.
$post = Post::first_or_new(
[
'title' => 'example',
],
[
'content' => 'This is an example post.',
]
);
first_or_create
Similar to first_or_new()
, the first_or_create()
method will retrieve the
first model matching the query or create a new instance of the model if no
matching model exists. It will save the model. The second argument is an array
of attributes to set on the model if it is created.
use App\Models\Post;
// Retrieve the first post with the title "example" or create a new post.
$post = Post::first_or_create(
[
'title' => 'example',
],
[
'content' => 'This is an example post.',
]
);
Model Not Found Exceptions
The first_or_fail()
method will retrieve the first model matching the query or
throw a Mantle\Database\Model\Model_Not_Found_Exception
if no matching model
exists.
use App\Models\Post;
// Retrieve the first post with the title "example" or throw an exception.
$post = Post::where( 'title', 'example' )->first_or_fail();
The find_or_fail()
method will retrieve the model by its primary key or throw a
Mantle\Database\Model\Model_Not_Found_Exception
if no matching model exists.
use App\Models\Post;
// Retrieve the post with the ID of 1 or throw an exception.
$post = Post::find_or_fail( 1 );
update_or_create
The update_or_create()
method will retrieve the first model matching the query,
create it if it does not exist, or update it if it exists. The second argument
is an array of attributes to set on the model if it is created.
use App\Models\Post;
// Retrieve the first post with the title "example" or create a new post.
$post = Post::update_or_create(
[
'title' => 'example',
],
[
'content' => 'This is an example post.',
]
);
Querying Multiple Models
Multiple models of the same type (posts/terms/etc.) can be queried together. There are some limitations and features that cannot be used including query scopes.
use Mantle\Database\Query\Post_Query_Builder;
use App\Models\Post;
use App\Models\Another_Post;
Post_Query_Builder::create( [ Post::class, Another_Post::class ] )
->whereMeta( 'shared-meta', 'meta-value' )
->get();
Pagination
All models support pagination of results to make traversal of large sets of data easy. The paginators will display links to the next and previous pages as well as other pages that can be styled by your application.
Length Aware Pagination
By default the paginate()
method will use the Length Aware Paginator which
will calculate the total number of pages in a result set. This is what you
probably expect from a paginator: previous / next links as well as links to a
few pages before and after the current one.
use App\Models\Posts;
$posts = Posts::paginate( 20 );
Basic Pagination
'Basic' pagination is purely a next / previous link relative to the current
page. It also sets 'no_found_rows' => true
on the query to help performance for very
large data sets.
use App\Model\Posts;
$posts = Posts::simple_paginate( 20 );
Displaying Paginator Results
The paginator instances both support iteration over it to allow you to easily loop through and display the current page's results.
<ul class="post-list">
@foreach ( $posts as $post )
<li>
<a href="{{ $post->url() }}">{{ $post->title() }}</a>
</li>
@endforeach
</ul>
{{ $posts->links() }}
Customizing the Paginator Links
By default the paginator will use query strings to paginate and determine the current URL.
/blog/
/blog/?page=2
...
/blog/?page=100
The paginator can also be set to use path pages to paginate. For example, a
paginated URL would look like /blog/page/2/
.
{{ $posts->use_path()->links() }}
Append Query Parameters to Paginator Links
Arbitrary query parameters can be append to paginator links.
{{ $posts->append( [ 'key' => 'value' ] )->links() }}
The current GET query parameters can also be automatically included on the links as well.
{{ $posts->with_query_string()->links() }}
Customizing the Paginator Path
The paginator supports setting a custom base path for paginated results. By
default the paginator will use the current path (stripping /page/n/
if it
includes it).
{{ $posts->path( '/blog/' )->links() }}
Converting the Paginator to JSON
The paginator supports being returned directly as a route's response.
Route::get( '/posts', function() {
return App\Posts::paginate( 20 );
} );
The paginator will return the results in a JSON format.
{
"current_page": 1,
"data": [ ... ],
"first_page_url": "\/path",
"next_url": "\/path?page=2",
"path": "\/path",
"previous_url": null
}