二つの時刻を調べて結果を秒数で返すのに、DateTimeImmutable と strtotime のどちらが早いか調べてみた

PHP で日付や時刻を取得するには、date()関数やstrtotime()関数、DateTimeクラスやDateTimeImmutableクラスを用いると思います。
今回は、strtotime()関数とDateTimeImmutableクラスの処理速度を比較していきます。

DateTimeImmutableとは

DateTimeImmutableは日付や時刻を表すことができるクラスです。
DateTimeImmutableクラスで日付や時刻を取得するには、オブジェクトを作成します。
DateTimeImmutableクラスはDateTimeクラスとほぼ同じですが、 DateTimeImmutableクラスでは、作成したオブジェクトをメソッドで扱う際に、元のオブジェクトは変更せずに新しいオブジェクトを返します。
詳細はこちらをご覧ください。

使用例

下記を2021年6月28日の15時30分に実行します。

<?php
$datetime = new DateTimeImmutable('now', new DateTimeZone('Asia/Tokyo'));
echo $datetime->format('Y-m-d H:i:s'); //2021-06-28 15:30:24

strtotimeとは

strtotimeは指定日時をUnix タイムスタンプに変換するメソッドです。
Unix タイムスタンプの詳細に関してはこちらをご覧ください。

使用例

<?php
$unix_time = strtotime("2021-06-28 15:30:24");
echo $unix_time; //1624894224

実際にやってみる

今回は2021年4月18日21時15分42秒のUnix タイムスタンプと2021年4月18日21時25分32秒のUnix タイムスタンプの差を出す処理を10万回ループさせて実行速度を比較します。

<?php

//DateTimeImmutableの場合
$time_start = microtime(true);

$count = 0;
while($count <= 100000) {
    $datetime01 = new DateTimeImmutable("2021-04-18 21:15:42");
    $datetime02 = new DateTimeImmutable("2021-04-18 21:25:32");
    $diff = $datetime01->getTimestamp() - $datetime02->getTimestamp();
    $count++;
}

$time = microtime(true) - $time_start;
echo "{$time} 秒";


//strtotimeの場合
$time_start2 = microtime(true);

$count = 0;
while($count <= 100000) {
    $unix_time01 = strtotime("2021-04-18 21:15:42");
    $unix_time02 = strtotime("2021-04-18 21:25:32");
    $diff2 = $unix_time01 - $unix_time02;
    $count++;
}

$time2 = microtime(true) - $time_start2;
echo "{$time2} 秒";

結果

0.51721596717834 秒
0.29946088790894 秒

結果としては、strtotimeの方が実行速度が早く、10万回繰り返すと約0.2秒の差が生じることがわかりました。

ちなみに、DateTimeImmutableクラスにはふたつのDateTimeオブジェクトの差を返すdiff()関数というものがあるのでそちらの処理速度も出してみます。

<?php

$time_start1 = microtime(true);

$count = 0;
while($count <= 100000) {
    $datetime1 = new DateTimeImmutable("2021-04-18 21:15:42");
    $datetime2 = new DateTimeImmutable("2021-04-18 21:25:32");
    $diff1 = $datetime1->diff($datetime2);
    $durationInSeconds = ($diff1->s)
        + ($diff1->i * 60)
        + ($diff1->h * 60 * 60)
        + ($diff1->d * 60 * 60 * 24)
        + ($diff1->m * 60 * 60 * 24 * 30)
        + ($diff1->y * 60 * 60 * 24 * 365);
    $count++;
}

$time1 = microtime(true) - $time_start1;
echo "{$time1} 秒";

結果

0.52968621253967 秒

DateTimeImuutableのgetTimestamp()メソッドを使った処理とそこまで差がないことがわかりました。

参考サイト:

WordPress Salient パンくずリストの導入

パンくずリスト、好きですか?

ウェブサイトによくある
「TOP > すごいカテゴリー > イケてる記事」
みたいな階層構造を示しているあれです。便利ですね。

僕は特に好きでも嫌いでもないです。

