このエントリーをはてなブックマークに追加

PHPでは、変数に 0.1 を代入すると float 型と判定されます。

sprintf() で表示したところ小数部は53桁あり、それ以降を切り捨てる内容の警告が表示されました。

float 型の 0.1 は、なぜこのような値に変換されるのでしょうか?
浮動小数点がメモリに格納される仕組みを解説しながら理由を探ります。

10進数と2進数の表記(整数)

数値はメモリ上に2進数で格納されます。

2進表記の整数は 右端(一の位) から、2の0乗、2の1乗、2の2乗、2の3乗…を表します。2進数なので、格納される値は「0」または「1」の二通りです。「0」または「1」が格納される領域を「ビット」と呼びます。

10進数の 10 は2進数では 1010 です。「1」が格納されたビット(2の1乗と、2の3乗)の合計(2 + 8)が10進表記になります。

10進数と2進数の表記(小数)

2進表記の小数は 左端(小数第一位) から、2の-1乗、2の-2乗、2の-3乗…を表します。マイナスのn乗は割り算で求めます。2のマイナス3乗は、1に対して「÷2」を3回くりかえした数です。

10進数の 0.625 は2進数では 0.101 です。「1」が格納されたビット(2の-1乗と、2の-3乗)の合計(0.5 + 0.125)が10進表記になります。

10進数から2進数の変換(整数)

10進数の 22 は2進数では 1 0110 です(読みやすいよう4桁で区切っています)。

  • 対象の10進数をひたすら2で割り算します
  • 割り算の結果が0か1になればそこで終了します
  • 余りの数を計算の新しい順に並べたものが、2進数の表記です

10進数から2進数の変換(小数)

10進数の 0.1 は2進数では 0.0001 1001 1001 1001... です。

  • 対象の10進数の小数部分を取り出し、ひたすら2で掛け算します
  • 次の計算に進む時も同じように小数部分を取り出します
  • 結果の整数部分を計算の古い順に並べたものが、2進数の表記です

0.1 の2進表記を求めた時、結果が1.6になるとそこから同じ計算をぐるぐる循環してしまいます。このように小数で同じ数字の並びが無限に繰り返されることを「循環小数」と呼びます。

循環小数である 0.0001 1001 1001 1001... は、有限のメモリ上に正確な値を格納することができません。小数の割り当てが4ビットだと 0.0001 、8ビットだと 0.0001 1001 というように、たくさんのビットを確保すればそれだけ詳細な表現ができるため誤差は少なくなります。このことを「精度」と呼びます。

浮動小数点のフォーマット

浮動小数点の標準は IEEE 754 で規定されます。数種類のフォーマットのうち、使用ビットが多いものほど高い精度を実現します。

ドキュメントを見ると、PHPの浮動小数点は IEEE 754Binary64(倍精度) であることが分かります。

浮動小数点数の精度は有限です。
システムに依存しますが、PHP は通常 IEEE 754 倍精度フォーマットを使います。

浮動小数点数 | PHPマニュアル
https://www.php.net/manual/ja/language.types.float.php

仮数部

仮数部は整数の一の位を「1」として格納するルールがあります。

0.0001 1001 1001 1001… の小数点を4回移動すると 1.1001 1001 1001… になります。この結果から 1. を除いた部分が「仮数部」に格納されます。

1001 1001 1001… という循環小数のように仮数部のビットに収まらない場合、丸め処理が行われます。

指数部

小数点を移動した回数に 1023 を足した数が「指数部」に格納されます。この数は「バイアス」と呼び、精度ごとに規定されています。今回の例の Binary64(倍精度) の場合 1023 になります。

小数点の移動回数はマイナスで表します。今回は4回移動したので 1023 + (-4)1019 が格納されます。

符号部

仮数部、符号部の他に、プラスマイナスを表す「符号部」があります。符号部は0なら正、1なら負を表します。

浮動小数点の取り出し

浮動小数点を10進数に変換する時は「仮数部」「指数部」「符号部」に格納された値から計算されます。この時、仮数部が丸められていると元の値に戻すことはできません。

sprintf('%.53f', $n) してみると、丸めが発生する 0.10.2Binary64(倍精度) の精度いっぱいまで桁を持ち、 0.5 のように2の-n乗で表現できるものは丸めが発生しないことが分かります。

まとめ

