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 MacroServiceProvider
class 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's organization_id.

  • whereConcat() → Searches multiple concatenated columns, ignoring spaces and case differences.

  • orWhereConcat() → Works the same way as whereConcat() but uses an OR condition instead of AND.


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