人が見て記事のカテゴリーなどがわかりやすいだけでなく、検索エンジンがサイトをクローリングするときにも都合がよくてSEO的にも良いらしいです。

WordPressサイトにパンくずリストを入れたときの業務メモみたいなものを記事にしました。

プラグイン選定

パンくずリストを実現するプラグインは、「breadcrumb」で検索すると無数に出てきます。

今回の扱うWordPressサイトは、テーマに「Salient」を使っていたのですが、導入してもテーマとの相性が悪く動かない(表示されない)プラグインなどもありました。

プラグインの設定画面ぽちぽちするだけで使えそうな良さげなやつとして、「Yoast SEO」というものを見つけました。

懸念があったので恐る恐るインストール→有効化したのですが、案の定「All In One SEO」と競合している旨の通知がぴょこぴょこと出てきました。
ファイルの編集を行うことなく自動的にパンくずリストを出してくれるため、大変便利なプラグインでしたが、SEO関連で意図しない誤動作や面倒な副作用が発生すると困るため、やむなく不採用となりました。

仕方がないので、人気の高い別のプラグインである「Breadcrumb NavXT」を使うことにしました。

後述する理由から、ショートコードやスニペットを埋め込む形式はメンテナンス面でかなり面倒な気配がするので避けたかったのですが……仕方ないということにします。

プラグインの設定

「Breadcrumb NavXT」は、WordPressのheader.phpにスニペットを追記して使うものでしたが、記載しても画面内に良い感じにパンくずリストは出ませんでした。

Yoastの設定解説を見ると、single.phpやpage.phpにスニペットを書き込むことでパンくずリストを表示できそうなことが書いてあります。
(不採用のプラグインでもリファレンスは活用していく)

パンくずの区切り文字を変更

デフォルトは「 &gt; 」(全角で書いていますが実際は半角です)でしたが、間隔を自由に調整したいので下記のように変更しました。

 > 
<span style="margin: 0px 7px 0px 7px">></span>

テンプレートの編集

デフォルトではサイトタイトルなどが出たり、マウスオーバーでツールチップが表示されたりするので調整。

  • ホームページテンプレート
    • %htitle%TOP に表示を変更
    • ツールチップの「Go to ~~」が不要だったのでtitle要素を削除
  • 他のテンプレート
    • ツールチップの「Go to ~~」が不要だったのでtitle要素を削除
  • カテゴリー・タグのテンプレート
    • ツールチップの文言を適当な日本語に調整

テンプレートの変更内容詳細は記事の最後に記載しました。長いので。

テーマのファイルを編集

Salient: single.php

「投稿」用です。

nectar_hook_before_content(); の前にスニペットを追記しました。

(略)
<div class="row">

	<?php

	nectar_hook_before_content();
(略)
(略)
<div class="row">

	<p class="breadcrumbs" typeof="BreadcrumbList" vocab="https://schema.org/">
	    <?php if(function_exists('bcn_display')) { bcn_display(); }?>
	</p>

	<?php

	nectar_hook_before_content();
(略)

Salient: page.php

「固定ページ」用です。

nectar_hook_before_content(); の前にスニペットを追記しました。
TOPではパンくずリスト部分を出したくないため、スニペットに少し手を加えました。

(略)
<div class="row">

	<?php

	nectar_hook_before_content();
(略)
(略)
<div class="row">
	
	<?php

	if(!is_front_page()) {
		echo '<p class="breadcrumbs" typeof="BreadcrumbList" vocab="https://schema.org/" style="padding-top:20px">'; 
	    if(function_exists('bcn_display')) { bcn_display(); }
		echo '</p>'; 
	}

	nectar_hook_before_content();
(略)

こんな感じでとりあえず「投稿」と「固定ページ」にパンくずリストを表示できました。

single.phpやpage.phpの編集ではうまくいかない場合

表示位置が意図した場所にならず困ってしまう場合があります。
今回は固定ページで発生したので、解決法も記載しておきます。

メンテコストとかの話でまた後々面倒くさくなりそうですが、下記の記事を参考に対処しました。
参考: パンくずリストプラグイン【Breadcrumb NavXT】をショートコードで呼び出す方法 | WordPressカスタマイズ事例【100ウェブ】

