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

予防に勝る防御なし - 堅牢なコードを導く様々な設計のヒント / 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
PRO

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 Slide

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

    View 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 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 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 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 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 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 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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 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 Slide

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


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


    *


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


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


    */


    public function findAll($params)


    {


    View Slide

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

    View Slide

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

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

    View Slide

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

    View Slide

  20. ܕΛߜΔ

    View Slide

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

    View Slide

  22. 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 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 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 Slide

  24. 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 Slide

  25. ૝ఆ͠ͳ͚Ε͹ͳΒͳ͍ঢ়گ͕ݮͬͨ
    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 Slide

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

    View Slide

  27. ஋ΛߜΔ

    View Slide

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

    View Slide

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


    enum Status: string


    {


    case Open = 'OPEN';


    case New = 'NEW';


    case Fixed = 'FIXED';


    }
    IUUQTXXXQIQOFUNBOVBMKBMBOHVBHFFOVNFSBUJPOTQIQ

    View Slide

  30. 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 Slide

  31. 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 Slide

  32. 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 Slide

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

    View Slide

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

    View Slide

  35. 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 Slide

  36. 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 Slide

  37. 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  43. 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 Slide

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

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

    View Slide

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

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

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

    View Slide

  49. ෆมੑ
    JNNVUBCJMJUZ

    ঢ়ଶมԽΛഇ͢Δ

    View Slide

  50. ผ໊ࢀর໰୊
    "MJBTJOH

    View Slide

  51. 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 Slide

  52. $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 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->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 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->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 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->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 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->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 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->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 Slide

  58. $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 Slide

  59. $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 Slide

  60. Կ͕ݪҼͩͬͨͷ͔

    View Slide

  61. Կ͕ݪҼͩͬͨͷ͔
    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 Slide

  62. 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 Slide

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

    View Slide

  64. %BUF5JNFͱ%BUF5JNF*NNVUBCMF
    IUUQTXXXQIQOFUNBOVBMKBDMBTTEBUFUJNFJNNVUBCMFQIQ

    View Slide

  65. /**


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

  66. /**


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

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

    View Slide

  68. 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 Slide

  69. 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 Slide

  70. 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 Slide

  71. 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 Slide

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

    View Slide

  73. $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 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->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 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->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 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->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 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->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 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->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 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->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 Slide

  80. $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 Slide

  81. $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 Slide

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

    View Slide

  83. ί
    ϯ
    ε
    τ
    ϥ
    Ϋ
    λ
    ͸


    Ұ
    ճ
    ͠
    ͔
    ݺ
    ΂
    ͳ
    ͍
    ͱ



    ֮
    ͠
    ͯ
    ͍
    ͨ
    ʁ















    Ұ


    ͍
    ͭ
    ͔
    Β
    ŠŠŠŠŠŠ

    View Slide

  84. /**


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

  85. /**


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

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

    View Slide

  87. ίϯετϥΫλ಺Ͱ๷ޚత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 Slide

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

    View Slide

  89. /**


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

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

    View Slide

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

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


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

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

    View Slide

  94. %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 Slide

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


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

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

    View Slide

  97. %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 Slide

  98. 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 Slide

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

    View Slide

  100. ͡Ό͋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 Slide

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


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

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

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


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

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

    View Slide

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

    View Slide

  106. ஋ΦϒδΣΫτͷϦϑΝΫλϦϯάࣦഊΛςετ͕ڭ͑ͯ͘Εͨ
    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 Slide

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

    View Slide

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

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

    View Slide

  110. /**


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

  111. ͢΂ͯʹௐ࿨͕๚Εͨ
    $ ./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 Slide

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

    View Slide

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

    View Slide

  114. 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 Slide

  115. ׬શੑ JOUFHSJUZ

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

    View Slide

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

    View Slide

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

    View Slide

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


    {


    public function __construct(


    public readonly DateTimeEndpoint $startAt,


    public readonly DateTimeEndpoint $endAt,


    ) {


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


    }


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

    View Slide

  119. 1)1ͷBTTFSU
    IUUQQIQOFUNBOVBMKBGVODUJPOBTTFSUQIQ
    1
    )
    1

    View Slide

  120. ද໌Λ࢖Θͳ͍৔߹͸ɺར༻ଆͷ੹೚Ͱ͋Δ͜ͱΛࣔ͢ྫ֎Λ࢖͏
    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 Slide

  121. 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 Slide

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

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

    IUUQTXXXPINTIBDPKQCPPL IUUQTXXXBNB[PODPKQEQ9

    View Slide

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

    View Slide

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

  125. 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 Slide

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

    View Slide

  127. ϨΠϠʔͱ੹຿

    View Slide

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

    View Slide

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

    View Slide

  130. 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 Slide

  131. 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

  136. 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 Slide

  137. 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 Slide

  138. 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 Slide

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

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  146. 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 Slide

  147. 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 Slide

  148. 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 Slide

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

    View Slide

  150. ࢀߟจݙ

    View Slide