Laravel Macro
Laravel macros allow us to extend built-in classes with additional functionality. Below is a structured approach to implementing macros in Laravel using a service provider and a macro interface for maintainability.
1. Creating a Macro Service Provider
We'll first create a service provider to manage macro registrations.
Generate the Service Provider
php artisan make:provider MacroServiceProviderclass MacroServiceProvider extends ServiceProvider
{
#[\Override]
public function register(): void
{
//
}
public function boot(): void
{
//
}
}2. Creating a Macro Interface
To standardize macro implementation, we'll create an interface:
interface MacroInterface
{
public static function boot(): void;
public static function register(): void;
}Explanation:
boot()→ Called when macros are loaded.register()→ Used to register macros.
3. Creating QueryBuilder Macros
Now, we'll define macros for Laravel's QueryBuilder to add custom methods.
QueryBuilderMacro Class
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class QueryBuilderMacro implements MacroInterface
{
public static function boot(): void
{
self::registerSameOrganization();
self::registerWhereConcat();
}
public static function register(): void
{
// Future macros can be registered here.
}
/**
* Macro to filter queries based on the authenticated user's organization.
*/
public static function registerSameOrganization(): void
{
Builder::macro('sameOrganization', function (): object {
$this->where($this->from . '.organization_id', Auth::user()->organization_id);
return $this;
});
}
/**
* Macro to search multiple concatenated columns using a flexible query.
*/
public static function registerWhereConcat(): void
{
Builder::macro('whereConcat', function (array $columns, string $operator, ?string $value = null, string $boolean = 'and') {
if (null === $value) {
$value = $operator;
$operator = 'LIKE';
}
$validOperators = ['LIKE', 'NOT LIKE', '=', '!=', '>', '<', '>=', '<='];
if (!in_array(strtoupper($operator), $validOperators)) {
Log::error('Invalid operator provided. Supported operators: ' . implode(', ', $validOperators));
throw new \InvalidArgumentException('Invalid operator provided.');
}
$value = preg_replace('/\s+/', '', $value);
$driver = config('database.default');
// PostgreSQL / SQLite: Uses "||" for concatenation
$columnExpression = implode(" || ' ' || ", $columns);
// MySQL: Uses CONCAT() function
if ('mysql' === $driver) {
$columnExpression = 'CONCAT(' . implode(", ' ', ", $columns) . ')';
}
// Convert to lowercase and remove spaces for accurate search
$columnExpressionWithTrimAndLowercase = "LOWER(REPLACE({$columnExpression}, ' ', ''))";
$queryValue = ('LIKE' === $operator || 'NOT LIKE' === $operator) ? "%{$value}%" : $value;
return $this->whereRaw("{$columnExpressionWithTrimAndLowercase} {$operator} ?", [$queryValue], $boolean);
});
// Add `orWhereConcat` macro
Builder::macro('orWhereConcat', fn(array $columns, $operator, $value = null) => $this->whereConcat($columns, $operator, $value, 'or'));
}
}Explanation:
sameOrganization()→ Filters data based on the authenticated user'sorganization_id.whereConcat()→ Searches multiple concatenated columns, ignoring spaces and case differences.orWhereConcat()→ Works the same way aswhereConcat()but uses anORcondition instead ofAND.
4. Registering the Macros
Now, we register the macros inside the MacroServiceProvider.
class MacroServiceProvider extends ServiceProvider
{
#[\Override]
public function register(): void
{
QueryBuilderMacro::register();
}
public function boot(): void
{
QueryBuilderMacro::boot();
}
}5. Using the Macros
Once everything is set up, we can use our macros in Eloquent queries.
Example: Filtering by Organization
$users = User::query()->sameOrganization()->get();This retrieves only users that belong to the same organization as the authenticated user.
Even though User::query() returns an Eloquent Query Builder (Eloquent\Builder), Eloquent\Builder extends Query\Builder, so macros added to Query\Builder (like our whereConcat()) work in both.
example using Query Builder
$users = DB::table('users')->sameOrganization()->get();Example: Searching with Concatenated Fields
$results = User::query()
->whereConcat(['first_name', 'last_name'], 'LIKE', 'Mohamed Sheta')
->get();This searches for 'example text' inside both the name and description columns, ignoring spaces and case differences.
Example: Using OR Condition
$results = User::query()
->where('email', 'LIKE', '%mohamedsheta@gmail.com%')
->orWhereConcat(['first_name', 'last_name'], 'LIKE', 'Mohamed Sheta')
->get();This performs the same search but with an OR condition instead of AND.
Last updated