Salient: functions.php

末尾に下記のように追記しました。

/**
 * For Breadcrumb NavXT. (2021/06/24 niwa)
 */
if (!function_exists('display_breadcrumb')) {
    function display_breadcrumb() {
        $html = '<div class="breadcrumbs">'. bcn_display(true). '</div>';
        return $html;
    }
    add_shortcode('display_breadcrumb', 'display_breadcrumb');
}

各固定ページ

上記の変更により、[display_breadcrumb]というショートコードでパンくずリストの表示ができるようになりました。
固定ページを編集して、テキストブロックを追加してショートコードを記述。任意の位置に設置するだけでパンくずリストが表示されます。

WPBakery Page Builderだとこんな感じ

ゴリ押し感~~

🧍‍♀️今回はここまで🧍‍♀️

付録: Breadcrumb NavXT 設定 – テンプレートの変更内容詳細

HTML特殊記号の類(>とか’とか)をエスケープして記載しているのですが、普通に記号に変換されて表示されている様子……

一般 – ホームページテンプレート

<span property="itemListElement" typeof="ListItem"><a property="item" typeof="WebPage" title="Go to %title%." href="%link%" class="%type%" bcn-aria-current><span property="name">%htitle%</span></a><meta property="position" content="%position%"></span>
<span property="itemListElement" typeof="ListItem"><a property="item" typeof="WebPage" href="%link%" class="%type%" bcn-aria-current><span property="name">TOP</span></a><meta property="position" content="%position%"></span>

投稿タイプ – 投稿テンプレート, 固定ページ ページテンプレート, メディアテンプレート

※3項目とも同じ内容

<span property="itemListElement" typeof="ListItem"><a property="item" typeof="WebPage" title="Go to %title%." href="%link%" class="%type%" bcn-aria-current><span property="name">%htitle%</span></a><meta property="position" content="%position%"></span>
<span property="itemListElement" typeof="ListItem"><a property="item" typeof="WebPage" href="%link%" class="%type%" bcn-aria-current><span property="name">%htitle%</span></a><meta property="position" content="%position%"></span>

タクソノミー – カテゴリーテンプレート

<span property="itemListElement" typeof="ListItem"><a property="item" typeof="WebPage" title="Go to the %title% category archives." href="%link%" class="%type%" bcn-aria-current><span property="name">%htitle%</span></a><meta property="position" content="%position%"></span>
<span property="itemListElement" typeof="ListItem"><a property="item" typeof="WebPage" title="カテゴリー: %title%" href="%link%" class="%type%" bcn-aria-current><span property="name">%htitle%</span></a><meta property="position" content="%position%"></span>

タクソノミー – タグテンプレート

<span property="itemListElement" typeof="ListItem"><a property="item" typeof="WebPage" title="Go to the %title% tag archives." href="%link%" class="%type%" bcn-aria-current><span property="name">%htitle%</span></a><meta property="position" content="%position%"></span>
<span property="itemListElement" typeof="ListItem"><a property="item" typeof="WebPage" title="タグ: %title%" href="%link%" class="%type%" bcn-aria-current><span property="name">%htitle%</span></a><meta property="position" content="%position%"></span>

タクソノミー – 投稿フォーマットテンプレート

<span property="itemListElement" typeof="ListItem"><a property="item" typeof="WebPage" title="Go to the %title% archives." href="%link%" class="%type%" bcn-aria-current><span property="name">%htitle%</span></a><meta property="position" content="%position%"></span>
<span property="itemListElement" typeof="ListItem"><a property="item" typeof="WebPage" title="投稿: %title%" href="%link%" class="%type%" bcn-aria-current><span property="name">%htitle%</span></a><meta property="position" content="%position%"></span>

その他 – 投稿者テンプレート

<span property="itemListElement" typeof="ListItem"><span property="name"><a title="Go to the first page of posts by %title%" href="%link%" class="%type%" bcn-aria-current>%htitle%</a> の記事</span><meta property="position" content="%position%"></span>
<span property="itemListElement" typeof="ListItem"><span property="name"><a title="%title% の記事" href="%link%" class="%type%" bcn-aria-current>%htitle%</a> の記事</span><meta property="position" content="%position%"></span>

