메인 콘텐츠로 건너뛰기

Macroable 트레이트란

Macroable 트레이트는 클래스를 변경하지 않고 나중에 메서드를 동적으로 추가할 수 있는 구조입니다. Laravel 코어 클래스의 다수가 이 트레이트를 사용하고 있어, 코어 코드를 건드리지 않고 기능을 확장할 수 있습니다. 트레이트의 실체는 Illuminate\Support\Traits\Macroable에 있습니다. 내부적으로는 등록된 매크로를 정적 프로퍼티 $macros에 저장하고, __call / __callStatic 매직 메서드를 통해 호출합니다.

Macroable을 사용하는 클래스

Laravel에는 Macroable에 대응하는 클래스가 많이 있습니다.
클래스용도
Illuminate\Support\Collection컬렉션 조작
Illuminate\Support\Str문자열 조작
Illuminate\Support\Arr배열 조작
Illuminate\Http\RequestHTTP 요청
Illuminate\Http\ResponseHTTP 응답
Illuminate\Routing\Router라우터
Illuminate\Routing\ResponseFactory응답 팩토리
Illuminate\Database\Schema\Blueprint스키마 빌더
Illuminate\Pipeline\Pipeline파이프라인
Illuminate\Testing\TestResponse테스트 응답

macro() — 메서드 추가하기

macro()의 첫 번째 인수에 메서드명, 두 번째 인수에 클로저를 전달합니다.
use Illuminate\Support\Collection;

Collection::macro('toSentence', function (string $separator = '、') {
    /** @var Collection $this */
    return $this->implode($separator);
});

$result = collect(['りんご', 'みかん', 'ぶどう'])->toSentence();
// 'りんご、みかん、ぶどう'
클로저 내의 $this는 매크로를 호출한 인스턴스에 바인딩됩니다. 이를 통해 클래스의 프로퍼티나 메서드에 직접 접근할 수 있습니다.

mixin() — 여러 메서드를 한 번에 추가하기

많은 매크로를 한꺼번에 등록하고 싶은 경우에는 mixin()을 사용합니다. 믹스인 클래스의 public / protected 메서드가 모두 매크로로 등록됩니다.
namespace App\Mixins;

class CollectionMixin
{
    public function toCsv(): Closure
    {
        return function (string $separator = ',') {
            /** @var \Illuminate\Support\Collection $this */
            return $this->map(function ($item) use ($separator) {
                return is_array($item) ? implode($separator, $item) : $item;
            })->implode("\n");
        };
    }

    public function filterEmpty(): Closure
    {
        return function () {
            /** @var \Illuminate\Support\Collection $this */
            return $this->filter(fn ($item) => ! empty($item))->values();
        };
    }

    public function groupByFirst(): Closure
    {
        return function (string $key) {
            /** @var \Illuminate\Support\Collection $this */
            return $this->groupBy(fn ($item) => $item[$key][0] ?? '');
        };
    }
}
mixin()의 메서드는 매크로로 등록되는 클로저를 반환해야 합니다. 메서드 자체의 반환값이 매크로의 구현이 됩니다.

서비스 프로바이더에의 등록

매크로는 애플리케이션 부팅 시에 등록해야 합니다. AppServiceProviderboot() 메서드가 적절한 위치입니다.
namespace App\Providers;

use App\Mixins\CollectionMixin;
use Illuminate\Support\Collection;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use Illuminate\Http\Request;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // 単一マクロを登録
        Collection::macro('toSentence', function (string $separator = '、') {
            return $this->implode($separator);
        });

        // ミックスインをまとめて登録
        Collection::mixin(new CollectionMixin);

        // Strへのマクロ
        Str::macro('initials', function (string $name) {
            return collect(explode(' ', $name))
                ->map(fn ($word) => strtoupper($word[0]))
                ->implode('');
        });
    }
}

실전적인 유스케이스

컬렉션의 확장

컬렉션에의 커스텀 메서드 추가는 가장 일반적인 사용 예입니다.
// 数値コレクションの統計メソッド
Collection::macro('median', function () {
    $sorted = $this->sort()->values();
    $count = $sorted->count();

    if ($count === 0) {
        return null;
    }

    $middle = (int) floor($count / 2);

    if ($count % 2 === 0) {
        return ($sorted->get($middle - 1) + $sorted->get($middle)) / 2;
    }

    return $sorted->get($middle);
});

$median = collect([3, 1, 4, 1, 5, 9, 2, 6])->median();
// 3.5

// ページネーション情報を付与したコレクションを返すマクロ
Collection::macro('paginateArray', function (int $perPage = 15, int $page = 1) {
    return $this->slice(($page - 1) * $perPage, $perPage)->values();
});

Str 클래스의 확장

// 日本語の文字数カウント(マルチバイト対応)
Str::macro('mbLength', function (string $value) {
    return mb_strlen($value, 'UTF-8');
});

// スネークケースをドット記法に変換
Str::macro('toDotNotation', function (string $value) {
    return str_replace('_', '.', $value);
});

$length = Str::mbLength('こんにちは'); // 5
$dot = Str::toDotNotation('user_profile_name'); // 'user.profile.name'

Request 클래스의 확장

use Illuminate\Http\Request;

