Kodokon kodokon.com

PHP 8 المتقدّمة: السمات وreadonly والقيم القابلة للاستدعاء

استكشف الآلية الداخلية للسمات وreadonly والقابلات للاستدعاء من الدرجة الأولى والأنواع الصارمة في PHP 8.

10 دقيقة · 3 أسئلة

افتح هذا الدرس في Kodokon

تستبدل السمات (attributes) تعليقات docblock بأصناف حقيقية يتحقق منها المحرّك. فالسمة ليست سوى بيانات وصفية خامدة: إذ لا تُنشئ كتابة #[Route('/users')] أيّ كائن. ولا تدبّ فيها الحياة إلا حين يستعلمها كود ما عبر الانعكاس (reflection) ويستدعي newInstance() - وهو بالضبط ما تفعله أطر العمل عند الإقلاع لبناء جداول التوجيه لديها. أما الهدف المسموح به (TARGET_METHOD وTARGET_CLASS...) فيُعلَن هو نفسه بواسطة سمة على صنف السمة.

PHP
<?php
declare(strict_types=1);

#[Attribute(Attribute::TARGET_METHOD)]
final class Route
{
    public function __construct(
        public readonly string $path,
        public readonly string $method = 'GET'
    ) {}
}

final class UserController
{
    #[Route('/users', method: 'GET')]
    public function index(): array
    {
        return ['users' => []];
    }
}

$ref = new ReflectionMethod(
    UserController::class,
    'index'
);

foreach ($ref->getAttributes(Route::class) as $attr) {
    $route = $attr->newInstance();
    echo $route->method, ' ', $route->path, PHP_EOL;
}
لا تُنشأ السمة ككائن إلا في لحظة استدعاء newInstance().

يضمن readonly - على مستوى الخصائص في 8.1، وعلى مستوى الأصناف كاملةً في 8.2 - أن الخاصية تُكتب مرة واحدة فقط، من داخل نطاق الصنف. وهو الأداة المناسبة لكائنات القيمة: إذ يصبح أيّ "تعديل" إنشاءً لكائن جديد، ما يقضي على عائلة كاملة من العلل الناتجة عن الحالة المشتركة. ويشترط الصنف readonly أن تكون جميع خصائصه ذات أنواع محدّدة وغير ساكنة.

PHP
<?php
declare(strict_types=1);

final readonly class Money
{
    public function __construct(
        public int $amount,
        public string $currency
    ) {}

    public function add(Money $other): self
    {
        if ($other->currency !== $this->currency) {
            throw new InvalidArgumentException(
                'Currency mismatch'
            );
        }
        return new self(
            $this->amount + $other->amount,
            $this->currency
        );
    }
}

$total = new Money(500, 'EUR');
$total = $total->add(new Money(250, 'EUR'));
echo $total->amount;
كائن قيمة: تُنشئ عملية الجمع نسخة جديدة.

تُنشئ صياغة القابل للاستدعاء من الدرجة الأولى strtoupper(...) (PHP 8.1) كائن Closure من أيّ دالة أو دالة صنف. وخلافًا للسلاسل مثل 'strtoupper' أو المصفوفات مثل [$obj, 'method']، يُتحقَّق من الرمز عند الإنشاء، وتتابع أدوات التحليل الساكن الأنواعَ من البداية إلى النهاية. وبالنسبة لدالة نسخة، يلتقط الإغلاق (closure) الكائنَ؛ أما بالنسبة لدالة ساكنة، فيلتقط الصنف فقط.

PHP
<?php
declare(strict_types=1);

$words = ['php', 'rust', 'go'];

$upper = strtoupper(...);
var_dump($upper('php'));

print_r(array_map(strtoupper(...), $words));

final class Mailer
{
    public function send(string $to): bool
    {
        return $to !== '';
    }

    public static function ping(): string
    {
        return 'pong';
    }
}

$mailer = new Mailer();
$send = $mailer->send(...);
$ping = Mailer::ping(...);

var_dump($send('dev@example.org'), $ping());
يُنتِج كل تعبير على شكل f(...) كائن Closure يُتحقَّق منه عند الإنشاء.

اختبار المعرفة

تأكّد من أنك تذكّرت النقاط الأساسية في هذا الدرس.

  1. متى تُنشأ نسخة من صنف سمة #[Route(...)]؟
    • عند ترجمة الملف الذي يحملها
    • حالما تُستدعى الدالة المُزخرَفة بها
    • فقط عندما يستعلمها كود ما عبر الانعكاس ويستدعي newInstance()
  2. ماذا تضمن خاصية readonly بالضبط؟
    • أن الكائن الذي تشير إليه يصبح غير قابل للتغيير كليًا
    • أنها تُكتب مرة واحدة فقط، من داخل نطاق الصنف؛ أما الكائن المُشار إليه نفسه فيبقى قابلًا للتغيير
    • أنه لا يمكن قراءتها إلا من الصنف الذي يعلنها
  3. ما نطاق declare(strict_types=1)؟
    • المشروع بأكمله حالما يعلنه ملف واحد
    • الاستدعاءات المكتوبة في الملف الذي يحتوي على الإعلان
    • فقط الدوال المُعرَّفة في ذلك الملف، أينما استُدعيت