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

# 패키지의 문자열 유효성 검사 규칙 등록

> Laravel 13의 Factory::extend를 분석해 문자열 규칙의 호출 규약, 오류 메시지와 번역, Implicit·Dependent의 차이, 생성된 Validator의 수명과 테스트를 설명합니다.

배포 패키지에서 `acme_tracking:AC` 같은 문자열 규칙을 제공하려면 유효성 검사 `Factory`에 검사 처리를 등록합니다. 사용하는 애플리케이션의 폼 리퀘스트에서 내장 규칙과 같은 표기법으로 사용할 수 있습니다.

이 페이지는 Laravel Framework `v13.40.0`의 공식 구현을 확인한 내용입니다. [커스텀 유효성 검사 규칙](https://kawax.biz/ko/advanced/custom-validation-rules.md)의 기본 예제를 보완하여, 패키지의 공개 API로 유지보수할 때의 등록·번역·상태의 수명을 다룹니다.

## 규칙 객체와 구분해서 사용하기

공식 문서에서 소개하는 `ValidationRule` 객체는 패키지에서도 사용할 수 있습니다. 문자열로 지정할 수 있는 것 자체가 요구 사항이 아니라면, 생성자에서 타입이 지정된 인수를 받을 수 있는 객체를 검토합니다.

| 방식 | 적합한 용도 | 실패 통지 |
| - | - | - |
| `new CustomRule(...)` 같은 규칙 객체 | 타입이 지정된 설정이나 복잡한 검사를 클래스로 배포한다 | `validate()`에 전달된 `$fail`을 호출한다 |
| `acme_tracking:AC` 같은 등록된 문자열 규칙 | 문자열 기반 규칙 정의에도 넣을 수 있는 API를 제공한다 | 등록한 콜백이 `false`를 반환한다 |

같은 `validate()`라는 이름이라도 호출 규약이 다릅니다. `ValidationRule` 구현 클래스를 그대로 `Factory::extend()`의 클래스 문자열로 전달하면 세 번째 인수의 타입이나 반환값이 일치하지 않습니다.

## 등록·생성·검사를 분리하기

`Validator` 파사드의 `extend()`는 `Illuminate\Validation\Factory`에 대한 호출입니다. 입력을 검사하는 `Illuminate\Validation\Validator` 인스턴스와는 다른 객체입니다.

```mermaid theme={null}
flowchart TD
    A["패키지의 boot"] --> B["Factory에 규칙과 replacer를 등록"]
    B --> C["Factory::make로<br>새 Validator를 생성"]
    C --> D["등록된 확장과<br>fallback 메시지를 복사"]
    D --> E["각 속성의 규칙을 분석하고<br>실행 조건을 확인"]
    E --> F["클래스 또는 클로저로 검사"]
    F --> G{"반환값이 true?"}
    G -->|Yes| H["이 규칙은 성공"]
    G -->|No| I["메시지를 선택하고<br>플레이스홀더를 치환"]
```

그림은 문자열 규칙이 실행되는 경우의 경로입니다. 미입력이나 `nullable` 등에 의한 건너뛰기는 뒤에서 설명합니다.

Factory는 `validator`라는 키의 singleton으로 등록되지만, 표준 구현의 `make()`가 반환하는 Validator는 매번 새로 생성됩니다. **Factory에 나중에 등록해도 이미 생성된 Validator의 확장 배열은 갱신되지 않습니다.**

## 패키지 고유의 규칙 등록하기

예로, 영문 대문자 접두사·하이픈·8자리 숫자로 이루어진 가상의 추적 코드를 검사합니다. `AC-00000001` 같은 형식의 예이며, 실제 배송 회사의 번호 사양이 아닙니다.

패키지의 네임스페이스 `Acme\Courier\`가 Composer의 PSR-4로 `src/`에 대응하고, 일반적인 비지연 Service Provider가 로드되는 구성을 가정합니다. 프로바이더의 로드는 [패키지 자동 검출](https://kawax.biz/ko/advanced/package-discovery.md)을 참조하세요.

### 검사 처리를 클래스로 분리하기

`src/Validation/TrackingCode.php`에 문자열 규칙용 핸들러를 준비합니다. 이 클래스는 `ValidationRule` 인터페이스를 구현하지 않습니다.

```php theme={null}
<?php

namespace Acme\Courier\Validation;

use Illuminate\Validation\Validator;
use InvalidArgumentException;

final class TrackingCode
{
    public function validate(
        string $attribute,
        mixed $value,
        array $parameters,
        Validator $validator,
    ): bool {
        if (count($parameters) !== 1
            || ! is_string($parameters[0])
            || preg_match('/\A[A-Z]{2,8}\z/', $parameters[0]) !== 1
        ) {
            throw new InvalidArgumentException(
                'acme_tracking expects one uppercase ASCII prefix of 2-8 characters.',
            );
        }

        return is_string($value)
            && preg_match(
                '/\A'.preg_quote($parameters[0], '/').'-[0-9]{8}\z/',
                $value,
            ) === 1;
    }
}
```

콜백에는 속성 이름·입력값·파라미터 배열·검사 중인 Validator의 네 인수가 전달됩니다. `acme_tracking:AC`의 경우 파라미터 배열은 `['AC']`입니다. 규칙 쪽에서도 값의 타입을 확인하여 단독으로 사용되어도 타입 오류가 나지 않도록 하고 있습니다.

잘못된 접두사나 파라미터 개수는 사용자의 입력 오류가 아니라 **규칙 정의의 설정 실수**로 보고 예외를 던집니다. 빈 값으로 규칙이 건너뛰어지는 경우에는 이 설정 확인도 실행되지 않으므로, 정상적인 비어 있지 않은 값을 사용한 테스트도 필요합니다.

### Factory가 해석된 후에 등록하기

`src/CourierServiceProvider.php`의 `boot()`에서 규칙과 메시지 치환 처리를 등록합니다.

```php theme={null}
<?php

namespace Acme\Courier;

use Acme\Courier\Validation\TrackingCode;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Factory;

final class CourierServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->callAfterResolving('validator', function (Factory $factory): void {
            $factory->extend(
                'acme_tracking',
                TrackingCode::class.'@validate',
                'The :attribute must be :prefix- followed by eight digits.',
            );

            $factory->replacer(
                'acme_tracking',
                static function (
                    string $message,
                    string $attribute,
                    string $rule,
                    array $parameters,
                ): string {
                    return str_replace(':prefix', $parameters[0] ?? '', $message);
                },
            );
        });
    }
}
```

`callAfterResolving()`은 아직 해석되지 않았다면 생성을 기다리고, 이미 해석된 Factory에도 즉시 등록합니다. 훅 자체에 대한 자세한 내용은 [패키지의 서비스 해석 후 훅](https://kawax.biz/ko/advanced/package-service-resolution.md)에서 설명합니다.

클래스 문자열 `TrackingCode::class.'@validate'`는 검사 시 컨테이너에서 클래스를 해석하여 메서드를 호출합니다. `@validate`를 생략한 클래스 이름만으로도 기본 메서드 이름은 `validate`입니다. 생성자 의존성이 있는 경우에는 컨테이너에서 해석할 수 있도록 바인딩합니다.

이 예제에는 singleton 핸들러 등록이나 입력을 저장하는 프로퍼티를 두지 않았습니다. 검사 시의 데이터는 인수로 다루고, Factory가 보유하는 클로저에 요청 고유의 상태를 담지 않는 설계로 합니다.

### 사용하는 애플리케이션에서 지정하기

```php theme={null}
$request->validate([
    'tracking_code' => ['bail', 'required', 'string', 'acme_tracking:AC'],
]);
```

`bail`은 해당 속성의 앞선 규칙이 실패하면 이후 규칙을 중단합니다. 필수 여부는 `required`, 형식은 `acme_tracking`처럼 역할을 나눕니다.

공개하는 이름은 `acme_tracking` 같은 고유한 snake case로 통일합니다. 같은 종류의 확장에 같은 이름으로 등록하면 나중 처리가 덮어씁니다. 또한 내장 `validateRequired()` 등이 존재하는 경우에는 그 메서드가 직접 호출되므로, 같은 이름의 확장으로 내장 검사를 대체할 수 없습니다.

## 사용자의 오류 메시지 유지하기

일반 문자열 규칙의 메시지는 대체로 다음 우선순위로 선택됩니다. size 규칙 등의 특수한 분기는 여기서 다루지 않습니다.

| 우선순위 | 메시지 지정 위치 |
| - | - |
| 1 | `Validator::make()`의 세 번째 인수나 폼 리퀘스트의 `messages()` |
| 2 | 사용하는 애플리케이션의 `validation.custom.속성명.규칙명` 번역 |
| 3 | `validation.acme_tracking` 같은 규칙 이름의 번역 |
| 4 | `Factory::extend()`의 세 번째 인수로 등록한 fallback 문자열 |

Laravel이 `:attribute` 등을 치환한 후에 커스텀 replacer가 호출됩니다. replacer의 인수는 선택된 메시지·속성 이름·규칙 이름·파라미터 배열·Validator의 다섯 개입니다. 앞의 예제에서는 사용하지 않는 다섯 번째 인수를 생략했습니다.

> [!WARNING]
>
> replacer에서 메시지 전체를 항상 고정 문장으로 바꾸면 사용자의 `messages()`나 번역의 덮어쓰기를 무시하게 됩니다. 앞의 예제는 전달된 `$message`의 `:prefix`만 치환합니다.

애플리케이션 측에서 일본어 기본 문장을 지정하려면 기존 `lang/ja/validation.php`의 반환 배열에 `acme_tracking`을 추가합니다. 아래 예는 추가할 항목의 예이며, 다른 내장 메시지를 삭제하는 것이 아닙니다.

```php theme={null}
<?php

