> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Macroable 트레이트

> Illuminate\Support\Traits\Macroable 트레이트를 사용해 기존 Laravel 클래스에 고유 메서드를 추가하는 방법을 해설합니다.

## Macroable 트레이트란

`Macroable` 트레이트는 클래스를 변경하지 않고 나중에 메서드를 동적으로 추가할 수 있는 구조입니다. Laravel 코어 클래스의 다수가 이 트레이트를 사용하고 있어, 코어 코드를 건드리지 않고 기능을 확장할 수 있습니다.

트레이트의 실체는 `Illuminate\Support\Traits\Macroable`에 있습니다. 내부적으로는 등록된 매크로를 정적 프로퍼티 `$macros`에 저장하고, `__call` / `__callStatic` 매직 메서드를 통해 호출합니다.

## Macroable을 사용하는 클래스

Laravel에는 Macroable에 대응하는 클래스가 많이 있습니다.

| 클래스                                    | 용도      |
| -------------------------------------- | ------- |
| `Illuminate\Support\Collection`        | 컬렉션 조작  |
| `Illuminate\Support\Str`               | 문자열 조작  |
| `Illuminate\Support\Arr`               | 배열 조작   |
| `Illuminate\Http\Request`              | HTTP 요청 |
| `Illuminate\Http\Response`             | HTTP 응답 |
| `Illuminate\Routing\Router`            | 라우터     |
| `Illuminate\Routing\ResponseFactory`   | 응답 팩토리  |
| `Illuminate\Database\Schema\Blueprint` | 스키마 빌더  |
| `Illuminate\Pipeline\Pipeline`         | 파이프라인   |
| `Illuminate\Testing\TestResponse`      | 테스트 응답  |

## macro() — 메서드 추가하기

`macro()`의 첫 번째 인수에 메서드명, 두 번째 인수에 클로저를 전달합니다.

```php theme={null}
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` 메서드가 모두 매크로로 등록됩니다.

```php theme={null}
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] ?? '');
        };
    }
}
```

<Info>
  `mixin()`의 메서드는 매크로로 등록되는 클로저를 반환해야 합니다. 메서드 자체의 반환값이 매크로의 구현이 됩니다.
</Info>

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

매크로는 애플리케이션 부팅 시에 등록해야 합니다. `AppServiceProvider`의 `boot()` 메서드가 적절한 위치입니다.

```php theme={null}
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('');
        });
    }
}
```

## 실전적인 유스케이스

### 컬렉션의 확장

컬렉션에의 커스텀 메서드 추가는 가장 일반적인 사용 예입니다.

```php theme={null}
// 数値コレクションの統計メソッド
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 클래스의 확장

```php theme={null}
// 日本語の文字数カウント（マルチバイト対応）
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 클래스의 확장

```php theme={null}
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';
});
```

```php theme={null}
// コントローラーで使用
public function index(Request $request)
{
    if ($request->isFromMobile()) {
        return response()->json($this->getMobileData());
    }

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

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

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

```php theme={null}
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();
});
```

```php theme={null}
// マイグレーションで使用
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->text('body');
    $table->addTimestampsWithTimezone();
    $table->addUserTracking();
});
```

### 테스트 응답의 확장

테스트 전용 어서션 메서드를 추가할 수 있습니다.

```php theme={null}
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);
});
```

```php theme={null}
// テストで使用
$this->getJson('/api/posts')->assertPaginated();
$this->postJson('/api/orders', $data)->assertApiSuccess();
```

## hasMacro() — 매크로 존재 확인

```php theme={null}
use Illuminate\Support\Collection;

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

## flushMacros() — 매크로 리셋

테스트에서 매크로를 리셋하고 싶은 경우에 사용합니다.

```php theme={null}
use Illuminate\Support\Collection;

// テスト内でのリセット
Collection::flushMacros();
```

<Warning>
  `flushMacros()`는 해당 클래스의 모든 매크로를 삭제합니다. 테스트 간 독립성을 유지하기 위해 `tearDown()`에서 호출하는 경우가 있지만, 다른 테스트에서 등록한 매크로도 사라지므로 주의가 필요합니다.
</Warning>

## 정적 매크로

매크로는 인스턴스 메서드로서 뿐만 아니라 정적 메서드로서도 동작합니다. `__callStatic`에 의해 처리됩니다.

```php theme={null}
Str::macro('randomHex', function (int $length = 8) {
    return substr(bin2hex(random_bytes($length)), 0, $length);
});

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

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

직접 만든 클래스에도 `Macroable`을 넣을 수 있습니다.

```php theme={null}
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);
    }
}
```

```php theme={null}
// サービスプロバイダーで拡張
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();
```

## 내부 구현의 상세

```php theme={null}
// __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 오브젝트 등)의 경우에는 바인딩되지 않습니다.

<Tip>
  IDE의 지원을 받으려면, 매크로의 어노테이션을 `@mixin`을 사용한 Doc 블록으로 정의하거나, Laravel IdeHelper 패키지를 사용해 헬퍼 파일을 자동 생성하는 방법이 있습니다.
</Tip>

## 다음 단계

<Card title="Pipeline 패턴" icon="arrow-right-arrow-left" href="/ko/advanced/pipeline">
  파이프라인 패턴을 사용해 여러 처리 단계를 직렬로 구성하는 방법을 배웁니다.
</Card>


## Related topics

- [Dumpable 트레이트](/ko/advanced/dumpable.md)
- [InteractsWithData 트레이트](/ko/advanced/interacts-with-data.md)
- [tap() 헬퍼와 Tappable 트레이트](/ko/advanced/tap.md)
- [ForwardsCalls 트레이트](/ko/advanced/forwards-calls.md)
- [Conditionable 트레이트](/ko/advanced/conditionable.md)
