投稿者: koji

  • WordPress: 直近7日間のアクセス数をカウントする

    今日を含む直近7日間のアクセス数をカウントするアクセスカウンターを作りました。「最近読まれているリスト」ページは、このアクセス数の多い順に並んでいます。

    概要

    今日を含む直近の7日間分のアクセス履歴とアクセス数の合計を保存する。

    アクセス履歴は日付をキー、アクセス数を値に持つ連想配列とし、投稿メタ(カスタムフィールド)に保存する。

    アクセス数の合計も別の投稿メタとして保存する。これを降順にソートして投稿を取り出すことで「最近読まれているリスト」を表示できるようにする。

    クローラーのアクセスは簡易ロジックによって除外する。ただしアクセス履歴の更新だけを行わせてメンテナンスを手伝ってもらう。

    2022/1/7、未アクセス時
    Array (
        [20211230] => 10
        [20220101] => 20
        [20220105] => 30
    )
    アクセスカウンター: 60
    2022/1/7、1アクセス後
    Array (
        [20220101] => 20
        [20220105] => 30
        [20220107] => 1
    )
    アクセスカウンター: 51
    2022/1/7、1アクセス後(botによるアクセス)
    Array (
        [20220101] => 20
        [20220105] => 30
    )
    アクセスカウンター: 50

    コード

    add_action( 'wp_footer', 'LF_Access_Count::access_count', 30 );
    
    class LF_Access_Count {
        public static function access_count() {
            // 管理者からのアクセス、およびlistカテゴリ以外へのアクセスを除外
            if ( current_user_can( 'administrator' ) ||
                 ! has_category( 'list' )
            ) {
                return;
            }
    
            // カスタムフィールドからカウント履歴を取得
            $count_history = get_post_meta( get_the_ID(), 'count_history', true );
    
            // 7日前の YYYYMMDD を取得
            $expdate = (int)wp_date( 'Ymd', strtotime( '-7 days' ) );
    
            // 7日前の日付以前の記録を削除(全削除なら空配列が返る)
            $count_history = array_filter(
                // アクセス履歴が無ければget_post_meta()が空文字を返すのでarrayにキャスト
                (array)$count_history, 
                function( $k ) use( $expdate ) {
                    return ( $expdate < $k ) ? true : false;
                }, 
                ARRAY_FILTER_USE_KEY 
            );
    
            // ユーザーエージェントを取得(未設定なら空文字)
            $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
    
            // 簡易クローラー判定。bot、bots、spiderで終わる単語が含まれる
            // 場合はクローラーと見なす。クローラーでなければカウントアップ
            if ( 1 !== preg_match( '/.*(bots?|spider)\b/i', $ua ) ) {
                // 今日の日付をYYYYMMDD形式の整数として取得
                $today = (int)wp_date( 'Ymd' );
                // 今日の日付をキーに持つ要素がアクセス履歴に存在すれば
                // 値をインクリメント、なければ1
                isset( $count_history[$today] ) ? $count_history[$today]++ : $count_history[$today] = 1;
            }
    
            // アクセス履歴を投稿メタに保存
            update_post_meta( get_the_ID(), 'count_history', $count_history );
    
            // アクセス履歴の値の合計を投稿メタに保存
            update_post_meta( get_the_ID(), 'lf_counter', array_sum( $count_history ) );
        }
    }
  • WordPress: イベントのスケジュールとcron

    イベントのスケジュール

    全体で8つの定期イベントがあり、バラバラに登録しているとデバッグが面倒なので、1ファイルにまとめている。

    class LF_Events {
        private static $events = [
            [ // LOTD更新(毎日0:01)
                'hook'       => 'lf_event_renew_lotd',
                'args'       => [],
                'timing'     => 'tomorrow 0:01',
                'recurrence' => 'daily',
                'callback'   => [ 'LF_LOTD', 'renew' ],
            ],
            [ // 以下同様
            ],
        ]
    
        public static function init() {
            foreach ( self::$events as $event ) {
    
                if ( ! wp_next_scheduled( $event['hook'], $event['args'] ) ) {
            
                    $etime = new DateTimeImmutable( $event['timing'], wp_timezone() );
            
                    $ret = wp_schedule_event(
                        $etime->getTimeStamp(),
                        $event['recurrence'],
                        $event['hook'],
                        $event['args'],
                        true
                    );
            
                    if ( ! $ret || is_wp_error( $ret ) ) {
                        $errmsg['location'] = __METHOD__.' '.__LINE__;
                        $errmsg['$event']   = $event;
                        $errmsg['$ret']     = $ret;
                        error_log( print_r( $errmsg, true ) );
                        continue;
                    }
                }
            
                add_action( $event['hook'], $event['callback'], 10, count( $event['args'] ) );
            }
        }
    }
    

    cron

    ユーザーアクセスによる疑似cronではなくサーバーのcronを使う。10分おきに wp-cron.php を起動。

    (/wp-config.php)
    define( 'DISABLE_WP_CRON', true );
    
    (crontab -l)
    */10 * * * * /usr/bin/php7.x /home/(...snip...)/wp-cron.php
  • WordPress: 現在のプラグインディレクトリ名を取得する

    WordPress のカスタマイズファイルはすべて KHCustom という自作プラグインに収めています。ディレクトリ名は kh-custom。

    /wp-contents/plugins/kh-custom/src/class/test.php
    から 'kh-custom' を取得しようとしたのですが、意外に簡単ではありませんでした。

    plugins_url() などそれらしい標準関数はあるのですが、プラグインディレクトリだけを取り出すことができません。出力させてみると次のようになりました。

    print_r( [
    	plugins_url(),
    	plugins_url( 'test.css', __FILE__ ),
    	plugin_dir_url( __FILE__ ),
    	plugin_dir_path( __FILE__ ),
    	plugin_basename( __FILE__ ),
    ] ,true ) );
    
    Array (
        [0] => https://listfreak.com/wp-content/plugins
        [1] => https://listfreak.com/wp-content/plugins/kh-custom/src/class/test.css
        [2] => https://listfreak.com/wp-content/plugins/kh-custom/src/class/
        [3] => /home/(...snip...)/wp-content/plugins/kh-custom/src/class/
        [4] => kh-custom/src/class/LF_Custom_CSS.php
    )

    plugin_basename() の先頭にプラグインディレクトリが来ているので、'/' で分割して先頭を取得すればよいことになります。

    current( explode( '/', plugin_basename( __FILE__ ) ) )

    あるいはプラグインのメインファイルで次のように定義しておくか。

    define( 'KH_PLUGIN_SLUG', basename(__DIR__) );
  • 痩せた財布を太らせる七つの道具

    まえがき

    『ここにお集まりの皆さんに、痩せた財布を太らせるその七つの道具をご紹介いたしましょう。大枚の金貨を手にしたいと願うすべてのかたにお勧めする方法です。』

    リスト

    あとがき

    まえがきを含めて、ジョージ・S・クレイソン『小説版 バビロン大富豪の教え 「お金」と「幸せ」を生み出す五つの黄金法則』 (文響社、2021年)より。

    原著は著作権が切れた古い本なので公開されています。またこの七か条は Wikipedia(英語版) にも引用されています。そこでは “Seven Cures For a Lean Purse”、直訳すれば「痩せた財布のための7つの治療」です。

    リストを引用した邦訳では「黄金に愛される七つ道具」となっていましたが、ちょっと大仰なので、本文からすこし原著に近い表現を抜き出してリストのタイトルとしました。

    • 9つの基本的なリサーチ技法

      まえがき

      『リサーチとは、単なる情報収集のことではありません。ビジネスの意思決定を後押しするインサイト(=洞察)を抽出する行為です。補足すれば、「物事を観察して、その本質や奥底にあるものを見抜くこと」です。』

      リスト

      あとがき

      まえがきを含めて、宮尾 大志、アクセンチュア製造流通本部一般消費財業界グループ『外資系コンサルのリサーチ技法: 事象を観察し本質を見抜くスキル』 (東洋経済新報社、2015年)より。

      1~5が「さがす系」、6~9が「つくる系」。

      1~3が「常識やトレンドを素早くつかむ」、
      4~6が「体系化された全体像をつかむ」、
      7~9が「個別に深い情報をつかむ」。

      • プランBの効用

        まえがき

        『決心を妨害する万一の事態に備えるために立てたプランBとは何か? プランBが失敗したときに備えた対策は何か? 成功した人たちは失敗した人たちが予想できなかったことを予想し、幸せな人たちは不幸な人たちが考えもしなかった対応策を準備する。』

        リスト

        あとがき

        まえがきを含めて、イ・ミンギュ『「後回し」にしない技術 「すぐやる人」になる20の方法』 (文響社、2021年)より。

        • 5D – エネルギー産業における環境変化

          まえがき

          『エネルギー産業は4つのDと呼ばれる大きな経営環境変化のなかにあるといわれる。』

          リスト

          あとがき

          まえがきを含めて、ボストン コンサルティング グループ『BCGが読む経営の論点2022』 (日経BP、2021年)より。

          まえがきに「4つのD」とあるのにリスト項目は5つあります。これは4つのDの後『これにDeregulation (自由化/規制緩和)を加えて5つのDとすることもある』という文を反映させたため。

          「~といわれる」とありますが、故事成語ならともかく誰かの作品なのだろうから引用元を示してほしいところです。

          • タイトルBCGが読む経営の論点2022
          • 著者: ボストン コンサルティング グループ(著)
          • 出版社: 日経BP
          • 出版日: 2021-11-25

            参考文献

          • 「このページを共有する」dialogタグ版

            iOS 15.4からSafariで<dialog>タグがサポートされたのを機に、「このページを共有する」機能をjQueryから<dialog>に移し替えた。

            動作イメージ

            ソーシャルメニューの 🔗 をクリックすると、下記のようなモーダルダイアログが表示される。[COPY]をクリックすると、ページ情報をクリップボードにコピーしてダイアログが閉じられる。[CANCEL]をクリックすると、ただダイアログが閉じられる。

            ダイアログの出力 (PHP)

            上記のダイアログは次のように出力した。

            function echo_sharethis_dialog() {
            
                // 現在表示しているページのURL
                $url = $_SERVER['REQUEST_SCHEME'].'://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
            
                // 現在表示しているページのタイトルと合わせてページ情報を生成
                $pageinfo = wp_get_document_title().PHP_EOL.$url.PHP_EOL;
            
                echo '
                <dialog id="lf-sharethis-dialog">
                    <p><b>このページを共有する</b><br>([COPY]でクリップボードにコピー)</p>
                    <textarea id="lf-sharethis-area" readonly>',$pageinfo,'</textarea>
                    <button id="lf-sharethis-btn-copy">Copy</button>
                    <button id="lf-sharethis-btn-cancel">Cancel</button>
                </dialog>';
            }

            イベント処理 (JavaScript)

            // ダイアログ
            const shareThisDialog = document.getElementById( 'lf-sharethis-dialog' );
            
            // ソーシャルメニューのアイコン(クリックしてダイアログを表示)
            document.getElementsByClassName( 'lf-sharethis-menu' )[1].addEventListener( 'click', {
                object: shareThisDialog,
                handleEvent: function( e ) {
                    this.object.showModal();
                }
            } );
            
            // グローバルメニュー(クリックしてダイアログを表示)
            document.getElementsByClassName( 'lf-sharethis-menug' )[0].addEventListener( 'click', {
                object: shareThisDialog,
                handleEvent: function( e ) {
                    this.object.showModal();
                }
            } );
            
            // Copy ボタン(共有ダイアログ内)
            document.getElementById( 'lf-sharethis-btn-copy' ).addEventListener( 'click', {
                object: shareThisDialog,
                handleEvent: function( e ) {
                    navigator.clipboard.writeText(
                        document.getElementById( 'lf-sharethis-area' ).value
                    );
                    this.object.close();
                }
            } );
            
            // Cancel ボタン(共有ダイアログ内)
            document.getElementById( 'lf-sharethis-btn-cancel' ).addEventListener( 'click', {
                object: shareThisDialog,
                handleEvent: function( e ) {
                    this.object.close();
                }
            } );

            L5-10: Twenty Twenty テーマのソーシャルメニューの一つをクリックするとダイアログを表示する。メニュー項目には id 属性を割り当てることができず、class 属性しか割り当てられない。

            L13-18: L5-10 と同様の処理をグローバルメニューについて行う。

            L21-29: COPYボタンがクリックされたら、テキストエリアの内容をクリップボードに書き出してダイアログを閉じる。

            L32-37: CANCELボタンがクリックされたらダイアログを閉じる。

            JavaScriptを登録する

            上記の JavaScript を登録する。ファイルの更新時刻をバージョン番号とすることで最新のファイルが読み込まれることを保証する。

            function enqueue() {
            
                $jsfile = '/'.KH_PLUGIN_SLUG.'/js/lf_sitewide_setting.js';
            
                wp_enqueue_script(
                    'lf_sitewide_setting',
                    plugins_url( $jsfile ),
                    array(),                            // No Dependency
                    filemtime( WP_PLUGIN_DIR.$jsfile ), // Version
                    true                                // In Footer
                );
            }

            上記の処理をアクションフックのコールバックとして登録する

            上記の関数を一つのクラスにまとめた。

            
            class LF_SiteWide_Setting {
            
                public static function init() {
                    add_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue' ], 11 );
                    add_action( 'wp_footer', [ __CLASS__, 'echo_sharethis_dialog' ], 19 );
                }
            
                public static function enqueue() {
                }
            
                public static function echo_sharethis_dialog() {
                }
            }

            ダイアログのHTMLを書き出す関数は、’wp_footer’ へのコールバックとして優先順位19で登録した。これは、JavaScriptを書き出す処理(wp_print_footer_scripts())が ‘wp_footer’ へのコールバックとして優先順位20で登録されているため、それよりも先に読み込ませるためである。

            そして、個人的なカスタマイズをまとめたプラグインのメインPHPファイルから以下の要領で呼び出している。

            LF_SiteWide_Setting::init();
          • 報酬が効果的でない5つの理由

            まえがき

            『「これをすればあれをあげるよ」と言えば、関心は「これ」ではなくて「あれ」に行ってしまう。(略)報酬は動機づけになるか。完璧になる。報酬をめざす動機づけとなる。』

            リスト

            あとがき

            まえがきを含めて、アルフィ・コーン『報酬主義をこえて〈新装版〉』 (法政大学出版局、2011年)より。

            リスト項目1から4までが第4章にまとめられ、項目5は単独で第5章を費やして論じられています。外発的な動機づけである報酬や罰は、効果がないどころか逆効果がある。これは調査によってわかっている。ではその理由がどこにあるのか。それを詳しく考察しています。

            まえがきは第4章の最後の段落から引用しました。全体の要約というわけではないのですが、報酬のはたらきを端的に説明するよい文章だと思ったので。なお、『報酬は動機づけになるか。完璧になる。報酬をめざす動機づけとなる。』には傍点が打たれていました。

            この本からの他のリスト

          • 「このページをシェア」(jQuery版)

            Twenty Twenty テーマのソーシャルメニュー付けていた「このページをシェア」機能をjQuery版からdialogタグ版に移行した。以下はjQuery版の実装。

            ダイアログのイメージ

            PHP / HTML

            class LF_Share_This {
            
            	/**
            	 * コールバックを登録する
            	 */
            	public static function init() {
            
            		// コアが登録しているスクリプトよりも優先順位を下げるために11とした
            		add_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue' ], 11 );
            
            		// シェアメニューの🔗がクリックされたら表示するダイアログの内容
            		add_action( 'wp_footer', [ __CLASS__, 'echo' ], 21 );
            	}
            
            	/**
            	 * jQuery UI Dialog と自作のjavascriptを登録する
            	 */
            	public static function enqueue() {
            
            		// jQuery UI Dialog の読み込み(bodyの下で)
            		wp_enqueue_script( 'jquery-ui-dialog', false, [ 'jquery' ], true );
            
            		// jQuery UI Dialog 用 CSSの読み込み(CSSは$srcの指定が必要)
            		wp_enqueue_style( 
            			'jquery-ui-dialog-min-css', 
            			includes_url().'css/jquery-ui-dialog.min.css' 
            		);
            
            		$version = filemtime( WP_PLUGIN_DIR.'/'.KH_PLUGIN_SLUG.'/js/lf_sharethis_button.js' );
            
            		// 共有リンクがクリックされたらダイアログを表示するスクリプトの読み込み
            		wp_enqueue_script(
            			'lf_sharethis_button',
            			plugins_url( 'js/lf_sharethis_button.js', dirname( __FILE__, 2 ) ),
            			[ 'jquery-ui-dialog' ],
            			$version,
            			true
            		);
            	}
            
            	/**
            	 * シェアメニューの🔗がクリックされたら表示するダイアログの内容
            	 */
            	public static function echo() {
            
            		// 現在表示しているページのURL
            		$url = $_SERVER['REQUEST_SCHEME'].'://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
            
            		// 現在表示しているページのタイトルと合わせてページ情報を生成
            		$pageinfo = wp_get_document_title().PHP_EOL.$url.PHP_EOL;
            
            		echo '
            <div id="lf-sharethis-box" title="ページの共有">
              <textarea id="lf-sharethis-text" readonly>'.$pageinfo.'</textarea>
              <p>[COPY]をクリック → (クリップボードにコピーされる → )共有先でペースト</p>
              <button id="lf-sharethis-copy" title="ページ情報をクリップボードにコピーして閉じる">Copy</button>
            </div>
            ';
            	}
            }

            JavaScript

            // https://api.jquery.com/jQuery/#jQuery3
            jQuery(function($) {
            
                // Open dialog on clicking the link menu
                $(".lf-sharethis-link").on('click', function() {
                    $("#lf-sharethis-box").dialog();
                } );
            
                // copy info to share and close dialog
                $("#lf-sharethis-copy").on( 'click', function() {
                    navigator.clipboard.writeText( $("#lf-sharethis-text").text() );
                    $("#lf-sharethis-box").dialog("close");
                } );
            } );

            CSS

            /*
             * ソーシャルメニュー
             */
            div#lf-sharethis-box {
            	display: none;
            }
            /* ソーシャルメニュー → 共有ボタン→ Dialog → Pタグの文字の大きさ */
            #lf-sharethis-box p {
            	font-size: smaller;
            }