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

予防に勝る防御なし - 堅牢なコードを導く様々な設計のヒント / Growing Reliab...

予防に勝る防御なし - 堅牢なコードを導く様々な設計のヒント / Growing Reliable Code PHPerKaigi 2022

PHPerKaigi 2022
2022/04/10 10:40〜 Track A レギュラートーク(40分)

PHP はバージョンを追う毎に型宣言、例外、表明、列挙型などの機能が大幅に強化され、堅牢なコードを書くための機能が充実してきました。それらの機能はどう使うと効果的なのでしょうか。

本講演では PHP 8.1 をベースにして、誤りを想定してチェックするのではなく、そもそも誤りにくい設計とはどのようなものか、つまり「予防」の観点を軸足に、堅牢なコードを導くための様々な設計のヒントをご紹介します。

Agenda
- 型宣言
- 列挙型
- ドメインモデリング
- 不変性と等価性
- 完全性
- レイヤーと責務

Takuto Wada

April 10, 2022
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. $bugs = $repo->findAll([ 'startAt' => '2021-01-01', 'endAt' => '2022-01-01', 'status'

    => 'OPEN' ]); print_r($bugs); $ php example.php Array ( [0] => PhperKaigi\Bug Object ( [bug_id] => 42 [summary] => 保存処理でクラッシュする [reported_at] => 2021-12-24 17:14:42.246896+00 ) [1] => PhperKaigi\Bug Object ( [bug_id] => 43 [summary] => XMLのサポート [reported_at] => 2021-12-25 11:53:18.203906+00 ) [2] => PhperKaigi\Bug Object ( [bug_id] => 44 [summary] => パフォーマンスの向上 [reported_at] => 2021-12-27 13:24:19.251937+00 ) ) ࣮ߦ͢Δͱɺ͜Μͳ݁Ռ
  3. ॲཧࣦഊͷݪҼʹͳΔՄೳੑ͕͋Δͷ͸Ͳͷߦʁ # " % 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); }
  4. ॲཧࣦഊͷݪҼʹͳΔՄೳੑ͕͋Δͷ͸Ͳͷߦʁ # " % 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); } 🙅ςʔϒϧ໊΍ΧϥϜ໊͕୭͔ʹมߋ͞Εͨ 🙅 ͜͜Ͱ σʔλϕʔε઀ଓΤϥʔ
  5. ॲཧࣦഊͷݪҼʹͳΔՄೳੑ͕͋Δͷ͸Ͳͷߦʁ # " % 🙅QBSBNT͕OVMM 🙅QBSBNTͷΩʔ໊΍਺ͷෆҰக 🙅QBSBNTͷ஋͕จࣈྻʹม׵ෆೳ 🙅QBSBNTͷ஋͕೔࣌ͱͯ͠ղऍͰ͖ͳ͍ 🙅 ͜͜Ͱ

    σʔλϕʔε઀ଓΤϥʔ 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); }
  6. ॲཧࣦഊͷݪҼʹͳΔՄೳੑ͕͋Δͷ͸Ͳͷߦʁ # " % 🙅#VHΫϥε͕ະఆٛ 🙅 ͜͜Ͱ σʔλϕʔε઀ଓΤϥʔ 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); }
  7. 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); } ॲཧࣦഊͷݪҼʹͳΔՄೳੑ͕͋Δͷ͸Ͳͷߦʁ # " % ͚ͬ͜͏͋Δͳ😇 ᶃ ᶄ ᶅ
  8. 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"'); } ͨͩͻͨ͢ΒೖྗΛνΣοΫ͠Α͏ͱͨ͠Γ # " %
  9. ෆਖ਼ͳೖྗ͕͋ͬͯ΋ࣗ෼ͰԿͱ͔͠Α͏ͱͨ͠Γ # " % $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);
  10. υΩϡϝϯτͰؒҧ͍΍͢͞Λิ͓͏ͱͨ͠Γ # " % /** * 指定した範囲の日時およびステータスに合致する Bug を検索し、ヒットした全件を Bug

    オブジェクトの配列として返す。 * * @param array $params 格納した検索条件の連想配列。キー "startAt" に検索範囲の 始点日時をstringで, キー "endAt" に検索範囲の終点日時をstringで, キー "status" にス テータス文字列をstringで指定すること。キー、値それぞれNULLは不可とする。 * @return Bug[] 検索結果を Bug オブジェクトにマッピングして返す。検索結果が0件 のときは空配列を返す。 */ public function findAll($params) {
  11. w ʮ๷ޚతϓϩάϥϛϯάʯͱ͸ɺ 
 ໰୊ൃੜΛࣄલʹ๷͝͏ͱ͍͏ίʔσΟϯάελΠϧ w Մಡੑͷߴ͍ίʔυͱద੾ͳ໋໊نଇ w શͯͷؔ਺ͷ໭Γ஋ΛνΣοΫ w σβΠϯύλʔϯͷ࠾༻

    w ཁ͢Δʹɺྑࣝ͋Δ࣮ફͷੵΈॏͶͰ͋Δ w ๷ޚతϓϩάϥϛϯά͸ɺਖ਼͍͠ίʔυ࡞੒ͷͨΊͷن཯Λ ϓϩάϥϚ͕Ұ؏ͯ͠ద༻͢ΔͨΊͷҰछͷίʔσΟϯάඪ४ ๷ޚతϓϩάϥϛϯάͱ͸ྑࣝ͋Δ࣮ફͷੵΈॏͶ IUUQTXXXBNB[PODPKQEQ
  12. 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"'); } ໰୊ͱͳ͍ͬͯΔ͜ͷ࿈૝഑ྻΛ ݱঢ়
  13. 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"'); } ͦΕͧΕܕએݴ͞ΕͨҾ਺ʹ෼ղ͢Δ ܕએݴʹΑͬͯʮग़དྷ͍͍ͯ͜ͱ͚ͩΛग़དྷΔʯΑ͏ʹ
  14. 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"'); } ๷ޚతνΣοΫ͕΄΅ෆཁʹ
  15. ૝ఆ͠ͳ͚Ε͹ͳΒͳ͍ঢ়گ͕ݮͬͨ 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ܕͷΧϥϜʹ֨ೲ͍ͯ͠Δͱߟ͍͑ͯͩ͘͞
  16. ྻڍܕΛ࢖͓͏ 1 ) 1   enum Status: string {

    case Open = 'OPEN'; case New = 'NEW'; case Fixed = 'FIXED'; } IUUQTXXXQIQOFUNBOVBMKBMBOHVBHFFOVNFSBUJPOTQIQ
  17. 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); } ݱঢ়
  18. 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); } Ҿ਺Λจࣈྻ͔Βྻڍܕʹม͑Δ จࣈྻ͔Βྻڍܕʹม͑Δ
  19. 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); } ૝ఆ͠ͳ͚Ε͹ͳΒͳ͍ঢ়گ͕͞Βʹݮͬͨ
  20. public function testFindAll(): void { $xmas2020 = new \DateTime('2020-12-25'); $xmas2021

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

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

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

    $startAt, public readonly DateTimeEndpoint $endAt, ) {} } final class DateTimeEndpoint { public function __construct( public readonly \DateTime $value, public readonly bool $inclusive, ) {} } 1)1Ͱ͸ $POTUSVDUPS1SPQFSUZ1SPNPUJPOͱ 3FBEPOMZ1SPQFSUJFTͷ૊Έ߹ΘͤͰ γϯϓϧʹॻ͚Δ লུͤ͞ͳ͍ʢσϑΥϧτ஋Λ༻ҙ͠ͳ͍ʣ͜ͱͰ ୺఺Λҙࣝ͠΍͘͢ͳΔ جૅͱͳΔܕΛ͍ͭͬͯ͘͘ 1 ) 1  
  24. ͜ΕͰؒҧ͍ʹ͘͘ͳ͕ͬͨɺࠓ౓͸࢖͏ͷ͕΍΍໘౗ʜʜ public function testFindAll(): void { $startAt = new DateTimeEndpoint(

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

    \DateTime $value, public readonly 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 ); } } ʢ ෬ ઢ ʣ
  26. ͜ΕͰར༻ଆΛ୹͘ॻ͚ΔΑ͏ʹͳͬͨ public function testFindAll(): void { $range = new DateTimeRange(

    startAt: DateTimeEndpoint::including('2020-12-25'), endAt: DateTimeEndpoint::excluding('2021-12-25') ); $bugs = $this->repo->findAll(searchRange: $range, status: Status::New); $this->assertCount(3, $bugs); } ʢ ෬ ઢ ʣ
  27. 4VCTDSJQUJPOΦϒδΣΫτΛྫʹผ໊ࢀর໰୊ΛֶͿ class Subscription implements \Stringable { public function __construct( private

    readonly string $name, private readonly 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ΦϒδΣΫτΛ࢖ͬͯΈΔ # " % αϒεΫϦϓγϣϯܖ໿Λ೥ߋ৽͢Δϝιου
  28. $year2021 = new DateTimeRange( startAt: DateTimeEndpoint::including('2021-01-01'), endAt: DateTimeEndpoint::excluding('2022-01-01') ); $phpstorm

    = new Subscription('PhpStorm', $year2021); echo $phpstorm->toString() . PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) $pycharm = new Subscription('PyCharm', $year2021); echo $pycharm->toString() . PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) $phpstorm->renew(); echo $phpstorm->toString() . PHP_EOL; // PhpStorm(2022-01-01 -> 2023-01-01) echo $pycharm->toString() . PHP_EOL; // PyCharm(2022-01-01 -> 2023-01-01) echo $year2021->startAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2022-01-01 00:00:00 echo $year2021->endAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2023-01-01 00:00:00 ผ໊ࢀর໰୊
  29. $year2021 = new DateTimeRange( startAt: DateTimeEndpoint::including('2021-01-01'), endAt: DateTimeEndpoint::excluding('2022-01-01') ); $phpstorm

    = new Subscription('PhpStorm', $year2021); echo $phpstorm, PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) $pycharm = new Subscription('PyCharm', $year2021); echo $pycharm->toString() . PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) $phpstorm->renew(); echo $phpstorm->toString() . PHP_EOL; // PhpStorm(2022-01-01 -> 2023-01-01) echo $pycharm->toString() . PHP_EOL; // PyCharm(2022-01-01 -> 2023-01-01) echo $year2021->startAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2022-01-01 00:00:00 echo $year2021->endAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2023-01-01 00:00:00 ผ໊ࢀর໰୊
  30. $year2021 = new DateTimeRange( startAt: DateTimeEndpoint::including('2021-01-01'), endAt: DateTimeEndpoint::excluding('2022-01-01') ); $phpstorm

    = new Subscription('PhpStorm', $year2021); echo $phpstorm, PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) $pycharm = new Subscription('PyCharm', $year2021); echo $pycharm->toString() . PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) $phpstorm->renew(); echo $phpstorm->toString() . PHP_EOL; // PhpStorm(2022-01-01 -> 2023-01-01) echo $pycharm->toString() . PHP_EOL; // PyCharm(2022-01-01 -> 2023-01-01) echo $year2021->startAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2022-01-01 00:00:00 echo $year2021->endAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2023-01-01 00:00:00 ผ໊ࢀর໰୊
  31. $year2021 = new DateTimeRange( startAt: DateTimeEndpoint::including('2021-01-01'), endAt: DateTimeEndpoint::excluding('2022-01-01') ); $phpstorm

    = new Subscription('PhpStorm', $year2021); echo $phpstorm, PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) $pycharm = new Subscription('PyCharm', $year2021); echo $pycharm, PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) $phpstorm->renew(); echo $phpstorm->toString() . PHP_EOL; // PhpStorm(2022-01-01 -> 2023-01-01) echo $pycharm->toString() . PHP_EOL; // PyCharm(2022-01-01 -> 2023-01-01) echo $year2021->startAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2022-01-01 00:00:00 echo $year2021->endAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2023-01-01 00:00:00 ผ໊ࢀর໰୊
  32. $year2021 = new DateTimeRange( startAt: DateTimeEndpoint::including('2021-01-01'), endAt: DateTimeEndpoint::excluding('2022-01-01') ); $phpstorm

    = new Subscription('PhpStorm', $year2021); echo $phpstorm, PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) $pycharm = new Subscription('PyCharm', $year2021); echo $pycharm, PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) $phpstorm->renew(); echo $phpstorm->toString() . PHP_EOL; // PhpStorm(2022-01-01 -> 2023-01-01) echo $pycharm->toString() . PHP_EOL; // PyCharm(2022-01-01 -> 2023-01-01) echo $year2021->startAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2022-01-01 00:00:00 echo $year2021->endAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2023-01-01 00:00:00 ผ໊ࢀর໰୊ 1IQ4UPSNͷαϒεΫϦϓγϣϯܖ໿Λ೥ߋ৽͢Δ
  33. $year2021 = new DateTimeRange( startAt: DateTimeEndpoint::including('2021-01-01'), endAt: DateTimeEndpoint::excluding('2022-01-01') ); $phpstorm

    = new Subscription('PhpStorm', $year2021); echo $phpstorm, PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) $pycharm = new Subscription('PyCharm', $year2021); echo $pycharm, PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) $phpstorm->renew(); echo $phpstorm, PHP_EOL; // PhpStorm(2022-01-01 -> 2023-01-01) echo $pycharm->toString() . PHP_EOL; // PyCharm(2022-01-01 -> 2023-01-01) echo $year2021->startAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2022-01-01 00:00:00 echo $year2021->endAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2023-01-01 00:00:00 ผ໊ࢀর໰୊
  34. $year2021 = new DateTimeRange( startAt: DateTimeEndpoint::including('2021-01-01'), endAt: DateTimeEndpoint::excluding('2022-01-01') ); $phpstorm

    = new Subscription('PhpStorm', $year2021); echo $phpstorm, PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) $pycharm = new Subscription('PyCharm', $year2021); echo $pycharm, PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) $phpstorm->renew(); echo $phpstorm, PHP_EOL; // PhpStorm(2022-01-01 -> 2023-01-01) echo $pycharm, PHP_EOL; // PyCharm(2022-01-01 -> 2023-01-01) echo $year2021->startAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2022-01-01 00:00:00 echo $year2021->endAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2023-01-01 00:00:00  ผ໊ࢀর໰୊ 1Z$IBSNͷܖ໿ظؒ΋ ߋ৽͞Εͯ͠·͍ͬͯΔ
  35. $year2021 = new DateTimeRange( startAt: DateTimeEndpoint::including('2021-01-01'), endAt: DateTimeEndpoint::excluding('2022-01-01') ); $phpstorm

    = new Subscription('PhpStorm', $year2021); echo $phpstorm, PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) $pycharm = new Subscription('PyCharm', $year2021); echo $pycharm, PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) $phpstorm->renew(); echo $phpstorm, PHP_EOL; // PhpStorm(2022-01-01 -> 2023-01-01) echo $pycharm, PHP_EOL; // PyCharm(2022-01-01 -> 2023-01-01) echo $year2021->startAt->value->format('Y-m-d H:i:s'), PHP_EOL; // 2022-01-01 00:00:00 echo $year2021->endAt->value->format('Y-m-d H:i:s'), PHP_EOL; // 2023-01-01 00:00:00 ผ໊ࢀর໰୊
  36. Կ͕ݪҼͩͬͨͷ͔ class Subscription implements \Stringable { public function __construct( private

    readonly string $name, private readonly 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); } } # " % ͕͜͜໰୊ ࠶୅ೖͰ͸ͳ͘ͱ΋ഁյతมߋ͕ग़དྷͯ͠·͏
  37. 1)1ͷ%BUF5JNFΫϥε͕ഁյతมߋΛڐͯ͠͠·͏ final class DateTimeEndpoint { public function __construct( public readonly

    \DateTime $value, public readonly bool $inclusive, ) {} } final class DateTimeRange { public function __construct( public readonly DateTimeEndpoint $startAt, public readonly DateTimeEndpoint $endAt, ) {} (後略) 1)1ͷ%BUF5JNFΫϥε͕ഁյతมߋΛڐͯ͠͠·͍ͬͯͨ # " %
  38. /** * @test * @group learning */ public function DateTimeのaddは自身の状態を変更しつつ自身を返す():

    void { $halloween = new \DateTime('2021-10-31'); $oneYear = \DateInterval::createFromDateString('1 year'); $halloween2022 = $halloween->add($oneYear); $this->assertSame($halloween, $halloween2022); $this->assertEquals('2022-10-31', $halloween->format('Y-m-d')); $this->assertEquals('2022-10-31', $halloween2022->format('Y-m-d')); } /** * @test * @group learning */ public function DateTimeImmutableのaddは自身の状態を変更せず新しい状態を伴う新しいインスタンスを返す(): void { $halloween = new \DateTimeImmutable('2021-10-31'); $oneYear = \DateInterval::createFromDateString('1 year'); $halloween2022 = $halloween->add($oneYear); $this->assertNotSame($halloween, $halloween2022); $this->assertEquals('2021-10-31', $halloween->format('Y-m-d')); $this->assertEquals('2022-10-31', $halloween2022->format('Y-m-d')); } %BUF5JNFͱ%BUF5JNF*NNVUBCMFͷҧ͍Λֶशςετʹ͢Δ ֶशςετʢֶͼ͕໨తͷςετʣΛ ݟ෼͚ΔͨΊʹ MFBSOJOHλάΛ͚͍ͭͯ·͢
  39. /** * @test * @group learning */ public function DateTimeのaddは自身の状態を変更しつつ自身を返す():

    void { $halloween = new \DateTime('2021-10-31'); $oneYear = \DateInterval::createFromDateString('1 year'); $halloween2022 = $halloween->add($oneYear); $this->assertSame($halloween, $halloween2022); $this->assertEquals('2022-10-31', $halloween->format('Y-m-d')); $this->assertEquals('2022-10-31', $halloween2022->format('Y-m-d')); } /** * @test * @group learning */ public function DateTimeImmutableのaddは自身の状態を変更せず新しい状態を伴う新しいインスタンスを返す(): void { $halloween = new \DateTimeImmutable('2021-10-31'); $oneYear = \DateInterval::createFromDateString('1 year'); $halloween2022 = $halloween->add($oneYear); $this->assertNotSame($halloween, $halloween2022); $this->assertEquals('2021-10-31', $halloween->format('Y-m-d')); $this->assertEquals('2022-10-31', $halloween2022->format('Y-m-d')); } %BUF5JNFͱ%BUF5JNF*NNVUBCMFͷҧ͍Λֶशςετʹ͢Δ
  40. final class DateTimeEndpoint { public function __construct( public readonly \DateTime

    $value, public readonly 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 # " %
  41. final class DateTimeEndpoint { public function __construct( public readonly \DateTimeImmutable

    $value, public readonly 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Λ࢖͍ͬͯ͘ ʢ ෬ ઢ ʣ
  42. class Subscription implements \Stringable { public function __construct( private readonly

    string $name, private readonly 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ͩͬͨ # " %
  43. class Subscription implements \Stringable { public function __construct( private readonly

    string $name, private readonly DateTimeRange $range, ) { } (中略) 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: new DateTimeEndpoint( value: $startAt->value->add($oneYear), inclusive: $startAt->inclusive ), endAt: new DateTimeEndpoint( value: $endAt->value->add($oneYear), inclusive: $endAt->inclusive ) ) ); } *NNVUBCMFͳ4VCTDSJQUJPO ࣗ਎ͷঢ়ଶΛมߋͤͣɺ ৽͍͠ঢ়ଶΛอ࣋ͨ͠ෆมΦϒδΣΫτΛฦ͢ %BUF5JNF3BOHF͕อ࣋͢Δ %BUF5JNF&OEQPJOU͕อ࣋͢Δ೔෇ܕ͕ %BUF5JNF*NNVUBCMFʹมߋ͞Εͨ
  44. $year2021 = new DateTimeRange( startAt: DateTimeEndpoint::including('2021-01-01'), endAt: DateTimeEndpoint::excluding('2022-01-01') ); $phpstorm

    = new Subscription('PhpStorm', $year2021); echo $phpstorm->toString() . PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) $pycharm = new Subscription('PyCharm', $year2021); echo $pycharm->toString() . PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) $phpstorm2022 = $phpstorm->renew(); echo $phpstorm2022->toString() . PHP_EOL; // PhpStorm(2022-01-01 -> 2023-01-01) echo $phpstorm->toString() . PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) echo $pycharm->toString() . PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) echo $year2021->startAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2021-01-01 00:00:00 echo $year2021->endAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2022-01-01 00:00:00 ෆมΦϒδΣΫτʹΑͬͯผ໊ࢀর໰୊Λղܾ
  45. $year2021 = new DateTimeRange( startAt: DateTimeEndpoint::including('2021-01-01'), endAt: DateTimeEndpoint::excluding('2022-01-01') ); $phpstorm

    = new Subscription('PhpStorm', $year2021); echo $phpstorm, PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) $pycharm = new Subscription('PyCharm', $year2021); echo $pycharm->toString() . PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) $phpstorm2022 = $phpstorm->renew(); echo $phpstorm2022->toString() . PHP_EOL; // PhpStorm(2022-01-01 -> 2023-01-01) echo $phpstorm->toString() . PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) echo $pycharm->toString() . PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) echo $year2021->startAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2021-01-01 00:00:00 echo $year2021->endAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2022-01-01 00:00:00 ෆมΦϒδΣΫτʹΑͬͯผ໊ࢀর໰୊Λղܾ
  46. $year2021 = new DateTimeRange( startAt: DateTimeEndpoint::including('2021-01-01'), endAt: DateTimeEndpoint::excluding('2022-01-01') ); $phpstorm

    = new Subscription('PhpStorm', $year2021); echo $phpstorm, PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) $pycharm = new Subscription('PyCharm', $year2021); echo $pycharm->toString() . PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) $phpstorm2022 = $phpstorm->renew(); echo $phpstorm2022->toString() . PHP_EOL; // PhpStorm(2022-01-01 -> 2023-01-01) echo $phpstorm->toString() . PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) echo $pycharm->toString() . PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) echo $year2021->startAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2021-01-01 00:00:00 echo $year2021->endAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2022-01-01 00:00:00 ෆมΦϒδΣΫτʹΑͬͯผ໊ࢀর໰୊Λղܾ
  47. $year2021 = new DateTimeRange( startAt: DateTimeEndpoint::including('2021-01-01'), endAt: DateTimeEndpoint::excluding('2022-01-01') ); $phpstorm

    = new Subscription('PhpStorm', $year2021); echo $phpstorm, PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) $pycharm = new Subscription('PyCharm', $year2021); echo $pycharm, PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) $phpstorm2022 = $phpstorm->renew(); echo $phpstorm2022->toString() . PHP_EOL; // PhpStorm(2022-01-01 -> 2023-01-01) echo $phpstorm->toString() . PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) echo $pycharm->toString() . PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) echo $year2021->startAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2021-01-01 00:00:00 echo $year2021->endAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2022-01-01 00:00:00 ෆมΦϒδΣΫτʹΑͬͯผ໊ࢀর໰୊Λղܾ
  48. $year2021 = new DateTimeRange( startAt: DateTimeEndpoint::including('2021-01-01'), endAt: DateTimeEndpoint::excluding('2022-01-01') ); $phpstorm

    = new Subscription('PhpStorm', $year2021); echo $phpstorm, PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) $pycharm = new Subscription('PyCharm', $year2021); echo $pycharm, PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) $phpstorm2022 = $phpstorm->renew(); echo $phpstorm2022->toString() . PHP_EOL; // PhpStorm(2022-01-01 -> 2023-01-01) echo $phpstorm->toString() . PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) echo $pycharm->toString() . PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) echo $year2021->startAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2021-01-01 00:00:00 echo $year2021->endAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2022-01-01 00:00:00 ෆมΦϒδΣΫτʹΑͬͯผ໊ࢀর໰୊Λղܾ
  49. $year2021 = new DateTimeRange( startAt: DateTimeEndpoint::including('2021-01-01'), endAt: DateTimeEndpoint::excluding('2022-01-01') ); $phpstorm

    = new Subscription('PhpStorm', $year2021); echo $phpstorm, PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) $pycharm = new Subscription('PyCharm', $year2021); echo $pycharm, PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) $phpstorm2022 = $phpstorm->renew(); echo $phpstorm2022, PHP_EOL; // PhpStorm(2022-01-01 -> 2023-01-01) echo $phpstorm->toString() . PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) echo $pycharm->toString() . PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) echo $year2021->startAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2021-01-01 00:00:00 echo $year2021->endAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2022-01-01 00:00:00 ෆมΦϒδΣΫτʹΑͬͯผ໊ࢀর໰୊Λղܾ
  50. $year2021 = new DateTimeRange( startAt: DateTimeEndpoint::including('2021-01-01'), endAt: DateTimeEndpoint::excluding('2022-01-01') ); $phpstorm

    = new Subscription('PhpStorm', $year2021); echo $phpstorm, PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) $pycharm = new Subscription('PyCharm', $year2021); echo $pycharm, PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) $phpstorm2022 = $phpstorm->renew(); echo $phpstorm2022, PHP_EOL; // PhpStorm(2022-01-01 -> 2023-01-01) echo $phpstorm, PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) echo $pycharm->toString() . PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) echo $year2021->startAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2021-01-01 00:00:00 echo $year2021->endAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2022-01-01 00:00:00 ෆมΦϒδΣΫτʹΑͬͯผ໊ࢀর໰୊Λղܾ
  51. $year2021 = new DateTimeRange( startAt: DateTimeEndpoint::including('2021-01-01'), endAt: DateTimeEndpoint::excluding('2022-01-01') ); $phpstorm

    = new Subscription('PhpStorm', $year2021); echo $phpstorm, PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) $pycharm = new Subscription('PyCharm', $year2021); echo $pycharm, PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) $phpstorm2022 = $phpstorm->renew(); echo $phpstorm2022, PHP_EOL; // PhpStorm(2022-01-01 -> 2023-01-01) echo $phpstorm, PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) echo $pycharm, PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) echo $year2021->startAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2021-01-01 00:00:00 echo $year2021->endAt->value->format('Y-m-d H:i:s') . PHP_EOL; // 2022-01-01 00:00:00 ෆมΦϒδΣΫτʹΑͬͯผ໊ࢀর໰୊Λղܾ 👍
  52. $year2021 = new DateTimeRange( startAt: DateTimeEndpoint::including('2021-01-01'), endAt: DateTimeEndpoint::excluding('2022-01-01') ); $phpstorm

    = new Subscription('PhpStorm', $year2021); echo $phpstorm, PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) $pycharm = new Subscription('PyCharm', $year2021); echo $pycharm, PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) $phpstorm2022 = $phpstorm->renew(); echo $phpstorm2022, PHP_EOL; // PhpStorm(2022-01-01 -> 2023-01-01) echo $phpstorm, PHP_EOL; // PhpStorm(2021-01-01 -> 2022-01-01) echo $pycharm, PHP_EOL; // PyCharm(2021-01-01 -> 2022-01-01) echo $year2021->startAt->value->format('Y-m-d H:i:s'), PHP_EOL; // 2021-01-01 00:00:00 echo $year2021->endAt->value->format('Y-m-d H:i:s'), PHP_EOL; // 2022-01-01 00:00:00 ෆมΦϒδΣΫτʹΑͬͯผ໊ࢀর໰୊Λղܾ
  53. ί ϯ ε τ ϥ Ϋ λ ͸  

    Ұ ճ ͠ ͔ ݺ ΂ ͳ ͍ ͱ   ࡨ ֮ ͠ ͯ ͍ ͨ ʁ ㅟ ㅟ ㅟ ㅟ ㅟ ㅟ ㅟ ㅟ ㅟ ㅟ ㅟ ㅟ ㅟ ㅟ ㅟ Ұ ମ  ͍ ͭ ͔ Β ŠŠŠŠŠŠ
  54. /** * @test * @group learning */ public function コンストラクタをもう一度呼ぶと破壊的変更ができてしまう():

    void { $dt = new \DateTimeImmutable('2021-12-24'); $this->assertSame('2021-12-24', $dt->format('Y-m-d')); $dt->__construct('2022-01-01'); $this->assertSame('2022-01-01', $dt->format('Y-m-d')); } ͳΜʜʜͩͱʜʜʂʁ  ޙ೔ஊ͜ͷߨԋΛ͖͔͚ͬʹQIQTSDʹJTTVFͱͯ͠ใࠂ͞Εɺٞ࿦͕ߦΘΕ͍ͯ·͢ɻ IUUQTHJUIVCDPNQIQQIQTSDJTTVFT ޙ೔ஊ͜ͷߨԋΛ͖͔͚ͬʹ1)14UBO 1TBMNʹػೳఏҊ͕ߦΘΕɺ 1)14UBOʹ!NVOP@͞Μ͕࡞੒ͨ͠QVMMSFRVFTU͕࠾༻͞ΕɺϦϦʔε͞Ε·ͨ͠ɻ IUUQTHJUIVCDPNQIQTUBOQIQTUBOTSDQVMM
  55. /** * @test * @group debugging */ public function 生成時に渡したvalueを後から破壊されても影響を受けないこと():

    void { $dt = new \DateTimeImmutable('2020-12-25'); $endpoint = new DateTimeEndpoint(value: $dt, inclusive: false); $this->assertSame('2020-12-25', $endpoint->value->format('Y-m-d')); $dt->__construct('2022-01-01'); $this->assertSame('2020-12-25', $endpoint->value->format('Y-m-d')); } ෆ҆Λςετʹ຋༁ͯ͠ݕূ͢Δˠෆ҆తத 1) PhperKaigi\DateTimeEndpointTest::生成時に渡したvalueを後から破壊されても影響を受けないこと Failed asserting that two strings are identical. --- Expected +++ Actual @@ @@ -'2020-12-25' +'2022-01-01' ໰୊Λ࠶ݱ͢Δςετʹ͸ EFCVHHJOHλάΛ͚͍ͭͯ·͢
  56. ίϯετϥΫλ಺Ͱ๷ޚతDMPOFΛߦ͏ final class DateTimeEndpoint { public readonly \DateTimeImmutable $value; public

    function __construct( \DateTimeImmutable $value, public readonly bool $inclusive, ) { $this->value = clone $value; } ॳظԽ࣌ʹ౉͞Εͨ%BUF5JNF*NNVUBCMFΛ DMPOFͯ͠อ࣋͢Δ $ ./vendor/bin/phpunit tests/ PHPUnit 9.5.19 #StandWithUkraine ...................................................... 54 / 54 (100%) Time: 00:00.030, Memory: 6.00 MB OK (54 tests, 115 assertions) ΍͔ͬͨ
  57. /** * @test * @group learning * @group debugging */

    public function readonlyなvalueプロパティのコンストラクタを呼んで破壊できるか(): void { $endpoint = new DateTimeEndpoint(value: new \DateTimeImmutable('2020-12-25'), inclusive: false); $this->assertSame('2020-12-25', $endpoint->value->format('Y-m-d')); $endpoint->value->__construct('2022-01-01'); $this->assertSame('2020-12-25', $endpoint->value->format('Y-m-d')); } 1) PhperKaigi\DateTimeEndpointTest::readonlyなvalueプロパティのコンストラクタを呼んで破壊できるか Failed asserting that two strings are identical. --- Expected +++ Actual @@ @@ -'2020-12-25' +'2022-01-01' ݁ہݺ΂ͯ͠·͏ ෆ҆Λςετʹ຋༁ͯ͠ݕূ͢Δˠෆ҆తத
  58. ઃܭมߋ๷ޚతίϐʔͱ๷ޚతΞΫηοα final class DateTimeEndpoint { private readonly \DateTimeImmutable $value; public

    function __construct( \DateTimeImmutable $value, public readonly bool $inclusive, ) { $this->value = clone $value; } public function value(): \DateTimeImmutable { return clone $this->value; } ˞ϓϩύςΟͱϝιουͷҰ؏ੑରশੑ่͕Εͯ͠·ͬͨͷͰ $PNQVUFE1SPQFSUZͷΑ͏ͳ࢓૊ΈΛ ಋೖ͍ͨ͠ͱ͜ΖͰ͕͢ 1)1ݴޠ࢓༷ʹ͸·ͩͳ͍ͷͰ ຊߨԋͰ͸είʔϓ֎ͱ͠·͢ ॳظԽ࣌ʹ౉͞Εͨ%BUF5JNF*NNVUBCMFΛ DMPOFͯ͠อ࣋͢Δ HFUUFSͰNVUBCMFͳΦϒδΣΫτͷࢀরΛฦ͢ͱ ഁյతมߋ͕͋Γ͏ΔͨΊDMPOFͨ͠΋ͷΛฦ͢ ϓϩύςΟ͸QSJWBUF͔ͭSFBEPOMZʹ ʢ ෬ ઢ ʣ
  59. ઃܭมߋ ϓϩύςΟˠϝιου ʹ͋ΘͤͯςετΛௐ੔͠੒ޭΛ֬ೝ /** * @test * @group debugging */

    public function 生成時に渡したvalueを後から破壊されても影響を受けないこと(): void { $dt = new \DateTimeImmutable('2020-12-25'); $endpoint = new DateTimeEndpoint(value: $dt, inclusive: false); $this->assertSame('2020-12-25', $endpoint->value()->format('Y-m-d')); $dt->__construct('2022-01-01'); $this->assertSame('2020-12-25', $endpoint->value()->format('Y-m-d')); } /** * @test * @group debugging */ public function valueメソッドの戻り値を破壊しても影響を受けないこと(): void { $endpoint = new DateTimeEndpoint(value: new \DateTimeImmutable('2020-12-25'), inclusive: false); $this->assertSame('2020-12-25', $endpoint->value()->format('Y-m-d')); $endpoint->value()->__construct('2022-01-01'); $this->assertSame('2020-12-25', $endpoint->value()->format('Y-m-d')); } ΍͔ͬͨ
  60. %BUF5JNF*NNVUBCMFͷअѱͳαϒΫϥεΛ࡞ͬͯΈΔ class DestructiveDateTime extends \DateTimeImmutable { private static array $instances;

    public function __construct($datetime, $timezone = null) { parent::__construct($datetime, $timezone); self::$instances[] = $this; } public function __clone(): void { self::$instances[] = $this; } public static function bringEverythingBackToEpoch(): void { // 全てを無に還すッ……!! foreach(self::$instances as $dt) { $dt->__construct('1970-01-01T00:00:00.000000+00:00'); } } } TUBUJDྖҬʹશΠϯελϯε΁ͷࢀরΛอ࣋͢Δ TUBUJDྖҬʹશΠϯελϯε΁ͷࢀরΛอ࣋͢Δ શΠϯελϯεʹഁյతมߋΛߦ͏ अ ѱ
  61. ѱҙ͋ΔαϒΫϥεʹΑΔഁյ͕Ͱ͖ͯ͠·ͬͨ /** * @test * @group debugging */ public function

    悪意あるサブクラスによる破壊(): void { $ddt = new DestructiveDateTime('2020-12-25', new \DateTimeZone('Asia/Tokyo')); $endpoint = new DateTimeEndpoint(value: $ddt, inclusive: false); $this->assertSame('2020-12-25T00:00:00+09:00', $endpoint->value()->format('Y-m-d\TH:i:sP')); DestructiveDateTime::bringEverythingBackToEpoch(); $this->assertSame('1970-01-01T00:00:00+00:00', $endpoint->value()->format('Y-m-d\TH:i:sP')); }  अ ѱ
  62. %BUF5JNF*NNVUBCMFͷෆ஫ҙͳαϒΫϥεΛ࡞ͬͯΈΔ class TimeZoneCachingDateTime extends \DateTimeImmutable { private readonly ?\DateTimeZone $tz;

    public function __construct($datetime, $timezone = null) { parent::__construct($datetime, $timezone); $this->tz = $timezone; } public function getTimezone(): \DateTimeZone|false { return $this->tz; } } %BUF5JNF;POF΁ͷࢀরΛอ࣋͢Δ͕ɺ @@DMPOFϝιουͷ࣮૷Λ๨Ε͍ͯΔ ෆ ஫ ҙ
  63. DMPOF͸TIBMMPXDPQZͳͷͰϛε͸ى͜Δ /** * @test * @group learning */ public function

    cloneはシャローコピーなので参照を共有してしまう(): void { $dt = new TimeZoneCachingDateTime('2020-12-25', new \DateTimeZone('Asia/Tokyo')); $endpoint = new DateTimeEndpoint(value: $dt, inclusive: false); $this->assertSame('Asia/Tokyo', $endpoint->value()->getTimezone()->getName()); $endpoint->value()->getTimezone()->__construct('Europe/Berlin'); $this->assertSame('Europe/Berlin', $endpoint->value()->getTimezone()->getName()); } TimeZoneCachingDateTime DateTimeZone TimeZoneCachingDateTime clone DMPOF͸TIBMMPXDPQZͳͷͰࢀরΛڞ༗͢Δ ෆ ஫ ҙ
  64. ͡Ό͋TUSJOHͷSFBEPOMZQSPQFSUZͳΒݎ࿚ͩΖʜʜʂʂ final class DateTimeEndpoint { private readonly string $datetime; private

    readonly string $tzname; public function __construct( \DateTimeImmutable $value, public readonly bool $inclusive, ) { $this->datetime = $value->format('Y-m-d\TH:i:s.u'); $this->tzname = $value->getTimezone()->getName(); } public function value(): \DateTimeImmutable { return new \DateTimeImmutable($this->datetime, new \DateTimeZone($this->tzname)); } DMPOFΛ࢖ΘͣɺॳظԽ࣌ʹ࣌ࠁͱλΠϜ κʔϯͷ৘ใ͚ͩΛอ࣋͢Δ ΞΫηοαʹ͓͍ͯ ຖճ%BUF5JNF*NNVUBCMFΛੜ੒͢Δ ʢ ෬ ઢ ʣ
  65. ࣦഊ͍ͯͨ͠αϒΫϥεؔ܎ͷςετ͕௨ΔΑ͏ʹͳͬͨ /** * @test * @group debugging */ public function

    悪意あるサブクラスによる破壊の影響を受けないこと(): void { $ddt = new DestructiveDateTime('2020-12-25', new \DateTimeZone('Asia/Tokyo')); $endpoint = new DateTimeEndpoint(value: $ddt, inclusive: false); $this->assertSame('2020-12-25T00:00:00+09:00', $endpoint->value()->format('Y-m-d\TH:i:sP')); DestructiveDateTime::bringEverythingBackToEpoch(); $this->assertSame('1970-01-01T00:00:00+00:00', $ddt->format('Y-m-d\TH:i:sP')); $this->assertSame('2020-12-25T00:00:00+09:00', $endpoint->value()->format('Y-m-d\TH:i:sP')); } /** * @test * @group debugging */ public function 参照の共有による副作用を生じないこと(): void { $tcdt = new TimeZoneCachingDateTime('2020-12-25', new \DateTimeZone('Asia/Tokyo')); $endpoint = new DateTimeEndpoint(value: $tcdt, inclusive: false); $this->assertSame('Asia/Tokyo', $endpoint->value()->getTimezone()->getName()); $endpoint->value()->getTimezone()->__construct('Europe/Berlin'); $this->assertSame('Asia/Tokyo', $endpoint->value()->getTimezone()->getName()); } ΍͔ͬͨ
  66. Ήɺ͍΍ɺطଘͷςετ͕͍͔ࣦͭ͘ഊ͢Δͧʜʜʁʁ $ ./vendor/bin/phpunit tests/ PHPUnit 9.5.19 #StandWithUkraine .......F...................................F............ 56 /

    56 (100%) Time: 00:00.015, Memory: 6.00 MB There were 2 failures: 1) PhperKaigi\DateTimeEndpointTest::valueのタイムゾーンが異なっても同じ時刻を指している場合は等価とみなす Failed asserting that two objects are equal. --- Expected +++ Actual @@ @@ PhperKaigi\DateTimeEndpoint Object ( - 'datetime' => '2021-12-24T15:00:00.000000' - 'tzname' => '+00:00' + 'datetime' => '2021-12-25T00:00:00.000000' + 'tzname' => 'Asia/Tokyo' 'inclusive' => true ) /usr/src/myapp/tests/PhperKaigi/DateTimeEndpointTest.php:77 2) PhperKaigi\DateTimeRangeTest::保持する端点が異なるタイムゾーンであっても同じ時刻の区間を指していれば等価とみなす Failed asserting that two objects are equal. --- Expected +++ Actual @@ @@ PhperKaigi\DateTimeRange Object ( 'startAt' => PhperKaigi\DateTimeEndpoint Object ( - 'datetime' => '2021-12-24T15:00:00.000000' - 'tzname' => '+00:00' + 'datetime' => '2021-12-25T00:00:00.000000' + 'tzname' => 'Asia/Tokyo' 'inclusive' => true ) 'endAt' => PhperKaigi\DateTimeEndpoint Object ( - 'datetime' => '2022-12-24T15:00:00.000000' - 'tzname' => '+00:00' + 'datetime' => '2022-12-25T00:00:00.000000' + 'tzname' => 'Asia/Tokyo' 'inclusive' => true ) ) /usr/src/myapp/tests/PhperKaigi/DateTimeRangeTest.php:100 FAILURES! Tests: 56, Assertions: 120, Failures: 2.
  67. ࣦഊ͍ͯ͠ΔςετΛҰͭݟͯΈΔ /** * @test */ public function valueのタイムゾーンが異なっても同じ時刻を指している場合は等価とみなす(): void {

    $utc = new \DateTimeImmutable('2021-12-24T15:00:00+00:00'); $jst = new \DateTimeImmutable('2021-12-25T00:00:00', new \DateTimeZone('Asia/Tokyo')); $ep1 = new DateTimeEndpoint(value: $utc, inclusive: true); $ep2 = new DateTimeEndpoint(value: $jst, inclusive: true); $this->assertEquals($ep1, $ep2); $this->assertTrue($ep1 == $ep2); } 1) PhperKaigi\DateTimeEndpointTest::valueのタイムゾーンが異なっても同じ時刻を指している場合は等価とみなす Failed asserting that two objects are equal. --- Expected +++ Actual @@ @@ PhperKaigi\DateTimeEndpoint Object ( - 'datetime' => '2021-12-24T15:00:00.000000' - 'tzname' => '+00:00' + 'datetime' => '2021-12-25T00:00:00.000000' + 'tzname' => 'Asia/Tokyo' 'inclusive' => true ) 🤔
  68. ஋ΦϒδΣΫτͷϦϑΝΫλϦϯάࣦഊΛςετ͕ڭ͑ͯ͘Εͨ 1) PhperKaigi\DateTimeEndpointTest::valueのタイムゾーンが異なっても 同じ時刻を指している場合は等価とみなす Failed asserting that two objects are

    equal. --- Expected +++ Actual @@ @@ PhperKaigi\DateTimeEndpoint Object ( - 'datetime' => '2021-12-24T15:00:00.000000' - 'tzname' => '+00:00' + 'datetime' => '2021-12-25T00:00:00.000000' + 'tzname' => 'Asia/Tokyo' 'inclusive' => true ) 🦁ॻ͍ͯͯΑ͔ͬͨࣗಈςετ🦁 %BUF5JNF*NNVUBCMF͸λΠϜκʔϯΛ·͍ͨͩ౳ՁੑΛఏڙ͍͕ͯͨ͠ɺ TUSJOHͷϓϩύςΟͭʹ෼ղͨ͠Β౳Ձੑ͕੒Γཱͨͳ͘ͳͬͨ
  69. ղܾࡦDPOWFSTJPOGBDUPSZΛ࢖͓͏ final class DateTimeEndpoint { private readonly \DateTimeImmutable $value; public

    function __construct( \DateTimeImmutable $value, public readonly bool $inclusive, ) { $this->value = \DateTimeImmutable::createFromInterface($value); } public function value(): \DateTimeImmutable { return \DateTimeImmutable::createFromInterface($this->value); } DPOWFSTJPOGBDUPSZΛ࢖ͬͯ %BUF5JNF*NNVUBCMFͷ ৽͍͠Πϯελϯεʹม׵͢Δ λΠϜκʔϯΛ·͍ͨͩ౳ՁੑΛఏڙ͍ͯ͠Δ%BUF5JNF*NNVUBCMFΛ QSJWBUF͔ͭSFBEPOMZϓϩύςΟͱͯ͠อ࣋͢Δ ΞΫηοαʹ͓͍ͯ΋ίϐʔΛฦ͢ ʢͪ͜Β͸DMPOFͰ΋ྑ͍ʣ 1 ) 1 
  70. /** * @test */ public function DateTimeEndpointのコンストラクタを2回以上呼び出せないこと(): void { $dt

    = new \DateTimeImmutable('2020-12-25'); $endpoint = new DateTimeEndpoint(value: $dt, inclusive: false); $newDate = new \DateTimeImmutable('2022-01-01'); try { $endpoint->__construct(value: $newDate, inclusive: true); } catch(\Error $expected) { $this->assertSame('Cannot modify readonly property PhperKaigi\DateTimeEndpoint::$inclusive', $expected->getMessage()); return; } $this->fail('例外が発生していない'); } ΋ͪΖΜςετΛॻ͜͏ SFBEPOMZQSPQFSUZͷ͋Γ͕ͨΈ͕Θ͔ͬͨ ͜ͷ఺ʹ͓͍ͯ͸SFBEPOMZQSPQFSUZ͸ ศར͔ͭ҆৺ͱ͍͑ͦ͏ &SSPS "TTFSUJPO&SSPSͷ਌ྫ֎ ͕ظ଴஋ͳͷͰUSZͷதʹGBJM͸ॻ͚ͣɺ USZGBJMDBUDIΠσΟΦϜͷมܗͰςετΛॻ͘
  71. 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ͷೖΓޱͰߦ͏ͷ͸ے͕ѱ͍
  72. ࣄલ৚݅ར༻ଆͷ੹೚Ͱ͋Δ৔߹͸ද໌Λ࢖͏ final class DateTimeRange { public function __construct( public readonly

    DateTimeEndpoint $startAt, public readonly DateTimeEndpoint $endAt, ) { assert($startAt->value() < $endAt->value()); } } JOWBSJBOU ৗʹ੒Γཱͭ΂͖ෆม৚݅ ΛࣜͰॻ͘
  73. ද໌Λ࢖Θͳ͍৔߹͸ɺར༻ଆͷ੹೚Ͱ͋Δ͜ͱΛࣔ͢ྫ֎Λ࢖͏ final class DateTimeRange { public function __construct( public readonly

    DateTimeEndpoint $startAt, public readonly DateTimeEndpoint $endAt, ) { if ($startAt->value() > $endAt->value()) { throw new \InvalidArgumentException('startAt > endAt'); } } } ར༻ଆͷޡΓͰ͋Δࢫͷྫ֎Λൃੜͤ͞Δ
  74. Throwable ├── Error │ ├── ArithmeticError │ │ └── DivisionByZeroError

    │ ├── AssertionError │ ├── CompileError │ │ └── ParseError │ ├── FiberError │ ├── TypeError │ │ └── ArgumentCountError │ ├── UnhandledMatchError │ └── ValueError └── Exception ├── ClosedGeneratorException ├── DOMException ├── ErrorException ├── JsonException ├── LogicException │ ├── BadFunctionCallException │ │ └── BadMethodCallException │ ├── DomainException │ ├── InvalidArgumentException │ ├── LengthException │ └── OutOfRangeException ├── PharException ├── ReflectionException ├── RuntimeException │ ├── OutOfBoundsException │ ├── OverflowException │ ├── PDOException │ ├── RangeException │ ├── UnderflowException │ └── UnexpectedValueException └── SodiumException 😇3VOUJNF&YDFQUJPOܥྫ֎Λࣔ͢ 👮-PHJD&YDFQUJPOܥόάΛࣔ͢ 🔥&SSPSܥ಺෦ΤϥʔόάΛࣔ͢ 1)1ͷྫ֎ܧঝߏ଄ 1 ) 1 
  75. Ұ఺ू߹Λ࣮ݱ͢Δද໌όʔδϣϯ final class DateTimeRange { public function __construct( public readonly

    DateTimeEndpoint $startAt, public readonly DateTimeEndpoint $endAt, ) { assert($startAt->value() < $endAt->value() || self::isEquivalentAndInclusive($startAt, $endAt)); } public static function isEquivalentAndInclusive(DateTimeEndpoint $startAt, DateTimeEndpoint $endAt): bool { return $startAt->inclusive && $endAt->inclusive && $startAt->value() == $endAt->value(); } } ෆม৚݅ͷࣜΛߋ৽͢Δ
  76. final class DateTimeRange { public function __construct( public readonly DateTimeEndpoint

    $startAt, public readonly DateTimeEndpoint $endAt, ) { $startAtValue = $startAt->value(); $endAtValue = $endAt->value(); if ($startAtValue > $endAtValue) { throw new \InvalidArgumentException('startAt > endAt'); } if ($startAtValue == $endAtValue) { if (!$startAt->inclusive || !$endAt->inclusive) { throw new \InvalidArgumentException('Both endpoints should be inclusive if startAt == endAt'); } } } } Ұ఺ू߹Λ࣮ݱ͢Δྫ֎όʔδϣϯ ྫ֎όʔδϣϯͱද໌όʔδϣϯͷ ॻ͖ຯ΍ՄಡੑͳͲΛݟൺ΂ͯΈ͍ͯͩ͘͞
  77. final class DateTimeEndpoint { private readonly \DateTimeImmutable $value; public function

    __construct( \DateTimeImmutable $value, public readonly bool $inclusive, ) { $this->value = \DateTimeImmutable::createFromInterface($value); } public function value(): \DateTimeImmutable { return \DateTimeImmutable::createFromInterface($this->value); } 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΁ͷม׵ʹ͸ ๲େͳ૊Έ߹Θ͕ͤ͋Δ͠ɺ λΠϜκʔϯͷѻ͍΋ෆे෼ʹͳͬͯ͠·͍ͬͯΔɻ จࣈྻҾ਺ʹλΠϜκʔϯͷΦϑηοτදهΛؚΊͳ͍ ͱσϑΥϧτͷλΠϜκʔϯʹͳͬͯ͠·͏  ͭ·Γ͜͜Ͱߦ͏ͷ͸ෆద੾ɻ
  78. public function testFindAll(): void { $range = new DateTimeRange( startAt:

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

    __construct( \DateTimeImmutable $value, public readonly bool $inclusive, ) { $this->value = \DateTimeImmutable::createFromInterface($value); } public function value(): \DateTimeImmutable { return \DateTimeImmutable::createFromInterface($this->value); } 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 # " %
  80. final class DateTimeEndpoint { private readonly \DateTimeImmutable $value; public function

    __construct( \DateTimeImmutable $value, public readonly bool $inclusive, ) { $this->value = \DateTimeImmutable::createFromInterface($value); } public function value(): \DateTimeImmutable { return \DateTimeImmutable::createFromInterface($this->value); } 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 Simple 4 JN Q MF
  81. 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
  82. use PHPUnit\Framework\TestCase; use PhperKaigi\DateTimeShorthandHelper as DT; class DateTimeRangeTest extends TestCase

    { public function testDateRangeShorthand(): void { $range = new DateTimeRange( startAt: DateTimeEndpoint::including(DT::jst('2020-12-25')), endAt: DateTimeEndpoint::excluding(DT::jst('2021-12-25')) ); $this->assertSame('2020-12-25', $range->startAt->value()->format('Y-m-d')); $this->assertSame('2021-12-25', $range->endAt->value()->format('Y-m-d')); } &BTZ͞͸ผͷϨΠϠʔͰఏڙ͢Δɻྫ͑͹ςετ༻ͷϔϧύʔ ςετίʔυ্Ͱ4JNQMFͱ&BTZΛ߹ྲྀͤ͞Δɻ ͜͏ॻ͚Ε͹े෼ͩͬͨɻ
  83. final class DateTimeEndpoint { private readonly \DateTimeImmutable $value; public function

    __construct( \DateTimeImmutable $value, public readonly bool $inclusive, ) { $this->value = \DateTimeImmutable::createFromInterface($value); } public function value(): \DateTimeImmutable { return \DateTimeImmutable::createFromInterface($this->value); } 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 ); } } %BUF5JNF&OEQPJOU ۠ؒͷ୺఺Λࣔ͢ɻ ๷ޚతίϐʔɺ๷ޚతΞΫηοα౳Ͱ ঢ়ଶมԽΛ๷͙ɺ ෆมͷ஋ΦϒδΣΫτ
  84. final class DateTimeRange { public function __construct( public readonly DateTimeEndpoint

    $startAt, public readonly DateTimeEndpoint $endAt, ) { $startAtValue = $startAt->value(); $endAtValue = $endAt->value(); if ($startAtValue > $endAtValue) { throw new \InvalidArgumentException('startAt > endAt'); } if ($startAtValue == $endAtValue) { if (!$startAt->inclusive || !$endAt->inclusive) { throw new \InvalidArgumentException('Both endpoints should be inclusive if startAt == endAt'); } } } } %BUF5JNF3BOHF ۠ؒΛࣔ͢ɻ ίϯετϥΫλͰෆม৚݅Λ੒ཱͤ͞ɺ Ҏ߱ঢ়ଶ͕มԽ͠ͳ͍ɺ ׬શੑΛ൐ͬͨ஋ΦϒδΣΫτ
  85. final class BugRepository implements BugRepositoryInterface { const TIMESTAMP_FORMAT = 'Y-m-d\TH:i:s.uP';

    public function __construct( private readonly \PDO $pdo ) { } public function findAll(DateTimeRange $searchRange, Status $status): array { $startAt = $searchRange->startAt->value(); $endAt = $searchRange->endAt->value(); $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); } } #VH3FQPTJUPSZ fi OE"MMͷҾ਺͕ଘࡏ͢Δ࣌఺Ͱطʹਖ਼͍͠ঢ়ଶͳͷͰ ʢ׬શੑʣɺ๷ޚతνΣοΫ͸ෆཁʹͳΔ
  86. ͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ w ๷ޚతϓϩάϥϛϯάͱ͸ѱ͍ίʔυʹឺ૑ߣΛ͋ͯΔ͜ͱͰ͸ͳ͘ɺྑࣝ͋Δ࣮ફͷੵΈॏͶ w ܕએݴͰ૝ఆ͢΂͖ೖྗͷܕΛߜΓɺ๷ޚతνΣοΫΛେ෯ʹݮΒ͢ w ྻڍܕͰऔΓಘΔ஋ΛߜΓɺ๷ޚతνΣοΫΛେ෯ʹݮΒ͢ w جૅͱͳΔܕΛϞσϦϯά͠ɺᐆດ͞΍ؒҧ͍΍͢͞Λ࡟ݮ͢Δ w

    جૅͱͳΔܕΛෆมΦϒδΣΫτʹ͠ɺঢ়ଶมԽ΍෭࡞༻ʹىҼ͢ΔόάΛ༧๷͢Δ w جૅͱͳΔܕΛ஋ΦϒδΣΫτʹ͠ɺΠϯελϯεͰ͸ͳ͘஋Ͱ౳ՁੑΛ൑அ͢Δ w ΦϒδΣΫτੜ੒࣌ʹෆม৚݅Λ੒ཱͤ͞ɺ͔ͭͦͷΦϒδΣΫτ͕ෆมΦϒδΣΫτͰ͋Δͳ Β͹ɺੜ੒͞ΕͨΦϒδΣΫτ͸ৗʹਖ਼͍͠ʢ׬શੑʣ w 4JNQMF͞ʢ֓೦ͱͯ͠ͷཁૉͷগͳ͞ʣͱ&BTZ͞ʢख਺ͷগͳ͞ʣΛෆ༻ҙʹࠞͥͳ͍ w ਖ਼౰ੑΛॏΜ͡ΔϨΠϠʔͱݎ࿚ੑΛॏΜ͡ΔϨΠϠʔΛ෼͚Δ w ઃܭͱ͸੹຿ͷ࠷ద഑ஔΛٻΊଓ͚Δ͜ͱɻ୭͕ԿΛ஌͍ͬͯͯԿΛ஌Δ΂͖Ͱͳ͍͔ɺԿΛ΍ Δ΂͖ͰԿΛ΍Δ΂͖Ͱͳ͍͔Λৗʹߟ͑ଓ͚Δ͜ͱ