BASEプロダクトチームブログ

ネットショップ作成サービス「BASE ( https://thebase.in )」、ショッピングアプリ「BASE ( https://thebase.in/sp )」のプロダクトチームによるブログです。

PHP Conference Japan 2023 にBASEのエンジニアが登壇・ゴールドスポンサーとして協賛しました

BASEのスポンサーブースの様子

こんにちは!すっかり秋らしくなり、涼しくなってきましたね。皆さんいかがお過ごしでしょうか。
さて、この度は、2023/10/08(日)に開催された PHP Conference Japan 2023 にゴールドスポンサーとして協賛し、BASEのエンジニアが登壇しました。

今回は、登壇者からコメントと、会場やスポンサーブースの様子についてお届けします!

PHP Conference Japan 2023とは

PHP Conference Japan 2023は、2023/10/08(日)に開催された国内最大級のPHPカンファレンスです。
BASE はこれまでにも開催されている PHP カンファレンスへの登壇並びにスポンサーをコミュニティ貢献活動として行って参りました。今回はゴールドスポンサーとして当カンファレンスに協賛しています。

画面左側にBASEのコーポレートロゴ

登壇者のコメント

glassmonkey (@glassmonekey)

イベントお疲れ様でした。永野です。
PHPというテーマを題材にみなさんと交流できて最高に楽しい1日でした。
同時に見たいセッションが複数あったりで体が複数あったら!!と思う1日でした。

登壇内容自体は業務では現状使ってないものの、今後使う可能性が高いWasmについてお話をさせていただきました。 いかがでしたでしょうか?

私自身、Wasmはブラウザ上の技術という固定観念があったのですが、
学習していくうちにバックエンドエンジニアとしてキャッチアップしておいたほうが良い技術であるという学びがありました。
今回のトークで、その一端を皆さんに少しでもお伝え出来たのなら嬉しく思います。

ぜひ、良きWasmライフを!! speakerdeck.com

スポンサーブースの様子

ここからはスポンサーブースの様子をお届けします!

今年は「BASEからのPHPerへの挑戦状」というクイズ企画を開催しました。

「BASEからのPHPerへの挑戦状」を解いている参加者の様子

スポンサーブースに来て頂いた方に、ネットショップ作成サービス「BASE」をご利用いただいているCOZY COFFEEさんのコーヒーパックを配布しました。

cozycoffee.thebase.in

コーヒーパックとBASEロゴにもあるtipi

「BASEからのPHPerへの挑戦状」の解説

ここからは「BASEからのPHPerへの挑戦状」にて出題したクイズの正解と、正解率、解説をします。

問題1 (正解率: 71.4%)

PHP8.0.xのセキュリティサポートの終了日はいつでしょうか? 次の選択肢から1つ選んでください。

  1. 2022年11月28日
  2. 2023年11月26日
  3. 2024年11月25日
  4. 2025年12月8日

正解と解説はこちら

2. 2023年11月26日

解説

メジャーバージョン、マイナーバージョンのセキュリティサポート終了日は以下のページで知ることができます。

こちらによれば各日付は以下のPHPバージョンのセキュリティサポート終了日になります。

  1. 2022年11月28日はPHP7.4
  2. 2023年11月26日はPHP8.0.x
  3. 2024年11月25日はPHP8.1.x
  4. 2025年12月8日はPHP8.2.x

問題2 (出題ミスがありました。詳しくは「正解と解説はこちら」に記載) (参考正解率: 54.3%)

emptyの挙動で正しいのはどれでしょか? 正しい説明だと思うのを1つ選んでください。

  1. 変数が存在し、かつ空文字列またはゼロまたはfalseの場合にtrueを返します。
  2. 変数が存在し、かつ空でない文字列またはゼロまたはfalseの場合にtrueを返します。
  3. 変数が存在するかどうかを確認し、変数が存在しない場合にtrueを返します。
  4. 変数が存在しない場合にtrueを返し、それ以外の場合にfalseを返します。

正解と解説はこちら

出題側で事前に想定していた正解は2. 変数が存在し、かつ空でない文字列またはゼロまたはfalseの場合にtrueを返します。でした。 しかしクイズに挑戦いただいた何名かの方から異議がありましたので問題文と答えを見直したところ、正解が異なっていそうだという話になりました。

申し訳ありませんでした。

正しい説明1つを選択する問題でしたが、正解は 選択肢1. と 選択肢3. になります。

解説

empty() で、trueを返すパターンは以下のPHPマニュアルに解説があります。

PHP 型の比較表

empty($x);trueを返すのは以下のパターンです。

  • パターン1. $x = ""; (空文字列)
  • パターン2. $x = null; (null)
  • パターン3. var $x;(PHP5.0未満でも使用可能な、クラスのプロパティを宣言する方法。)
  • パターン4. $x が未定義
  • パターン5. $x = []; (空の配列)
  • パターン6. $x = false; bool型のfalse
  • パターン7. $x = 0; 数値の0
  • パターン8. $x = "0"; 文字列の0

こちらを踏まえて、それぞれの選択肢がemptyの正しい挙動かどうかみていきます。

選択肢1, 2, 3は、empty()がtrueを返す場合の説明、選択肢4はempty()がtrueを返す場合とfalseを返す場合について説明していますが、

選択肢1. 変数が存在し、かつ空文字列またはゼロまたはfalseの場合にtrueを返します。

パターン1〜8 を網羅してはいませんが、パターン1, 7, 6 の場合にtrueを返す、とのことなので、選択肢1は正解

選択肢2. 変数が存在し、かつ空でない文字列またはゼロまたはfalseの場合にtrueを返します。

パターン7, 6については正しい説明になっていますが、 パターン8に関連した説明「空でない文字列でtrueを返す」のは "0"の場合のみであり、 それ以外の文字列では false を返すので、選択肢2は不正解

選択肢3. 変数が存在するかどうかを確認し、変数が存在しない場合にtrueを返します。

https://www.php.net/manual/ja/function.empty.php

には以下の記述があります。

つまり、empty() は本質的に !isset($var) || $var == false と同じことを簡潔に記述しているだけです。

こちらの記述に合致しているためempty()は、変数の存在を確認していますし、 選択肢3は正解と言えます。(パターン4の説明をしている)

選択肢4. 変数が存在しない場合にtrueを返し、それ以外の場合にfalseを返します。

こちらもパターン4に合致していますが、他にtrueを返すパターン1〜3, 5~8について間違った説明になっているので選択肢4は不正解

問題3 (正解率: 8.6%)

PHP8.3からできるようになった Random\Randomizer クラスの挙動で正しいのはどれでしょうか? 正しい説明だと思うのを2つ選んでください。

  1. echo $randomizer->getBytes(10);
  2. echo $randomizer->getInt(0, 100)
  3. echo $randomizer->getBytesFromString('abcdefghijklmnopqrstuvwxyz0123456789', 16);
  4. echo $randomizer->nextFloat();
  5. echo implode(',', $randomizer->shuffleArray(['佐藤', '鈴木', '高橋']));

正解と解説はこちら

  • 3. echo $randomizer->getBytesFromString('abcdefghijklmnopqrstuvwxyz0123456789', 16);
  • 4. echo $randomizer->nextFloat();

選択肢1,2,5はPHP 8.2からできるので不正解です。

選択肢3,4はPHP 8.3からできるようになったので正解です。

問題4 (正解率: 14.3%)

PHP8.2で書かれたサンプルコードがあります。インスタンスメソッド、「Sample::func()」の仮引数の型に指定してもPHPエラーが発生しないものを3つ選んでください。 (※PHPバージョンは8.2で、PHPコードで定義されているクラスは Sample 以外にないものとします)

  1. int|string
  2. null
  3. namespace
  4. void
  5. self
  6. static
  7. never

正解と解説はこちら

  • 1. int|string
  • 2. null
  • 5. self

解説

正解の選択肢1, 2, 5 は仮引数の型に指定できます。選択肢3. は型として指定できません。 選択肢4, 6, 7は、戻り値型には指定できますが、仮引数の型には指定できません。

問題5 (正解率: 25.0%)

あなたはBASEに入社して、プロダクトコードを読んでいます。 以下のコードがあったとき、最終的な商品の金額はいくらになるでしょうか?次の選択肢から選んでください。 なおPHPバージョンは8.2で、今回の問題で表示されているクラス以外は無いものとします。

  1. ¥1,045
  2. ¥1,815
  3. ¥2,035
  4. ¥2,145
  5. ¥2,585

https://gist.github.com/cocoeyes02/a51d1855d31d7b85f6b2ec9973b12c36

(コードの行数が長いため、gistに表示しています)

正解と解説はこちら

4. ¥2,145

以下のリンクより、2145が出力されていることが確認できます。 php-play.dev

解説

この問題は、商品、オプション、送料、クーポンを使って税込の合計金額を算出する問題です。

計算の順番は

  1. 税抜の合計金額を算出する
  2. 税込の合計金額を算出する

というようになっています。

税抜の合計金額を算出

まずProductクラスのtotalメソッドを見ると、税抜の合計金額が商品の金額 、オプション料金の合計金額 、送料、割引額で構成されていることがわかります。

<?php

public function total(): Price
{
    $optionTotalPrice = $this->optionGroup->total();
    $discount = $this->coupon->getDiscount();

    $totalPrice = new Amount(0);
    $totalPrice = $totalPrice->add($this->productPrice->value) // 商品の金額
        ->add($optionTotalPrice) // オプション料金の合計金額
        ->add($this->fee->value) // 送料
        ->sub($discount); // 割引額

    if ($totalPrice->isNegative()) {
        throw new Error('total price is negative');
    }

    return (new Price($totalPrice))->calculateTaxIncluded(ItemTax::getTaxPercentageByItemType($this->itemType));
}

一つずつ算出してみましょう。

まず商品の金額は、Productクラスのconstructで代入された $productPrice をそのまま使っているだけなので1,000になります。

<?php

public function __construct(
    public ProductName $name,
    public Price       $productPrice, // 商品の金額
    ...
)
{
}
<?php
$price = Price::fromInt(1000);

オプション料金の合計金額は、OptionGroupクラスのtotalメソッドより、constructで代入されたOptionの金額を合計しています。 今回はオプションを2つ追加しており、100 + 50 = 150になります。

<?php

public function total(): Amount
{
    $total = new Amount(0);
    foreach ($this->options as $option) {
        $total = $total->add($option->price->value); // 各Optionの金額の合計値を算出
    }
    return $total;
}
<?php

$optionGroup = new OptionGroup(
    [
        new Option('ラッピング', new Price(new Amount(50))),
        new Option('名前入れ', new Price(new Amount(100))),
    ],
);

送料は、FeeクラスのcreateFromSubtotalメソッドより、商品の金額とオプションの合計金額の合計値から、クーポンの割引額を差し引いた小計を使って算出しています。
今回の小計は、1,000 + 150 - 200 = 950 です。

それから、FeeTypeクラスのcreateFromIntメソッドにある対応表に算出した小計をあてはめて、送料を算出しています。
今回は小計が1,000を下回るので、送料は1,000です。

<?php

public static function createFromSubtotal(
    Price       $price,
    OptionGroup $optionGroup,
    Coupon      $coupon,
): self
{
    $subtotal = new Amount(0); // 小計
    $subtotal->add($price->value) // 商品の金額
        ->add($optionGroup->total()) // オプションの合計金額
        ->sub($coupon->getDiscount()); // クーポンの割引額
    $fee = FeeType::createFromInt($subtotal->toInt());

    return new self(new Amount($fee->value));
}
<?php

enum FeeType: int
{
    case Free = 0;
    case FiveHundred = 500;
    case OneThousand = 1000;

    public static function createFromInt(int $value): self
    {
        return match (true) {
            $value >= 1500 => self::Free, // 金額が1500円以上だったら送料無料
            $value >= 1000 => self::FiveHundred, // 金額が1500円未満、1000円以上だったら送料500円
            $value >= 0 => self::OneThousand, // 金額が1000円未満だったら送料1000円
        };
    }
}

割引額は、クーポンの割引額より算出しています。

ただし、ItemTypeがデジタルコンテンツである場合は割引が効きません。
今回は、ItemTypeが予約商品であるため割引が効きます。

よって割引額は200です。

<?php

public function getDiscount(): Amount
{
    if (!$this->isAvailable()) {
        return new Amount(0); // クーポンが使えない場合は割引額は0円
    }

    return new Amount($this->discountAmount->toInt());
}

public function isAvailable(): bool
{
    return match ($this->itemType) {
        ItemType::Lottery, ItemType::PreOrder => true,
        ItemType::DigitalContent => false,
    };
}
<?php

$coupon = new Coupon($itemType, new Amount(200));

これで税抜の合計金額を求める上で必要な金額が全て算出できました。

改めて書くと

  • 商品の金額:1,000
  • オプション料金の合計金額:150
  • 送料:1000
  • 割引額:200

です。

よって計算すると、1000 + 150 + 1000 - 200 = 1,950になり、税抜の合計金額は¥1,950です。

税込の合計金額を算出

税抜の合計金額を算出したら、税込の金額を算出します。

税込の金額は、商品タイプによって税率が決まっています。今回は予約商品を扱っているので、税率は10%になります。

<?php

return (new Price($totalPrice))->calculateTaxIncluded(ItemTax::getTaxPercentageByItemType($this->itemType));
<?php

public function calculateTaxIncluded(TaxType $taxType): self
{
    $amount = $this->value->multiply($taxType->getPercentage());
    return new self($amount);
}
<?php

class ItemTax
{
    public static function getTaxPercentageByItemType(ItemType $itemType): TaxType
    {
        return match ($itemType) {
            ItemType::DigitalContent, ItemType::PreOrder => TaxType::Ten, // 予約商品の税率は10%
            default => TaxType::Eight,
        };
    }
}

税抜の合計金額は1,950なので、計算すると 1,950 × 1.1 = 2,145

税込の合計金額は¥2,145になります。

今回はtotalメソッドの値をそのまま出力しているため、totalメソッドの返り値である税込の合計金額が答えになります。

よって答えは、選択肢4の¥2,145です。

<?php

echo $product->total()->value->toInt();

謝辞

協賛・社員のスピーカー参加を通して PHP コミュニティの盛り上がりに貢献でき、弊社としても大変有意義な時間となりました。
スタッフの方々には業務でお忙しいにも関わらず、多くの時間をカンファレンス準備へ注いでいただいたかと思います。この場を借りて御礼申し上げます。

それでは、来年もお会いしましょう!