Attribute ها در php8

ترجمه و تالیف : فاطمه شیرزادفر
تاریخ انتشار : 15 مرداد 99
خواندن در 4 دقیقه
دسته بندی ها : پی اچ پی

از php8 ،ما قادر به استفاده از Attribute ها شده‌ایم. هدف از این Attribute ها، که در بسیاری از زبان‌های دیگر به عنوان annotations یا حاشیه‌نویس نیز شناخته می‌شود؛ اضافه کردن متا دیتا به کلاس‌ها، متدها، متغییرها و … به روش ساختاری است.

مفهوم attributes اصلاً چیز جدیدی نیست، ما سال‌هاست که از docblocks برای شبیه سازی رفتار آن‌ها استفاده می‌کنیم. اگر چه با اضافه کردن attribute ها ، اکنون یک تابع  فرست کلاس در این زبان برای نمایش این نوع متا دیتا داریم، به جای اینکه بخواهیم docblock ها را به صورت دستی parse یا تجزیه کنیم.

خب حالا آن‌ها چه شکلی هستند؟ چگونه می‌توانیم attribute های سفارشی بسازیم؟ آیا هشدار و اخطاری وجود دارد؟ این‌ها سؤالاتی هستند که در این مقاله از راکت به آن‌ها خواهیم پرداخت. پس بیایین شروع کنیم!

Rundown

اول از همه ، در این‌جا چقدر attribute عالی به نظر می‌رسد.

use \Support\Attributes\ListensTo;

class ProductSubscriber
{
    <<ListensTo(ProductCreated::class)>>
    public function onProductCreated(ProductCreated $event) { /* … */ }

    <<ListensTo(ProductDeleted::class)>>
    public function onProductDeleted(ProductDeleted $event) { /* … */ }
}

بعداً در این مقاله نمونه‌های دیگری را به شما نشان خواهم داد، اما فکر می‌کنم در ابتدا مثالی از event subscribers نمونه خوبی برای شرح موارد استفاده از attribute ها باشد.

همچنین می‌دانم که شاید این سینتکس ترجیح شما نباشد و انتظار استفاده از  @ یا @: یا docblock ها و … را داشته باشید، اما بهتر است یادبگیرید که با آن کنار بیایید چراکه اینجا از این سینتکس استفاده خواهیم کرد. تنها چیزی که در سینتکس ارزش گفتن را دارد این است که همه‌ی گزینه‌ها مورد بحث قرار گرفته‌اند و این‌ها دلایل بسیار خوبی برای انتخاب این سینتکس بوده است. شما می‌تواندی خلاصه کوتاهی درباره آن در RFC را بخوانید؛ یا اصلامی‌توانید کل بحث درباره RFC را در اینجا بخوانید.

اول از همه، اجازه دهید روی چیزهای جالب تمرکز کنیم: این ListensTo چگونه می‌توانند به صورت مخفی و پنهانی کار کند؟

اول از همعه، attributes های سفارشی کلاس‌های ساده‌ای هستند، که خود را با ویژگی<<PhpAttribute>>  حاشیه نویسی یا annotated می‌کند.

اینجا را نگاه کنید، چیزی که به نظر می‌رسد این است:

<<Attribute>>
class ListensTo
{
    public string $event;

    public function __construct(string $event)
    {
        $this->event = $event;
    }
}

این خیلی ساده‌ است نه؟ هدف attribute ها را در نظر داشته باشید:هدف آن‌ها اضافه کردن متا دیتا به کلاس‌ها و متدهاست، نه بیشتر. آن‌ها نباید و نمی‌توانند برای مثال برای اعتبار سنجی آرگون‌های ورودی، مورد استفاده قرار بگیرند؛ به عبارت دیگر : شما نمی‌توانید به پارامترهایی که به متد داخل attributes آن passed شده است دسترسی داشته باشید.

RFC قبلی وجود داشت که اجازه این رفتار را می‌داد، اما این RFC به طور خاص همه چیز را ساده‌تر نگه می‌داشت.

برمی‌گردیم به مثال event subscriber : ما هنوز نیاز داریم که متا دیتاها را بخوانیم وsubscriber ها یا مشترکان خودمان را بر اساس جایی ثبت‌نام کنیم.  با استفاده از بک‌گراند لاراول، من می‌توانم از یک service provider به عنوان محلی برای انجام این کار استفاده کنم، ولی می‌توانید با راه‌حل‌هایی که احساس آزادی می‌کنید کار کنید.

