Navigation

Laravel

Laravel Macros: Creating Custom Framework Extensions

Master Laravel's macro system to extend core functionality. Learn to create custom methods for collections, queries, responses and more without modifying framework code.
Laravel Macros: Creating Custom Framework Extensions

Extend Laravel's core functionality without modifying source code. Learn how to add custom methods to collections, queries, responses, and more using Laravel's powerful macro system.

Laravel's macro system is one of its most powerful yet underutilized features. It allows you to dynamically add methods to Laravel's core classes without inheritance or modifying the framework's source code. This capability opens up endless possibilities for extending functionality while maintaining clean, reusable code.

Table Of Contents

Understanding Laravel Macros

Macros in Laravel leverage PHP's magic __call and __callStatic methods to enable dynamic method registration. When you call a method that doesn't exist on a macroable class, Laravel checks if a macro with that name has been registered and executes it if found.

The beauty of macros lies in their flexibility—you can add methods to collections, query builders, responses, requests, and many other Laravel components. This feature transforms Laravel from a framework you use into one you can truly extend.

Basic Macro Implementation

Let's start with a simple example. The most common use case is extending collections with custom functionality:

use Illuminate\Support\Collection;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        Collection::macro('toSelectArray', function ($value, $text) {
            return $this->map(function ($item) use ($value, $text) {
                return [
                    'value' => data_get($item, $value),
                    'text' => data_get($item, $text)
                ];
            })->toArray();
        });
    }
}

Now you can use this macro anywhere in your application:

$users = User::all()->toSelectArray('id', 'name');
// Returns: [['value' => 1, 'text' => 'John'], ['value' => 2, 'text' => 'Jane']]

Advanced Collection Macros

Collections are perfect candidates for macros because they're used extensively throughout Laravel applications. Here are some advanced examples:

Pagination Macro

Collection::macro('paginate', function ($perPage = 15, $page = null, $pageName = 'page') {
    $page = $page ?: request()->get($pageName, 1);
    $offset = ($page - 1) * $perPage;
    
    return new LengthAwarePaginator(
        $this->slice($offset, $perPage)->values(),
        $this->count(),
        $perPage,
        $page,
        [
            'path' => request()->url(),
            'pageName' => $pageName
        ]
    );
});

Statistics Macro

Collection::macro('statistics', function ($key = null) {
    $values = $key ? $this->pluck($key) : $this;
    
    return [
        'count' => $values->count(),
        'sum' => $values->sum(),
        'average' => $values->avg(),
        'median' => $values->median(),
        'min' => $values->min(),
        'max' => $values->max()
    ];
});

Query Builder Macros

Eloquent query builders are another excellent target for macros, especially for complex or frequently used query patterns:

use Illuminate\Database\Query\Builder;

Builder::macro('whereLike', function ($column, $value) {
    return $this->where($column, 'like', '%' . $value . '%');
});

Builder::macro('orWhereLike', function ($column, $value) {
    return $this->orWhere($column, 'like', '%' . $value . '%');
});

Builder::macro('whereDate', function ($column, $date) {
    return $this->whereDate($column, $date);
});

Response Macros

Response macros help standardize API responses across your application:

use Illuminate\Http\JsonResponse;

JsonResponse::macro('success', function ($data = [], $message = 'Success') {
    return response()->json([
        'success' => true,
        'message' => $message,
        'data' => $data
    ]);
});

JsonResponse::macro('error', function ($message = 'Error', $errors = [], $code = 400) {
    return response()->json([
        'success' => false,
        'message' => $message,
        'errors' => $errors
    ], $code);
});

Usage becomes incredibly clean:

return response()->success($users, 'Users retrieved successfully');
return response()->error('Validation failed', $validator->errors(), 422);

Request Macros

Extend request handling with custom validation and data extraction methods:

use Illuminate\Http\Request;

Request::macro('validateAndGet', function (array $rules) {
    $this->validate($rules);
    return $this->only(array_keys($rules));
});

Request::macro('isApi', function () {
    return $this->expectsJson() || $this->is('api/*');
});

Creating Macro Packages

For reusable functionality, create dedicated macro packages:

namespace App\Macros;

use Illuminate\Support\Collection;

class CollectionMacros
{
    public static function register()
    {
        Collection::macro('recursive', function () {
            return $this->map(function ($value) {
                if (is_array($value) || is_object($value)) {
                    return collect($value)->recursive();
                }
                return $value;
            });
        });
        
        Collection::macro('transpose', function () {
            $items = array_map(function (...$items) {
                return $items;
            }, ...$this->values());
            
            return new static($items);
        });
    }
}

Register in your service provider:

public function boot()
{
    CollectionMacros::register();
    QueryBuilderMacros::register();
    ResponseMacros::register();
}

Testing Macros

Always test your macros to ensure they work correctly:

class CollectionMacroTest extends TestCase
{
    public function test_to_select_array_macro()
    {
        $collection = collect([
            ['id' => 1, 'name' => 'John'],
            ['id' => 2, 'name' => 'Jane']
        ]);
        
        $result = $collection->toSelectArray('id', 'name');
        
        $this->assertEquals([
            ['value' => 1, 'text' => 'John'],
            ['value' => 2, 'text' => 'Jane']
        ], $result);
    }
}

Performance Considerations

Macros are resolved at runtime, so they have a slight performance overhead compared to native methods. However, this overhead is minimal and rarely impacts application performance. For high-performance scenarios, consider using traditional inheritance or composition.

Best Practices

  1. Use descriptive names: Make macro names clear and specific to avoid conflicts
  2. Group related macros: Organize macros into logical groups or dedicated classes
  3. Document thoroughly: Since macros aren't visible in IDE autocompletion by default, good documentation is crucial
  4. Test extensively: Macros can be harder to debug, so comprehensive testing is essential
  5. Consider alternatives: Sometimes traditional OOP patterns might be more appropriate

IDE Support

Many IDEs support macro definitions through helper files or packages. Laravel IDE Helper can generate macro definitions for better autocompletion:

php artisan ide-helper:generate
php artisan ide-helper:models
php artisan ide-helper:meta

Laravel macros represent the framework's commitment to extensibility without complexity. They provide a clean, maintainable way to add functionality that feels native to Laravel's API. Whether you're building reusable packages or just cleaning up repetitive code in your application, macros offer an elegant solution that maintains Laravel's expressive, readable syntax.

By mastering macros, you're not just extending Laravel—you're participating in its design philosophy of making complex functionality simple and enjoyable to use.

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Laravel