その他 – 日付テンプレート

<span property="itemListElement" typeof="ListItem"><a property="item" typeof="WebPage" title="Go to the %title% archives." href="%link%" class="%type%" bcn-aria-current><span property="name">%htitle%</span></a><meta property="position" content="%position%"></span>
<span property="itemListElement" typeof="ListItem"><a property="item" typeof="WebPage" title="%title%" href="%link%" class="%type%" bcn-aria-current><span property="name">%htitle%</span></a><meta property="position" content="%position%"></span>

その他 – 検索テンプレート

<span property="itemListElement" typeof="ListItem"><span property="name">Search results for '<a property="item" typeof="WebPage" title="Go to the first page of search results for %title%." href="%link%" class="%type%" bcn-aria-current>%htitle%</a>'</span><meta property="position" content="%position%"></span>
<span property="itemListElement" typeof="ListItem"><span property="name">'<a property="item" typeof="WebPage" title="%title% の検索結果" href="%link%" class="%type%" bcn-aria-current>%htitle%</a>' の検索結果</span><meta property="position" content="%position%"></span>

Redmineにプラグインを入れる!

前置き

突然ですが、Redmineのガントチャートって激見づらくないですか?
そんな事象を解決するために、プラグインで解決しようじゃありませんか…


環境

LightSailのbitnamiを使ってRedmineが動いています。
     ↓こんな感じで動いてます↓


プラグインを入れる

①プラグインフォルダに移動してプラグインを入れる

cd ~/apps/redmine/htdocs/plugins

ここで、git clone なりでプラグインを引っ張ってきます。

②いったんhtdocsに戻ります

cd ../

③必要なものをインストールします

bundle install --no-deployment

④変更をマイグレーションします

bundle exec rake redmine:plugins:migrate RAILS_ENV=production

⑤再起動します

sudo /opt/bitnami/ctlscript.sh restart

■もしかすると、③を行った際に、mimemagicが~と怒られるかもしれませんが、安心してください。下記コマンドを実行すればおそらく解決します。

sudo apt install shared-mime-info

mimemagicの箇所でかなり手こずりました💦

Lambdaでもpipがしたい!

Lambdaでもpipしたいですよね….いろいろ面倒だし…..

やり方

①ローカルの作業ディレクトリにpipして使いたいものをためておきます。
  サンプルとして、pymsteamsを入れる形です。

pip install pymsteams -t .

 そうすると、作業ディレクトリにこんな感じでぶわっとファイルが量産されま  す↓

②それらをzipします。upload.zipだったりの名前にしておきます。

③作成したupload.zipをLambdaのアップロードでzip形式を選択後、該当ファイルをアップロード(既にアップロードされている状態….💦)

これでひとまず使えるようになりました…

レイヤーとかに追加してあげると、後で使うときに便利になるっぽいです。

AWS SESの送信クォータを監視する

AWSのSESには、送信クォータや送信レートという、アカウントごとに一定期間に送信できるメール数の上限が定められています。

送信クォータ
24時間のうちに何通のメールを送信できるか。上限を超えた場合には、送信クォータの上限に達した旨のエラーとなりメールが送信されなくなる(再送処理等はない)。

送信レート
SESが1秒間に受け入れ可能なメールの最大数です。この値を瞬間的に超えることは問題ありませんが、制限を超えた状態が長時間続く場合、送信クォータ同様メールの送信ができなくなる恐れがあります。

送信クォータの上限に達しているかどうかは、CloudWatchアラームなどでリアルタイムに監視するメトリクスが用意されておらず、SESのコンソールやAPIを通じて把握する必要があります。若干の面倒臭さが漂ってきますね。

送信レートの監視をする場合には秒間単位での監視になることや、上限を超えても直ちにペナルティが無いなどのはっきりとしない状況から、より明確なラインで確実な配信不能が訪れる送信クォータの監視をするのが良さそうです。