return [
    'acme_tracking' => ':attributeは:prefix-に続く8桁の数字で入力してください。',
];
```

번역 파일 준비나 로케일 설정은 [로컬라이제이션](https://kawax.biz/ko/localization.md)을 참조하세요. 등록 시 `__('...')`를 실행해 fallback에 저장하면 그 시점 로케일의 문자열이 Factory에 남습니다. 로케일을 전환하는 애플리케이션에서는 오류 생성 시의 번역을 사용합니다.

### 패키지의 네임스페이스 번역을 사용하는 경우

`boot()`에서 `loadTranslationsFrom(__DIR__.'/../lang', 'acme-courier')`를 호출하면 패키지의 `lang/ja/validation.php` 등을 `acme-courier::validation.tracking_code`로 참조할 수 있습니다.

단, **`extend()`의 세 번째 인수에 번역 키를 전달하는 것만으로는 자동으로 번역되지 않습니다.** Factory는 단순한 fallback 문자열로 보유합니다. 앞의 훅 안의 `extend()`와 `replacer()`를 다음 등록으로 바꾸면 fallback이 선택되었을 때만 패키지의 번역을 가져올 수 있습니다.

```php theme={null}
$factory->extend(
    'acme_tracking',
    TrackingCode::class.'@validate',
    'acme-courier::validation.tracking_code',
);

$factory->replacer(
    'acme_tracking',
    static function (
        string $message,
        string $attribute,
        string $rule,
        array $parameters,
        \Illuminate\Validation\Validator $validator,
    ): string {
        if ($message === 'acme-courier::validation.tracking_code') {
            return $validator->getTranslator()->get($message, [
                'attribute' => $validator->getDisplayableAttribute($attribute),
                'prefix' => $parameters[0] ?? '',
            ]);
        }

        return str_replace(':prefix', $parameters[0] ?? '', $message);
    },
);
```

패키지의 각 로케일 `validation.php`에 `tracking_code` 항목을 준비합니다. 일본어 문장은 앞의 `acme_tracking` 항목과 같은 것을 사용할 수 있습니다.

여기서는 replacer 안에서 새 번역 문자열을 가져오므로 `:attribute`도 명시적으로 번역에 전달합니다. Laravel의 표준 치환이 이 반환값에 다시 적용되는 것은 아닙니다. 사용자의 자체 메시지가 선택된 경우에는 그 문장을 유지합니다. 배포 번역의 덮어쓰기는 [패키지 번역의 덮어쓰기와 갱신](https://kawax.biz/ko/advanced/package-translations.md)을 참조하세요.

## Implicit와 nullable의 실행 조건 이해하기

`extendImplicit()`은 누락된 속성이나 빈 문자열이어도 검사 대상이 되는 규칙을 등록합니다. "반드시 실패시킨다"거나 "다른 건너뛰기 조건을 모두 무시한다"는 의미가 아닙니다.

| 입력·지정 | 일반 `extend()` | `extendImplicit()` |
| - | - | - |
| 속성이 없음 | 실행하지 않음 | 실행 대상이 됨 |
| 빈 문자열·공백만 있는 문자열 | 실행하지 않음 | 실행 대상이 됨 |
| 존재하는 속성의 `null`, `nullable` 없음 | 실행 대상이 됨 | 실행 대상이 됨 |
| 존재하는 속성의 `null`, `nullable` 있음 | 실행하지 않음 | 실행 대상이 됨 |
| 속성이 없고 `sometimes` 있음 | 실행하지 않음 | 실행하지 않음 |

표는 각 규칙의 실행 조건 비교입니다. `bail`, 속성 제외, `stopOnFirstFailure()` 등으로 먼저 처리가 멈추는 경우는 별개입니다. HTTP 입력에서는 빈 문자열이 미들웨어에 의해 `null`로 변환되기도 하므로 빈 문자열과 `null`을 따로 테스트합니다.

이번 형식 규칙은 일반 `extend()`와 `required`의 조합으로 충분합니다. 공개된 규칙을 Implicit로 변경하면 이전에는 검사되지 않던 입력이 실패할 수 있으므로 호환성 변경으로 다룹니다.

## Dependent 규칙으로 배열 안의 참조 대상 해석하기

다른 필드의 이름을 파라미터로 받는 규칙에서는 `extendDependent()`를 사용할 수 있습니다. `items.*.confirmed_tracking`을 검사할 때 파라미터의 `items.*.tracking`이 같은 행의 `items.0.tracking` 등으로 변환됩니다. 일반 `extend()`에서는 이 치환이 이루어지지 않습니다.

다음은 이 구조를 확인하기 위한 일치 규칙입니다. 단순한 일치 검사만이라면 실제 애플리케이션에서는 내장 `same`을 사용할 수 있습니다. 패키지 고유의 필드 간 검사에 응용할 때의 예로 봐 주세요.

훅에서 받은 Factory에 추가합니다.

```php theme={null}
$factory->extendDependent(
    'acme_matches_field',
    static function (
        string $attribute,
        mixed $value,
        array $parameters,
        \Illuminate\Validation\Validator $validator,
    ): bool {
        if (count($parameters) !== 1
            || ! is_string($parameters[0])
            || $parameters[0] === ''
        ) {
            throw new \InvalidArgumentException('acme_matches_field expects one field name.');
        }

        $data = $validator->getData();

        return \Illuminate\Support\Arr::has($data, $parameters[0])
            && $value === \Illuminate\Support\Arr::get($data, $parameters[0]);
    },
    'The :attribute does not match the related field.',
);
```

```php theme={null}
$request->validate([
    'items' => ['required', 'array'],
    'items.*' => ['array:tracking,confirmed_tracking'],
    'items.*.tracking' => ['required', 'string', 'acme_tracking:AC'],
    'items.*.confirmed_tracking' => [
        'required',
        'string',
        'acme_matches_field:items.*.tracking',
    ],
]);
```

Dependent로 등록해도 참조 대상의 존재 확인이나 검사 완료 상태는 보장되지 않습니다. 네 번째 인수의 `getData()`는 **유효성 검사 대상 데이터**이며, 검사가 끝난 데이터만은 아닙니다. 참조하는 쪽과 참조되는 쪽 모두에 필요한 규칙을 정의합니다.

또한 Dependent로 해도 Implicit가 되지는 않습니다. 누락이나 빈 값에 대한 필수 여부는 별도로 지정합니다. 구현은 파라미터에 점의 이스케이프 처리와 `*` 치환을 수행하므로, 필드 이름 이외의 임의 문자열을 섞는 API는 피합니다.

## 애플리케이션에서 등록부터 검사까지 테스트하기

패키지와 Service Provider가 로드되는 Laravel 13 애플리케이션에서 `tests/Feature/TrackingRuleTest.php`를 작성합니다. 클래스 단위 테스트에 더해, 실제 Factory를 통해 등록과 메시지 처리를 검사합니다.

```php theme={null}
<?php

