php8 에 도입되는 attributes
php 8 부터는 attrubutes 라는 기능을 사용할 수 있습니다. 다른 많은 언어에서는 annontation 이라고 불리는 것이죠.
(이미 php8 소개하는 많은 블로그들이 있었서 늦은감이 있네요...)
일단 여기서는 어떻게 사용하는지, 또 어떻게 커스텀 attributes 를 만드는지 등에 대해서 다뤄 보겠습니다.
개요
먼저 attribute 가 작성된 예제를 올려봅니다.
php1use \Support\Attributes\ListensTo;23class ProductSubscriber4{5 #[ListensTo(ProductCreated::class)]6 public function onProductCreated(ProductCreated $event) { /* … */ }78 #[ListensTo(ProductDeleted::class)]9 public function onProductDeleted(ProductDeleted $event) { /* … */ }10}
뒷부분에 실제 사용하는 다른 코드를 보여드리겠습니다만. 이 코드가 attirbutes 를 가장 설명하기 좋은 예라고 생각합니다.
custom attribute 는 다음과 같이 작성됩니다.
php1#[Attribute]2class ListensTo3{4 public string $event;56 public function __construct(string $event)7 {8 $this->event = $event;9 }10}
간단하죠? custom attribute 를 작성하신다면 목표를 한정해서 작성하세요. attribute 는 클래스와 메소드에 메타데이터를 추가하기 위한 것이며, 그 이상은 아닙니다.
예를 들어 parameter 입력 유효성 검사에 사용할 수 없습니다.
즉 attribute 내에서 메소드에 전달된 parameter 에는 접근할 수 없습니다.
원래는 이 동작을 허용하는 이전 RFC 가 있었지만 이후 논의를 통해 제외 되었습니다.
아까 event subscriber 예제를 다시 보자면, 여전히 meta data 를 읽고 어딘가의 subscriber 들에게 전달하여야 합니다.
좀 더 내용을 추가하기 위해, 지루한 boilerplate 코드를 몇개 작성해봅시다.
php1class EventServiceProvider extends ServiceProvider2{3 // In real life scenarios,4 // we'd automatically resolve and cache all subscribers5 // instead of using a manual array.6 private array $subscribers = [7 ProductSubscriber::class,8 ];910 public function register(): void11 {12 // The event dispatcher is resolved from the container13 $eventDispatcher = $this->app->make(EventDispatcher::class);1415 foreach ($this->subscribers as $subscriber) {16 // We'll resolve all listeners registered17 // in the subscriber class,18 // and add them to the dispatcher.19 foreach (20 $this->resolveListeners($subscriber)21 as [$event, $listener]22 ) {23 $eventDispatcher->listen($event, $listener);24 }25 }26 }27}
이제 resolveListeners 를 한번 보실까요!
php1private function resolveListeners(string $subscriberClass): array2{3 $reflectionClass = new ReflectionClass($subscriberClass);45 $listeners = [];67 foreach ($reflectionClass->getMethods() as $method) {8 $attributes = $method->getAttributes(ListensTo::class);910 foreach ($attributes as $attribute) {11 $listener = $attribute->newInstance();1213 $listeners[] = [14 // The event that's configured on the attribute15 $listener->event,1617 // The listener for this event18 [$subscriberClass, $method->getName()],19 ];20 }21 }2223 return $listeners;24}
역주 : php 8 이전에는 annotation 을 문법적으로 지원하지 못해, php 소스코드의 주석을 string 으로 parsing 해서 구현하였습니다. ㅎㄷㄷ ReflectionProperty::getDocComment
좀 어려운 포인트가 두가지 있는데요, 한번 정리해보겠습니다.
이 필터링을 이해하려면 먼저 attributes 에 대해서 알아야 할 것이 한 가지 더 있습니다. method 뿐만 아니라 class, property 또는 constant에 여러 attribute 을 추가할 수 있다는 것입니다.
아래는 class 에 선언된 예제입니다.
php1#[Route(Http::POST, '/products/create'), Autowire,]2class ProductsCreateController3{4 public function __invoke() { /* … */ }5}
여기에서는 controller 의 route 를 parsing 하는것에 촛점을 맞추고 Route attribute 에 대해서만 관심을 가지도록 하겠습니다. 해당 클래스를 필터로 쉽게 전달 할 수 있습니다.
php1$attributes = $reflectionClass->getAttributes(Route::class);
두번째 parameter는 필터링이 수행되는 방식을 변경합니다. 지정된 인터페이스를 구현하는 모든 속성을 반환하도록 하는 ReflectionAttribute::IS_INSTANCEOF 를 전달할 수 있습니다.
예를 들어 여러 attribute 에 의존하는 컨테이너 정의를 구문을 parsing 한다고 가정한다면...
php1$attributes = $reflectionClass->getAttributes(2 ContainerAttribute::class,3 ReflectionAttribute::IS_INSTANCEOF4);
좀 더 풀어서 쉽게 설명드리자면... class 아래의 모든 하위 method, property, constants 의 attribute 를 가져올때 선언하면 됩니다.
php1$r_atts = $rc->getAttributes(SomeAttribute::class, 0); // 0 is default, just given class2echo json_encode(array_map(fn(ReflectionAttribute $r_att) => $r_att->getName(), $r_atts)), PHP_EOL;34$r_atts = $rc->getAttributes(SomeAttribute::class, 2); // given class and children classes5echo json_encode(array_map(fn(ReflectionAttribute $r_att) => $r_att->getName(), $r_atts)), PHP_EOL;
Real World
그렇다면 실제 활용할 수 있는 예제는 무엇일까요?
routes 라는 디렉토리의 web.php / api.php 에 연결할 컨트롤러를 등록하는 형식이었죠. 아래와 같이요.
php1Route::get('user/profile', [UserProfileController::class, 'show'])->name('profile');
php1use Spatie\RouteAttributes\Attributes\Get;23class UserProfileController4{5 #[Get('user/profile')]6 public function show()7 {89 }10}
설정 파일을 통하지 않고, 컨트롤러에 attribute 작성하는 것 만으로도 충분하게 되었죠.
이상입니다.
Ref.