【PHP】Carbon::now()は誰のnow?

こんにちは、毎日暑いですね。エンジニアの三宅です。

最近、仕事でPHPのコードリーディングを少ししたので、その過程を記載します!

Carbon::now()はOSの現在のシステム時刻

結論はこれです。OSのシステム時刻を持ってきています。

これ以降の内容は、コードを見ながら本当にシステム時刻から取ってきているのかを確認するためのものです。

Carbonとは

今回扱っているCarbonはPHPのDateTimeクラスを継承し、時間操作を簡易化したり便利化したものを提供する拡張クラスになります!

carbon.nesbot.com

これが所謂Carbon::now()の使用例

<?php
require 'vendor/autoload.php';
use Carbon\Carbon;

$now = Carbon::now();
$now->format('Y-m-d H:i:s'); // 2022-08-24 19:31:22

PHPの時間操作

DateTimeはPHP標準クラスです!

www.php.net

Carbonはこれを継承しています。

コードリーディング方針

今回は如何にWEB完結でコードリーディングしていくかの手順を書いていきますが、本格的に処理を追うのであれば debug実行出来る環境を手元に用意すると良いと思います。

WEBでコードリーディング実践

Carbonクラスのコードリーディング

では早速Carbonから追っていきましょう

まずはCarbonのgithubからCarbon::now();の処理を確認します。 github.com

GitHub上で検索する際には"function now"とダブルクォーテーションで括ります こうすると目指す関数に当たりやすいです!

ちなみに対象関数を探すのは、GitHub上よりもgit cloneしてローカルファイルをエディターで検索した方が楽です

ただ、GitHub検索するとissueやcommitメッセージなども対象になるため色んな情報を得る可能性があるというメリットもあります

検索の結果、どうやらここにあるようです!! github.com

nowメソッド

/**
     * Get a Carbon instance for the current date and time.
     *
     * @param DateTimeZone|string|null $tz
     *
     * @return static
     */
    public static function now($tz = null)
    {
        return new static(null, $tz); // ← ここでインスタンス生成して返している
    }

newしたものを返しているようです! new staticは実行されるクラスのインスタンスを生成します。

実行クラスのコンストラクタが第一引数nullで呼ばれますので、そちらの処理を確認

public function __construct($time = null, $tz = null)
    {
        if ($time instanceof DateTimeInterface) {
            $time = $this->constructTimezoneFromDateTime($time, $tz)->format('Y-m-d H:i:s.u');
        }

        if (is_numeric($time) && (!\is_string($time) || !preg_match('/^\d{1,14}$/', $time))) {
            $time = static::createFromTimestampUTC($time)->format('Y-m-d\TH:i:s.uP');
        }

        // If the class has a test now set and we are trying to create a now()
        // instance then override as required
        $isNow = empty($time) || $time === 'now';

        if (method_exists(static::class, 'hasTestNow') &&
            method_exists(static::class, 'getTestNow') &&
            static::hasTestNow() &&
            ($isNow || static::hasRelativeKeywords($time))
        ) {
            static::mockConstructorParameters($time, $tz);
        }

        // Work-around for PHP bug https://bugs.php.net/bug.php?id=67127
        if (!str_contains((string) .1, '.')) {
            $locale = setlocale(LC_NUMERIC, '0');
            setlocale(LC_NUMERIC, 'C');
        }

        try {

            // ここで親コンストラクタを呼んでいる!!!!!!
            parent::__construct($time ?: 'now', static::safeCreateDateTimeZone($tz) ?: null); 
        } catch (Exception $exception) {
            throw new InvalidFormatException($exception->getMessage(), 0, $exception);
        }

        $this->constructedObjectId = spl_object_hash($this);

        if (isset($locale)) {
            setlocale(LC_NUMERIC, $locale);
        }

        self::setLastErrors(parent::getLastErrors());
    }

親のコンストラクタを呼んでいます。

parent::__construct($time ?: 'now', static::safeCreateDateTimeZone($tz) ?: null); *

Carbonの親クラスは前述通りDateTimeクラスになります。 そもそも元のコンストラクタの第一引数である$timeはnullできているので、このCarbon→DateTimeのコンストラクタへ移行するタイミングで ’now’という引数が設定されることが読み取れます!

そうすると今度はDateTimeクラスになっていきます