در اینجا تنظیمات خسته‌کننده‌ی boilerplate ،‌ فقط برای ارائه کمی context :

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] برای شما ناآشناست، می‌توانید با این پست که درباره array destructuring هست، آن را با سرعت بیشتری یادبگیرید.

حالا بیایید به 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;
}

می‌توانید خواندن متا دیتاها را به این روش آسان‌تر ببینید،  در مقایسه با ترجمه یا parse کردن docblock string ها، دو پیچدگی وجود دارد که ارزش جستجو در آن‌ها وجود دارد.

ابتدا فراخوانی $attribute→newInstance() را انجام دهید. این درواقع مکانی است که کلاس attribute سفارشی شده ما شبیه‌سازی می‌شود. این، پارامترهای لیست شده در attribute تعریف شده که در کلاس subscriber ماست و به constructor منتقل یا pass شده را می‌گیرد.

این بدان معنی است که، از نظر فنی، شما حتی نیازی به ساخت attribute سفارش ندارید. شما می‌توانید مستقیماً $attribute->getArguments() را فراخوانی کنید. علاوه بر این، نمونه سازی کلاس‌ها به این معنی است که شما از انعطاف‌پذیری constructor ورودی ترجمه شده به هر روشی که دوست دارید برخوردار هستید. در کل می‌خواهم بگویم که خوب است همیشه attribute را با استفاده از newInstance() شبیه‌سازی کنید.

مورد دوم که باید به آن اشاره کنیم، استفاده از ReflectionMethod::getAttributes() است، این فانکشن همه‌ی attribute ها را برای متد بازمی‌گرداند. شما می‌توانید برای فیلتر کردن خروجی آن، دو آرگومان را برای آن ارسال کنید.

برای درک این فیلتر، ابتدا چیز دیگری وجود دارد که باید در مورد attribute ها بدانید؛ این ممکن است برای شما واضح باشد، اما به هر حال می‌خواهم سریع این را برای شما بگویم: این ممکن است که چندین attribute را به همان متد، کلاس،property یا constant اضافه کند.

برای مثال می‌توانید این کار را انجام دهید:

<<Route(Http::POST, '/products/create')>>
<<Autowire>>
class ProductsCreateController
{
    public function __invoke() { /* … */ }
}

با توجه به این نکته، روشن است که چرا Reflection*::getAttributes() آرایه‌ای را برمی‌گرداند، بنابراین بیایید ببینیم که چگونه می‌توان خروجی آن را فیلتر کرد. 

می‌گویند controller routes را ترجمه یا parse کنید، و شما فقط به Route attribute علاقه‌ دارید. به راحتی می‌توانید آن کلاس را به عنوان فیلتر pass کنید.

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


پارامتر دوم نحوه انجام آن فیلتر را تغییر می‌دهد. می‌توانید در ReflectionAttribute::IS_INSTANCEOF، عبور یا pass کنید، که تمام attribute های پیاده‌سازی شده یک اینترفیس داده شده را برمی‌گرداند.

برای مثال، شما تعاریف container را pars یا تجزیه می‌کنید، که به چندین attributes متکی است؛ خب می‌توانید کاری شبیه به این را انجام دهید:

$attributes = $reflectionClass->getAttributes(
    ContainerAttribute::class, 
    ReflectionAttribute::IS_INSTANCEOF
);

این کار یک مختصر نویسی خوب و مفید ، ساخته شده در هسته است.

تئوری فنی

اکنون که شما یک ایده درباره چگونگی عملکرد attribute ها در ذهن دارید، زمان آن رسیده که از درک کامل برخی  تئوری‌های دیگر نیز مطمئن شوید. اول از همه، من این را قبلاً به صورت مختصر ذکر کرده‌ام، attribute ها می‌توانند در چندین جا اضافه شوند.

در کلاس‌ها و همینطور در anonymous classes;

<<ClassAttribute>>
class MyClass { /* … */ }

$object = new <<ObjectAttribute>> class () { /* … */ };

Propertieها و constantها

<<PropertyAttribute>>
public int $foo;

<<ConstAttribute>>
public const BAR = 1;

متدها و فانکشن‌ها 

<<MethodAttribute>>
public function doSomething(): void { /* … */ }

<<FunctionAttribute>>
function foo() { /* … */ }

و همچنین closure ها 

$closure = <<ClosureAttribute>> fn() => /* … */;

و پارامتر‌های متد و فانکشن

function foo(<<ArgumentAttribute>> $bar) { /* … */ }