精度の高い浮動小数点は、人間が読みやすいように表示されるため、正確な値であるかのように見えます。

内部的には2進数への変換と丸め処理により、わずかな誤差が発生することがあります。var_dump() による警告は、浮動小数点の表現の限界を示したものと言えます。

以上です。 float 型の理解の一助になれば幸いです。


このエントリーをはてなブックマークに追加

\ Symfonyアドベントカレンダー2023 22日目です /

気になってたSymfony UXを初めて使ってみたので、そのメモです。

今回はパスワード入力画面でよくみる、パスワードの表示・非表示を切り替える機能を実装します。 SymfonyUXでは簡単にできるようにパッケージが用意されているので、すぐに実装できます。

概要

symfony/skeletonで新しいプロジェクトの作成

$ composer create-project symfony/skeleton:"6.4.*" form_exam

必要なバンドルのインストール

$ composer require form validator orm twig

$ composer require --dev symfony/maker-bundle

symfony/webpack-encore-bundleのインストール

$ composer update symfony/flex
$ composer require symfony/webpack-encore-bundle

$ npm install
$ npm run dev

symfony/stimulus-bundleのインストール

$ composer require symfony/stimulus-bundle

symfony/ux-toggle-passwordのインストール

$ composer require symfony/ux-toggle-password

$ npm install --force
$ npm run watch

パスワード入力フォームの作成

$ symfony console make:controller CredentialFormController

// CredentialFormTypeを作成し、CredentialFormControllerとテンプレートの微調整

src/Form/CredentialFormType.php

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\FormBuilderInterface;

class CredentialFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('password', PasswordType::class)
        ;
    }
}

src/Controller/CredentialFormController.php

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class CredentialFormController extends AbstractController
{
    #[Route('/credential/form', name: 'app_credential_form')]
    public function index(Request $request): Response
    {
        $form = $this->createForm(CredentialFormType::class);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            //todo
        }

        return $this->render('credential_form/index.html.twig', [
            'form' => $form->createView(),
        ]);
    }
}

templates/credential_form/index.html.twig

...
{% block body %}
    {{ form_start(form) }}
    {{ form_row(form.password) }}
    <button type="submit">Login</button>
    {{ form_end(form) }}
{% endblock %}

こんな感じで普通のパスワード入力フォームができました。 常にパスワードは非表示です。

PasswordTypeを使ったフォーム画面

パスワードを表示/非表示に切り替えれるようにする

  • CredentialFormTypeにtoggle=trueをつける
  • base.html.twigにcssとjsのエントリーポイントを追加する 👈忘れないように!

src/Form/CredentialFormType.php

     public function buildForm(FormBuilderInterface $builder, array $options)
     {
         $builder
-            ->add('password', PasswordType::class)
+            ->add('password', PasswordType::class, ['toggle' => true])
         ;
     }
 }

templates/base.html.twig

        {% block stylesheets %}
+            {{ encore_entry_link_tags('app') }}
        {% endblock %}
 
         {% block javascripts %}
+            {{ encore_entry_script_tags('app') }}
         {% endblock %}
     </head>

右上のShow/Hideボタンで、パスワードの表示/非表示を切り替えれるようになりました :tada:

PasswordTypeを使ったフォーム画面(表示) PasswordTypeを使ったフォーム画面(非表示)

さいごに

とっても簡単にできました! webpack encoreのインストールで少しハマりました :sweat_smile:


このエントリーをはてなブックマークに追加

こんにちは、開発部の鈴木です。
この記事は Symfony Advent Calendar 2023 の21日目の記事です。
今回はSymfonyアプリケーションの開発における、Messengerの利用シーンと利用方法について紹介します。

はじめに

WEBアプリケーションを開発する際に、ユーザーがアクションを行った数分後に特定の処理を行いたい場合があります。

例えば、外部APIを使用する際には、処理の完了に時間がかかるものや、ステータスの更新が反映されるまでに10~30分かかることがあります。

ユーザーがアプリケーションを操作した後、このような非同期な機能を使用して処理を実行し、その完了を確認した上で、データベースを更新したり、ユーザーにメールを送信したりする必要があります。

このようなケースで、以前はcronを使用して定期的に非同期処理の実行状態を確認していました。しかし、最近SymfonyのMessengerを使用してメッセージの処理を意図的に指定した時間だけ遅延させることができることを知り、実際に実装してみてcronよりもMessengerが適していると感じました。