DateTimeクラスのコードリーディング

DateTimeクラスのコンストラクタには説明ドキュメントがありました

www.php.net

PHPの元コードはC言語で書かれています!

GitHubで開発はされていないのですが、ミラーされたコードが上がっています。

github.com

まずは"class DateTime"で検索してみます。

C言語ではなくPHPのstabファイルで引っ掛かりました。

/**
 * Representation of date and time.
 * @link https://php.net/manual/en/class.datetime.php
 */
class DateTime implements DateTimeInterface
{
  ...
}

ここでファイルのディレクトリに注目!! dateを扱ってそうなディレクトリです。

php_date.cが怪しいなといって見ていきます。

今度はブラウザ検索で"PHP_FUNCTION"を見ていきます。

PHP_FUNCTIONC言語のマクロで定義されています。 機能は引数でもらった名前の関数をPHPで動作させるものです。

DateTime classのコンストラクタに当たりそうな処理が見つかりました!

github.com

コメントにReturns new DateTime objectとありますね!

/* {{{ Returns new DateTime object */
PHP_FUNCTION(date_create)
{
    zval           *timezone_object = NULL;
    char           *time_str = NULL;
    size_t          time_str_len = 0;

    ZEND_PARSE_PARAMETERS_START(0, 2)
        Z_PARAM_OPTIONAL
        Z_PARAM_STRING(time_str, time_str_len)
        Z_PARAM_OBJECT_OF_CLASS_OR_NULL(timezone_object, date_ce_timezone)
    ZEND_PARSE_PARAMETERS_END();

    php_date_instantiate(date_ce_date, return_value);
    if (!php_date_initialize(Z_PHPDATE_P(return_value), time_str, time_str_len, NULL, timezone_object, 0)) {
        zval_ptr_dtor(return_value);
        RETURN_FALSE;
    }
}
/* }}} */

上記コードはdate_create()というPHP関数の実行処理内容です!

www.php.net

date_create()を知っていれば、最初からdate_createで検索すればよかったのでは?と思うかと思います。

その通りです!

PHP関数名が分かっていればストレートに検索していけば問題なくたどり着けます!

さて、PHP_FUNCTION(date_create)の中を見てきます。。 php_date_initialize関数が正にその実行処理のようです! コードを追っていきます。