namespace Tests\Feature;

use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Factory;
use Tests\TestCase;

final class TrackingRuleTest extends TestCase
{
    public function test_format_presence_and_null_are_separate(): void
    {
        $rules = ['code' => ['acme_tracking:AC']];

        foreach ([
            [['code' => 'AC-00000001'], true],
            [['code' => 'ZX-00000001'], false],
            [['code' => ['AC-00000001']], false],
            [['code' => ''], true],
            [['code' => null], false],
            [[], true],
        ] as [$data, $expected]) {
            $this->assertSame($expected, Validator::make($data, $rules)->passes());
        }

        $this->assertTrue(Validator::make(
            ['code' => null],
            ['code' => ['nullable', 'acme_tracking:AC']],
        )->passes());

        $this->assertFalse(Validator::make(
            [],
            ['code' => ['required', 'acme_tracking:AC']],
        )->passes());
    }

    public function test_application_message_and_display_name_are_kept(): void
    {
        $validator = Validator::make(
            ['code' => 'invalid'],
            ['code' => ['acme_tracking:AC']],
            ['code.acme_tracking' => ':attributeには:prefixのコードを入力してください。'],
            ['code' => '追跡番号'],
        );

        $this->assertSame(
            '追跡番号にはACのコードを入力してください。',
            $validator->errors()->first('code'),
        );
    }

