$30 off During Our Annual Pro Sale. View Details »

PHP8の機能を使って堅牢にコードを書く

 PHP8の機能を使って堅牢にコードを書く

PHPerKaigi2024で登壇したときの資料です。
https://fortee.jp/phperkaigi-2024/proposal/ae2ded4d-8e7e-47a0-85d1-26a8c92308ac

Futoshi Endo

March 08, 2024
Tweet

More Decks by Futoshi Endo

Other Decks in Programming

Transcript

  1. © 2012-2024 BASE, Inc. 2 氏名:Futoshi Endo 所属:BASE株式会社 業務:バックエンド エンジニア

    趣味:料理、音楽鑑賞、散歩 奥さんと、豆柴(どんちゃん)の三人で楽しく暮 らしています。 • Endo Tech Blog • https://scrapbox.io/fendo181/ Fendo181 自己紹介
  2. © 2012-2024 BASE, Inc. アジェンダ 6 • 今日の発表ゴール • PHP8の機能の紹介

    • PHP8の機能を使ってコードを書いてみる • まとめ
  3. © 2012-2024 BASE, Inc. 16 PHP8.0 (2020/11/26) • 名前付き引数 •

    Attribute • Constructor Property Promotion • Match 式 • Union 型 • Nullsafe 演算子 など
  4. © 2012-2024 BASE, Inc. 17 Constructor Property Promotionは、クラスのプロパティ宣言とコンストラ クタでの初期化を一つのステップで行えるようにする機能です。これにより、 コードの冗長性を減らし、より簡潔で読みやすいコードを書くことが可能になり

    ます。 <?php class User { private string $name; private int $age; public function __construct(string $name, int $age) { $this->name = $name; $this->age = $age; } <?php class User { public function __construct (private string $name, private int $age) {}
  5. © 2012-2024 BASE, Inc. 18 <?php $statusCode = 200; switch

    ($statusCode) { case 200: $status = 'OK'; break; case 400: $status = 'Bad Request'; break; case 500: $status = 'Internal Server Error'; break; default: $status = 'Unknown'; break; } echo $status; <?php $statusCode = 200; $status = match ($statusCode) { 200 => 'OK', 400 => 'Bad Request', 500 => 'Internal Server Error', default => 'Unknown', }; echo $status; match式を使用すると、switch文より簡潔に記述する事ができます。match式 は自動的にbreakを含んでいるため、複数のケースが誤って実行される心配はあ りません。また、match式 は、型と値についても厳密な比較を行います
  6. © 2012-2024 BASE, Inc. 20 PHP8.0 (2020/11/26) • 名前付き引数 •

    Attribute • Constructor Property Promotion • Match 式 • Union 型 • Nullsafe 演算子 など PHP8.1 (2021/11/25) • Enum型 • 読み取り専用プロパティ (Readonly Properties) • 交差型 • Never型 など
  7. © 2012-2024 BASE, Inc. 21 Enum型を使用することで、期待される特定の値のみを受け入れるように制約を かけることができ、これによりコードの意図が明確になり、エラーの可能性を減 らすことができます。 <?php enum

    Status: string { case Draft = 'Draft'; case Published = 'Published'; case Archived = 'Archived'; } // Enum の値を使用 function publishArticle(Status $status) { if ($status === Status::Published) { echo "Article is already published."; } else { echo "Publishing article..."; // ここで実際の記事公開の処理を行う } } // Status Enum を使って関数を呼び出す publishArticle(Status::Draft); // "Publishing article..." publishArticle(Status::Published); // "Article is already published."
  8. © 2012-2024 BASE, Inc. 22 読み取り専用プロパティ(Readonly Properties)は、プロパティが初期化さ れた後に変更することができないことを保証します。この機能は、不変の状態を 持つオブジェクトを作成する際に特に有効です。 <?php

    class User { public function __construct( public readonly string $name, public readonly int $age ) {} } $user = new User(name: "Endu", age: 30); // ユーザーの名前と年齢を取得 echo $user->name; // Endu echo $user->age; // 30 // readonly プロパティは変更できないため、以下のコードはエラーになる $user->name = "Bob"; // エラー: Cannot modify readonly property User::$name
  9. © 2012-2024 BASE, Inc. 23 PHP8.0 (2020/11/26) • 名前付き引数 •

    Attribute • Constructor Property Promotion • Match 式 • Union 型 • Nullsafe 演算子 など PHP8.1 (2021/11/25) • Enum型 • 読み取り専用プロパティ (Readonly Properties) • 交差型 • Never型 など PHP8.2(2022/12/08 ) • 読み取り専用クラス (Readonly Classes) • Random 拡張モジュール • Never型 など • null, false, true が独立し た型として使える • 動的なプロパティが非推奨 に など
  10. © 2012-2024 BASE, Inc. 24 <?php readonly class UserProfile {

    public function __construct( public string $name, public int $age, public string $email ) {} } $profile = new UserProfile(name: "Endu", age: 30, email: "[email protected]"); // プロファイルのデータを読み取ることはできます echo $profile->name; // Endu // しかし、プロパティは読み取り専用なので、変更しようとするとエラーになります $profile->name = "Bob"; // エラー: Cannot modify readonly property UserProfile::$name 読み取り専用クラス(Readonly Classes)はクラスのすべてのプロパティが読 み取り専用であることを意味します。この機能を使用すると、クラスのインスタ ンスが一度作成されると、その状態が変更されないことが保証されます。読み取 り専用クラスは、データオブジェクトが不変であるべき場合に特に便利です。
  11. © 2012-2024 BASE, Inc. 25 PHP8.3 (2023/11/23) • クラス定数の型付け •

    クラス定数の文字列指定 • #[\Override]の追加 • json_validate()の追加 • 読み取り専用プロパティの ディープクローン など... PHP8.4 (???)
  12. © 2012-2024 BASE, Inc. ここまでのまとめ 26 • PHP8.0 ~ PHP8.3までで追加された機能をざっくり紹介しました!

    • 一部の機能しかとりあげれてないが、他にも沢山の改善がある ◦ PHP JIT ◦ throw式の導入 • PHPStanと組み合わせて使うとより、より安全にコードが書けるようになる。 • ちなみにPHP 8.0のセキュリティサポートは既に 2023/11/26 で終了している...! ◦ PHP8.1のセキュリティサポートは 2024/11/25で終了 ▪ なるべく新しいPHP8.xを使っていきましょう...!!
  13. © 2012-2024 BASE, Inc. 28 • Blogの投稿内容を管理するオブジェクト • statusはdraft、published、archived の3つのステータスが存在するが、

    setStatusメソッド内で管理している • setStatus内では、期待してないステー タスが来た場合に例外を投げる PHP8の機能を使わないパターン <?php class BlogPost { protected $title; protected $content; protected $status; // 'draft', 'published', 'archived' public function __construct($title, $content, $status) { $this->title = $title; $this->content = $content; $this->setStatus($status); } public function setStatus($status) { if (!in_array($status, ['draft', 'published', 'archived'])) { throw new InvalidArgumentException('Invalid status'); } $this->status = $status; } public function getStatus(): string { return $this->status; }
  14. © 2012-2024 BASE, Inc. 29 <?php class BlogPost { protected

    $title; protected $content; protected $status; // 'draft', 'published', 'archived' public function __construct($title, $content, $status) { $this->title = $title; $this->content = $content; $this->setStatus($status); } public function setStatus($status) if (!in_array($status, ['draft', 'published', 'archived'])) { throw new InvalidArgumentException('Invalid status'); } $this->status = $status; } public function getStatus(): string { return $this->status; } // 実行イメージ $blog = new BlogPost( title: 'New blog post', content: 'This is the content of the blog post.', status: PostStatus::Draft ); var_dump($blog->getStatus()); // 'draft' $blog->setStatus('published'); var_dump($blog->getStatus()); // 'published' PHP8の機能を使わないパターン
  15. © 2012-2024 BASE, Inc. 30 どこに問題があるか? PHP8の機能を使わないパターン <?php class BlogPost

    { protected $title; protected $content; protected $status; // 'draft', 'published', 'archived' public function __construct($title, $content, $status) { $this->title = $title; $this->content = $content; $this->setStatus($status); } public function setStatus($status) { if (!in_array($status, ['draft', 'published', 'archived'])) { throw new InvalidArgumentException('Invalid status'); } $this->status = $status; } public function getStatus(): string { return $this->status; }
  16. © 2012-2024 BASE, Inc. <?php class BlogPost { protected $title;

    protected $content; protected $status; // 'draft', 'published', 'archived' public function __construct($title, $content, $status) { $this->title = $title; $this->content = $content; $this->setStatus($status); } public function setStatus($status) { if (!in_array($status, ['draft', 'published', 'archived'])) { throw new InvalidArgumentException('Invalid status'); } $this->status = $status; } public function getStatus(): string { return $this->status; } 31 • 🤔変数が増えてきたら、その分コンス トラクタに渡す値が増えそう PHP8の機能を使わないパターン
  17. © 2012-2024 BASE, Inc. <?php class BlogPost { protected $title;

    protected $content; protected $status; // 'draft', 'published', 'archived' public function __construct($title, $content, $status) { $this->title = $title; $this->content = $content; $this->setStatus($status); } public function setStatus($status) { if (!in_array($status, ['draft', 'published', 'archived'])) { throw new InvalidArgumentException('Invalid status'); } $this->status = $status; } public function getStatus(): string { return $this->status; } 32 PHP8の機能を使わないパターン • 🤔変数が増えてきたら、その分コンス トラクタに渡す値が増えそう • 🤔setStatus内では in_array を使って 判定しているが、第三引数にtrueをいれ ない限りは厳密な型比較を行わない。 • 🤔statusが増えてきたらその分、 in_arrayの第2引数の配列に値を追加す る必要がある
  18. © 2012-2024 BASE, Inc. 33 PHP8.0 (2020/11/26) • 名前付き引数 •

    Attribute • Constructor Property Promotion • Match 式 • Union 型 • Nullsafe 演算子 など PHP8.1 (2021/11/25) • Enum型 • 読み取り専用プロパティ • 交差型 • Never型 など PHP8.2(2022/12/08 ) • 読み取り専用クラス • Random 拡張モジュール • Never型 など • null, false, true が、独立 した型に • 動的なプロパティが非推奨 に など PHP8の機能を使うパターン
  19. © 2012-2024 BASE, Inc. <?php enum PostStatus: string { case

    Draft = 'draft'; case Published = 'published'; case Archived = 'archived'; } class BlogPost { protected PostStatus $status; public function __construct( protected readonly string $title, protected readonly string $content, PostStatus $status ) { $this->status = $status; } public function setStatus(PostStatus $status): void { $this->status = $status; } } public function getStatus(): string { return $this->status; } 34 • 😊 Enum型を導入することでstatusプロパティ が取り得る値をdraft、published、archivedの みに限定した。これにより、無効な値が設定さ れることを防ぎ、コードの安全性を向上させま す PHP8の機能を使うパターン
  20. © 2012-2024 BASE, Inc. <?php enum PostStatus: string { case

    Draft = 'draft'; case Published = 'published'; case Archived = 'archived'; } class BlogPost { protected PostStatus $status; public function __construct( protected readonly string $title, protected readonly string $content, PostStatus $status ) { $this->status = $status; } public function setStatus(PostStatus $status): void { $this->status = $status; } } public function getStatus(): string { return $this->status; } 35 • 😊 Enum型を導入することでstatusプロパティ が取り得る値をdraft、published、archivedの みに限定した。これにより、無効な値が設定さ れることを防ぎ、コードの安全性を向上させま す • 😊 titleとcontentプロパティをread onlyとし て宣言することで、これらのプロパティが初期 化後に変更されないことを保証する PHP8の機能を使うパターン
  21. © 2012-2024 BASE, Inc. <?php enum PostStatus: string { case

    Draft = 'draft'; case Published = 'published'; case Archived = 'archived'; } class BlogPost { protected PostStatus $status; public function __construct( protected readonly string $title, protected readonly string $content, PostStatus $status ) { $this->status = $status; } public function setStatus(PostStatus $status): void { $this->status = $status; } } public function getStatus(): string { return $this->status; } 36 • 😊 Enum型を導入することでstatusプロパティ が取り得る値をdraft、published、archivedの みに限定した。これにより、無効な値が設定さ れることを防ぎ、コードの安全性を向上させま す • 😊 titleとcontentプロパティをread onlyとし て宣言することで、これらのプロパティが初期 化後に変更されないことを保証する • 😊 setStatusは引数にEnum型 のPostStatusを 指定する事で、Enumに定義してない値は型で弾 く事ができる。加えて定義を追加するのは PostStatusだけで良くなります。 PHP8の機能を使うパターン
  22. © 2012-2024 BASE, Inc. 37 <?php enum PostStatus: string {

    case Draft = 'draft'; case Published = 'published'; case Archived = 'archived'; } class BlogPost { protected PostStatus $status; public function __construct( protected readonly string $title, protected readonly string $content, PostStatus $status ) { $this->status = $status; } public function setStatus(PostStatus $status): void { $this->status = $status; } } public function getStatus(): string { return $this->status; } // 実行イメージ $blog = new BlogPost( title: 'New blog post', content: 'This is the content of the blog post.', status: PostStatus::Draft ); var_dump($blog->getStatus()->value); // 'draft' $blog->setStatus(PostStatus::Published); var_dump($blog->getStatus()->value); // 'published' PHP8の機能を使うパターン
  23. © 2012-2024 BASE, Inc. まとめ 39 • 型指定は当然だが、Enum型、match式、Readonly Properties が登場し

    た事でより安全にかつ、シンプルにコードを書けるようになっている。 ◦ “「出来てはならぬことを禁じる」のではなく、はじめから「出来ていい ことだけを出来るようにする」と考える” • PHP8の機能を使わなくも堅牢なコードを書けるが、より使いやすくなっ ていると感じる。 ◦ 開発者が意識しなくても機能を使う事で堅牢な状態を実現できる • まだまだPHP8.xはこれからバージョンアップするので、堅牢な状態を意 識してコードを書いていきましょう!
  24. © 2012-2024 BASE, Inc. 42 • PHPConference2016 Track1 (3) PHP7で堅牢なコードを書く

    • PHPerKaigi 2022: 予防に勝る防御なし - 堅牢なコードを導く様々な設計の ヒント • https://blog.shin1x1.com/entry/impression-of-php8-new-features • https://qiita.com/kakiyuta/items/5de76509337ea40c19e3 • https://kojirooooocks.hatenablog.com/entry/2021/01/08/004707 • https://qiita.com/blue32a/items/91bb1b57b92aa1a5a377 • https://kinsta.com/jp/blog/php-8 スライドを作るにあたって参考にさせて頂きました。 ありがとうございました!!