PHPAPI bool php_date_initialize(php_date_obj *dateobj, const char *time_str, size_t time_str_len, const char *format, zval *timezone_object, int flags) /* {{{ */
{
    timelib_time   *now;
    timelib_tzinfo *tzi = NULL;
    timelib_error_container *err = NULL;
    int type = TIMELIB_ZONETYPE_ID, new_dst = 0;
    char *new_abbr = NULL;
    timelib_sll new_offset = 0;
    time_t sec;
    suseconds_t usec;
    int options = 0;

    if (dateobj->time) {
        timelib_time_dtor(dateobj->time);
    }
    if (format) {
        if (time_str_len == 0) {
            time_str = "";
        }
        dateobj->time = timelib_parse_from_format(format, time_str, time_str_len, &err, DATE_TIMEZONEDB, php_date_parse_tzfile_wrapper);
    } else {
        if (time_str_len == 0) {
            time_str = "now";
            time_str_len = sizeof("now") - 1;
        }
        dateobj->time = timelib_strtotime(time_str, time_str_len, &err, DATE_TIMEZONEDB, php_date_parse_tzfile_wrapper);
    }

んー、まだnowの時間を取ってはなさそうですね..!

 /* update last errors and warnings */
    update_errors_warnings(err);

    /* If called from a constructor throw an exception */
    if ((flags & PHP_DATE_INIT_CTOR) && err && err->error_count) {
        /* spit out the first library error message, at least */
        zend_throw_exception_ex(NULL, 0, "Failed to parse time string (%s) at position %d (%c): %s", time_str,
            err->error_messages[0].position, err->error_messages[0].character, err->error_messages[0].message);
    }
    if (err && err->error_count) {
        timelib_time_dtor(dateobj->time);
        dateobj->time = 0;
        return 0;
    }

    if (timezone_object) {
        php_timezone_obj *tzobj;

        tzobj = Z_PHPTIMEZONE_P(timezone_object);
        switch (tzobj->type) {
            case TIMELIB_ZONETYPE_ID:
                tzi = tzobj->tzi.tz;
                break;
            case TIMELIB_ZONETYPE_OFFSET:
                new_offset = tzobj->tzi.utc_offset;
                break;
            case TIMELIB_ZONETYPE_ABBR:
                new_offset = tzobj->tzi.z.utc_offset;
                new_dst    = tzobj->tzi.z.dst;
                new_abbr   = timelib_strdup(tzobj->tzi.z.abbr);
                break;
        }
        type = tzobj->type;
    } else if (dateobj->time->tz_info) {
        tzi = dateobj->time->tz_info;
    } else {
        tzi = get_timezone_info();
        if (!tzi) {
            return 0;
        }
    }

    now = timelib_time_ctor();
    now->zone_type = type;
    switch (type) {
        case TIMELIB_ZONETYPE_ID:
            now->tz_info = tzi;
            break;
        case TIMELIB_ZONETYPE_OFFSET:
            now->z = new_offset;
            break;
        case TIMELIB_ZONETYPE_ABBR:
            now->z = new_offset;
            now->dst = new_dst;
            now->tz_abbr = new_abbr;
            break;
    }

timezone関連の処理が続いてますね。。。

    php_date_get_current_time_with_fraction(&sec, &usec);
    timelib_unixtime2local(now, (timelib_sll) sec);
    php_date_set_time_fraction(now, usec);

きた!

遂に見つけました...!!

php_date_get_current_time_with_fraction

正にというような名前ですね。

static void php_date_get_current_time_with_fraction(time_t *sec, suseconds_t *usec)
{
#if HAVE_GETTIMEOFDAY
    struct timeval tp = {0}; /* For setting microseconds */

    gettimeofday(&tp, NULL);
    *sec = tp.tv_sec;
    *usec = tp.tv_usec;
#else
    *sec = time(NULL);
    *usec = 0;
#endif
}

#if HAVE_GETTIMEOFDAYこの記述はC言語ifdef機能で、条件コンパイラのための機能です。 HAVE_GETTIMEOFDAY0 or 1で定義することで、どちらのコードが取り込まれるかが決まります。

C言語の現在時間取得メソッドである gettimeofday(&tp, NULL);time(NULL); 処理があります。

gettimeofday は POSIX に準拠した関数で、マイクロ秒単位の精度で現在の時刻を取得します。 linuxjm.osdn.jp

timeはC言語の時間ライブラリの関数です。 www.geeksforgeeks.org

ちなみにWindowsのようなOSの場合はどうなるのか??

windows環境ではHAVE_GETTIMEOFDAY1です github.com

windows環境でのgettimeofdayはここにある処理が走ります github.com

結果としてここのコードでwindows OSの時間を取得しています https://github.com/php/php-src/blob/5b01c4863fe9e4bc2702b2bbf66d292d23001a18/win32/time.c#L31

static zend_always_inline MyGetSystemTimeAsFileTime get_time_func(void)
{/*{{{*/
    MyGetSystemTimeAsFileTime timefunc = NULL;
    HMODULE hMod = GetModuleHandle("kernel32.dll");

    if (hMod) {
        /* Max possible resolution <1us, win8/server2012 */
        timefunc = (MyGetSystemTimeAsFileTime)GetProcAddress(hMod, "GetSystemTimePreciseAsFileTime");
    }

    if(!timefunc) {
        /* 100ns blocks since 01-Jan-1641 */
        timefunc = (MyGetSystemTimeAsFileTime) GetSystemTimeAsFileTime;
    }

    return timefunc;
}/*}}}*/

GetSystemTimePreciseAsFileTimeGetSystemTimeAsFileTimeという関数がwindowsの関数ですね。

Windows8以降でより高精度の時間を取得する関数が増えたために分岐があるようです。 metatrading.hatenablog.com

まとめ

ということで、PHPでCarbon::nowをコールするとPHP内部のC言語でOSのシステム時刻を取得しています!

Carbon::now(Carbonクラス) → DateTime(PHP) →gettimeofday/ time (C言語)→ OS ...

OSがどの様に時刻を刻んでいるか、その仕組みなど興味があれば色々調べてみると面白いです。 ja.wikipedia.org

何気なく使っている関数の裏側にはどんな処理が走っているか、WEB上や自分の環境で確認できる手段を持つことは理解を深める一手になるのではないかと思います。