Query Builder Macros allow you to extend QueryBuilder with custom methods. This feature enables you to create reusable query logic that can be called as methods on QueryBuilder instances, making your code more expressive and maintainable.
- Overview
- Registering Macros
- Using Macros
- Macro Parameters
- Return Values
- Examples
- Best Practices
- Differences from Scopes
Macros provide a way to register custom methods that can be called on QueryBuilder instances. They are registered globally and can be used with any QueryBuilder instance, making them ideal for:
- Custom query methods: Create domain-specific query methods (e.g.,
active(),published()) - Reusable query patterns: Encapsulate common query logic
- Method chaining: Macros can return QueryBuilder instances for fluent chaining
- Flexible returns: Macros can return QueryBuilder instances or any other value
Macros are registered using the static macro() method on QueryBuilder:
use tommyknocker\pdodb\query\QueryBuilder;
QueryBuilder::macro('active', function (QueryBuilder $query) {
return $query->where('status', 'active');
});The macro callable receives the QueryBuilder instance as the first argument, followed by any additional arguments passed when calling the macro.
Once registered, macros can be called as methods on QueryBuilder instances:
$activeProducts = $db->find()
->table('products')
->active()
->get();Macros can be chained with other QueryBuilder methods:
$result = $db->find()
->table('products')
->active()
->orderBy('created_at', 'DESC')
->limit(10)
->get();You can check if a macro is registered using hasMacro():
if (QueryBuilder::hasMacro('active')) {
// Macro exists
}Macros can accept parameters, just like regular methods:
QueryBuilder::macro('wherePrice', function (QueryBuilder $query, string $operator, float $price) {
return $query->where('price', $price, $operator);
});
// Use the macro with parameters
$expensive = $db->find()
->table('products')
->wherePrice('>', 100.00)
->get();Macros support default parameters:
QueryBuilder::macro('recent', function (QueryBuilder $query, int $days = 7) {
$date = date('Y-m-d H:i:s', strtotime("-{$days} days"));
return $query->where('created_at', $date, '>=');
});
// Use default (7 days)
$recent = $db->find()->table('products')->recent()->get();
// Use custom parameter
$veryRecent = $db->find()->table('products')->recent(1)->get();Macros can return different types of values:
When a macro returns a QueryBuilder instance, it can be chained with other methods:
QueryBuilder::macro('active', function (QueryBuilder $query) {
return $query->where('status', 'active');
});
$result = $db->find()
->table('products')
->active()
->orderBy('name')
->get();Macros can return any value:
QueryBuilder::macro('countActive', function (QueryBuilder $query) {
return $query->where('status', 'active')->getValue('COUNT(*)');
});
$count = $db->find()->table('products')->countActive();QueryBuilder::macro('active', function (QueryBuilder $query) {
return $query->where('status', 'active');
});
QueryBuilder::macro('inactive', function (QueryBuilder $query) {
return $query->where('status', 'inactive');
});
$activeProducts = $db->find()->table('products')->active()->get();
$inactiveProducts = $db->find()->table('products')->inactive()->get();QueryBuilder::macro('wherePrice', function (QueryBuilder $query, string $operator, float $price) {
return $query->where('price', $price, $operator);
});
QueryBuilder::macro('inCategory', function (QueryBuilder $query, int $categoryId) {
return $query->where('category_id', $categoryId);
});
$result = $db->find()
->table('products')
->wherePrice('>', 100.00)
->inCategory(1)
->get();QueryBuilder::macro('available', function (QueryBuilder $query) {
return $query
->where('status', 'active')
->andWhere('price', 0, '>')
->andWhereNotNull('stock');
});
$availableProducts = $db->find()->table('products')->available()->get();QueryBuilder::macro('recent', function (QueryBuilder $query, int $days = 7) {
$date = date('Y-m-d H:i:s', strtotime("-{$days} days"));
return $query->where('created_at', $date, '>=');
});
QueryBuilder::macro('thisMonth', function (QueryBuilder $query) {
$startOfMonth = date('Y-m-01 00:00:00');
$endOfMonth = date('Y-m-t 23:59:59');
return $query
->where('created_at', $startOfMonth, '>=')
->andWhere('created_at', $endOfMonth, '<=');
});
$recentProducts = $db->find()->table('products')->recent(30)->get();
$monthProducts = $db->find()->table('products')->thisMonth()->get();QueryBuilder::macro('countActive', function (QueryBuilder $query) {
return $query->where('status', 'active')->getValue('COUNT(*)');
});
$activeCount = $db->find()->table('products')->countActive();QueryBuilder::macro('active', function (QueryBuilder $query) {
return $query->where('status', 'active');
});
QueryBuilder::macro('inStock', function (QueryBuilder $query) {
return $query->where('stock', 0, '>');
});
QueryBuilder::macro('featured', function (QueryBuilder $query) {
return $query->where('featured', 1);
});
$result = $db->find()
->table('products')
->active()
->inStock()
->featured()
->orderBy('price', 'ASC')
->limit(10)
->get();If you call a non-existent macro, a RuntimeException is thrown:
try {
$db->find()->table('products')->nonExistentMacro();
} catch (\RuntimeException $e) {
echo "Macro not found: " . $e->getMessage();
}-
Use descriptive names: Choose macro names that clearly indicate their purpose
// Good QueryBuilder::macro('active', ...); QueryBuilder::macro('published', ...); // Avoid QueryBuilder::macro('filter1', ...); QueryBuilder::macro('q', ...);
-
Return QueryBuilder for chaining: When creating query-building macros, return the QueryBuilder instance
QueryBuilder::macro('active', function (QueryBuilder $query) { return $query->where('status', 'active'); // Return $query });
-
Keep macros focused: Each macro should do one thing well
// Good - focused QueryBuilder::macro('active', function (QueryBuilder $query) { return $query->where('status', 'active'); }); // Avoid - too many responsibilities QueryBuilder::macro('activeAndFeaturedAndInStock', function (QueryBuilder $query) { // Too many things });
-
Document parameters: If macros accept parameters, document their purpose
/** * Filter products by price range. * * @param QueryBuilder $query * @param string $operator Comparison operator (>, <, >=, <=, =) * @param float $price Price threshold */ QueryBuilder::macro('wherePrice', function (QueryBuilder $query, string $operator, float $price) { return $query->where('price', $price, $operator); });
-
Use default parameters: Make macros flexible with default values when appropriate
QueryBuilder::macro('recent', function (QueryBuilder $query, int $days = 7) { // Default to 7 days if not specified });
-
Register macros early: Register macros during application bootstrap or in service providers
Macros and scopes serve similar purposes but have different use cases:
- Global registration: Registered once, available everywhere
- Method-like syntax: Called as methods on QueryBuilder
- No model dependency: Work with any QueryBuilder instance
- No automatic application: Must be called explicitly
- Can return any value: Not limited to QueryBuilder instances
- Model-specific: Defined in Model classes
- Global scopes: Automatically applied to all queries
- Local scopes: Applied via
scope()method - Can be disabled:
withoutGlobalScope()to temporarily disable - Model context: Better for model-specific logic
Use macros when you need:
- Custom method names that read naturally (
active(),published()) - Methods that work across different models/tables
- Reusable query patterns used throughout your application
- Methods that can return values other than QueryBuilder
Use scopes when you need:
- Model-specific query logic
- Automatic application (global scopes)
- Ability to temporarily disable scopes
- Logic that belongs to a specific model
- Query Scopes - Model-based reusable query logic
- Query Builder Basics - QueryBuilder fundamentals
- Filtering Conditions - WHERE clause patterns