Kodokon kodokon.com

PHP 8 ขั้นสูง: แอตทริบิวต์, readonly, callable

สำรวจกลไกภายในของแอตทริบิวต์, readonly, first-class callable และการกำหนดชนิดแบบเข้มงวดของ PHP 8

10 นาที · 3 คำถาม

เปิดบทเรียนนี้ใน Kodokon

แอตทริบิวต์ (attribute) แทนที่คำอธิบายประกอบแบบ 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 รับประกันว่าคุณสมบัติจะถูกเขียนเพียง ครั้งเดียว จากภายในขอบเขตของคลาสเท่านั้น มันคือเครื่องมือสำหรับ value object การ "แก้ไข" ใด ๆ จะกลายเป็นการสร้างอ็อบเจกต์ใหม่ ซึ่งกำจัดบั๊กทั้งตระกูลที่เกิดจากสถานะที่ใช้ร่วมกัน (shared state) คลาส 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;
value object การบวกจะสร้างอินสแตนซ์ใหม่

ไวยากรณ์ first-class callable strtoupper(...) (PHP 8.1) สร้าง Closure จากฟังก์ชันหรือเมท็อดใด ๆ ต่างจากสายอักขระอย่าง 'strtoupper' หรืออาร์เรย์อย่าง [$obj, 'method'] ตรงที่สัญลักษณ์จะถูกตรวจสอบตอนสร้าง และเครื่องมือวิเคราะห์แบบสถิตติดตามชนิดได้ตลอดทั้งสาย สำหรับเมท็อดของอินสแตนซ์ โคลเชอร์จะจับอ็อบเจกต์ไว้ ส่วนเมท็อดแบบสแตติกจะจับเฉพาะคลาส

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) คืออะไร?
    • ทั้งโปรเจกต์ ทันทีที่ไฟล์เดียวประกาศมัน
    • การเรียกที่เขียนอยู่ในไฟล์ที่มีการประกาศ
    • เฉพาะฟังก์ชันที่นิยามในไฟล์นั้น ไม่ว่าจะถูกเรียกที่ไหนก็ตาม