می‌توانند قبل یا بعد از docblock ها اعلان شوند;

/** @return void */
<<MethodAttribute>>
public function doSomething(): void { /* … */ }

و می‌تواند هیچ‌یک یا چندین آرگومان را که توسط attribute's constructor تعریف شده را بگیرد:

<<Listens(ProductCreatedEvent::class)>>
<<Autowire>>
<<Route(Http::POST, '/products/create')>>

در مورد پارامترهای مجاز می‌توانید به یک attribute آن را pass کنید، قبلاً مشاهده کردید که class constant ها، نام‌ها و انواع scalar ها ::class مجاز هستند. در این مورد کمی باید بیشتر گفت: attribute ها فقط عبارات ثابت یا constant expression ها را به عنوان آرگومان‌های ورودی می‌پذیرند.

این به این معنیست که scalar expression ها مجاز هستند- حتی bit shift ها- و همچنین مثل ::class ، ثابت‌ها، آرایه‌ها، عبارات بولین و اپراتور null coalescing. لیستی از همه چیزهایی که به عنوان یک عبارت ثابت مجاز است را می‌توانید در این سورس کد پیدا کنید.

<<AttributeWithScalarExpression(1+1)>>
<<AttributeWithClassNameAndConstants(PDO::class, PHP_VERSION_ID)>>
<<AttributeWithClassConstant(Http::POST)>>
<<AttributeWithBitShift(4 >> 1, 4 << 1)>>

پیکربندی Attribute

به صورت پیش‌فرض، Attribute ها می‌توانند به چندین مکان،‌ مثل لیست زیر اضافه شوند. با وجود این، این امکان‌پذیر است که پیکربندی آن‌ها فقط در مکان‌های خاص مورد استفاده قرار گیرد. به عنوان مثال می‌توانید آن‌را بسازید تا ClassAttribute فقط بتواند در کلاس‌ها و نه جای دیگری، مورد استفاده قرار بگیرد. انجام این کار با pass کردن یک flag به صفت Attribute در کلاس attribute امکان‌پذیر است.

مثل این:

<<Attribute(Attribute::TARGET_CLASS)>>
class ClassAttribute
{
}

flag های زیر در دسترس است:

Attribute::TARGET_CLASS
Attribute::TARGET_FUNCTION
Attribute::TARGET_METHOD
Attribute::TARGET_PROPERTY
Attribute::TARGET_CLASS_CONSTANT
Attribute::TARGET_PARAMETER
Attribute::TARGET_ALL

این‌ها bitmask flag ها هستند، بنابراین می‌توانید آن‌ها را با استفاده از عملگرهای Bitwise ترکیب کنید:

<<Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_FUNCTION)>>
class ClassAttribute
{
}

یکی دیگر از پیکربندی‌های flag در رابطه با تکرار پذیری یا repeatability است. به طور پیش‌فرض دو attribute یکسان نمی‌تواند دوبار اعمال شود، مگر اینکه به طور خاص تکرار شود. این کار به همان روش پیکربندی هدف یا target configuration ، یا مقدار کمی flag انجام پذیر است.

<<Attribute(Attribute::IS_REPEATABLE)>>
class ClassAttribute
{
}

توجه داشته باشید که همه‌ی این flag ها فقط در هنگام فراخوانی $attribute→newInstance() معتبر هستند، نه زودتر.

ویژگی‌های داخلی (Built-in attributes)

پس از قبول بیس و پایه RFC، فرصت‌های جدیدی برای افزودن attributes های داخلی به هسته ایجاد شد. یک نمونه از این ویژگی‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌ها، ویژگی <<Deprecated>> است و یک نمونه محبوب هم ویژگی <<Jit>> بود. اگر نمی‌دانید که مورد آخر چیست، می‌توانید این مقاله را در باره JIT مطالعه کنید.

من مطمئنم که در آینده شاهد Built-in attribute های بیشتری خواهیم بود.

امیدوارم که از خواندن این مقاله لذت برده باشید و براتون مفید بوده باشه.

منبع

گردآوری و تالیف فاطمه شیرزادفر
آفلاین
user-avatar

تجربه کلمه‌ای هست که همه برای توصیف اشتباهاتشون ازش استفاده میکنن، و من همیشه دنبال اشتباهات جدیدم! برنامه‌نویس هستم و لینوکس‌ دوست

دیدگاه‌ها و پرسش‌ها

برای ارسال نظر لازم است ابتدا وارد سایت شوید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید