Yeppyshiba Blog

About
coding

php8 에 도입되는 annotation (attributes)

12/1/2021

devcodingphpannotation

php8 에 도입되는 attributes

php 8 부터는 attrubutes 라는 기능을 사용할 수 있습니다. 다른 많은 언어에서는 annontation 이라고 불리는 것이죠.

(이미 php8 소개하는 많은 블로그들이 있었서 늦은감이 있네요...)

일단 여기서는 어떻게 사용하는지, 또 어떻게 커스텀 attributes 를 만드는지 등에 대해서 다뤄 보겠습니다.

개요

먼저 attribute 가 작성된 예제를 올려봅니다.

php
1use \Support\Attributes\ListensTo;
2
3class ProductSubscriber
4{
5 #[ListensTo(ProductCreated::class)]
6 public function onProductCreated(ProductCreated $event) { /* … */ }
7
8 #[ListensTo(ProductDeleted::class)]
9 public function onProductDeleted(ProductDeleted $event) { /* … */ }
10}

뒷부분에 실제 사용하는 다른 코드를 보여드리겠습니다만. 이 코드가 attirbutes 를 가장 설명하기 좋은 예라고 생각합니다.

이 문법이 여러분들이 기대했던 것이 아닐 수도 있습니다. 보통 @ 혹은 @: /* */ 같은 주석을 선호할 수 도 있습니다. 사실 이런 문법에 대한 논쟁들은 php rfc 에 대한 토론을 읽어보길 권장 드립니다. PHP RFC: Shorter Attribute Syntax
일단 코드에 집중 하도록 하겠습니다. 저 ListensTo 는 어떻게 작동할가요?
이제 보여 드릴 custom attributes 는 #[Attributes] 라는 attribute 가 선언된 간단한 클래스 입니다. 이 기본 attribute 는 원래 PhpAttribute 라고 RFC 에서 결정하였지만, 나중에 다른 토론을 통해 변경 되었습니다. PHP RFC: Attribute Amendments

custom attribute 는 다음과 같이 작성됩니다.

php
1#[Attribute]
2class ListensTo
3{
4 public string $event;
5
6 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 코드를 몇개 작성해봅시다.

php
1class EventServiceProvider extends ServiceProvider
2{
3 // In real life scenarios,
4 // we'd automatically resolve and cache all subscribers
5 // instead of using a manual array.
6 private array $subscribers = [
7 ProductSubscriber::class,
8 ];
9
10 public function register(): void
11 {
12 // The event dispatcher is resolved from the container
13 $eventDispatcher = $this->app->make(EventDispatcher::class);
14
15 foreach ($this->subscribers as $subscriber) {
16 // We'll resolve all listeners registered
17 // 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}
[$event, $listener] 문법이 친숙하지 않을 수 있겠지만, 코드 생산성을 높이기 문법입니다. (배열 구조 분해 / ES6 를 생각해보세요!)

이제 resolveListeners 를 한번 보실까요!

php
1private function resolveListeners(string $subscriberClass): array
2{
3 $reflectionClass = new ReflectionClass($subscriberClass);
4
5 $listeners = [];
6
7 foreach ($reflectionClass->getMethods() as $method) {
8 $attributes = $method->getAttributes(ListensTo::class);
9
10 foreach ($attributes as $attribute) {
11 $listener = $attribute->newInstance();
12
13 $listeners[] = [
14 // The event that's configured on the attribute
15 $listener->event,
16
17 // The listener for this event
18 [$subscriberClass, $method->getName()],
19 ];
20 }
21 }
22
23 return $listeners;
24}
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 에 선언된 예제입니다.

php
1#[Route(Http::POST, '/products/create'), Autowire,]
2class ProductsCreateController
3{
4 public function __invoke() { /* … */ }
5}
이를 염두에 두시고 Reflection::getAttributes() 가 배열을 반환하는 이유가 명확하므로 결과를 필터링하는 방법을 살펴보겠습니다.

여기에서는 controller 의 route 를 parsing 하는것에 촛점을 맞추고 Route attribute 에 대해서만 관심을 가지도록 하겠습니다. 해당 클래스를 필터로 쉽게 전달 할 수 있습니다.

php
1$attributes = $reflectionClass->getAttributes(Route::class);

두번째 parameter는 필터링이 수행되는 방식을 변경합니다. 지정된 인터페이스를 구현하는 모든 속성을 반환하도록 하는 ReflectionAttribute::IS_INSTANCEOF 를 전달할 수 있습니다.

예를 들어 여러 attribute 에 의존하는 컨테이너 정의를 구문을 parsing 한다고 가정한다면...

php
1$attributes = $reflectionClass->getAttributes(
2 ContainerAttribute::class,
3 ReflectionAttribute::IS_INSTANCEOF
4);

좀 더 풀어서 쉽게 설명드리자면... class 아래의 모든 하위 method, property, constants 의 attribute 를 가져올때 선언하면 됩니다.

php
1$r_atts = $rc->getAttributes(SomeAttribute::class, 0); // 0 is default, just given class
2echo json_encode(array_map(fn(ReflectionAttribute $r_att) => $r_att->getName(), $r_atts)), PHP_EOL;
3
4$r_atts = $rc->getAttributes(SomeAttribute::class, 2); // given class and children classes
5echo 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 에 연결할 컨트롤러를 등록하는 형식이었죠. 아래와 같이요.

php
1Route::get('user/profile', [UserProfileController::class, 'show'])->name('profile');
하지만 php attributes 에 대한 도입으로 이런 설정 파일을 따로 작성할 필요가 없어졌습니다. laravel-route-attributes
php
1use Spatie\RouteAttributes\Attributes\Get;
2
3class UserProfileController
4{
5 #[Get('user/profile')]
6 public function show()
7 {
8
9 }
10}

설정 파일을 통하지 않고, 컨트롤러에 attribute 작성하는 것 만으로도 충분하게 되었죠.

이상입니다.

Ref.

Relate
Stories

svn 사용자를 위한 git 안내서

svn 사용자를 위한 git 안내서

coding
2 years ago, 6 Views

여태까지 많은 git 안내서가 있지만, 이번에는 subversion 사용자를 위한 타겟으로 글을 작성해보겠습니다.

Read more
각 레벨의 개발자에 기대하는 것들

각 레벨의 개발자에 기대하는 것들

coding
2 years ago, 30 Views

현업에 오래 있다보면, 가끔씩 나의 위치와 회사 (혹은 주변에서) 바라는 것들이 무엇인지 또한 다른 조직이나 혹은 더 넘어가서 다른 나라의 개발자들은 어떤 위치에 있는지 물어보게 됩니다.

© 2022-2023 Yeppyshiba Blog. All rights reserved.

Akita inu icons created by tulpahn - Flaticon