PHP Reflection API란
PHP Reflection API는, 클래스·메서드·프로퍼티·함수·파라미터 등의 메타 정보를 실행 시에 취득·검사할 수 있는 PHP의 내장 기능입니다. 클래스의 컨스트럭터가 무엇을 인수로 취하는지, 메서드에 어떤 어트리뷰트가 붙어 있는지 등을, 소스 코드를 직접 다시 쓰지 않고 조사할 수 있습니다.
Laravel은 Illuminate/Container/Container.php의 내부에서 Reflection API를 다용하고 있으며, DI 컨테이너의 자동 해결·PHP 어트리뷰트의 읽어들이기·메서드 인젝션 등을 실현하고 있습니다.
주요 클래스
클래스 주요 용도 ReflectionClass클래스의 메타 정보를 취득하는 기점 ReflectionMethod메서드의 인수·접근 수식자·어트리뷰트를 취득 ReflectionProperty프로퍼티의 타입·기본값·어트리뷰트를 취득 ReflectionParameter메서드·함수의 인수 정보(타입 힌트·기본값)를 취득 ReflectionFunction함수·클로저의 메타 정보를 취득 ReflectionAttribute어트리뷰트의 클래스명·인수를 취득
ReflectionClass — 클래스 정보를 취득한다
$ref = new ReflectionClass ( UserController :: class );
$ref -> getName (); // クラスの完全修飾名
$ref -> getShortName (); // クラス名のみ
$ref -> isInstantiable (); // インスタンス化できるか
$ref -> getConstructor (); // コンストラクタを ReflectionMethod で返す
$ref -> getMethods (); // すべてのメソッドを ReflectionMethod[] で返す
$ref -> getProperties (); // すべてのプロパティを ReflectionProperty[] で返す
$ref -> getAttributes (); // クラスに付いたアトリビュートを ReflectionAttribute[] で返す
ReflectionParameter — 컨스트럭터 인수를 검사한다
$ref = new ReflectionClass ( UserController :: class );
$constructor = $ref -> getConstructor ();
if ( $constructor ) {
foreach ( $constructor -> getParameters () as $param ) {
$param -> getName (); // 引数名
$param -> getType (); // 型ヒント(ReflectionType)
$param -> isOptional (); // 省略可能かどうか
$param -> isVariadic (); // 可変長引数かどうか
$param -> getDefaultValue (); // デフォルト値(存在する場合)
}
}
Laravel 컨테이너와 Reflection API
Laravel의 IoC 컨테이너는, Reflection API를 사용해 컨스트럭터 인젝션(의존성의 자동 해결)을 실현하고 있습니다. app()->make(SomeClass::class)나 의존성 주입이 작동하는 구조를 이해합시다.
컨테이너의 build() 메서드(간략판)
실제의 Container.php의 build() 메서드는 대략 다음과 같은 코드입니다.
// Illuminate\Container\Container::build() を簡略化
public function build ( $concrete )
{
// 1. ReflectionClass でクラスを検査
$reflector = new ReflectionClass ( $concrete );
// インスタンス化できないクラス(interface, abstract など)はエラー
if ( ! $reflector -> isInstantiable ()) {
throw new BindingResolutionException ( "[ $concrete ] is not instantiable." );
}
// 2. コンストラクタを取得
$constructor = $reflector -> getConstructor ();
// コンストラクタなし → そのままインスタンス化
if ( is_null ( $constructor )) {
return new $concrete ;
}
// 3. コンストラクタのパラメーターをすべて取得
$dependencies = $constructor -> getParameters ();
// 4. 各パラメーターを再帰的に解決
$instances = $this -> resolveDependencies ( $dependencies );
return new $concrete ( ... $instances );
}
protected function resolveDependencies ( array $dependencies ) : array
{
$results = [];
foreach ( $dependencies as $dependency ) {
// 型ヒントが取れる場合はコンテナで再帰解決
$className = Util :: getParameterClassName ( $dependency );
$results [] = is_null ( $className )
? $this -> resolvePrimitive ( $dependency ) // プリミティブ型
: $this -> resolveClass ( $dependency , $className ); // クラス型
}
return $results ;
}
Util::getParameterClassName()은 $parameter->getType()의 결과에서 타입명의 문자열을 꺼내는 유틸리티입니다. ReflectionParameter::getType()이 반환하는 ReflectionNamedType을 다루기 쉽게 래핑하고 있습니다.
PHP 어트리뷰트의 읽어들이기
PHP 8.0 이후, Reflection API를 사용해 클래스·메서드·프로퍼티에 붙은 어트리뷰트를 취득할 수 있습니다. Laravel은 이 구조를 사용해 큐 어트리뷰트나 Eloquent 어트리뷰트를 처리하고 있습니다.
PHP 어트리뷰트와 Laravel에의 짜넣기에 대해서는 PHP 어트리뷰트 도 함께 참조해 주세요.
어트리뷰트를 읽어들이는 기본 패턴
use ReflectionClass ;
// 1. クラスに付いたアトリビュートを取得
$ref = new ReflectionClass ( ProcessOrder :: class );
$attrs = $ref -> getAttributes ( Queue :: class ); // 特定のアトリビュートだけ取得
foreach ( $attrs as $attr ) {
$instance = $attr -> newInstance (); // アトリビュートクラスをインスタンス化
echo $instance -> queue ; // アトリビュートのプロパティを読む
}
// 2. すべてのアトリビュートを取得(フィルターなし)
$allAttrs = $ref -> getAttributes ();
foreach ( $allAttrs as $attr ) {
echo $attr -> getName (); // アトリビュートクラス名(FQCN)
print_r ( $attr -> getArguments ()); // コンストラクタ引数
}
Laravel이 Queue 속성을 읽어들이는 구조(간략판)
// InteractsWithQueue トレイトの ReadsQueueAttributes より簡略化
protected function setJobInstanceForQueue ( object $job ) : void
{
$reflection = new ReflectionClass ( $job );
foreach ( $reflection -> getAttributes ( Queue :: class ) as $attribute ) {
$instance = $attribute -> newInstance ();
$job -> queue = $instance -> queue instanceof \ UnitEnum
? $instance -> queue -> value
: $instance -> queue ;
}
}
메서드의 어트리뷰트를 읽어들인다
$ref = new ReflectionClass ( UserController :: class );
foreach ( $ref -> getMethods () as $method ) {
$attrs = $method -> getAttributes ( Route :: class );
foreach ( $attrs as $attr ) {
$route = $attr -> newInstance ();
echo "{ $method -> getName ()} => { $route -> path }" ;
}
}
패키지 개발에서의 활용 예
클래스의 구현 인터페이스를 검사한다
패키지에서 클래스가 특정 인터페이스를 구현하고 있는지를 동적으로 확인할 수 있습니다.
use ReflectionClass ;
function isQueueable ( string $class ) : bool
{
$ref = new ReflectionClass ( $class );
return $ref -> implementsInterface ( \Illuminate\Contracts\Queue\ ShouldQueue :: class );
}
메타데이터 취득 — 어트리뷰트로 라우트를 자동 등록한다
어트리뷰트와 Reflection을 조합해 라우트를 자동 수집하는 패턴입니다.
// 独自のRouteアトリビュート定義
#[ \Attribute ( \Attribute :: TARGET_METHOD )]
class Route
{
public function __construct (
public string $method ,
public string $path ,
) {}
}
// コントローラーで使う
class UserController
{
#[ Route ( 'GET' , '/users' )]
public function index () { /* ... */ }
#[ Route ( 'POST' , '/users' )]
public function store () { /* ... */ }
}
// アトリビュートからルートを自動登録するサービスプロバイダー
class AttributeRouteServiceProvider extends ServiceProvider
{
public function boot ( Router $router ) : void
{
$ref = new ReflectionClass ( UserController :: class );
foreach ( $ref -> getMethods ( \ReflectionMethod :: IS_PUBLIC ) as $method ) {
foreach ( $method -> getAttributes ( Route :: class ) as $attr ) {
$route = $attr -> newInstance ();
$router -> addRoute (
$route -> method ,
$route -> path ,
[ UserController :: class , $method -> getName ()],
);
}
}
}
}
동적인 메서드 호출 — 메서드 인젝션
Laravel의 call() 메서드는 Reflection을 사용해 인수를 자동 해결합니다. 같은 구조를 패키지에서 구현하는 예입니다.
use ReflectionFunction ;
use ReflectionMethod ;
function callWithDependencies ( callable $callable , Container $container ) : mixed
{
if ( is_array ( $callable )) {
[ $object , $method ] = $callable ;
$ref = new ReflectionMethod ( $object , $method );
$params = $ref -> getParameters ();
} else {
$ref = new ReflectionFunction ( $callable );
$params = $ref -> getParameters ();
}
$args = [];
foreach ( $params as $param ) {
$type = $param -> getType () ?-> getName ();
$args [] = $type ? $container -> make ( $type ) : null ;
}
return $callable ( ... $args );
}
// 使用例
callWithDependencies ([ new UserController (), 'index' ], app ());
프로퍼티의 기본값을 수집한다
설정 클래스의 기본값을 Reflection으로 취득하는 패턴입니다.
use ReflectionClass ;
use ReflectionProperty ;
function getDefaults ( string $class ) : array
{
$ref = new ReflectionClass ( $class );
$defaults = [];
foreach ( $ref -> getProperties ( ReflectionProperty :: IS_PUBLIC ) as $prop ) {
if ( $prop -> hasDefaultValue ()) {
$defaults [ $prop -> getName ()] = $prop -> getDefaultValue ();
}
}
return $defaults ;
}
class DatabaseConfig
{
public string $driver = 'mysql' ;
public int $port = 3306 ;
public bool $strict = true ;
}
// ['driver' => 'mysql', 'port' => 3306, 'strict' => true]
$defaults = getDefaults ( DatabaseConfig :: class );
퍼포먼스에의 배려
Reflection API는 클래스 정보를 매번 파스하기 때문에 비용이 듭니다. 프로덕션 코드에서는 결과를 캐시하는 것이 베스트 프랙티스입니다.
class ReflectionCache
{
private static array $cache = [];
public static function getClass ( string $class ) : \ReflectionClass
{
return self :: $cache [ $class ] ??= new \ReflectionClass ( $class );
}
}
// 使用例
$ref = ReflectionCache :: getClass ( UserController :: class );
PHP의 OPcache는 Reflection의 결과를 캐시하지 않습니다. 대량의 클래스를 루프로 검사하는 경우에는 독자 캐시를 검토해 주세요. 다만, Laravel 컨테이너 자체도 동일 요청 내에서는 ReflectionClass 인스턴스를 재이용하고 있습니다.
다음 단계
PHP 어트리뷰트 ReflectionClass::getAttributes()를 사용해 읽어들이는 PHP 어트리뷰트의 상세를 배웁니다.
패키지 개발 Reflection API를 활용한 Laravel 패키지의 개발 방법을 배웁니다.