آشنایی با Attribute در PHP 8 – به زبان ساده


در نسخه هشتم از زبان برنامهنویسی PHP امکان استفاده از خصوصیتها (Attributes) فراهم آمده است. هدف این خصوصیتها که در بسیاری از زبانهای دیگر به نام «حاشیهنویسی» (Annotations) نیز شناخته میشوند، آن است که دادههای متا را به کلاسها، متدها، متغیرها و موارد مشابه به روش ساختیافته اضافه کنیم. ما در این مقاله با مفهوم و کاربرد Attribute در PHP 8 آشنا خواهیم شد.
مفهوم خصوصیت به هیچ وجه جدید نیست و ما سالها است که از DocBlock برای شبیهسازی آن بهره میگیریم. با این حال با اضافه شدن خصوصیتها اکنون ابزاری اختصاصی برای بازنمایی این نوع از فرادادهها داریم و دیگر لازم نیست DocBlock-ها را به صورت دستی تحلیل کنیم.
اینک احتمالاً سوالهای جدیدی برای شما پیش آمده است. ظاهر خصوصیت چگونه است؟ چطور میتوان خصوصیت سفارشی ساخت؟ آیا هیچ نقطه ضعفی دارند؟ در ادامه این مقاله با عنوان آشنایی با Attribute در PHP 8 به همه این پرسشها پاسخ خواهیم داد.
آشنایی مقدماتی با مفهوم Attribute در PHP 8
قبل از هر چیز باید بدانید که ظاهر کلی یک خصوصیت به شکل زیر است:
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}
در ادامه این مقاله مثالهای دیگری را معرفی خواهیم کرد، اما این مثال از «مشترک رویداد» (Event Subscriber)، مثالی خوبی برای تشریح استفاده اصلی خصوصیتها محسوب میشود.
احتمالاً این ساختاری نیست که شما دوست داشتید یا امیدوار بودید ببینید. شاید استفاده از @ یا @: یا docblocks یا موارد دیگر را بیشتر ترجیح میدادید، اما به هر حال ساختار آن در PHP این گونه نیست. تنها نکتهای که در خصوص ساختار، ارزش اشاره را دارد، این است که همه گزینهها مورد بررسی قرار گرفتهاند و دلایل خوبی برای انتخاب این ساختار وجود دارد. برای مشاهده این مباحث و استدلالها میتوانید به این صفحه (+) مراجعه کنید.
طرز کار داخلی
اینک نوبت آن رسیده که روی موارد جالبی مانند طرز کار داخلی ListensTo تمرکز کنیم. قبل از هر چیز باید اشاره کنیم که خصوصیتهای سفارشی، کلاسهای سادهای هستند که خود را با خصوصیت [Attribute]# حاشیهنویسی کردهاند. این [Attribute]# مبنا در مستندات معمولاً PhpAttribute نامیده میشد، اما در ادامه عنوان آن تغییر یافت. ظاهر آن چنین است:
1#[Attribute]
2class ListensTo
3{
4 public string $event;
5
6 public function __construct(string $event)
7 {
8 $this->event = $event;
9 }
10}
کافی است به خاطر بسپارید که هدف از خصوصیتها افزودن فراداده به کلاسها و متدها است و نه چیز دیگر. برای مثال نمیتوان و نباید از آنها برای اعتبارسنجی ورودی آرگومان استفاده کرد. به بیان دیگر شما نباید به پارامترهای ارسال شده به یک متد درون خصوصیتهایش دسترسی داشته باشید. قبلاً این کار امکانپذیر بود، اما اینک روش کار تغییر یافته تا از پیچیدگیهای غیرضروری اجتناب کنیم.
اگر به مثال مشترکان رویداد خود بازگردیم، متوجه میشویم که همچنان نیاز داریم که فراداده را بخوانیم و مشترکان خود را در جایی ثبت کنیم. اگر سابقه کار با لاراول را داشته باشید، در این موارد از یک «ارائهدهنده سرویس» (Service Provider) به عنوان محلی برای انجام این کار استفاده میکنید، اما استفاده از راهکارهای دیگر هم مشکلی ندارد. برای نمونه در کد زیر یک کد تکراری ملالآور را میبینید که صرفاً برای ارائه زمینهای آمده است:
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}
اینک نوبت آن رسیده که نگاهی به resolveListeners بیاندازیم که همه کارهای مهم در آن اتفاق میافتند:
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}
نکات مهم
چنان که میبینید خواندن فرادادهها به این روش در قیاس با تحلیل رشتههای docblock آسانتر است. با این حال دو نکته ظریف وجود دارد که باید به آنها توجه داشته باشیم.
نکته نخست این است که یک فراخوانی ()attribute->newInstance$ وجود دارد. این جا در عمل مکانی است که کلاس خصوصیت سفارشی «وهلهسازی» (instantiate) میشود. این متد پارامترهای لیست شده در تعریف خصوصیت در کلاس مشترک را دریافت کرده و آنها را به «سازنده» (constructor) ارسال میکند.
این بدان معنی است که از لحاظ فنی حتی نیاز به ساخت خصوصیت سفارشی نیز وجود ندارد. شما میتوانستید ()attribute->getArguments$ را به صورت مستقیم فراخوانی کنید. به علاوه وهلهسازی کلاس به آن معنی است که انعطافپذیری سازنده برای تحلیل هر چیزی که دوست دارید را به دست میآورید. در نهایت باید اشاره کنیم که همواره بهتر است خصوصیت را با استفاده از ()newInstance وهلهسازی کنیم.
نکته دوم که در این مقاله با عنوان آشنایی با Attribute در PHP 8 باید مورد اشاره قرار دهیم، استفاده از ReflectionMethod::getAttributes() است. این تابعی است که همه خصوصیتهای یک متد را بازگشت میدهد. شما میتوانید دو آرگومان را به آن ارسال کنید تا خروجیاش را فیلتر نمایید.
با این حال برای درک این فیلترینگ یک نکته دیگر را نیز باید در مورد خصوصیتها بدانید. شاید این نکته برای شما بدیهی باشد، اما اجازه بدهید اشاره گذرایی به آن داشته باشیم. امکان اضافه کردن چند خصوصیت به متد، کلاس، مشخصه یا ثابت وجود دارد. برای نمونه میتوانید به صورت زیر عمل کنید:
1#[
2 Route(Http::POST, '/products/create')
3 Autowire
4]
5class ProductsCreateController
6{
7 public function __invoke() { /* … */ }
8}
با توجه به این نکته، کاملاً روشن است که چرا ()Reflection*::getAttributes یک آرایه بازگشت میدهد. در بخش بعدی با روش فیلتر کردن خروجی آن آشنا میشویم.
فیلتر کردن خروجی
فرض کنید مشغول تحلیل مسیرهای کنترلر هستید و صرفاً به خصوصیت Route علاقه دارید. به این ترتیب میتوانید فقط آن کلاس را به عنوان یک فیلتر ارسال کنید:
1$attributes = $reflectionClass->getAttributes(Route::class);
پارامتر دوم شیوه اجرای فیلترینگ را تغییر میدهد. شما میتوانید ReflectionAttribute::IS_INSTANCEOF را ارسال کنید که همه خصوصیتهای پیادهسازی شده یک اینترفیس مفروض را بازگشت میدهد.
برای نمونه فرض کنید تعاریف کانتینر را تحلیل میکنید که روی چند خصوصیت متمرکز است. در این حالت باید کاری مانند زیر انجام دهید:
1$attributes = $reflectionClass->getAttributes(
2 ContainerAttribute::class,
3 ReflectionAttribute::IS_INSTANCEOF
4);
چنان که میبینید این یک میانبر زیبا است که در هسته مرکزی این زبان تعبیه شده است.
تئوری فنی
اکنون که ایدهای از چگونگی کارکرد عملی خصوصیتها یافتهاید، نوبت آن رسیده که کمی هم به مباحث تئوری Attribute در PHP 8 بپردازیم تا مطمئن شویم که مفهوم خصوصیتها را در PHP به طور کامل یاد گرفتهایم. قبل از هر چیز قبلاً به اجمال اشاره کردیم که خصوصیتها میتوانند در چند نقطه اضافه شوند. برای نمونه امکان افزودن آنها در کلاسها و کلاسهای بینام وجود دارد:
1#[ClassAttribute]
2class MyClass { /* … */ }
3
4$object = new #[ObjectAttribute] class () { /* … */ };
همچنین میتوانیم آنها را به مشخصهها و ثابتها اضافه کنیم:
1#[PropertyAttribute]
2public int $foo;
3
4#[ConstAttribute]
5public const BAR = 1;
برای اضافه کردن خصوصیت به متدها و تابعها نیز به روش زیر عمل میکنیم:
1#[MethodAttribute]
2public function doSomething(): void { /* … */ }
3
4#[FunctionAttribute]
5function foo() { /* … */ }
در مورد کلوژرها میتوانید از ساختار زیر کمک بگیرید:
1$closure = #[ClosureAttribute] fn() => /* … */;
و پارامترهای متدها و تابعها نیز به صورت زیر خواهد بود:
1function foo(#[ArgumentAttribute] $bar) { /* … */ }
توجه کنید که امکان اعلان خصوصیتها پیش و پس از docblock-ها وجود دارد:
1/** @return void */
2#[MethodAttribute]
3public function doSomething(): void { /* … */ }
همچنین باید اشاره کنیم که خصوصیتها میتوانند صفر، یک یا چند آرگومان داشته باشند که از سوی سازنده خصوصیت تعریف میشود:
1#[Listens(ProductCreatedEvent::class)]
2#[Autowire]
3#[Route(Http::POST, '/products/create')]
همانند پارامترهای مجاز که میتوان به یک خصوصیت ارسال کرد، قبلاً دیدیم که ثابتهای کلاس، نامهای ::class و نوعهای اسکالر نیز مجاز هستند. با این حال چند نکته دیگر نیز در این خصوص وجود دارند که باید مورد اشاره قرار دهیم. خصوصیتها تنها عبارتهای ثابت را به عنوان آرگومان ورودی میپذیرند.
این بدان معنی است که عبارتهای اسکالر و حتی شیفت بیتها (bit shifts) مجاز هستند. همچنین امکان استفاده از class::، ثابتها، آرایهها و باز کردن آرایهها، عبارتهای بولی و عملگر null coalescing وجود دارد.
1#[AttributeWithScalarExpression(1 + 1)]
2#[AttributeWithClassNameAndConstants(PDO::class, PHP_VERSION_ID)]
3#[AttributeWithClassConstant(Http::POST)]
4#[AttributeWithBitShift(4 >> 1, 4 << 1)]
1#[AttributeWithScalarExpression(1 + 1)]
2#[AttributeWithClassNameAndConstants(PDO::class, PHP_VERSION_ID)]
3#[AttributeWithClassConstant(Http::POST)]
4#[AttributeWithBitShift(4 >> 1, 4 << 1)]
پیکربندی خصوصیت
چنان که در بخش قبل این مقاله با عنوان آشنایی با Attribute در PHP 8 دیدیم، خصوصیتها را به صورت پیشفرض میتوان در مکانهای متعددی اضافه کرد. با این حال، امکان پیکربندی آنها به صورتی که تنها در مکانهای خاصی بتوانند استفاده شوند نیز وجود دارد. برای نمونه میتوانیم کاری کنیم که ClassAttribute تنها بتواند در کلاسها و نه جای دیگر استفاده شود. برای بهرهگیری از این رفتار باید یک فلگ به خصوصیت Attribute روی کلاس خصوصیت ارسال کنیم. به مثال زیر توجه کنید:
1#[Attribute(Attribute::TARGET_CLASS)]
2class ClassAttribute
3{
4}
استفاده از فلگهای زیر ممکن است:
Attribute::TARGET_CLASS Attribute::TARGET_FUNCTION Attribute::TARGET_METHOD Attribute::TARGET_PROPERTY Attribute::TARGET_CLASS_CONSTANT Attribute::TARGET_PARAMETER Attribute::TARGET_ALL
اینها فلگهای bitmask هستند و از این رو میتوانید آنها را با استفاده از یک عملگر OR باینری ترکیب کنید.
1#[Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_FUNCTION)]
2class ClassAttribute
3{
4}
فلگ پیکربندی بندی دیگر به بحث تکرارپذیری مربوط است. به طور پیشفرض یک خصوصیت نمیتواند دو بار مورد استفاده قرار گیرد، مگر این که به صورت صریح به صورت تکرارپذیر مشخص شده باشد. این کار به همان روش پیکربندی هدف با یک فلگ بیت انجام میپذیرد:
1#[Attribute(Attribute::IS_REPEATABLE)]
2class ClassAttribute
3{
4}
توجه کنید که همه این فلگها تنها زمانی که ()attribute->newInstance$ فراخوانی شود و نه قبلتر، اعتبارسنجی میشوند.
سخن پایانی
زمانی که قابلیت خصوصیت به صورت ابتدایی به این زبان اضافه شد، زمینه افزودن خصوصیتهای داخلی دیگر نیز به آن مهیا شد. برای نمونه یکی از این موارد خصوصیت [Deprecated]# که مثال رایجی از آن خصوصیت [Jit]# است. ما در آینده شاهد خصوصیتهای داخلی بیشتری خواهیم بود.
به عنوان نکته نهایی در این مقاله آشنایی با Attribute در PHP 8 باید اشاره کنیم که اگر در مورد ژنریکها نگران هستید، ذکر این نکته ضروری است که این ساختار تداخلی با آنها نخواهد داشت، هر چند هنوز ساختار ژنریک به زبان PHP اضافه نشده است.