Upgrade to Pro — share decks privately, control downloads, hide ads and more …

予防に勝る防御なし(2025年版) - 堅牢なコードを導く様々な設計のヒント / Growin...

予防に勝る防御なし(2025年版) - 堅牢なコードを導く様々な設計のヒント / Growing Reliable Code PHP Conference Fukuoka 2025

PHPカンファレンス福岡2025

2025/11/08 11:40〜
ホライズンテクノロジーホール
レギュラートーク(30分)

https://fortee.jp/phpcon-fukuoka-2025/proposal/cf0925df-c846-4774-b203-a111e57f1235

本講演は2016年から続けている「PHPで堅牢なコードを書く」シリーズの最新版です。

PHPはバージョンを追う毎に機能が強化され、堅牢なコードを書くための機能が充実してきました。本講演ではPHP 8.4(および 8.5)をベースにして、誤りを想定してチェックするのではなく、そもそも誤りにくい設計とはどのようなものか、つまり「予防」の観点を軸足に、堅牢なコードを導くための様々な設計のヒントをご紹介します。

Agenda
1. 型宣言
2. 列挙型
3. 静的解析
4. 型のモデリング
5. 不変性
6. 完全性
7. 責務の配置

Avatar for Takuto Wada

Takuto Wada PRO

November 08, 2025
Tweet

More Decks by Takuto Wada

Other Decks in Programming