今回は、送信クォータの60%を消費した場合に、Slackに通知が来るようなLambda関数を紹介します。
※このLambda関数は、送信数に問題がない間は実行ログを吐き出す以外沈黙します。

Lambda関数

大まかな処理の流れは下記のとおりです。

  1. 送信クォータのステータスを取得
  2. 直近24時間の送信数 / 24時間での最大送信数 * 100 [%] を算出
  3. 60%以上消費していた場合、Slackへ通知(60%未満なら何もしない)
    1. 投稿メッセージを作成
    2. WebhookでSlackへPOST

Slackへの投稿はなるべく簡素にし、現在はメタデータなども含めないように(コメントアウト箇所)しています。

import boto3
import json
import urllib.request


def build_message_block(response, send_quota_usage):
    blocks = [
        {
            "type": "header",
            "text": {
                "type": "plain_text",
                "text": "SES Send quota use " + str(int(send_quota_usage)) + "%"
            }
        },
        {
            "type": "divider"
        },
        {
            "type": "section",
            "fields": [
                {
                    "type": "mrkdwn",
                    "text": "*SentLast24Hours (usage):* " + str(f"{int(response['SentLast24Hours']):,}")
                }
            ]
        },
        {
            "type": "section",
            "fields": [
                {
                    "type": "mrkdwn",
                    "text": "*Max24HourSend (limt):*   " + str(f"{int(response['Max24HourSend']):,}")
                }
            ]
        },
        {
            "type": "section",
            "fields": [
                {
                    "type": "mrkdwn",
                    "text": "*MaxSendRate:* " + str(f"{int(response['MaxSendRate']):,}")
                }
            ]
        }
    ]
    
    # if response['ResponseMetadata']:
    #     meta_data_json = json.dumps(response['ResponseMetadata'])
    #     blocks += [
    #         {
    #             "type": "section",
    #             "text": {
    #                 "type": "mrkdwn",
    #                 "text": "*ResponseMetadata:*\n```" + meta_data_json + "```"
    #             }
    #         }
    #     ]
    
    return blocks


def post_slack(blocks, message=None):
    send_data = {
        "channel": "XXXXXXXXXXX",   # SESの通知用channel
        "username": "SES Monitor",
        "icon_emoji": ":chart_with_upwards_trend:",
        "text": message,
        "blocks": blocks
    }
    send_text = "payload=" + json.dumps(send_data)
    
    print('post_slack() send_text: ', send_text)
    
    request = urllib.request.Request(
        "(SlackのWebhook URL)",
        data=send_text.encode('utf-8'),
        method="POST"
    )
    
    with urllib.request.urlopen(request) as response:
        response_body = response.read().decode('utf-8')


def get_ses_send_quota():
    ses = boto3.client('ses')
    response = ses.get_send_quota()
    send_quota_usage = response['SentLast24Hours'] / response['Max24HourSend'] * 100
    
    if send_quota_usage >= 60:
        message_body = build_message_block(response, send_quota_usage)
        notice_message = 'SES send quota usage: ' + str(send_quota_usage) + '%'
        print(message_body)
        print(notice_message)
        post_slack(message_body, notice_message)


def lambda_handler(event, context):
    get_ses_send_quota()
    
    return

Slackへの投稿内容をBlockで作成している都合上、どうしてもその箇所が長大になってしまっています……

Lambda関数に付与する権限

デフォルトのCloudWatchログ関係の他に、「AmazonSESReadOnlyAccess」を雑に付与してありました。もうちょっと精査しても良かったと思う。

実行方法

Lambda関数のトリガーに「EventBridge (CloudWatch Events)」を指定し、cronで定期実行しています。

送信されるメッセージの例

しきい値を下げてテストしたものがこちら。
※緩和申請済みのため送信クォータの上限が高いです。

これで、知らぬ間に送信クォータを使い果たしてメール配信不能に陥るような事故をケアすることができるようになりました。
緩和申請が間に合えば配信不能には陥りませんし、送信クォータの利用率がわからない状態で日々運用するよりも幾分精神衛生に良いと思います。