Messengerの使用方法

まず、Messengerを使用するためには、Messageクラスとそれに対応したMessageHandlerクラスを作成し、MessageBusを使用してメッセージを送信します。

今回はユーザーのアクションで外部APIの非同期のタスクを実行し、そのタスクの実行状態を定期的に確認して完了したらメールを送信するといった実装例を紹介します。

Messageクラスを作成

メッセージの送受信に使用するためのMessageクラスを作成します。

namespace App\Message;

class CheckTaskMessage
{
    public function __construct(
        private int $taskId,
    ) {
    }

    public function getTaskId(): int
    {
        return $this->taskId;
    }
}

MessageHandlerクラスを作成

先程作成したCheckTaskMessageを受け取り、タスクが完了していればメールを送信するMessageHandlerクラスを作成します。

namespace App\MessageHandler;

use App\Message\CheckTaskMessage;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
class CheckTaskMessageHandler
{
    public function __invoke(CheckTaskMessage $message)
    {
        $taskId = $message->getTaskId();

        // ... $status = 外部APIを使用して$taskIdのタスクの実行状態を取得する

        if ($status === 'completed') {
            // ... メールを送信する
        }
    }
}

メッセージの送信

DIしたMessageBusInterfaceを使用してメッセージを送信します。

namespace App\Controller;

use App\Message\CheckTaskMessage;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;

class DefaultController extends AbstractController
{
    public function index(MessageBusInterface $messageBus): Response
    {
        
        // ... $taskId = 外部APIを使用しての非同期なタスクの実行を依頼する

        // タスクの実行状態を確認して何らかの処理を行うメッセージを送信する
        $message = new CheckTaskMessage($taskId);
        $messageBus->dispatch($message);

        // ...
    }
}

DelayStampでメッセージの処理を遅延させる

このままでは、非同期なタスクの実行を依頼した直後にタスクの実行状態を確認することしかできません。
タスクの完了には10 ~ 30分ほど時間がかかる見込みとして、EnvelopeとDelayStampを使用してタスクの実行状態を確認するのを10分待ってから行うようにします。

DelayStampにはメッセージの処理を遅延させる時間(ミリ秒)を指定します。

namespace App\Controller;

use App\Message\CheckTaskMessage;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\DelayStamp;

class DefaultController extends AbstractController
{
    public function index(MessageBusInterface $messageBus): Response
    {
        
        // ... $taskId = 外部APIを使用しての非同期なタスクの実行を依頼する

        // タスクの実行状態を確認して何らかの処理を行うメッセージを送信する
        $message = new CheckTaskMessage($taskId);
        
        $envelope = new Envelope($message, [
            new DelayStamp(1000 * 60 * 10),
        ]);

        $messageBus->dispatch($envelope);

        // ...
    }
}

再帰的にメッセージを送信する

タスクの実行状態の確認を10分遅延させることができるようになりましたが、このままではタスクの実行状態を1度しか確認することができません。
再帰的にメッセージを送信することで、タスクが完了するまで10分毎に実行状態を確認するようにします。

namespace App\MessageHandler;

use App\Message\CheckTaskMessage;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\DelayStamp;

#[AsMessageHandler]
class CheckTaskMessageHandler
{
    public function __construct(
        private readonly MessageBusInterface $messageBus,
    ) {
    }

    public function __invoke(CheckTaskMessage $message)
    {
        $taskId = $message->getTaskId();

        // ... $status = 外部APIを使用して$taskIdのタスクのステータスを取得する

        if ($status === 'completed') {
            // ... メールを送信する
            return;
        }

        // タスクが完了していない場合、また10分後に実行状態を確認する
        $message = new CheckTaskMessage($taskId);
        
        $envelope = new Envelope($message, [
            new DelayStamp(1000 * 60 * 10),
        ]);

        $messageBus->dispatch($envelope);
    }
}

まとめ

今回はSymfonyのMessagerを遅延処理させ再帰的に使用し、非同期なタスクが完了するまで定期的に確認する実装例を紹介しました。

cronを使用して実装した場合と比べて、実行中のタスクがない場合の無駄な定期処理が無くなり、定期処理のスケジュールもシステム都合ではなく、ユーザーがアクションを行った時間を基準に行うことができる点が良いなと思いました。