php8 에 도입되는 annotation (attributes)
php8 에 도입되는 attributes
php 8 부터는 attrubutes 라는 기능을 사용할 수 있습니다. 다른 많은 언어에서는 annontation 이라고 불리는 것이죠.
(이미 php8 소개하는 많은 블로그들이 있었서 늦은감이 있네요…)
일단 여기서는 어떻게 사용하는지, 또 어떻게 커스텀 attributes 를 만드는지 등에 대해서 다뤄 보겠습니다.
개요
먼저 attribute 가 작성된 예제를 올려봅니다.
use \Support\Attributes\ListensTo;
class ProductSubscriber
{
#[ListensTo(ProductCreated::class)]
public function onProductCreated(ProductCreated $event) { /* … */ }
#[ListensTo(ProductDeleted::class)]
public function onProductDeleted(ProductDeleted $event) { /* … */ }
}
뒷부분에 실제 사용하는 다른 코드를 보여드리겠습니다만. 이 코드가 attirbutes 를 가장 설명하기 좋은 예라고 생각합니다.
이 문법이 여러분들이 기대했던 것이 아닐 수도 있습니다. 보통 @ 혹은 @: /* */ 같은 주석을 선호할 수 도 있습니다. 사실 이런 문법에 대한 논쟁들은 php rfc 에 대한 토론을 읽어보길 권장 드립니다. PHP RFC: Shorter Attribute Syntax
일단 코드에 집중 하도록 하겠습니다. 저 ListensTo 는 어떻게 작동할가요?
이제 보여 드릴 custom attributes 는 #[Attributes] 라는 attribute 가 선언된 간단한 클래스 입니다. 이 기본 attribute 는 원래 PhpAttribute 라고 RFC 에서 결정하였지만, 나중에 다른 토론을 통해 변경 되었습니다. PHP RFC: Attribute Amendments
custom attribute 는 다음과 같이 작성됩니다.
#[Attribute]
class ListensTo
{
public string $event;
public function __construct(string $event)
{
$this->event = $event;
}
}
간단하죠? custom attribute 를 작성하신다면 목표를 한정해서 작성하세요. attribute 는 클래스와 메소드에 메타데이터를 추가하기 위한 것이며, 그 이상은 아닙니다.
예를 들어 parameter 입력 유효성 검사에 사용할 수 없습니다.
즉 attribute 내에서 메소드에 전달된 parameter 에는 접근할 수 없습니다.
원래는 이 동작을 허용하는 이전 RFC 가 있었지만 이후 논의를 통해 제외 되었습니다.
아까 event subscriber 예제를 다시 보자면, 여전히 meta data 를 읽고 어딘가의 subscriber 들에게 전달하여야 합니다.
좀 더 내용을 추가하기 위해, 지루한 boilerplate 코드를 몇개 작성해봅시다.
class EventServiceProvider extends ServiceProvider
{
// In real life scenarios,
// we'd automatically resolve and cache all subscribers
// instead of using a manual array.
private array $subscribers = [
ProductSubscriber::class,
];
public function register(): void
{
// The event dispatcher is resolved from the container
$eventDispatcher = $this->app->make(EventDispatcher::class);
foreach ($this->subscribers as $subscriber) {
// We'll resolve all listeners registered
// in the subscriber class,
// and add them to the dispatcher.
foreach (
$this->resolveListeners($subscriber)
as [$event, $listener]
) {
$eventDispatcher->listen($event, $listener);
}
}
}
}
[$event, $listener] 문법이 친숙하지 않을 수 있겠지만, 코드 생산성을 높이기 문법입니다. (배열 구조 분해 / ES6 를 생각해보세요!)
이제 resolveListeners 를 한번 보실까요!
private function resolveListeners(string $subscriberClass): array
{
$reflectionClass = new ReflectionClass($subscriberClass);
$listeners = [];
foreach ($reflectionClass->getMethods() as $method) {
$attributes = $method->getAttributes(ListensTo::class);
foreach ($attributes as $attribute) {
$listener = $attribute->newInstance();
$listeners[] = [
// The event that's configured on the attribute
$listener->event,
// The listener for this event
[$subscriberClass, $method->getName()],
];
}
}
return $listeners;
}
ReflectionMethod::getAttributes() 를 통해 주석 문자열을 parsing 하는 것보다 메타 데이터를 쉽게 읽을 수 있는 것을 확인할 수 있습니다.
역주 : php 8 이전에는 annotation 을 문법적으로 지원하지 못해, php 소스코드의 주석을 string 으로 parsing 해서 구현하였습니다. ㅎㄷㄷ ReflectionProperty::getDocComment
좀 어려운 포인트가 두가지 있는데요, 한번 정리해보겠습니다.
먼저 $attribute->newInstance() 호출이 있습니다. 이 코드는 실제로 우리가 작성한 custom attribute 를 인스턴스화되는 장소입니다. 우리가 작성한 subscriber 클래스의 attribute 에 나열된 매개 변수를 사용하여 생성자에 전달합니다.
즉, 기술적으로는 custom attribute 에 매개 변수를 전달할 필요가 없습니다. 물론 $attribute->getArguments() 를 직접 호출할 수 있습니다. 또한 클래스를 인스턴스화한다는 것은 원하는 방식으로 구문 분석 입력을 생성할 수 있다는 것을 의미합니다. 대체로 **newInstance()**를 사용하여 속성을 인스턴스화하는 것이 좋습니다.
두번째로 ReflectionMethod::getAttributes() 를 이용한 메쏘드의 모든 attributes 를 반환하는 함수의 사용입니다. 두가지 parameter 를 사용하여 반환값 필터링할 수 있습니다. 자세한건 아래 문서를 확인해주세요. ReflectionFunctionAbstract::getAttributes
이 필터링을 이해하려면 먼저 attributes 에 대해서 알아야 할 것이 한 가지 더 있습니다. method 뿐만 아니라 class, property 또는 constant에 여러 attribute 을 추가할 수 있다는 것입니다.
아래는 class 에 선언된 예제입니다.
#[Route(Http::POST, '/products/create'), Autowire,]
class ProductsCreateController
{
public function __invoke() { /* … */ }
}
이를 염두에 두시고 Reflection::getAttributes() 가 배열을 반환하는 이유가 명확하므로 결과를 필터링하는 방법을 살펴보겠습니다.
여기에서는 controller 의 route 를 parsing 하는것에 촛점을 맞추고 Route attribute 에 대해서만 관심을 가지도록 하겠습니다. 해당 클래스를 필터로 쉽게 전달 할 수 있습니다.
$attributes = $reflectionClass->getAttributes(Route::class);
두번째 parameter는 필터링이 수행되는 방식을 변경합니다. 지정된 인터페이스를 구현하는 모든 속성을 반환하도록 하는 ReflectionAttribute::IS_INSTANCEOF 를 전달할 수 있습니다.
예를 들어 여러 attribute 에 의존하는 컨테이너 정의를 구문을 parsing 한다고 가정한다면…
$attributes = $reflectionClass->getAttributes(
ContainerAttribute::class,
ReflectionAttribute::IS_INSTANCEOF
);
좀 더 풀어서 쉽게 설명드리자면… class 아래의 모든 하위 method, property, constants 의 attribute 를 가져올때 선언하면 됩니다.
$r_atts = $rc->getAttributes(SomeAttribute::class, 0); // 0 is default, just given class
echo json_encode(array_map(fn(ReflectionAttribute $r_att) => $r_att->getName(), $r_atts)), PHP_EOL;
$r_atts = $rc->getAttributes(SomeAttribute::class, 2); // given class and children classes
echo json_encode(array_map(fn(ReflectionAttribute $r_att) => $r_att->getName(), $r_atts)), PHP_EOL;
Real World
그렇다면 실제 활용할 수 있는 예제는 무엇일까요?
laravel 을 예를 들면, php7 까지의 laravel 에서 route 를 등록하기 위해 route 에 대한 설정값이 존재하였습니다. 라우팅
routes 라는 디렉토리의 web.php / api.php 에 연결할 컨트롤러를 등록하는 형식이었죠. 아래와 같이요.
Route::get('user/profile', [UserProfileController::class, 'show'])->name('profile');
하지만 php attributes 에 대한 도입으로 이런 설정 파일을 따로 작성할 필요가 없어졌습니다. laravel-route-attributes
use Spatie\RouteAttributes\Attributes\Get;
class UserProfileController
{
#[Get('user/profile')]
public function show()
{
}
}
설정 파일을 통하지 않고, 컨트롤러에 attribute 작성하는 것 만으로도 충분하게 되었죠.
이상입니다.
Ref.
관련 태그 글
Phaser 4 Pseudo 3D 레이싱 게임 - 핸들링 감각을 로그로 튜닝하기
Apex Seoul의 주행 감각을 OutRun식 grip 기준으로 다시 보고, 고정 화면 비율, WebGL 전환, 속도 표시 보정, shader speed cue, 자동 handling simulation으로 핸들링 후보를 반복 비교했습니다.
Phaser 4 Pseudo 3D 레이싱 게임 - 차량 스프라이트를 도로에 붙이기
Apex Seoul의 approved 128px 차량 스프라이트를 런타임에 연결하고, 조향 3way, 고저차 접지감, 주행 telemetry, silhouette shadow pass로 실제 게임 화면에서 검증했습니다.
Phaser 4 Pseudo 3D 레이싱 게임 — 실제 차량 3D 모델로 스프라이트 POC 하기
Apex Seoul 차량 스프라이트 품질을 올리기 위해 GT86, Kia Stinger, Genesis G70 3D 모델을 source로 가져와 실제 전장 기준으로 정렬하고, 후처리 자동화 방향을 다시 잡았습니다.
Phaser 4 Pseudo 3D 레이싱 게임 — GPT 이미지 생성으로 차량 스프라이트 만들기
Apex Seoul의 Raven Coupe 차량을 3D pose sheet, GPT 이미지 변환, alpha cleanup, anchor metadata 후처리까지 이어지는 게임 자산 파이프라인으로 제작했습니다.