    public function test_existing_validator_keeps_its_extensions(): void
    {
        $factory = new Factory($this->app['translator'], $this->app);
        $factory->extend('acme_probe', static fn (): bool => true);
        $before = $factory->make(['code' => 'x'], ['code' => ['acme_probe']]);

        $factory->extend('acme_probe', static fn (): bool => false, 'Rejected.');
        $after = $factory->make(['code' => 'x'], ['code' => ['acme_probe']]);

        $this->assertTrue($before->passes());
        $this->assertFalse($after->passes());
    }
}
```

```bash theme={null}
php artisan test --compact tests/Feature/TrackingRuleTest.php
```

마지막 테스트는 공유 Factory를 변경하지 않고, 새 Factory를 만들어 등록 내용의 복사를 확인합니다. 네임스페이스 번역이나 Dependent 예제도 제공하는 경우에는 해당 등록을 포함한 환경에서 추가 테스트를 준비합니다.

## 장기 유지보수에서 확인할 사항

* 규칙 이름, 파라미터의 의미·개수, 필수 여부, 받아들이는 값의 타입을 공개 API로 기록한다.
* 잘못된 파라미터, 누락·빈 문자열·`null`, `nullable`·`sometimes`·`bail`의 조합을 검사한다.
* 사용자의 `messages()`, 속성 표시 이름, 로케일 전환, 패키지 번역의 덮어쓰기를 확인한다.
* Dependent 규칙은 여러 행과 중첩된 배열에서 다른 행의 값을 참조하지 않는지 확인한다.
* 미해석·해석된 Factory에 대한 등록을 확인하고, 이미 생성된 Validator에 소급 반영되기를 기대하지 않는다.
* 여러 패키지와의 이름 충돌을 피하고, 같은 이름을 일반·Implicit·Dependent 등록에 섞어 쓰지 않는다.
* 장시간 실행되는 워커에 변경을 반영할 때는 프로세스를 재시작하고, 입력이나 사용자 정보를 공유 핸들러에 남기지 않는다.

패키지 단독 검증 환경은 [패키지 테스트](https://kawax.biz/ko/advanced/package-testing.md)를, 호환성 관리는 [패키지 버전 호환성 관리](https://kawax.biz/ko/advanced/package-versioning.md)도 참조하세요.

## 참조한 1차 정보

* [Laravel 공식 문서: 커스텀 규칙과 Implicit 규칙](https://github.com/laravel/docs/blob/10add0d2dade04cacfab436dcc3a8df7ca004e74/validation.md#custom-validation-rules)
* [Laravel 공식 문서: 패키지의 Service Provider와 번역](https://github.com/laravel/docs/blob/10add0d2dade04cacfab436dcc3a8df7ca004e74/packages.md)
* [Factory: make・extend・extendImplicit・extendDependent・replacer](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Validation/Factory.php)
* [Validator: 실행 조건・확장 등록・클래스 해석](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Validation/Validator.php)
* [ValidationRuleParser: 문자열 규칙 분석](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Validation/ValidationRuleParser.php)
* [FormatsMessages: 메시지 우선순위와 치환](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Validation/Concerns/FormatsMessages.php)
* [ValidationServiceProvider: validator의 singleton 등록](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Validation/ValidationServiceProvider.php)
* [ServiceProvider: callAfterResolving](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Support/ServiceProvider.php#L310-L317)
* [공식 테스트: Factory에서 확장과 fallback 전달](https://github.com/laravel/framework/blob/v13.40.0/tests/Validation/ValidationFactoryTest.php)
* [공식 테스트: 문자열・클래스・Implicit・Dependent 규칙](https://github.com/laravel/framework/blob/v13.40.0/tests/Validation/ValidationValidatorTest.php#L7372-L7471)


## Related topics

- [패키지의 서비스 해석 후 훅](https://kawax.biz/ko/advanced/package-service-resolution.md)
- [패키지의 Blade 확장과 컴파일 캐시](https://kawax.biz/ko/advanced/package-blade-extensions.md)
- [패키지의 커스텀 세션 드라이버](https://kawax.biz/ko/advanced/package-session-drivers.md)
- [패키지의 동적 관계 등록](https://kawax.biz/ko/advanced/package-dynamic-relations.md)
- [커스텀 검증 규칙](https://kawax.biz/ko/advanced/custom-validation-rules.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.