Transcript

  1. class BugRepository { private $pdo; public function __construct($pdo) { $this->pdo

    = $pdo; } public function findAll($params) { $sql = 'SELECT bug_id, summary, reported_at FROM Bugs WHERE reported_at >= :startAt AND reported_at < :endAt AND status = :status'; $stmt = $this->pdo->prepare($sql); $stmt->execute($params); return $stmt->fetchAll(\PDO::FETCH_CLASS, Bug::class); } ஫ॻ੶ʰ42-Ξϯνύλʔϯʢॳ൛ʣʱͷͻͲ͍ίʔυྫΛ͞ΒʹͻͲ͘ΞϨϯδͯ͠ॻ͍͍ͯ·͢ ͱ͋Δͱ͜Ζʹɺ͜Μͳίʔυ͕͋Γ·ͨ͠ # " % IUUQTXXXPSFJMMZDPKQCPPLT
  2. public function findAll($params) { $sql = 'SELECT bug_id, summary, reported_at

    FROM Bugs WHERE reported_at >= :startAt AND reported_at < :endAt AND status = :status'; $stmt = $this->pdo->prepare($sql); $stmt->execute($params); return $stmt->fetchAll(\PDO::FETCH_CLASS, Bug::class); } ॲཧࣦഊͷݪҼʹͳΔՄೳੑ͕͋Δͷ͸Ͳͷߦʁ # " % ᶃ ᶄ ᶅ ᶃςʔϒϧ໊΍ΧϥϜ໊͕୭͔ʹมߋ͞Εͨ ᶄQBSBNT͕OVMM ᶄQBSBNTͷΩʔ໊΍਺ͷෆҰக ᶄQBSBNTͷ஋͕จࣈྻʹม׵ෆೳ ᶄQBSBNTͷ஋͕೔࣌ͱͯ͠ղऍͰ͖ͳ͍ ᶅ#VHΫϥε͕ະఆٛ ᶃᶄᶅ్தͰσʔλϕʔε઀ଓΤϥʔ ຊ೔ͷߨԋ͸ ͜ΕΒͷ໰୊ʹूத͠·͢ 👉 👉 👉 👉
  3. public function findAll($params) { if (is_null($params)) { throw new \InvalidArgumentException('params

    should not be null'); } if (!is_array($params)) { throw new \InvalidArgumentException('params should be an array'); } if (count($params) !== 3) { throw new \InvalidArgumentException('params should have exact three items'); } if (!array_key_exists('startAt', $params) || !array_key_exists('endAt', $params) || !array_key_exists('status', $params)) { throw new \InvalidArgumentException('params should have keys "startAt", "endAt" and "status"'); } if (!is_string($params['startAt'])) { throw new \InvalidArgumentException('params["startAt"] should be a string'); } if (!is_string($params['endAt'])) { throw new \InvalidArgumentException('params["endAt"] should be a string'); } if (!is_string($params['status'])) { throw new \InvalidArgumentException('params["status"] should be a string'); } if (!in_array($params['status'], ['OPEN', 'NEW', 'FIXED'], true)) { throw new \InvalidArgumentException('params["status"] should be in "OPEN","NEW","FIXED"'); } ͨͩͻͨ͢ΒೖྗΛ๷ޚతνΣοΫͨ͠Γ # " %
  4. ෆਖ਼ͳೖྗ͕͋ͬͯ΋ࣗ෼ͰԿͱ͔͠Α͏ͱͨ͠Γ # " % $sql = 'SELECT bug_id, summary, reported_at

    FROM Bugs WHERE reported_at >= :startAt AND reported_at < :endAt AND status = :status'; $stmt = $this->pdo->prepare($sql); $safeParams = [ 'startAt' => $params['startAt'] ?? $params['start_at'] ?? '1970-01-01', 'endAt' => $params['endAt'] ?? $params['end_at'] ?? 'now', 'status' => $params['status'] ?? 'OPEN', ]; $stmt->execute($safeParams); $className = class_exists('Bug') ? 'Bug' : 'BugModel'; return $stmt->fetchAll(\PDO::FETCH_CLASS, $className);
  5. υΩϡϝϯτͰؒҧ͍΍͢͞Λิ͓͏ͱͨ͠Γ # " % /** * 指定した範囲の日時およびステータスに合致する Bug を検索し、ヒットした全件を Bug

    オブジェクトの配列として返す。 * * @param array $params 格納した検索条件の連想配列。キー "startAt" に検索範囲の 始点日時をstringで, キー "endAt" に検索範囲の終点日時をstringで, キー "status" にス テータス文字列をstringで指定すること。キー、値それぞれNULLは不可とする。 * @return Bug[] 検索結果を Bug オブジェクトにマッピングして返す。検索結果が0件 のときは空配列を返す。 */ public function findAll($params) {
  6. Agenda  ܕએݴ  ྻڍܕ  ੩తղੳ  ܕͷϞσϦϯά 

    ෆมੑ  ׬શੑ  ੹຿ͷ഑ஔ 👉
  7. public function findAll($params) { if (is_null($params)) { throw new \InvalidArgumentException('params

    should not be null'); } if (!is_array($params)) { throw new \InvalidArgumentException('params should be an array'); } if (count($params) !== 3) { throw new \InvalidArgumentException('params should have exact three items'); } if (!array_key_exists('startAt', $params) || !array_key_exists('endAt', $params) || !array_key_exists('status', $params)) { throw new \InvalidArgumentException('params should have keys "startAt", "endAt" and "status"'); } if (!is_string($params['startAt'])) { throw new \InvalidArgumentException('params["startAt"] should be a string'); } if (!is_string($params['endAt'])) { throw new \InvalidArgumentException('params["endAt"] should be a string'); } if (!is_string($params['status'])) { throw new \InvalidArgumentException('params["status"] should be a string'); } if (!in_array($params['status'], ['OPEN', 'NEW', 'FIXED'], true)) { throw new \InvalidArgumentException('params["status"] should be in "OPEN","NEW","FIXED"'); } ໰୊ͱͳ͍ͬͯΔ͜ͷ࿈૝഑ྻΛ ݱঢ়
  8. public function findAll(\DateTime $startAt, \DateTime $endAt, string $status): array {

    if (is_null($params)) { throw new \InvalidArgumentException('params should not be null'); } if (!is_array($params)) { throw new \InvalidArgumentException('params should be an array'); } if (count($params) !== 3) { throw new \InvalidArgumentException('params should have exact three items'); } if (!array_key_exists('startAt', $params) || !array_key_exists('endAt', $params) || !array_key_exists('status', $params)) { throw new \InvalidArgumentException('params should have keys "startAt", "endAt" and "status"'); } if (!is_string($params['startAt'])) { throw new \InvalidArgumentException('params["startAt"] should be a string'); } if (!is_string($params['endAt'])) { throw new \InvalidArgumentException('params["endAt"] should be a string'); } if (!is_string($params['status'])) { throw new \InvalidArgumentException('params["status"] should be a string'); } if (!in_array($params['status'], ['OPEN', 'NEW', 'FIXED'], true)) { throw new \InvalidArgumentException('params["status"] should be in "OPEN","NEW","FIXED"'); } ͦΕͧΕܕએݴ͞ΕͨҾ਺ʹ෼ղ͢Δ ܕએݴʹΑͬͯʮग़དྷ͍͍ͯ͜ͱ͚ͩΛग़དྷΔʯΑ͏ʹ
  9. public function findAll(\DateTime $startAt, \DateTime $endAt, string $status): array {

    if (is_null($params)) { throw new InvalidArgumentException('params should not be null'); } if (!is_array($params)) { throw new InvalidArgumentException('params should be an array'); } if (count($params) !== 3) { throw new InvalidArgumentException('params should be have exact three items'); } if (!array_key_exists('startAt', $params) || !array_key_exists('endAt', $params) || !array_key_exists('status', $params)) { throw new InvalidArgumentException('params should have key "startAt", "endAt" and "status" only'); } if (!is_string($params['startAt'])) { throw new InvalidArgumentException('params["startAt"] should be a string'); } if (!is_string($params['endAt'])) { throw new InvalidArgumentException('params["endAt"] should be a string'); } if (!is_string($params['status'])) { throw new InvalidArgumentException('params["status"] should be a string'); } if (!in_array($params['status'], ['OPEN', 'NEW', 'FIXED'], true)) { throw new \InvalidArgumentException('params["status"] should be in "OPEN","NEW","FIXED"'); } ๷ޚతνΣοΫ͕΄΅ෆཁʹ
  10. ૝ఆ͠ͳ͚Ε͹ͳΒͳ͍ঢ়گ͕ݮͬͨ const TIMESTAMP_FORMAT = 'Y-m-d\TH:i:s.uP'; public function findAll(\DateTime $startAt, \DateTime

    $endAt, string $status): array { if (!in_array($status, ['OPEN', 'NEW', 'FIXED'], true)) { throw new \InvalidArgumentException('status should be in "OPEN","NEW","FIXED"'); } $sql = 'SELECT bug_id, summary, reported_at FROM Bugs WHERE reported_at >= :startAt AND reported_at < :endAt AND status = :status'; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':startAt', $startAt->format(self::TIMESTAMP_FORMAT), \PDO::PARAM_STR); $stmt->bindValue(':endAt', $endAt->format(self::TIMESTAMP_FORMAT), \PDO::PARAM_STR); $stmt->bindValue(':status', $status, \PDO::PARAM_STR); $stmt->execute(); return $stmt->fetchAll(\PDO::FETCH_CLASS, Bug::class); } ˞ຊߨԋͰ͸1PTUHSF42-ͷUJNFTUBNQXJUIUJNF[POFܕͷΧϥϜʹ֨ೲ͍ͯ͠Δͱߟ͍͑ͯͩ͘͞
  11. Agenda  ܕએݴ  ྻڍܕ  ੩తղੳ  ܕͷϞσϦϯά 

    ෆมੑ  ׬શੑ  ੹຿ͷ഑ஔ 👉
  12. ྻڍܕΛ࢖͓͏ 1 ) 1   enum Status: string {

    case Open = 'OPEN'; case New = 'NEW'; case Fixed = 'FIXED'; } IUUQTXXXQIQOFUNBOVBMKBMBOHVBHFFOVNFSBUJPOTQIQ
  13. const TIMESTAMP_FORMAT = 'Y-m-d\TH:i:s.uP'; public function findAll(\DateTime $startAt, \DateTime $endAt,

    string $status): array { if (!in_array($status, ['OPEN', 'NEW', 'FIXED'], true)) { throw new \InvalidArgumentException('status should be in "OPEN","NEW","FIXED"'); } $sql = 'SELECT bug_id, summary, reported_at FROM Bugs WHERE reported_at >= :startAt AND reported_at < :endAt AND status = :status'; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':startAt', $startAt->format(self::TIMESTAMP_FORMAT), \PDO::PARAM_STR); $stmt->bindValue(':endAt', $endAt->format(self::TIMESTAMP_FORMAT), \PDO::PARAM_STR); $stmt->bindValue(':status', $status, \PDO::PARAM_STR); $stmt->execute(); return $stmt->fetchAll(\PDO::FETCH_CLASS, Bug::class); } ݱঢ়
  14. const TIMESTAMP_FORMAT = 'Y-m-d\TH:i:s.uP'; public function findAll(\DateTime $startAt, \DateTime $endAt,

    Status $status): array { if (!in_array($status, ['OPEN', 'NEW', 'FIXED'], true)) { throw new \InvalidArgumentException('status should be in "OPEN","NEW","FIXED"'); } $sql = 'SELECT bug_id, summary, reported_at FROM Bugs WHERE reported_at >= :startAt AND reported_at < :endAt AND status = :status'; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':startAt', $startAt->format(self::TIMESTAMP_FORMAT), \PDO::PARAM_STR); $stmt->bindValue(':endAt', $endAt->format(self::TIMESTAMP_FORMAT), \PDO::PARAM_STR); $stmt->bindValue(':status', $status->value, \PDO::PARAM_STR); $stmt->execute(); return $stmt->fetchAll(\PDO::FETCH_CLASS, Bug::class); } Ҿ਺Λจࣈྻ͔Βྻڍܕʹม͑Δ จࣈྻ͔Βྻڍܕʹม͑Δ
  15. const TIMESTAMP_FORMAT = 'Y-m-d\TH:i:s.uP'; public function findAll(\DateTime $startAt, \DateTime $endAt,

    Status $status): array { if (!in_array($status, ['OPEN', 'NEW', 'FIXED'], true)) { throw new \InvalidArgumentException('params["status"] should be in "OPEN","NEW","FIXED"'); } $sql = 'SELECT bug_id, summary, reported_at FROM Bugs WHERE reported_at >= :startAt AND reported_at < :endAt AND status = :status'; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':startAt', $startAt->format(self::TIMESTAMP_FORMAT), \PDO::PARAM_STR); $stmt->bindValue(':endAt', $endAt->format(self::TIMESTAMP_FORMAT), \PDO::PARAM_STR); $stmt->bindValue(':status', $status->value, \PDO::PARAM_STR); $stmt->execute(); return $stmt->fetchAll(\PDO::FETCH_CLASS, Bug::class); } ૝ఆ͠ͳ͚Ε͹ͳΒͳ͍ঢ়گ͕͞Βʹݮͬͨ
  16. Agenda  ܕએݴ  ྻڍܕ  ੩తղੳ  ܕͷϞσϦϯά 

    ෆมੑ  ׬શੑ  ੹຿ͷ഑ஔ 👉
  17. 1)1͋Δ͋Δ໭Γ஋͕ΦϒδΣΫτ·ͨ͸GBMTF public function findAll(\DateTime $startAt, \DateTime $endAt, Status $status): array

    { $sql = 'SELECT bug_id, summary, reported_at FROM Bugs WHERE reported_at >= :startAt AND reported_at < :endAt AND status = :status'; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':startAt', $startAt->format(self::TIMESTAMP_FORMAT), \PDO::PARAM_STR); $stmt->bindValue(':endAt', $endAt->format(self::TIMESTAMP_FORMAT), \PDO::PARAM_STR); $stmt->bindValue(':status', $status->value, \PDO::PARAM_STR); $stmt->execute(); return $stmt->fetchAll(\PDO::FETCH_CLASS, Bug::class); } $BOOPUDBMMNFUIPEFYFDVUF PO1%04UBUFNFOUcGBMTF $BOOPUDBMMNFUIPECJOE7BMVF PO1%04UBUFNFOUcGBMTF $BOOPUDBMMNFUIPEGFUDI"MM PO1%04UBUFNFOUcGBMTF ੩తղੳΛߦΘͳ͍ͱؾ͖ͮʹ͍͘
  18. BTTFSUͰܕΛڱΊΔ public function findAll(\DateTime $startAt, \DateTime $endAt, Status $status): array

    { $sql = 'SELECT bug_id, summary, reported_at FROM Bugs WHERE reported_at >= :startAt AND reported_at < :endAt AND status = :status'; $stmt = $this->pdo->prepare($sql); assert($stmt !== false, 'PDO::ATTR_ERRMODE should be PDO::ERRMODE_EXCEPTION'); $stmt->bindValue(':startAt', $startAt->format(self::TIMESTAMP_FORMAT), \PDO::PARAM_STR); $stmt->bindValue(':endAt', $endAt->format(self::TIMESTAMP_FORMAT), \PDO::PARAM_STR); $stmt->bindValue(':status', $status->value, \PDO::PARAM_STR); $stmt->execute(); return $stmt->fetchAll(\PDO::FETCH_CLASS, Bug::class); } TUNUͷܕ͕1%04UBUFNFOUʹߜΒΕΔ ͜͜Ͱ͸TUNUͷܕ͕1%04UBUFNFOUcGBMTF
  19. Agenda  ܕએݴ  ྻڍܕ  ੩తղੳ  ܕͷϞσϦϯά 

    ෆมੑ  ׬શੑ  ੹຿ͷ഑ஔ 👉
  20. public function testFindAll(): void { $xmas2025 = new \DateTime('2025-12-25'); $xmas2026

    = new \DateTime('2026-12-25'); $bugs = $this->repo->findAll( $xmas2025, $xmas2026, Status::New ); $this->assertCount(3, $bugs); } ར༻ଆͷίʔυ Ҿ਺͕ͭ͋ΔͷͰ ͦΕͧΕͷ໾ׂ͕Θ͔Γʹ͍͘
  21. public function testFindAll(): void { $xmas2025 = new \DateTime('2025-12-25'); $xmas2026

    = new \DateTime('2026-12-25'); $bugs = $this->repo->findAll( startAt: $xmas2025, endAt: $xmas2026, status: Status::New ); $this->assertCount(3, $bugs); } ໊લ෇͖Ҿ਺ͰՄಡੑ͸޲্͕ͨ͠ɺ ݕࡧൣғʹFOE"Uؚ͕·ΕΔͷ͔ ؚ·Εͳ͍ͷ͔͕఻ΘΒͳ͍ ໊લ෇͖Ҿ਺ͰՄಡੑΛ্͛Δ 1 ) 1 
  22. public function testFindAll(): void { $xmas2025 = new \DateTime('2025-12-25'); $xmas2026

    = new \DateTime('2026-12-25'); $bugs = $this->repo->findAll( startAt: $xmas2025, endAtExclusive: $xmas2026, status: Status::New ); $this->assertCount(3, $bugs); } FOE"Uؚ͕·ΕΔ͔Ͳ͏͔Λ໊લͰࣔͯ͠΋ͬ͘͠Γ͜ͳ͍ Ҿ਺ͷ໊લͰࣔͯ͠΋ྑ͍͕ɺ ͜ͷઃܭ͸ৗʹద੾ͩΖ͏͔ʁ TUBS"Uͷํ͸Ͳ͏ͳΔͩΖ͏͔ʁ
  23. final readonly class DateTimeEndpoint { public function __construct( public \DateTime

    $value, public bool $inclusive, ) {} } final readonly class DateTimeRange { public function __construct( public DateTimeEndpoint $startAt, public DateTimeEndpoint $endAt, ) {} } fi OBMSFBEPOMZDMBTTͱ $POTUSVDUPS1SPQFSUZ1SPNPUJPO ͷ૊Έ߹Θͤ͸1)1ʹ͓͚Δ3FDPSEܕͷ Α͏ͳ΋ͷͱݴ͑ͦ͏ লུͤ͞ͳ͍ʢσϑΥϧτ஋Λ༻ҙ͠ͳ͍ʣ͜ͱͰ ୺఺Λҙࣝ͠΍͘͢ͳΔ جૅͱͳΔܕΛ͍ͭͬͯ͘͘ 1 ) 1   fi OBMͰܧঝΛېࢭ͢Δ SFBEPOMZͰϓϩύςΟมߋΛېࢭ͢Δ
  24. ͜ΕͰؒҧ͍ʹ͘͘ͳ͕ͬͨɺࠓ౓͸࢖͏ͷ͕΍΍໘౗ʜʜ public function testFindAll(): void { $startAt = new DateTimeEndpoint(

    value: new \DateTime('2025-12-25'), inclusive: true ); $endAt = new DateTimeEndpoint( value: new \DateTime('2026-12-25'), inclusive: false ); $range = new DateTimeRange($startAt, $endAt); $bugs = $this->repo->findAll(searchRange: $range, status: Status::New); $this->assertCount(3, $bugs); }
  25. खݎ͞ͱॻ͖΍͢͞ͷཱ྆ΛࢼΈΑ͏ final readonly class DateTimeEndpoint { (中略) public static function

    including(string $dateTimeStr): DateTimeEndpoint { return new DateTimeEndpoint( value: new \DateTime($dateTimeStr), inclusive: true ); } public static function excluding(string $dateTimeStr): DateTimeEndpoint { return new DateTimeEndpoint( value: new \DateTime($dateTimeStr), inclusive: false ); } } ෼͔Γ΍໊͍͢લͷϑΝΫτϦʔϝιουΛఆٛ ෼͔Γ΍໊͍͢લͷϑΝΫτϦʔϝιουΛఆٛ
  26. ͜ΕͰར༻ଆΛ୹͘ॻ͚ΔΑ͏ʹͳͬͨ public function testFindAll(): void { $range = new DateTimeRange(

    startAt: DateTimeEndpoint::including('2025-12-25'), endAt: DateTimeEndpoint::excluding('2026-12-25') ); $bugs = $this->repo->findAll(searchRange: $range, status: Status::New); $this->assertCount(3, $bugs); } ୹͘ॻ͚ΔΑ͏ʹͳͬͨ
  27. Agenda  ܕએݴ  ྻڍܕ  ੩తղੳ  ܕͷϞσϦϯά 

    ෆมੑ  ׬શੑ  ੹຿ͷ഑ஔ 👉
  28. 4VCTDSJQUJPOΦϒδΣΫτΛྫʹผ໊ࢀর໰୊ΛֶͿ final readonly class Subscription implements \Stringable { public function

    __construct( private string $name, private DateTimeRange $range, ) { } public function __toString(): string { $startAt = $this->range->startAt->value; $endAt = $this->range->endAt->value; return $this->name . '(' . $startAt->format('Y-m-d') . ' -> ' . $endAt->format('Y-m-d') . ')'; } public function renew(): void { $oneYear = \DateInterval::createFromDateString('1 year'); $this->range->startAt->value->add($oneYear); $this->range->endAt->value->add($oneYear); } } ઌ΄Ͳ࡞੒ͨ͠%BUF5JNF3BOHFΦϒδΣΫτΛ࢖ͬͯΈΔ # " % αϒεΫϦϓγϣϯܖ໿Λ೥ߋ৽͢Δϝιου
  29. $year2025 = new DateTimeRange( startAt: DateTimeEndpoint::including('2025-01-01'), endAt: DateTimeEndpoint::excluding('2026-01-01') ); $phpstorm

    = new Subscription('PhpStorm', $year2025); echo $phpstorm, PHP_EOL; // PhpStorm(2025-01-01 -> 2026-01-01) $oreilly = new Subscription('O\'Reilly', $year2025); echo $oreilly, PHP_EOL; // O'Reilly(2025-01-01 -> 2026-01-01) $phpstorm->renew(); echo $phpstorm, PHP_EOL; // PhpStorm(2026-01-01 -> 2027-01-01) echo $oreilly, PHP_EOL; // O'Reilly(2026-01-01 -> 2027-01-01) echo $year2025->startAt->value->format('Y-m-d H:i:s'), PHP_EOL; // 2026-01-01 00:00:00 echo $year2025->endAt->value->format('Y-m-d H:i:s'), PHP_EOL; // 2027-01-01 00:00:00 ผ໊ࢀর໰୊ 03FJMMZͷܖ໿ظؒ΋ ߋ৽͞Εͯ͠·͍ͬͯΔ  1IQ4UPSNͷαϒεΫϦϓγϣϯܖ໿Λ೥ߋ৽͢Δ
  30. Կ͕ݪҼͩͬͨͷ͔ # " % ͕͜͜໰୊ ࠶୅ೖͰ͸ͳ͘ͱ΋ഁյతมߋ͕ग़དྷͯ͠·͏ final readonly class Subscription

    implements \Stringable { public function __construct( private string $name, private DateTimeRange $range, ) { } public function __toString(): string { $startAt = $this->range->startAt->value; $endAt = $this->range->endAt->value; return $this->name . '(' . $startAt->format('Y-m-d') . ' -> ' . $endAt->format('Y-m-d') . ')'; } public function renew(): void { $oneYear = \DateInterval::createFromDateString('1 year'); $this->range->startAt->value->add($oneYear); $this->range->endAt->value->add($oneYear); } }
  31. 1)1ͷ%BUF5JNFΫϥε͕ഁյతมߋΛڐͯ͠͠·͏ 1)1ͷ%BUF5JNFΫϥε͕ഁյతมߋΛ ڐͯ͠͠·͍ͬͯͨɻ ՄมΦϒδΣΫτ΁ͷࢀর͕औಘͰ͖Δͱ fi OBMSFBEPOMZͰ͋ͬͯ΋ঢ়ଶΛมߋͰ͖ͯ͠·͏ # " % final

    readonly class DateTimeEndpoint { public function __construct( public \DateTime $value, public bool $inclusive, ) {} final readonly class DateTimeRange { public function __construct( public DateTimeEndpoint $startAt, public DateTimeEndpoint $endAt, ) {} (後略)
  32. use PhpconFuk\PHP8_2\Immutable; #[Immutable] final readonly class DateTimeEndpoint { public function

    __construct( public \DateTime $value, public bool $inclusive, ) {} public static function including(string $dateTimeStr): DateTimeEndpoint { return new DateTimeEndpoint( value: new \DateTime($dateTimeStr), inclusive: true ); } public static function excluding(string $dateTimeStr): DateTimeEndpoint { return new DateTimeEndpoint( value: new \DateTime($dateTimeStr), inclusive: false ); } } .VUBCMFͳ%BUF5JNF&OEQPJOU # " %
  33. use PhpconFuk\Immutable; #[Immutable] final readonly class DateTimeEndpoint { public function

    __construct( public \DateTimeImmutable $value, public bool $inclusive, ) {} public static function including(string $dateTimeStr): DateTimeEndpoint { return new DateTimeEndpoint( value: new \DateTimeImmutable($dateTimeStr), inclusive: true ); } public static function excluding(string $dateTimeStr): DateTimeEndpoint { return new DateTimeEndpoint( value: new \DateTimeImmutable($dateTimeStr), inclusive: false ); } } *NNVUBCMFͳ%BUF5JNF&OEQPJOU %BUF5JNF*NNVUBCMFΛ࢖͍ͬͯ͘ %BUF5JNF*NNVUBCMFΛ࢖͍ͬͯ͘
  34. namespace PhpconFuk; use Attribute; #[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] class Immutable {}

    Ԡ༻ಠࣗͷ"UUSJCVUFΛఆٛͯ͠ҙຯ͚ͮΛ͠Α͏ IUUQTXXXBNB[PODPKQEQ *NNVUBCMFͰ͋Δ͜ͱΛࣔ͢ ΞτϦϏϡʔτΛಠࣗఆٛ͢Δ
  35. use PhpconFuk\Immutable; #[Immutable] final readonly class DateTimeEndpoint { public function

    __construct( public \DateTimeImmutable $value, public bool $inclusive, ) {} public static function including(string $dateTimeStr): DateTimeEndpoint { return new DateTimeEndpoint( value: new \DateTimeImmutable($dateTimeStr), inclusive: true ); } public static function excluding(string $dateTimeStr): DateTimeEndpoint { return new DateTimeEndpoint( value: new \DateTimeImmutable($dateTimeStr), inclusive: false ); } } *NNVUBCMFͰ͋Δͱʢ1)1Ͱ͸ͳ͘νʔϜʹʣ఻͑Δ ͖͞΄Ͳఆٛͨ͠*NNVUBCMF ΞτϦϏϡʔτΛ࢖͏
  36. use PhpconFuk\Immutable; #[Immutable] final readonly class DateTimeEndpoint { public function

    __construct( public \DateTimeImmutable $value, public bool $inclusive, ) {} #[\NoDiscard] public function withValue(\DateTimeImmutable $newValue): DateTimeEndpoint { return clone($this, ["value" => $newValue]); } (後略) ෆมΦϒδΣΫτͷΫϩʔϯੜ੒ 1)1Ͱ͸ෆมΦϒδΣΫτͷҰ෦͚ͩͷ஋Λ มߋͨ͠ΫϩʔϯΛ࡞ΕΔΑ͏ʹͳͬͨ 1)1Ͱ௥Ճ͞Εͨ ໭Γ஋Λແࢹͤ͞ͳ͍ /P%JTDBSEΞτϦϏϡʔτΛ෇༩ 1 ) 1  
  37. final readonly class Subscription implements \Stringable { public function __construct(

    private string $name, private DateTimeRange $range, ) { } (中略) public function renew(): void { $oneYear = \DateInterval::createFromDateString('1 year'); $this->range->startAt->value->add($oneYear); $this->range->endAt->value->add($oneYear); } .VUBCMFͳ4VCTDSJQUJPO %BUF5JNFΦϒδΣΫτͷ ഁյతมߋΛલఏʹ͍ͯͨ͠ มߋલ͸%BUF5JNF3BOHF͕อ࣋͢Δ %BUF5JNF&OEQPJOU͕NVUBCMFͩͬͨ # " %
  38. use PhpconFuk\Immutable; #[Immutable] final readonly class Subscription implements \Stringable {

    public function __construct( private string $name, private DateTimeRange $range, ) { } (中略) #[\NoDiscard] public function renew(): Subscription { $oneYear = \DateInterval::createFromDateString('1 year'); $startAt = $this->range->startAt; $endAt = $this->range->endAt; return new Subscription( name: $this->name, range: new DateTimeRange( startAt: $startAt->withValue($startAt->value->add($oneYear)), endAt: $endAt->withValue($endAt->value->add($oneYear)) ) ); } *NNVUBCMFͳ4VCTDSJQUJPO ࣗ਎ͷঢ়ଶΛมߋͤͣɺ ৽͍͠ঢ়ଶΛอ࣋ͨ͠ෆมΦϒδΣΫτΛฦ͢ %BUF5JNF3BOHF͕อ࣋͢Δ %BUF5JNF&OEQPJOU͕อ࣋͢Δ೔෇ܕ͕ %BUF5JNF*NNVUBCMFʹมߋ͞Εͨ ໭Γ஋Λແࢹͤ͞ͳ͍ /P%JTDBSEΞτϦϏϡʔτΛ෇༩ 1 ) 1  
  39. $year2025 = new DateTimeRange( startAt: DateTimeEndpoint::including('2025-01-01'), endAt: DateTimeEndpoint::excluding('2026-01-01') ); $phpstorm

    = new Subscription('PhpStorm', $year2025); echo $phpstorm, PHP_EOL; // PhpStorm(2025-01-01 -> 2026-01-01) $oreilly = new Subscription('O\'Reilly', $year2025); echo $oreilly, PHP_EOL; // O'Reilly(2025-01-01 -> 2026-01-01) // Warning: The return value of method PhpconFuk\Subscription::renew() should either be used or intentionally ignored by casting it as (void) in /path/to/demo_aliasing.php $phpstorm->renew(); echo $oreilly, PHP_EOL; // O'Reilly(2025-01-01 -> 2026-01-01) echo $year2025->startAt->value->format('Y-m-d H:i:s'), PHP_EOL; // 2025-01-01 00:00:00 echo $year2025->endAt->value->format('Y-m-d H:i:s'), PHP_EOL; // 2026-01-01 00:00:00 ෆมΦϒδΣΫτʹΑͬͯผ໊ࢀর໰୊Λղܾ ໭Γ஋Λແࢹ͢Δͱ/P%JTDBSEΞτϦϏϡʔτʹΑͬͯܯࠂ͕ग़Δ
  40. $year2025 = new DateTimeRange( startAt: DateTimeEndpoint::including('2025-01-01'), endAt: DateTimeEndpoint::excluding('2026-01-01') ); $phpstorm

    = new Subscription('PhpStorm', $year2025); echo $phpstorm, PHP_EOL; // PhpStorm(2025-01-01 -> 2026-01-01) $oreilly = new Subscription('O\'Reilly', $year2025); echo $oreilly, PHP_EOL; // O'Reilly(2025-01-01 -> 2026-01-01) $phpstorm2026 = $phpstorm->renew(); echo $phpstorm2026, PHP_EOL; // PhpStorm(2026-01-01 -> 2027-01-01) echo $phpstorm, PHP_EOL; // PhpStorm(2025-01-01 -> 2026-01-01) echo $oreilly, PHP_EOL; // O'Reilly(2025-01-01 -> 2026-01-01) echo $year2025->startAt->value->format('Y-m-d H:i:s'), PHP_EOL; // 2025-01-01 00:00:00 echo $year2025->endAt->value->format('Y-m-d H:i:s'), PHP_EOL; // 2026-01-01 00:00:00 ෆมΦϒδΣΫτʹΑͬͯผ໊ࢀর໰୊Λղܾ 👍 ৽ͨͳαϒεΫϦϓγϣϯظؒΛ൐͏৽ͨͳΦϒδΣΫτΛฦ͢
  41. Agenda  ܕએݴ  ྻڍܕ  ੩తղੳ  ܕͷϞσϦϯά 

    ෆมੑ  ׬શੑ  ੹຿ͷ഑ஔ 👉
  42. public function findAll(DateTimeRange $searchRange, Status $status): array { $startAt =

    $searchRange->startAt->value; $endAt = $searchRange->endAt->value; if ($endAt < $startAt) { throw new \InvalidArgumentException('endAt < startAt'); } $startAtOp = $searchRange->startAt->inclusive ? '<=' : '<'; $endAtOp = $searchRange->endAt->inclusive ? '<=' : '<'; $sql = "SELECT bug_id, summary, reported_at FROM Bugs WHERE status = :status AND :startAt ${startAtOp} reported_at AND reported_at ${endAtOp} :endAt"; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':status', $status->value, \PDO::PARAM_STR); $stmt->bindValue(':startAt', $startAt->format(self::TIMESTAMP_FORMAT), \PDO::PARAM_STR); $stmt->bindValue(':endAt', $endAt->format(self::TIMESTAMP_FORMAT), \PDO::PARAM_STR); $stmt->execute(); return $stmt->fetchAll(\PDO::FETCH_CLASS, Bug::class); } ͜͜Ͱ΍Δͷ͸ے͕ѱ͍ fi OE"MMͷೖΓޱͰߦ͏ͷ͸ے͕ѱ͍
  43. খ͍͞ํͷ஋ΛԼ୺఺ɺେ͖͍ํͷ஋Λ্୺఺ʹ͢Ε͹Τϥʔ৚݅͸ͳ͍ final readonly class DateTimeRange { private function __construct( public

    DateTimeEndpoint $startAt, public DateTimeEndpoint $endAt, ) { } public static function createFromEndpoints( DateTimeEndpoint $endpoint1, DateTimeEndpoint $endpoint2 ): self { return $endpoint1->value <= $endpoint2->value ? new self($endpoint1, $endpoint2) : new self($endpoint2, $endpoint1); } }
  44. ࣄલ৚݅ར༻ଆͷ੹೚Ͱ͋Δ৔߹͸ද໌Λ࢖͏ final readonly class DateTimeRange { /** * 与えられた下端点と上端点からDateTimeRangeを生成します。 *

    上端点は下端点以上である必要があります。 * @param DateTimeEndpoint $startAt 下端点 * @param DateTimeEndpoint $endAt 上端点 */ public function __construct( public DateTimeEndpoint $startAt, public DateTimeEndpoint $endAt, ) { assert($startAt->value <= $endAt->value, '上端点は下端点以上である必要があります'); } JOWBSJBOU ৗʹ੒Γཱͭ΂͖ෆม৚݅ ΛࣜͰॻ͘ ࣄલ৚݅Λ͖ͪΜͱ఻͑Δ͜ͱ
  45. ද໌Λ࢖Θͳ͍৔߹͸ɺར༻ଆͷ੹೚Ͱ͋Δ͜ͱΛࣔ͢ྫ֎Λ࢖͏ final readonly class DateTimeRange { /** * 与えられた下端点と上端点からDateTimeRangeを生成します。 *

    @param DateTimeEndpoint $startAt 下端点 * @param DateTimeEndpoint $endAt 上端点 * @throws \InvalidArgumentException 下端点が上端点より大きい場合 */ public function __construct( public DateTimeEndpoint $startAt, public DateTimeEndpoint $endAt, ) { if ($startAt->value > $endAt->value) { throw new \InvalidArgumentException('startAt > endAt'); } ར༻ଆͷޡΓͰ͋Δࢫͷྫ֎Λൃੜͤ͞Δ ࣄલ৚݅Λ͖ͪΜͱ఻͑Δ͜ͱ
  46. Agenda  ܕએݴ  ྻڍܕ  ੩తղੳ  ܕͷϞσϦϯά 

    ෆมੑ  ׬શੑ  ੹຿ͷ഑ஔ 👉
  47. final readonly class DateTimeEndpoint { public function __construct( public \DateTimeImmutable

    $value, public bool $inclusive, ) {} (中略) public static function including(string $dateTimeStr): DateTimeEndpoint { return new DateTimeEndpoint( value: new \DateTimeImmutable($dateTimeStr), inclusive: true ); } public static function excluding(string $dateTimeStr): DateTimeEndpoint { return new DateTimeEndpoint( value: new \DateTimeImmutable($dateTimeStr), inclusive: false ); } } ઃܭͷෆ٢ͳष͍ʹؾ͍ͮͯ͠·ͬͨ # " % จࣈྻ͔Β%BUF5JNF*NNVUBCMF΁ͷม׵ʹ͸ ๲େͳ૊Έ߹Θ͕ͤ͋Δ͠ɺ λΠϜκʔϯͷѻ͍΋ෆे෼ʹͳͬͯ͠·͍ͬͯΔɻ จࣈྻҾ਺ʹλΠϜκʔϯͷΦϑηοτදهΛؚΊ ͳ͍ͱσϑΥϧτͷλΠϜκʔϯʹͳͬͯ͠·͏  ͭ·Γ͜͜Ͱߦ͏ͷ͸ෆద੾ɻ จࣈྻ͔Β%BUF5JNF*NNVUBCMF΁ͷม׵ʹ͸ ๲େͳ૊Έ߹Θ͕ͤ͋Δ͠ɺ λΠϜκʔϯͷѻ͍΋ෆे෼ʹͳͬͯ͠·͍ͬͯΔɻ จࣈྻҾ਺ʹλΠϜκʔϯͷΦϑηοτදهΛؚΊ ͳ͍ͱσϑΥϧτͷλΠϜκʔϯʹͳͬͯ͠·͏  ͭ·Γ͜͜Ͱߦ͏ͷ͸ෆద੾ɻ
  48. public function testFindAll(): void { $range = new DateTimeRange( startAt:

    DateTimeEndpoint::including('2025-12-25'), endAt: DateTimeEndpoint::excluding('2026-12-25') ); $bugs = $this->repo->findAll(searchRange: $range, status: Status::New); $this->assertCount(3, $bugs); } ࢖͏ͷ͕΍΍໘౗ͩͬͨͷͰɺ୹͘ॻ͚ΔศརͳϝιουΛఏڙ͔ͨͬͨ͠ͷͩͬͨ ༰қ͞ʹدΓ͗ͯ͢ɺ ँΓ΍͢͞΋্͕ͬͯ͠·͍ͬͯͳ͍ͩΖ͏͔ ςετίʔυͰ୹͘ॻ͖͍ͨͷͰ༰қ͞ʢ&BTZʣدΓʹόΠΞε͕͔͔ͬͯ͠·ͬͨɻ ςετίʔυͷେ෦෼Ͱ͸ಛʹλΠϜκʔϯΛҙࣝͤͣ୹͘ॻ͍͍͖͍͕ͯͨɺ ίϯτϩʔϥͰ͸λΠϜκʔϯΛҙࣝͯ͠ݫີʹѻ͍͍ͨɻ ςετίʔυ΋ͻͱͭͷίϯςΫετɻ ςετ͔ΒઃܭΛۦಈ͢Δͱ͖ʹ͜ͷόΠΞεʹ஫ҙ͢Δඞཁ͕͋Δɻ # " %
  49. final readonly class DateTimeEndpoint { public function __construct( public \DateTimeImmutable

    $value, public bool $inclusive, ) {} (中略) public static function including(string $dateTimeStr): DateTimeEndpoint { return new DateTimeEndpoint( value: new \DateTimeImmutable($dateTimeStr), inclusive: true ); } public static function excluding(string $dateTimeStr): DateTimeEndpoint { return new DateTimeEndpoint( value: new \DateTimeImmutable($dateTimeStr), inclusive: false ); } } 4JNQMFͱ&BTZ͕ࠞͬͯ͟͠·͍ͬͯͨ Simple Easy # " %
  50. final readonly class DateTimeEndpoint { public function __construct( public \DateTimeImmutable

    $value, public bool $inclusive, ) {} (中略) public static function including(\DateTimeImmutable $value): DateTimeEndpoint { return new DateTimeEndpoint( value: $value, inclusive: true ); } public static function excluding(\DateTimeImmutable $value): DateTimeEndpoint { return new DateTimeEndpoint( value: $value, inclusive: false ); } } جૅͱͳΔܕ͸4JNQMF͞Λอͭ จࣈྻ͔Βͷม׵ΛѻΘͳ͍ Simple 4 JN Q MF Simple
  51. final class DateTimeShorthandHelper { public static function jst(string $dateTimeStr): \DateTimeImmutable

    { return new \DateTimeImmutable($dateTimeStr, new \DateTimeZone('Asia/Tokyo')); } public static function utc(string $dateTimeStr): \DateTimeImmutable { return new \DateTimeImmutable($dateTimeStr, new \DateTimeZone('UTC')); } } &BTZ͞͸ผͷϨΠϠʔͰఏڙ͢Δɻྫ͑͹ςετ༻ͷϔϧύʔ &BTZ
  52. use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\Test; use PhpconFuk\DateTimeShorthandHelper as DT; class DateTimeRangeTest

    extends TestCase { #[Test] public function dateTimeShorthand(): void { $range = new DateTimeRange( startAt: DateTimeEndpoint::including(DT::jst('2025-12-25')), endAt: DateTimeEndpoint::excluding(DT::jst('2026-12-25')) ); $this->assertSame('2025-12-25', $range->startAt->value->format('Y-m-d')); $this->assertSame('2026-12-25', $range->endAt->value->format('Y-m-d')); } } &BTZ͞͸ผͷϨΠϠʔͰఏڙ͢Δɻྫ͑͹ςετ༻ͷϔϧύʔ ςετίʔυ্Ͱ4JNQMFͱ&BTZΛ߹ྲྀͤ͞Δɻ ͜͏ॻ͚Ε͹े෼ͩͬͨɻ
  53. ͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ w ݎ࿚ͳίʔυͱ͸ѱ͍ίʔυʹឺ૑ߣΛ͋ͯΔ͜ͱͰ͸ͳ͍ w ܕએݴͰ૝ఆ͢΂͖ೖྗͷܕΛߜΓɺ๷ޚతνΣοΫΛେ෯ʹݮΒ͢ w ྻڍܕͰऔΓಘΔ஋ΛߜΓɺ๷ޚతνΣοΫΛେ෯ʹݮΒ͢ w ੩తղੳπʔϧΛ࢖͍ɺίʔυΛಈ͔ͣ͞ͱ΋ݕূͰ͖ΔΑ͏ʹ͢Δ w

    جૅͱͳΔܕΛϞσϦϯά͠ɺᐆດ͞΍ؒҧ͍΍͢͞Λ࡟ݮ͢Δ w جૅͱͳΔܕΛෆมΦϒδΣΫτʹ͠ɺঢ়ଶมԽ΍෭࡞༻ʹىҼ͢ΔόάΛ༧๷͢Δ w ΦϒδΣΫτੜ੒࣌ʹෆม৚݅Λ੒ཱͤ͞ɺ͔ͭͦͷΦϒδΣΫτ͕ෆมΦϒδΣΫτͰ͋Δͳ Β͹ɺੜ੒͞ΕͨΦϒδΣΫτ͸ৗʹਖ਼͍͠ʢ׬શੑʣ w 4JNQMF͞ʢ֓೦ͱͯ͠ͷཁૉͷগͳ͞ʣͱ&BTZ͞ʢख਺ͷগͳ͞ʣΛෆ༻ҙʹࠞͥͳ͍ w ݕূࡁΈͷ஋Λܕͱͯ͠දݱ͠ɺͦͷޙ͸ܕͷอূʹཔΔ w ઃܭͱ͸੹຿ͷ࠷ద഑ஔΛٻΊଓ͚Δ͜ͱɻ୭͕ԿΛ஌͍ͬͯͯԿΛ஌Δ΂͖Ͱͳ͍͔ɺԿΛ΍ Δ΂͖ͰԿΛ΍Δ΂͖Ͱͳ͍͔Λৗʹߟ͑ଓ͚Δ͜ͱ