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

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

予防に勝る防御なし - 堅牢なコードを導く様々な設計のヒント / 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. ༧๷ʹউΔ๷ޚͳ͠
    ݎ࿚ͳίʔυΛಋ༷͘ʑͳઃܭͷώϯτ
    📷🙆 🙆
    ࿨ా୎ਓʢ!U@XBEBʣ
    #phperkaigi #a
    rev.15
    "QS !1)1FS,BJHJ

    View full-size slide

  2. illustrated by @mty_mno
    ΑΖ͓͘͠ئ͍͠·͢
    #phperkaigi #a

    View full-size slide

  3. 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

    View full-size slide

  4. $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


    )


    )
    ࣮ߦ͢Δͱɺ͜Μͳ݁Ռ

    View full-size slide

  5. ॲཧࣦഊͷݪҼʹͳΔՄೳੑ͕͋Δͷ͸Ͳͷߦʁ
    #
    "
    %
    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);


    }

    View full-size slide

  6. ॲཧࣦഊͷݪҼʹͳΔՄೳੑ͕͋Δͷ͸Ͳͷߦʁ
    #
    "
    %
    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);


    }
    🙅ςʔϒϧ໊΍ΧϥϜ໊͕୭͔ʹมߋ͞Εͨ
    🙅 ͜͜Ͱ
    σʔλϕʔε઀ଓΤϥʔ

    View full-size slide

  7. ॲཧࣦഊͷݪҼʹͳΔՄೳੑ͕͋Δͷ͸Ͳͷߦʁ
    #
    "
    %
    🙅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);


    }

    View full-size slide

  8. ॲཧࣦഊͷݪҼʹͳΔՄೳੑ͕͋Δͷ͸Ͳͷߦʁ
    #
    "
    %
    🙅#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);


    }

    View full-size slide

  9. 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);


    }
    ॲཧࣦഊͷݪҼʹͳΔՄೳੑ͕͋Δͷ͸Ͳͷߦʁ
    #
    "
    %
    ͚ͬ͜͏͋Δͳ😇



    View full-size slide

  10. ෆ۩߹ͷൃݟ͕
    ஗ΕΕ͹஗ΕΔ΄Ͳ
    ই͸ਂ͘ͳΔ
    IUUQTXXXBTURCPSHQSFTTSPPN*452#@$FSUJ
    fi
    DBUJPO@/FXT@@IUNM

    View full-size slide

  11. lݡ໌ͳιϑτ΢ΣΞٕज़ऀʹͳΔͨΊͷ
    ୈҰา͸ɺಈ͘ϓϩάϥϜΛॻ͘͜ͱͱ
    ਖ਼͍͠ϓϩάϥϜΛద੾ʹ࡞੒͢Δ͜ͱ
    ͷҧ͍Λೝࣝ͢Δ͜ͱz
    Š."+BDLTPO

    View full-size slide

  12. 💀ςʔϒϧ໊΍ΧϥϜ໊͕୭͔ʹมߋ͞Εͨ
    👮QBSBNT͕OVMM
    👮QBSBNTͷΩʔ໊΍਺ͷෆҰக
    👮QBSBNTͷ஋͕จࣈྻʹม׵ෆೳ
    👮QBSBNTͷ஋͕೔࣌ͱͯ͠ղऍͰ͖ͳ͍
    🐛#VHΫϥε͕ະఆٛ
    😇్தͰσʔλϕʔε઀ଓΤϥʔ
    👉
    👉
    👉
    ݱঢ়Λ෼ੳ͢Δͱɺؒҧͬͨ࢖ΘΕํΛ͞Ε΍͍͢ͱ͍͏໰୊͕େ͖͍
    👉
    ຊ೔ͷߨԋ͸
    ͜ΕΒͷ໰୊ʹूத͠·͢

    View full-size slide

  13. ڱٛͷ
    ๷ޚతϓϩά
    ϥϛϯά΁ͷޡղ

    View full-size slide

  14. 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"');


    }


    ͨͩͻͨ͢ΒೖྗΛνΣοΫ͠Α͏ͱͨ͠Γ
    #
    "
    %

    View full-size slide

  15. ෆਖ਼ͳೖྗ͕͋ͬͯ΋ࣗ෼ͰԿͱ͔͠Α͏ͱͨ͠Γ
    #
    "
    %
    $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);

    View full-size slide

  16. υΩϡϝϯτͰؒҧ͍΍͢͞Λิ͓͏ͱͨ͠Γ
    #
    "
    %
    /**


    * 指定した範囲の日時およびステータスに合致する Bug を検索し、ヒットした全件を
    Bug オブジェクトの配列として返す。


    *


    * @param array $params 格納した検索条件の連想配列。キー "startAt" に検索範囲の
    始点日時をstringで, キー "endAt" に検索範囲の終点日時をstringで, キー "status" にス
    テータス文字列をstringで指定すること。キー、値それぞれNULLは不可とする。


    * @return Bug[] 検索結果を Bug オブジェクトにマッピングして返す。検索結果が0件
    のときは空配列を返す。


    */


    public function findAll($params)


    {


    View full-size slide

  17. ๷ޚతϓϩάϥϛϯάͱ͸
    ѱ͍ίʔυʹឺ૑ߣΛ͋ͯΔ
    ͜ͱͰ͸ͳ͍

    View full-size slide

  18. w ʮ๷ޚతϓϩάϥϛϯάʯͱ͸ɺ

    ໰୊ൃੜΛࣄલʹ๷͝͏ͱ͍͏ίʔσΟϯάελΠϧ
    w Մಡੑͷߴ͍ίʔυͱద੾ͳ໋໊نଇ
    w શͯͷؔ਺ͷ໭Γ஋ΛνΣοΫ
    w σβΠϯύλʔϯͷ࠾༻
    w ཁ͢Δʹɺྑࣝ͋Δ࣮ફͷੵΈॏͶͰ͋Δ
    w ๷ޚతϓϩάϥϛϯά͸ɺਖ਼͍͠ίʔυ࡞੒ͷͨΊͷن཯Λ
    ϓϩάϥϚ͕Ұ؏ͯ͠ద༻͢ΔͨΊͷҰछͷίʔσΟϯάඪ४
    ๷ޚతϓϩάϥϛϯάͱ͸ྑࣝ͋Δ࣮ફͷੵΈॏͶ
    IUUQTXXXBNB[PODPKQEQ

    View full-size slide

  19. Agenda
    ܕએݴ
    ྻڍܕ
    ϞσϦϯά
    ෆมੑͱ౳Ձੑ
    ׬શੑ
    ੹຿ͷ഑ஔ
    👉

    View full-size slide

  20. ΤοηΠ+ݟ஌Β͵ਓͱ΋͏·͘΍Δʹ͸
    lʮग़དྷͯ͸ͳΒ͵͜ͱΛې͡
    ΔʯͷͰ͸ͳ͘ɺ͸͡Ί͔Β
    ʮग़དྷ͍͍ͯ͜ͱ͚ͩΛग़དྷΔ
    Α͏ʹ͢Δʯͱߟ͑ΔͷͰ͢z
    IUUQTXXXPSFJMMZDPKQCPPLT

    View full-size slide

  21. 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"');


    }


    ໰୊ͱͳ͍ͬͯΔ͜ͷ࿈૝഑ྻΛ
    ݱঢ়

    View full-size slide

  22. 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"');


    }


    ͦΕͧΕܕએݴ͞ΕͨҾ਺ʹ෼ղ͢Δ
    ܕએݴʹΑͬͯʮग़དྷ͍͍ͯ͜ͱ͚ͩΛग़དྷΔʯΑ͏ʹ

    View full-size slide

  23. 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"');


    }


    ๷ޚతνΣοΫ͕΄΅ෆཁʹ

    View full-size slide

  24. ૝ఆ͠ͳ͚Ε͹ͳΒͳ͍ঢ়گ͕ݮͬͨ
    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ܕͷΧϥϜʹ֨ೲ͍ͯ͠Δͱߟ͍͑ͯͩ͘͞

    View full-size slide

  25. Agenda
    ܕએݴ
    ྻڍܕ
    ϞσϦϯά
    ෆมੑͱ౳Ձੑ
    ׬શੑ
    ੹຿ͷ഑ஔ
    👉

    View full-size slide

  26. wݴޠ૊ΈࠐΈͷܕʢJOU TUSJOH౳ʣΛ࢖͏
    ͱɺऔΓಘΔ஋ͷ૊Έ߹Θ͕ͤ๲େʹͳΔ
    w໰୊ྖҬͷ஌ࣝΛ׆༻ͯ͠ݻ༗ͷܕΛ࡞Δ
    ͜ͱͰɺऔΓಘΔ૊Έ߹ΘͤΛେ෯ʹݮΒ
    ͤΔ
    ΤοηΠؔ਺ͷʮαΠζʯΛখ͘͢͞Δ
    IUUQTXXXPSFJMMZDPKQCPPLT

    View full-size slide

  27. ྻڍܕΛ࢖͓͏
    1
    )
    1


    enum Status: string


    {


    case Open = 'OPEN';


    case New = 'NEW';


    case Fixed = 'FIXED';


    }
    IUUQTXXXQIQOFUNBOVBMKBMBOHVBHFFOVNFSBUJPOTQIQ

    View full-size slide

  28. 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);


    }
    ݱঢ়

    View full-size slide

  29. 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);


    }
    Ҿ਺Λจࣈྻ͔Βྻڍܕʹม͑Δ
    จࣈྻ͔Βྻڍܕʹม͑Δ

    View full-size slide

  30. 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);


    }
    ૝ఆ͠ͳ͚Ε͹ͳΒͳ͍ঢ়گ͕͞Βʹݮͬͨ

    View full-size slide

  31. Agenda
    ܕએݴ
    ྻڍܕ
    ϞσϦϯά
    ෆมੑͱ౳Ձੑ
    ׬શੑ
    ੹຿ͷ഑ஔ
    👉

    View full-size slide

  32. ϞσϦϯάͰ
    ᐆດ͞ΛݮΒ͢

    View full-size slide

  33. 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);


    }
    ར༻ଆ
    Ҿ਺͋ΔͷͰ
    ͦΕͧΕͷ໾ׂ͕Θ͔Γʹ͍͘

    View full-size slide

  34. 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

    View full-size slide

  35. 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);


    }
    ؚ·ΕΔؚ·Εͳ͍Λ໊લͰࣔͯ͠΋͍͍͚ΕͲ
    Ҿ਺ͷ໊લͰࣔͯ͠΋ྑ͍͕ɺ
    ͜ͷઃܭ͸ৗʹద੾ͩΖ͏͔

    View full-size slide

  36. wྑ͍ΠϯλϑΣʔεͱ͸࣍ͷͭͷ৚݅Λຬͨ
    ͢ΠϯλϑΣʔε
    wਖ਼͘͠࢖༻͢Δํ͕ૢ࡞ϛεΛ͢ΔΑΓ؆୯
    wޡͬͨ࢖͍ํΛ͢Δ͜ͱ͕ࠔ೉
    ΤοηΠਖ਼͍͠࢖͍ํΛ؆୯ʹɺޡͬͨ࢖͍ํΛࠔ೉ʹ
    IUUQTXXXPSFJMMZDPKQCPPLT
    ΋ͬͱޡΓ΍͢͞ΛݮΒ͢ઃܭΛ͍ͨ͠

    View full-size slide

  37. ର৅ͷྖҬʢυϝΠϯʣ
    ʹֶ͍ͭͯͼɺ
    ϞσϦϯάʹΑͬͯ
    ޡΓ΍͢͞ΛݮΒ͢

    View full-size slide

  38. ൣғʢ۠ؒʣʹֶ͍ͭͯͿ୺఺
    IUUQTKBXJLJQFEJBPSHXJLJ&$#"&@&#&"%"

    View full-size slide

  39. ൣғʢ۠ؒʣʹֶ͍ͭͯͿด۠ؒɺ։۠ؒ
    IUUQTXFLBOB[BXBJUBDKQNBUIDBUFHPSZPUIFSTZVVHPVIFOLBOUFYDHJ UBSHFUNBUIDBUFHPSZPUIFSTZVVHPVLVLBOOIUNM

    View full-size slide

  40. ൣғʢ۠ؒʣʹֶ͍ͭͯͿ൒։۠ؒ
    IUUQTXFLBOB[BXBJUBDKQNBUIDBUFHPSZPUIFSTZVVHPVIFOLBOUFYDHJ UBSHFUNBUIDBUFHPSZPUIFSTZVVHPVLVLBOOIUNM

    View full-size slide

  41. 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


    View full-size slide

  42. ͜ΕͰؒҧ͍ʹ͘͘ͳ͕ͬͨɺࠓ౓͸࢖͏ͷ͕΍΍໘౗ʜʜ
    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);


    }

    View full-size slide

  43. wྑ͍ΠϯλϑΣʔεͱ͸࣍ͷͭͷ৚݅Λຬͨ
    ͢ΠϯλϑΣʔε
    wਖ਼͘͠࢖༻͢Δํ͕ૢ࡞ϛεΛ͢ΔΑΓ؆୯
    wޡͬͨ࢖͍ํΛ͢Δ͜ͱ͕ࠔ೉
    ΤοηΠਖ਼͍͠࢖͍ํΛ؆୯ʹɺޡͬͨ࢖͍ํΛࠔ೉ʹ
    IUUQTXXXPSFJMMZDPKQCPPLT
    ݱঢ়Ͱ͸ਖ਼͘͠࢖͏ͷ͕΍΍໘౗

    View full-size slide

  44. खݎ͞ͱॻ͖΍͢͞ͷཱ྆ΛࢼΈΑ͏
    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


    );


    }


    }
    ʢ


    ʣ

    View full-size slide

  45. ͜ΕͰར༻ଆΛ୹͘ॻ͚ΔΑ͏ʹͳͬͨ
    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);


    }
    ʢ


    ʣ

    View full-size slide

  46. Agenda
    ܕએݴ
    ྻڍܕ
    ϞσϦϯά
    ෆมੑͱ౳Ձੑ
    ׬શੑ
    ੹຿ͷ഑ஔ
    👉

    View full-size slide

  47. ෆมੑ
    JNNVUBCJMJUZ

    ঢ়ଶมԽΛഇ͢Δ

    View full-size slide

  48. ผ໊ࢀর໰୊
    "MJBTJOH

    View full-size slide

  49. 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ΦϒδΣΫτΛ࢖ͬͯΈΔ
    #
    "
    %
    αϒεΫϦϓγϣϯܖ໿Λ೥ߋ৽͢Δϝιου

    View full-size slide

  50. $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
    ผ໊ࢀর໰୊

    View full-size slide

  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->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
    ผ໊ࢀর໰୊

    View full-size slide

  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->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
    ผ໊ࢀর໰୊

    View full-size slide

  53. $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
    ผ໊ࢀর໰୊

    View full-size slide

  54. $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ͷαϒεΫϦϓγϣϯܖ໿Λ೥ߋ৽͢Δ

    View full-size slide

  55. $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
    ผ໊ࢀর໰୊

    View full-size slide

  56. $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ͷܖ໿ظؒ΋
    ߋ৽͞Εͯ͠·͍ͬͯΔ

    View full-size slide

  57. $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
    ผ໊ࢀর໰୊

    View full-size slide

  58. Կ͕ݪҼͩͬͨͷ͔

    View full-size slide

  59. Կ͕ݪҼͩͬͨͷ͔
    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);


    }


    }
    #
    "
    %
    ͕͜͜໰୊
    ࠶୅ೖͰ͸ͳ͘ͱ΋ഁյతมߋ͕ग़དྷͯ͠·͏

    View full-size slide

  60. 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Ϋϥε͕ഁյతมߋΛڐͯ͠͠·͍ͬͯͨ
    #
    "
    %

    View full-size slide

  61. ෆมΦϒδΣΫτΛ͔ͭ͏

    View full-size slide

  62. %BUF5JNFͱ%BUF5JNF*NNVUBCMF
    IUUQTXXXQIQOFUNBOVBMKBDMBTTEBUFUJNFJNNVUBCMFQIQ

    View full-size slide

  63. /**


    * @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λάΛ͚͍ͭͯ·͢

    View full-size slide

  64. /**


    * @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ͷҧ͍Λֶशςετʹ͢Δ

    View full-size slide

  65. ෆมΦϒδΣΫτΛͭ͘Δ

    View full-size slide

  66. 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
    #
    "
    %

    View full-size slide

  67. 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Λ࢖͍ͬͯ͘
    ʢ


    ʣ

    View full-size slide

  68. 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ͩͬͨ
    #
    "
    %

    View full-size slide

  69. 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ʹมߋ͞Εͨ

    View full-size slide

  70. ෆมΦϒδΣΫτͰͷ
    ໰୊ղܾ

    View full-size slide

  71. $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
    ෆมΦϒδΣΫτʹΑͬͯผ໊ࢀর໰୊Λղܾ

    View full-size slide

  72. $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
    ෆมΦϒδΣΫτʹΑͬͯผ໊ࢀর໰୊Λղܾ

    View full-size slide

  73. $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
    ෆมΦϒδΣΫτʹΑͬͯผ໊ࢀর໰୊Λղܾ

    View full-size slide

  74. $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
    ෆมΦϒδΣΫτʹΑͬͯผ໊ࢀর໰୊Λղܾ

    View full-size slide

  75. $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
    ෆมΦϒδΣΫτʹΑͬͯผ໊ࢀর໰୊Λղܾ

    View full-size slide

  76. $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
    ෆมΦϒδΣΫτʹΑͬͯผ໊ࢀর໰୊Λղܾ

    View full-size slide

  77. $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
    ෆมΦϒδΣΫτʹΑͬͯผ໊ࢀর໰୊Λղܾ

    View full-size slide

  78. $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
    ෆมΦϒδΣΫτʹΑͬͯผ໊ࢀর໰୊Λղܾ
    👍

    View full-size slide

  79. $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
    ෆมΦϒδΣΫτʹΑͬͯผ໊ࢀর໰୊Λղܾ

    View full-size slide

  80. ͕ͩͪΐͬͱ
    ଴ͬͯ΄͍͠

    View full-size slide

  81. ί
    ϯ
    ε
    τ
    ϥ
    Ϋ
    λ
    ͸


    Ұ
    ճ
    ͠
    ͔
    ݺ
    ΂
    ͳ
    ͍
    ͱ



    ֮
    ͠
    ͯ
    ͍
    ͨ
    ʁ















    Ұ


    ͍
    ͭ
    ͔
    Β
    ŠŠŠŠŠŠ

    View full-size slide

  82. /**


    * @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

    View full-size slide

  83. /**


    * @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λάΛ͚͍ͭͯ·͢

    View full-size slide

  84. %BUF5JNF*NNVUBCMF͕
    JNNVUBCMFͰ͸ͳ͍ͱͨ͠Β
    ઃܭΛͲ͏͢Δ͔
    %BUF5JNF*NNVUBCMFͷίϯετϥΫλϚδοΫϝιουʢA@@DPOTUSVDUAʣΛ໌ࣔతʹݺͿ͜ͱʹΑΔഁյత
    มߋ͸ɺݱ࣮తʹ͸΄ͱΜͲ৺഑͠ͳ͘ͱ΋ྑ͍ɺߨԋͷͨΊͷۃ୺ͳྫͰ͋Δͱ͸ݴ͑ΔͰ͠ΐ͏ɻ
    ࠓޙ͸੩తղੳπʔϧʹΑͬͯ͜͏͍ͬͨޡΓΛ๷͙ํ޲ʹਐΜͰ͍͘ͱࢥΘΕ·͢ɻ
    ͦ͜Ͱɺ͔͜͜ΒຊߨԋͰ͸ɺՄมΦϒδΣΫτΛෆมΦϒδΣΫτͷҰ෦ͱͯ͠ઃܭ͢Δࡍͷ
    ஫ҙ఺΍ҰൠతͳςΫχοΫΛɺ%BUF5JNF*NNVUBCMFΛՄมͷ΋ͷͱͯ͠ѻ͏͜ͱͰઆ໌͍͖ͯ͠·͢ɻ

    View full-size slide

  85. ίϯετϥΫλ಺Ͱ๷ޚత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)
    ΍͔ͬͨ

    View full-size slide

  86. QVCMJDQSPQFSUZ͕
    %BUF5JNF*NNVUBCMFͩͬͨΒ
    ͋ͱ͔ΒഁյͰ͖ΔͷͰ͸ʁ

    View full-size slide

  87. /**


    * @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'
    ݁ہݺ΂ͯ͠·͏
    ෆ҆Λςετʹ຋༁ͯ͠ݕূ͢Δˠෆ҆తத

    View full-size slide

  88. ݁࿦
    NVUBCMFͳΦϒδΣΫτ͸
    SFBEPOMZQSPQFSUZͰ͋ͬͯ΋
    QVCMJDQSPQFSUZͱͯ͠࿐ग़͍ͯ͠ΔݶΓ
    ޙ͔ΒഁյͰ͖ͯ͠·͏

    View full-size slide

  89. ઃܭมߋ๷ޚతίϐʔͱ๷ޚతΞΫηοα
    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ʹ
    ʢ


    ʣ

    View full-size slide

  90. ઃܭมߋ ϓϩύςΟˠϝιου
    ʹ͋ΘͤͯςετΛௐ੔͠੒ޭΛ֬ೝ
    /**


    * @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'));


    }
    ΍͔ͬͨ

    View full-size slide

  91. Ͱ΋%BUF5JNF*NNVUBCMF
    ͸
    fi
    OBMΫϥεͰ͸ͳ͍ɻ
    ͭ·ΓܧঝͰ͖Δɻ
    DMPOFͰେৎ෉ͩΖ͏͔ʁ

    View full-size slide

  92. %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ྖҬʹશΠϯελϯε΁ͷࢀরΛอ࣋͢Δ
    શΠϯελϯεʹഁյతมߋΛߦ͏

    ѱ

    View full-size slide

  93. ѱҙ͋ΔαϒΫϥεʹΑΔഁյ͕Ͱ͖ͯ͠·ͬͨ
    /**


    * @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'));


    }


    ѱ

    View full-size slide

  94. ͜͜·Ͱͷ໌നͳѱҙ͸ແ͘ͱ΋
    ϛε͸ى͜Δ
    ϋϯϩϯͷం౛ͱ΋ݴ͑Δ

    View full-size slide

  95. %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ϝιουͷ࣮૷Λ๨Ε͍ͯΔ


    ҙ

    View full-size slide

  96. 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ͳͷͰࢀরΛڞ༗͢Δ


    ҙ

    View full-size slide

  97. DMPOFΛ௒͑ͨ
    ͞Βʹ๷ޚతͳίϐʔ

    View full-size slide

  98. ͡Ό͋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Λੜ੒͢Δ
    ʢ


    ʣ

    View full-size slide

  99. ࣦഊ͍ͯͨ͠αϒΫϥεؔ܎ͷςετ͕௨ΔΑ͏ʹͳͬͨ
    /**


    * @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());


    }
    ΍͔ͬͨ

    View full-size slide

  100. Ήɺ͍΍ɺطଘͷςετ͕͍͔ࣦͭ͘ഊ͢Δͧʜʜʁʁ
    $ ./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.

    View full-size slide

  101. ࣦഊ͍ͯ͠ΔςετΛҰͭݟͯΈΔ
    /**


    * @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


    )
    🤔

    View full-size slide

  102. ஋ΦϒδΣΫτͱ
    ౳Ձੑ

    View full-size slide

  103. 1)1ʹ͓͚ΔΦϒδΣΫτͷ౳Ձੑ
    IUUQTXXXQIQOFUNBOVBMKBMBOHVBHFPPQPCKFDUDPNQBSJTPOQIQ

    View full-size slide

  104. ஋ΦϒδΣΫτͷϦϑΝΫλϦϯάࣦഊΛςετ͕ڭ͑ͯ͘Εͨ
    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ͷϓϩύςΟͭʹ෼ղͨ͠Β౳Ձੑ͕੒Γཱͨͳ͘ͳͬͨ

    View full-size slide

  105. IUUQTXXXQIQOFUNBOVBMKBEBUFUJNFJNNVUBCMFDSFBUFGSPNJOUFSGBDFQIQ
    ղܾࡦDPOWFSTJPOGBDUPSZΛ࢖͓͏
    1
    )
    1

    View full-size slide

  106. ղܾࡦ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

    View full-size slide

  107. ͱ͜ΖͰ
    %BUF5JNF&OEQPJOUͷ
    ίϯετϥΫλΛ
    ճҎ্ݺΜͩΒͲ͏ͳΔͷʁ

    View full-size slide

  108. /**


    * @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ΠσΟΦϜͷมܗͰςετΛॻ͘

    View full-size slide

  109. ͢΂ͯʹௐ࿨͕๚Εͨ
    $ ./vendor/bin/phpunit tests/


    PHPUnit 9.5.19 #StandWithUkraine


    ........................................................ 56 / 56 (100%)


    Time: 00:00.015, Memory: 6.00 MB


    OK (56 tests, 122 assertions)


    View full-size slide

  110. Agenda
    ܕએݴ
    ྻڍܕ
    ϞσϦϯά
    ෆมੑͱ౳Ձੑ
    ׬શੑ
    ੹຿ͷ഑ஔ
    👉

    View full-size slide

  111. ݕࡧ։࢝೔࣌ͱ
    ݕࡧऴྃ೔࣌ͷ
    ੔߹ੑΛ୲อ͢Δͷ͸
    ୭ͷ੹೚

    View full-size slide

  112. 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ͷೖΓޱͰߦ͏ͷ͸ے͕ѱ͍

    View full-size slide

  113. ׬શੑ JOUFHSJUZ

    ଘࡏ͢ΔͳΒ͹ৗʹਖ਼͍͠
    ʢෆม৚͕݅ৗʹ੒Γཱͭʣ
    ΦϒδΣΫτΛ໨ࢦ͢

    View full-size slide

  114. 'BJMGBTU
    ଎΍͔ʹࣦഊͤ͞Δ

    View full-size slide

  115. 5JQૣΊʹΫϥογϡͤ͞Δ͜ͱ
    w ίʔυதʹʮ͋Γಘͳ͍ʯͱࢥΘΕΔԿ͔͕ൃੜͨ͠৔߹ɺͦͷ࣌
    ఺ͰϓϩάϥϜ͸΋͸΍࣮ߦՄೳͳ΋ͷͱ͸ͳ͍ͬͯͳ͍
    w ԿΒ͔ͷ͍͕ٙ͋ΔͷͰ͋Ε͹ɺͲͷΑ͏ͳ৔߹Ͱ΋଎΍͔ʹఀࢭ
    ͤ͞Δ΂͖ɻ௨ৗͷ৔߹ɺো֐Λ๊͑ͯத్൒୺ʹಈ͍͍ͯΔϓϩ
    άϥϜΑΓ΋ࢮΜͩϓϩάϥϜͷ΄͏͕μϝʔδ͸গͳ͍
    IUUQTXXXPINTIBDPKQCPPL

    View full-size slide

  116. ࣄલ৚݅ར༻ଆͷ੹೚Ͱ͋Δ৔߹͸ද໌Λ࢖͏
    final class DateTimeRange


    {


    public function __construct(


    public readonly DateTimeEndpoint $startAt,


    public readonly DateTimeEndpoint $endAt,


    ) {


    assert($startAt->value() < $endAt->value());


    }


    }
    JOWBSJBOU ৗʹ੒Γཱͭ΂͖ෆม৚݅
    ΛࣜͰॻ͘

    View full-size slide

  117. 1)1ͷBTTFSU
    IUUQQIQOFUNBOVBMKBGVODUJPOBTTFSUQIQ
    1
    )
    1

    View full-size slide

  118. ද໌Λ࢖Θͳ͍৔߹͸ɺར༻ଆͷ੹೚Ͱ͋Δ͜ͱΛࣔ͢ྫ֎Λ࢖͏
    final class DateTimeRange


    {


    public function __construct(


    public readonly DateTimeEndpoint $startAt,


    public readonly DateTimeEndpoint $endAt,


    ) {


    if ($startAt->value() > $endAt->value()) {


    throw new \InvalidArgumentException('startAt > endAt');


    }


    }


    } ར༻ଆͷޡΓͰ͋Δࢫͷྫ֎Λൃੜͤ͞Δ

    View full-size slide

  119. 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

    View full-size slide

  120. ྫ֎ͱද໌ͷ࢖͍෼͚
    w ຊདྷͷΤϥʔॲཧʹද໌Λ࢖ͬͯ͸͍͚·ͤΜɻ
    ද໌͸ى͜Γಘͳ͍͜ͱΛνΣοΫ͢ΔͨΊͷ΋
    ͷͰ͢ ୡਓϓϩάϥϚʔୈ൛

    w ൃੜ͕༧૝͞ΕΔঢ়گʹ͸ΤϥʔॲཧίʔυΛ࢖
    ༻͠ɺൃੜͯ͠͸ͳΒͳ͍ঢ়گʹ͸Ξαʔγϣϯ
    Λ࢖༻͢Δ $0%&$0.1-&5&ୈ൛

    IUUQTXXXPINTIBDPKQCPPL IUUQTXXXBNB[PODPKQEQ9

    View full-size slide

  121. ͕ͩͪΐͬͱ·ͬͯ΄͍͠ɻ্୺఺ͱԼ୺఺͕౳͍͠ͱ͖͸ʁ
    IUUQTKBXJLJQFEJBPSHXJLJ&$#"&@&#&"%"

    View full-size slide

  122. Ұ఺ू߹Λ࣮ݱ͢Δද໌όʔδϣϯ
    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();


    }


    }
    ෆม৚݅ͷࣜΛߋ৽͢Δ

    View full-size slide

  123. 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');


    }


    }


    }


    }
    Ұ఺ू߹Λ࣮ݱ͢Δྫ֎όʔδϣϯ
    ྫ֎όʔδϣϯͱද໌όʔδϣϯͷ
    ॻ͖ຯ΍ՄಡੑͳͲΛݟൺ΂ͯΈ͍ͯͩ͘͞

    View full-size slide

  124. Agenda
    ܕએݴ
    ྻڍܕ
    ϞσϦϯά
    ෆมੑͱ౳Ձੑ
    ׬શੑ
    ੹຿ͷ഑ஔ
    👉

    View full-size slide

  125. ϨΠϠʔͱ੹຿

    View full-size slide

  126. w จࣈྻ͔Β%BUF5JNF*NNVUBCMFʹม׵͢Δͷ͸୭ͷ੹೚ Ͳ͜Ͱ΍Δ
    w %BUF5JNF*NNVUBCMF͔Βจࣈྻʹม׵͢Δͷ͸୭ͷ੹೚ Ͳ͜Ͱ΍Δ
    w ʢ͍··Ͱݟͯݟ͵ৼΓΛ͍ͯͨ͠ʣλΠϜκʔϯΛѻ͏ͷ͸୭ͷ੹೚ Ͳ͜Ͱ΍Δ
    ·ͩ͋Δ੹຿ઃܭͷٙ໰఺Λ·ͱΊ͍ͯ͜͏

    View full-size slide

  127. ෆద੾ͳ৔ॴͰจࣈྻ͔Β
    %BUF5JNF*NNVUBCMF΁ͷ
    ม׵Λߦ͍ͬͯΔ͜ͱʹ
    ؾ͍ͮͯ͠·ͬͨ

    View full-size slide

  128. 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΁ͷม׵ʹ͸
    ๲େͳ૊Έ߹Θ͕ͤ͋Δ͠ɺ
    λΠϜκʔϯͷѻ͍΋ෆे෼ʹͳͬͯ͠·͍ͬͯΔɻ
    จࣈྻҾ਺ʹλΠϜκʔϯͷΦϑηοτදهΛؚΊͳ͍
    ͱσϑΥϧτͷλΠϜκʔϯʹͳͬͯ͠·͏

    ͭ·Γ͜͜Ͱߦ͏ͷ͸ෆద੾ɻ

    View full-size slide

  129. 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ʣدΓʹόΠΞε͕͔͔ͬͯ͠·ͬͨɻ
    ςετίʔυͷେ෦෼Ͱ͸ಛʹλΠϜκʔϯΛҙࣝͤͣ୹͘ॻ͍͍͖͍͕ͯͨɺ
    ίϯτϩʔϥͰ͸λΠϜκʔϯΛҙࣝͯ͠ݫີʹѻ͍͍ͨɻ
    ςετίʔυ΋ͻͱͭͷίϯςΫετɻ
    ςετ͔ΒઃܭΛۦಈ͢Δͱ͖ʹ͜ͷόΠΞεʹ஫ҙ͢Δඞཁ͕͋Δɻ
    #
    "
    %

    View full-size slide

  130. wྑ͍ΠϯλϑΣʔεͱ͸࣍ͷͭͷ৚݅Λຬͨ
    ͢ΠϯλϑΣʔε
    wਖ਼͘͠࢖༻͢Δํ͕ૢ࡞ϛεΛ͢ΔΑΓ؆୯
    wޡͬͨ࢖͍ํΛ͢Δ͜ͱ͕ࠔ೉
    ΤοηΠਖ਼͍͠࢖͍ํΛ؆୯ʹɺޡͬͨ࢖͍ํΛࠔ೉ʹ
    IUUQTXXXPSFJMMZDPKQCPPLT
    ༰қ͞ΛٻΊͨ݁Ռɺ
    ޡͬͨ࢖͍ํΛ͢Δͷ΋༰қʹͳͬͯ͠·ͬͨ
    ༰қ͞ΛٻΊͨ݁Ռɺ
    ޡͬͨ࢖͍ํΛ͢Δͷ΋༰қʹͳͬͯ͠·ͬͨ

    View full-size slide

  131. 4JNQMFͱ&BTZ͸ࠞͥΔͳةݥ
    IUUQTUXJUUFSDPNU@XBEBTUBUVT IUUQTUXJUUFSDPNU@XBEBTUBUVT

    View full-size slide

  132. 4JNQMFͱ&BTZΛ
    ࠞͥͳ͍
    &BTZ͞͸
    ঢ়گʹΑΓҟͳΔ

    View full-size slide

  133. 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
    #
    "
    %

    View full-size slide

  134. 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

    View full-size slide

  135. 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

    View full-size slide

  136. 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Λ߹ྲྀͤ͞Δɻ
    ͜͏ॻ͚Ε͹े෼ͩͬͨɻ

    View full-size slide

  137. จࣈྻͱ%BUF5JNF*NNVUBCMFͷ૬ޓม׵΍

    λΠϜκʔϯͷѻ͍͸ͦΕͧΕ୭͕Ͳ͜Ͱ΍Δ΂͖ʁ

    View full-size slide

  138. ΈΜͳେ޷͖ͳ$MFBO"SDIJUFDUVSFʹग़ͯ͘Δ
    ಉ৺ԁΛྫʹͯ͠ߟ͑ͯΈΔ

    View full-size slide

  139. ੺ͱ੨ͷք໘ɺ૚ͷ๷ޚϥΠϯ͕͋Δ

    View full-size slide

  140. ਖ਼౰ੑͱݎ࿚ੑ
    w ࠷దͳΤϥʔॲཧ͸Τϥʔ͕ൃੜͨ͠ιϑτ΢ΣΞͷछྨʹΑΓҟͳΔ
    w ਖ਼౰ੑͱ͸ɺෆਖ਼֬ͳ݁ՌΛܾͯ͠ฦ͞ͳ͍͜ͱΛҙຯ͢Δɻෆਖ਼֬ͳ
    ݁ՌΛฦ͘͢Β͍ͳΒɺԿ΋ฦ͞ͳ͍ํ͕·͠Ͱ͋Δ
    w ݎ࿚ੑͱ͸ɺιϑτ΢ΣΞͷ࣮ߦΛܧଓͰ͖ΔΑ͏ʹखΛਚ͘͢͜ͱͰ
    ͋ΔɻͦΕʹΑͬͯෆਖ਼֬ͳ݁Ռ͕΋ͨΒ͞ΕΔ͜ͱ͕͋ͬͯ΋͔·Θ
    ͳ͍
    w ҆શੑʢ΍ਖ਼֬ੑʣΛॏࢹ͢ΔΞϓϦέʔγϣϯͰ͸ɺݎ࿚ੑΑΓ΋ਖ਼
    ౰ੑ͕༏ઌ͞ΕΔ܏޲ʹ͋Δ
    w ίϯγϡʔϚΞϓϦέʔγϣϯͰ͸ɺਖ਼౰ੑΑΓ΋ݎ࿚ੑ͕༏ઌ͞ΕΔ
    ܏޲ʹ͋Δ
    IUUQTXXXBNB[PODPKQEQ9

    View full-size slide

  141. ਖ਼౰ੑ
    ݎ࿚ੑ
    ৗʹਖ਼֬͞ɺਖ਼͠͞ΛॏΜ͡Δ
    ֎ք͔Βͷ༷ʑͳೖྗ΍ग़ྗΛɺίϯςΫετ
    ʹԠͯ͡ద੾ʹϋϯυϦϯά͠ɺม׵͢Δ

    View full-size slide

  142. DateTimeImmutable
    DateTimeRange
    DateTimeEndpoint
    DateTimeZone
    DateTimeZone
    DateTimeImmutable
    string
    string 8FC͔ΒདྷͨจࣈྻΛόϦσʔγϣϯͭͭ͠ɺʢΞϓϦ
    έʔγϣϯͰద੾ʹอ࣋ͨ͠ʣλΠϜκʔϯΛ൐ͬͯ
    %BUF5JNF*NNVUBCMFʹม׵͢Δ
    %BUF5JNF*NNVUBCMFΛ
    λΠϜκʔϯΦϑηοτΛ൐ͬͨ
    λΠϜελϯϓจࣈྻͱͯ͠
    %#ʹ౉͢

    View full-size slide

  143. ࠓճͷߨԋͷ
    ࠷ऴతͳઃܭ

    View full-size slide

  144. 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
    ۠ؒͷ୺఺Λࣔ͢ɻ
    ๷ޚతίϐʔɺ๷ޚతΞΫηοα౳Ͱ
    ঢ়ଶมԽΛ๷͙ɺ
    ෆมͷ஋ΦϒδΣΫτ

    View full-size slide

  145. 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
    ۠ؒΛࣔ͢ɻ
    ίϯετϥΫλͰෆม৚݅Λ੒ཱͤ͞ɺ
    Ҏ߱ঢ়ଶ͕มԽ͠ͳ͍ɺ
    ׬શੑΛ൐ͬͨ஋ΦϒδΣΫτ

    View full-size slide

  146. 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ͷҾ਺͕ଘࡏ͢Δ࣌఺Ͱطʹਖ਼͍͠ঢ়ଶͳͷͰ
    ʢ׬શੑʣɺ๷ޚతνΣοΫ͸ෆཁʹͳΔ

    View full-size slide

  147. ͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠
    w ๷ޚతϓϩάϥϛϯάͱ͸ѱ͍ίʔυʹឺ૑ߣΛ͋ͯΔ͜ͱͰ͸ͳ͘ɺྑࣝ͋Δ࣮ફͷੵΈॏͶ
    w ܕએݴͰ૝ఆ͢΂͖ೖྗͷܕΛߜΓɺ๷ޚతνΣοΫΛେ෯ʹݮΒ͢
    w ྻڍܕͰऔΓಘΔ஋ΛߜΓɺ๷ޚతνΣοΫΛେ෯ʹݮΒ͢
    w جૅͱͳΔܕΛϞσϦϯά͠ɺᐆດ͞΍ؒҧ͍΍͢͞Λ࡟ݮ͢Δ
    w جૅͱͳΔܕΛෆมΦϒδΣΫτʹ͠ɺঢ়ଶมԽ΍෭࡞༻ʹىҼ͢ΔόάΛ༧๷͢Δ
    w جૅͱͳΔܕΛ஋ΦϒδΣΫτʹ͠ɺΠϯελϯεͰ͸ͳ͘஋Ͱ౳ՁੑΛ൑அ͢Δ
    w ΦϒδΣΫτੜ੒࣌ʹෆม৚݅Λ੒ཱͤ͞ɺ͔ͭͦͷΦϒδΣΫτ͕ෆมΦϒδΣΫτͰ͋Δͳ
    Β͹ɺੜ੒͞ΕͨΦϒδΣΫτ͸ৗʹਖ਼͍͠ʢ׬શੑʣ
    w 4JNQMF͞ʢ֓೦ͱͯ͠ͷཁૉͷগͳ͞ʣͱ&BTZ͞ʢख਺ͷগͳ͞ʣΛෆ༻ҙʹࠞͥͳ͍
    w ਖ਼౰ੑΛॏΜ͡ΔϨΠϠʔͱݎ࿚ੑΛॏΜ͡ΔϨΠϠʔΛ෼͚Δ
    w ઃܭͱ͸੹຿ͷ࠷ద഑ஔΛٻΊଓ͚Δ͜ͱɻ୭͕ԿΛ஌͍ͬͯͯԿΛ஌Δ΂͖Ͱͳ͍͔ɺԿΛ΍
    Δ΂͖ͰԿΛ΍Δ΂͖Ͱͳ͍͔Λৗʹߟ͑ଓ͚Δ͜ͱ

    View full-size slide