Request::macro('isFromMobile', function () {
    /** @var Request $this */
    $userAgent = $this->userAgent() ?? '';

    return preg_match('/Mobile|Android|iPhone|iPad/i', $userAgent) === 1;
});

Request::macro('preferredLocale', function (array $available = ['ja', 'en']) {
    /** @var Request $this */
    foreach ($this->getLanguages() as $lang) {
        $short = substr($lang, 0, 2);
        if (in_array($short, $available)) {
            return $short;
        }
    }

    return $available[0] ?? 'en';
});
// コントローラーで使用
public function index(Request $request)
{
    if ($request->isFromMobile()) {
        return response()->json($this->getMobileData());
    }

    $locale = $request->preferredLocale(['ja', 'en', 'zh']);
    // ...
}

Blueprint의 확장(마이그레이션)

스키마의 컬럼 정의를 한꺼번에 매크로화하면 일관된 DB 설계를 유지할 수 있습니다.
use Illuminate\Database\Schema\Blueprint;

Blueprint::macro('addTimestampsWithTimezone', function () {
    /** @var Blueprint $this */
    $this->timestampsTz();
    $this->softDeletesTz();
});

Blueprint::macro('addUserTracking', function () {
    /** @var Blueprint $this */
    $this->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
    $this->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
});
// マイグレーションで使用
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->text('body');
    $table->addTimestampsWithTimezone();
    $table->addUserTracking();
});

테스트 응답의 확장

테스트 전용 어서션 메서드를 추가할 수 있습니다.
use Illuminate\Testing\TestResponse;

TestResponse::macro('assertPaginated', function () {
    /** @var TestResponse $this */
    return $this->assertJsonStructure([
        'data',
        'meta' => ['current_page', 'last_page', 'per_page', 'total'],
        'links' => ['first', 'last', 'prev', 'next'],
    ]);
});

TestResponse::macro('assertApiSuccess', function () {
    /** @var TestResponse $this */
    return $this->assertOk()->assertJsonPath('success', true);
});
// テストで使用
$this->getJson('/api/posts')->assertPaginated();
$this->postJson('/api/orders', $data)->assertApiSuccess();

hasMacro() — 매크로 존재 확인

use Illuminate\Support\Collection;

if (Collection::hasMacro('toSentence')) {
    $result = collect(['a', 'b'])->toSentence();
}

flushMacros() — 매크로 리셋

테스트에서 매크로를 리셋하고 싶은 경우에 사용합니다.
use Illuminate\Support\Collection;

// テスト内でのリセット
Collection::flushMacros();
flushMacros()는 해당 클래스의 모든 매크로를 삭제합니다. 테스트 간 독립성을 유지하기 위해 tearDown()에서 호출하는 경우가 있지만, 다른 테스트에서 등록한 매크로도 사라지므로 주의가 필요합니다.

정적 매크로

매크로는 인스턴스 메서드로서 뿐만 아니라 정적 메서드로서도 동작합니다. __callStatic에 의해 처리됩니다.
Str::macro('randomHex', function (int $length = 8) {
    return substr(bin2hex(random_bytes($length)), 0, $length);
});

// 静的呼び出し
$hex = Str::randomHex(16);

Macroable 트레이트를 자체 클래스에서 사용하기

직접 만든 클래스에도 Macroable을 넣을 수 있습니다.
namespace App\Services;

use Illuminate\Support\Traits\Macroable;

class ReportBuilder
{
    use Macroable;

    protected array $sections = [];

    public function addSection(string $name, callable $content): static
    {
        $this->sections[$name] = $content;

        return $this;
    }

    public function build(): array
    {
        return array_map(fn ($fn) => $fn(), $this->sections);
    }
}
// サービスプロバイダーで拡張
ReportBuilder::macro('withSummary', function (string $title) {
    /** @var ReportBuilder $this */
    return $this->addSection('summary', fn () => [
        'title' => $title,
        'generated_at' => now()->toIso8601String(),
    ]);
});

// 使用例
$report = app(ReportBuilder::class)
    ->withSummary('月次レポート')
    ->addSection('data', fn () => ['rows' => 42])
    ->build();

내부 구현의 상세

// __call の実装(インスタンスメソッド呼び出し)
public function __call($method, $parameters)
{
    if (! static::hasMacro($method)) {
        throw new BadMethodCallException(sprintf(
            'Method %s::%s does not exist.', static::class, $method
        ));
    }

    $macro = static::$macros[$method];

    if ($macro instanceof Closure) {
        // bindTo で $this をインスタンスにバインド
        $macro = $macro->bindTo($this, static::class);
    }

    return $macro(...$parameters);
}
클로저는 Closure::bindTo()에 의해 인스턴스에 바인딩됩니다. 이를 통해 $this가 매크로 호출자의 오브젝트를 가리키게 됩니다. 클로저 이외(invokable 오브젝트 등)의 경우에는 바인딩되지 않습니다.
IDE의 지원을 받으려면, 매크로의 어노테이션을 @mixin을 사용한 Doc 블록으로 정의하거나, Laravel IdeHelper 패키지를 사용해 헬퍼 파일을 자동 생성하는 방법이 있습니다.

다음 단계

Pipeline 패턴

파이프라인 패턴을 사용해 여러 처리 단계를 직렬로 구성하는 방법을 배웁니다.
마지막 수정일 2026년 7월 13일