こんにちは、毎日暑いですね。エンジニアの三宅です。
最近、仕事でPHPのコードリーディングを少ししたので、その過程を記載します!
Carbon::now()はOSの現在のシステム時刻
結論はこれです。OSのシステム時刻
を持ってきています。
これ以降の内容は、コードを見ながら本当にシステム時刻
から取ってきているのかを確認するためのものです。
Carbonとは
今回扱っているCarbonはPHPのDateTimeクラスを継承し、時間操作を簡易化したり便利化したものを提供する拡張クラスになります!
これが所謂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標準クラスです!
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クラスのコンストラクタには説明ドキュメントがありました
GitHubで開発はされていないのですが、ミラーされたコードが上がっています。
まずは"class DateTime"で検索してみます。
/**
* 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_FUNCTION
はC言語のマクロで定義されています。
機能は引数でもらった名前の関数をPHPで動作させるものです。
DateTime class
のコンストラクタに当たりそうな処理が見つかりました!
コメントに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関数の実行処理内容です!
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_GETTIMEOFDAY
を0
or 1
で定義することで、どちらのコードが取り込まれるかが決まります。
C言語の現在時間取得メソッドである
gettimeofday(&tp, NULL);
やtime(NULL);
処理があります。
gettimeofday は POSIX に準拠した関数で、マイクロ秒単位の精度で現在の時刻を取得します。 linuxjm.osdn.jp
timeはC言語の時間ライブラリの関数です。 www.geeksforgeeks.org
ちなみにWindowsのようなOSの場合はどうなるのか??
windows環境ではHAVE_GETTIMEOFDAY
が1
です
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; }/*}}}*/
GetSystemTimePreciseAsFileTime
かGetSystemTimeAsFileTime
という関数が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上や自分の環境で確認できる手段を持つことは理解を深める一手になるのではないかと思います。