20190527のPHPに関する記事は7件です。

laravel-slack-apiを使用して、Laravelからslackへ通知する

はじめに

Laravelではslack-notification-channelというslackへ通知する仕組みが存在しています。
https://laravel.com/docs/5.8/notifications#slack-notifications

しかし、Incoming Webhookを使用しているためfile upload等をすることができません。
今回自分はアプリケーションログをfile形式でslackへ通知したかったので、laravel-slack-apiライブラリを使用してみました。
今回は使い方の紹介となります。
※最終コミットが2016年でしたが、問題なく動きました。

アプリケーションログをfile形式でslackへ通知した際のイメージです。
スクリーンショット 2019-05-26 12.58.11.png

設定

laravel-slack-apiのreadme.mdに沿って進めています。

また、slack botの設定はこの記事の最後に手順を書いております。

インストール

composer require vluzrmos/slack-api

providers、config等の追加

config/app.php
'providers' => [
     Vluzrmos\SlackApi\SlackApiServiceProvider::class,
 ]

//...

'aliases' => [
    'SlackApi'              => Vluzrmos\SlackApi\Facades\SlackApi::class,
    'SlackChannel'          => Vluzrmos\SlackApi\Facades\SlackChannel::class,
    'SlackChat'             => Vluzrmos\SlackApi\Facades\SlackChat::class,
    'SlackGroup'            => Vluzrmos\SlackApi\Facades\SlackGroup::class,
    'SlackFile'             => Vluzrmos\SlackApi\Facades\SlackFile::class,
    'SlackSearch'           => Vluzrmos\SlackApi\Facades\SlackSearch::class,
    'SlackInstantMessage'   => Vluzrmos\SlackApi\Facades\SlackInstantMessage::class,
    'SlackUser'             => Vluzrmos\SlackApi\Facades\SlackUser::class,
    'SlackStar'             => Vluzrmos\SlackApi\Facades\SlackStar::class,
    'SlackUserAdmin'        => Vluzrmos\SlackApi\Facades\SlackUserAdmin::class,
    'SlackRealTimeMessage'  => Vluzrmos\SlackApi\Facades\SlackRealTimeMessage::class,
    'SlackTeam'             => Vluzrmos\SlackApi\Facades\SlackTeam::class,
]
config/services.php
'slack' => [
    'token' => env('SLACK_TOKEN'),
]

slackで発行されたトークンを記載する

 .env
SLACK_TOKEN='xoxb-xxxxx'

使い方

$php artisan tinkerから簡単に動作確認できます。

シンプルなメッセージを送る

SlackChat::message('#test', 'ピピッカチュウ');

スクリーンショット 2019-05-27 22.25.20.png

テキストをfileとして送る

SlackFile::upload([
    'filename' => 'ピーカ.txt',
    'initial_comment' => 'ピカピカーチュ',
    'content' => "ピーカ、チューウ!ピーカ、チューウ!ピーカ、チューウ!ピーカ、チューウ!\n",
    'channels' => '#test'
]);

スクリーンショット 2019-05-27 22.14.42.png


slackボットの設定

下記はslackボットの設定方法です。

slack Appの作成

slack apiから設定を行う。
スクリーンショット 2019-05-27 21.48.53.png

Bot Userの作成

スクリーンショット 2019-05-27 21.50.54.png

scopeの設定

botから投稿する場合はchat:write:botを付与する。
スクリーンショット 2019-05-27 21.51.36.png

連携許可

スクリーンショット 2019-05-27 21.51.46.png

Bot User OAuth Access Token

Bot User OAuth Access Tokenをlaravel側のslack tokenに設定してください。
スクリーンショット 2019-05-27 21.52.00.png

Botを対象のチャンネルに追加

スクリーンショット 2019-05-27 21.52.29.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

スネーク?キャメル?なんちゃらケースのまとめ

株式会社オズビジョンのユッコ (@terra_yucco) です。
外部の API なども使いながらプログラミングをしていると、同じ単語でも色々な表現方法に行き当たります。

例えば私の所属している会社オズビジョンの提供サービスの一つ、ハピタスコミック
サービスとしての正式表記はきちんとありますが、以下のような様々な表記が可能です。

  • HapitasComic
  • hapitasComic
  • HAPITAS_COMIC
  • hapitas_comic
  • hapitas-comic

このどの表記も見かけるケースがあるので、それぞれなんと呼ぶのかを調べてみました。

ケース一覧

前提

プログラミング言語では空白を命令の区切りとして扱うものが多いため、英語では hapitas comic などと空白で表現できる言葉を、空白に変わる何かで接続して表現する必要がある。
区切り方法は大きく分けて以下の 3 パターン。

  • 大文字小文字の違い
  • アンダースコア _
  • ハイフン -

ここに大文字小文字のバリエーションなどでいくつかのケースが生まれます。

アッパーキャメルケース / Upper camel case

HapitasComic

  • PHP
    • クラス名は Upper camel case (PSR-1)
  • 別名
    • パスカルケース / Pascal case (Pascal で使われていた)

ロウワーキャメルケース / Lower camel case

hapitasComic

  • PHP
    • メソッド名は Lower camel case (PSR-1)
  • 別名
    • 単に camel case の場合 Lower camel case であることが多い

アッパースネークケース / Upper snake case

HAPITAS_COMIC

ロウワースネークケース / Lower snake case

hapitas_comic

  • PHP
    • 組み込み関数はこれ preg_match str_replace etc.
    • PSR にはこれを使わなければいけない規定はないが、一部 FW (FuelPHP など) では規約にしているケースあり
  • 別名
    • 単に snake case の場合 Lower snake case であることが多い

ケバブケース / Kebab case

hapitas-comic

  • PHP
    • - は減算を表現する演算子のため使用できない
    • CSS 界隈でよく見かける
  • 別名
    • チェインケース / Chain case (鎖のように見える)

アッパーケバブケースってないの?

ここまでは普段よく見かける組み合わせでしたが、ケバブケースの大文字版ってないんだろうかとふと気になって調べてみたところ。
Kotlin 用の extension ですが UPPER-DASH-CASE のような文字列は以下の呼び方をするようです。

  • Upper dash case
  • Upper hyphen case
  • Upper kebab case

また jagaapple を見ると、以下のような呼び名もあります。

  • Train case

なので、厳密にいえば Kebab case hapitas-comic は Lower kebab case なのかもしれません。

気になる点

jagaapple では Upper snake case が HAPITAS_COMIC ではなく Hapitas_Comic と定義されています。
Upper camel case に業務で出会ったら、どちらを意図しているのかきちんと確認したほうが幸せになれそう。

Conclusion

業務中によく見かけて気になっていたケースについてまとめました。

区切り文字 大文字 ケース名
大文字小文字 単語区切り Upper camel
(Pascal Case)
大文字小文字 単語区切り
ただし最初は小文字
Lower camel
(Camel case)
アンダースコア 無し Snake case
アンダースコア 有り Upper snake case
(Constant case)
ハイフン 無し Kebab case
(Dash case)
ハイフン 有り Upper kebab case

PHP では使用しない (できない) ものもありますが、適切に使い分けてより可読性高く規約に沿ったコードを書けるようになればと思います!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【PHP7.4】__toString()が例外を吐けるようになる

プロパティの設定を必須にしたかったとしましょう。

class HOGE{
    public $var;
    public function __toString(){
        if(!$this->var){
            throw new \Exception('$var must set.');
        }
        return sprintf('$var is %1$s', $this->var);
    }
}

try{
    echo new HOGE();
}catch(\Exception $e){
    var_dump($e);
}

何の変哲も無いように見えるコードですが、これ動きません。
Fatal error: Method HOGE::__toString() must not throw an exception, caught ExceptionというFatalエラーを吐いて死にます。

実は__toString()メソッド内では例外を出すことができないのです。
他のあらゆるマジックメソッドにはこんな制限はないのに、__toString()だけそういうことになっています。
よくわかりませんね。

しかしまあ、そんなよくわからない制限なら要らないよね、ということでAllow throwing exceptions from __toStringのRFCが提出されました。

PHP RFC: Allow throwing exceptions from __toString()

Introduction

__toString()からの例外スローは禁止されており、致命的エラーになります。
これは__toString()から任意のコードを呼び出すことを困難にし、一般的なAPIとして使用することが難しくなります。
このRFCは、この制限を取り除くことを目的としています。

現在のような動作になっている理由は、PHPエンジンと標準ライブラリのあらゆるところで文字列変換が行われており、そして全ての箇所が例外を正しく処理できるようになっているわけではないからです。

技術的観点からすると、この制限は無駄でしかありません。
なぜならば、文字列変換中の例外は、回復可能なエラーを例外に変換するエラーハンドラによって引き起こされる可能性があるからです。

set_error_handler(function() {
    throw new Exception();
});

try {
    (string) new stdClass;
} catch (Exception $e) {
    echo "(string) threw an exception...\n";
}

実際、Symfonyは現在の制限を回避するためにこの抜け穴を使用しています。
残念ながら、これはPHP8で削除予定の$errcontextパラメータに依存しています。

Proposal

__toString()からの例外スローを許可します。
もう致命的なエラーは発生しません。

さらにPHP7のエラーポリシーに基づいて、could not be converted to stringおよび__toString() must return a string valueというrecoverable fatal errorを、適切なError exceptionに変更します。

Extension Guidelines

エクステンション開発者は、文字列変換からの例外を適切に処理するため、以下のガイドラインを考慮に入れてください。

zval_get_string()convert_to_string()および類似の関数は、例外を生成した場合でも文字列を生成します。この文字列を含めてください。必ずしも従う必要はありませんが、そうすることができます。
・文字列変換がエラーハンドラによって例外になった場合の結果は、オブジェクトから文字列への変換の場合は空文字列、配列の場合は"Array"という文字列になります。これは以前と同じ動作です。
・通常は(EG(exception))で例外がスローされたかどうかを確認すれば十分です。

zend_string *str = zval_get_string(val);
if (EG(exception)) {
    // リソース解放など
    return;
}

・失敗する可能性のある操作を行うヘルパーAPIが多数追加されます。

// zval_get_string()に似ているが、変換に失敗したらNULLを返す
zend_string *str = zval_try_get_string(val);
if (!str) {
    // リソース解放とか
    return;
}
// Main code.
zend_string_release(str);

// zval_get_tmp_string()に似ているが、変換に失敗したらNULLを返す
zend_string *tmp, *str = zval_try_get_tmp_string(val, &tmp);
if (!str) {
    // リソース解放とか
    return;
}
// Main code.
zend_tmp_string_release(tmp);

// convert_to_string()に似ているが、変換に成功したか否かのbooleanを返す
if (!try_convert_to_string(val)) {
    // リソース解放とか
    return;
}
// Main code.

try_convert_to_string()は、変換失敗時には元の値を返しません。そのため、これを使った方が、convert_to_string()と例外チェックを使うより安全です。
・あらゆる文字列変換操作にチェックを行うことができますが、チェックを省略しても通常は余剰な警告が発生するだけです。気をつけて実装しなければならないのは、主にデータベースのような、永続的な構造を変更する操作です。

Backward Incompatible Changes

recoverable fatal errorからError exceptionへの変更は、破壊的変更です。

Vote

投票開始は2019/05/22、終了が2019/06/05、投票数の2/3+1の賛成票で受理されます。

2019/05/27現在、賛成32反対0で、ほぼ確実に導入決定です。

感想

これによって、あらゆるメソッドから例外を出すことができるようになりました。

さて、なんでこんな簡単そうな変更が今まで残っていたの?
かというと、変更が簡単ではないからです。

最初に報告があったのはなんと2011年1月であり、2012年にはあのNikitaが実装が大変なんじゃよーと言っています。
しかし結局全てのcommitがNikita自身の手によって行われました。
最終的な修正は111ファイル1000行以上に及びます。
どんだけ働いてんだこの人

PHPフォーラムは基本的に保守的な人が多く、破壊的な変更はあまり受理されない傾向にあるのですが、Nikitaについては既に、Nikitaが言うなら仕方ないというレベルに達しているようです。
三項演算子のDeprecateとか、他の人が提案してたら絶対通らなかったよね。

しかしこれ、てっきりuninitialized__toString()したときのために作ったものなのかと思ったのですが、特に関係なかったみたいですね。

そして修正規模のわりに、あまり重要な使いどころが思いつかないというか、そもそも個人的には__toString()自体まず使うことがないですね。
デバッグにはvar_dump()という超絶便利関数がありますし、文字列化したいなら適当にメソッド生やします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

メモ

作業メモ用に。
Markdownになれる。

php_googleカレンダー連携

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Emacs PHP Modeユーザーマニュアル (Draft)

Emacs PHP Mode(php-mode)はEmacsでのPHPスクリプトの編集のためのメジャーモードです。

あと、この記事は書きかけです。

インストール方法

php-mode開発の歴史的経緯から、Emacs本体に標準添付されていません。しかし、標準のパッケージ機能を使ってMELPAから簡単にインストールできます。

package / use-package を使ったインストール

~/.emacs.d/init.el に以下のような記述(どちらか、または両方)があることを確認してください。

init.el
(add-to-list 'package-archives '("melpa-stable" . "https://stable.melpa.org/packages/") t)
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)

その上で、use-packageが導入済みであればインストールは簡単です。

init.el
(use-package php-mode :ensure t)

Emacsでinit.elを編集しているなら、 (use-package php-mode :ensure t)の直後にカーソルを移動してキーボードでC-x C-e(control+e control+xを順に押す)と、その場でPHP Modeのインストールが始まります。

packageまたはuse-packageでインストールしたパッケージは、通常~/.emacs.d/elpa/に配置されます。もし.emacs.dディレクトリをGitで管理しているなら、このファイルは.gitignoreで管理の対象外にすることを推奨します。

Lispパッケージは継続的な更新によって改善されているので、よりよいユーザー体験を求めるにはインストールよりも定期的にアップデートすることが重要です。Emacs標準のパッケージ機能またはuse-packageでインストールした場合、以下の手順でパッケージを更新できます。

  1. M-x list-packages でパッケージ一覧を表示する
  2. このリスト上でキーボードの U (Shift + u)を押して、更新されたパッケージをマークする
  3. 続いてキーボードの x を押して更新を実行する
  4. Emacsを再起動する

MELPA版のPHP Modeには実験的な実装が含まれていることがあります。より安定性を求める場合は :pin melpa-stable を設定することで、正式なバージョンリリース版がインストールされるようになります。

init.el
(use-package php-mode :ensure t :pin melpa-stable)

手動インストール

emacs-php/php-modegit clone するか、Releases · emacs-php/php-modeからzipまたはtarをダウンロードして展開してください。展開したあと make コマンドを実行すると、Lispファイルのバイトコンパイルとphp-mode-autoloads.elが生成されます。

init.el
(when (file-directory-p "~/path/to/php-mode")
  (load "~/path/to/php-mode/php-mode-autoloads.el"))

;; 通常、以下のコードはどれも書く必要がありません
;; (require 'php-mode)
;; (add-to-list 'load-path (expand-file-name "~/path/to/php-mode"))
;; (add-to-list 'auto-mode-alist '("\\.php\\'" . php-mode))

OSのパッケージシステムからのインストール

Debian 10(Buster)またはUbuntu 19.04(Disco Dingo)以降では、 apt install elpa-php-mode でインストールすることができます。しかしこれらのパッケージは少々古いため注意が必要です。Emacsの個人設定を持ち込めないサーバー運用作業での一時的な利用などには利用できますが、それ以外の環境ではEmacsのパッケージ機能などで最新バージョンの利用を強く推奨します。

以前のDebianおよびUbuntuにはPHP Modeはphp-elispというパッケージ名で収録されていました。しかしこのパッケージは2014年から更新されていなかったため問題が多く、強く非推奨です。

関連パッケージの導入

PHPStanを適切に設定することで、PhpStormなどの商用製品を上回る精度のコーディング時エラーチェックが可能になります。PHPで開発が捗るリアルタイムエラーチェックに書きました。

カスタマイズ

設定例

init.el
(defun my-php-mode-setup ()
  "My PHP-mode hook."
  (subword-mode 1)
  (setq show-trailing-whitespace t)

  (setq-local page-delimiter "\\_<\\(class\\|function\\|namespace\\)\\_>.+$")

  (require 'flycheck-phpstan)
  (flycheck-mode t)
  (add-to-list 'flycheck-disabled-checkers 'php-phpmd)
  (add-to-list 'flycheck-disabled-checkers 'php-phpcs))

(use-package php-mode
  :hook ((php-mode . my-php-mode-setup))
  :custom
  (php-manual-url 'ja)
  (php-mode-coding-style 'psr2)
  (php-mode-template-compatibility nil)
  :config
  (bind-key "[" (smartchr "[]" "array()" "[[]]") php-mode-map)
  (bind-key "]" (smartchr "array " "]" "]]")     php-mode-map)
  ;; (bind-key "C-}" 'cedit-barf php-mode-map)
  ;; (bind-key "C-)" 'cedit-slurp php-mode-map)
  (bind-key "C-c C-c" 'psysh-eval-region         php-mode-map)
  (bind-key "<f6>" 'phpunit-current-project      php-mode-map)
  (bind-key "C-c C--" 'php-current-class php-mode-map)
  (bind-key "C-c C-=" 'php-current-namespace php-mode-map))
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

swagger-uiでAPIを叩いて結果をjsonで見る

ローカルで開発中にUI上からAPIを実行して結果をjson形式で見たいと思ってLaravelプロジェクトで実現した件をまとめておきます。
Laravelは5.5を使っています。

最終的にはこういう事ができます

API実行画面
スクリーンショット 2019-05-26 22.51.13.png

レスポンス
スクリーンショット 2019-05-27 8.52.45.png

必要な作業

必要な対応は下記の通りです。

  • swagger-uiの環境を作る
  • swagger-phpを使ってopenapi.yamlを自動生成できるようにする
  • Cross Originなリクエストをswagger-uiから実行可能にする
  • swagger-uiからのリクエストの時はレスポンスをjsonで返す
  • swagger-uiからのリクエストの時は認証を無効にする
  • swagger-uiからのPOSTリクエスト時のトークンチェックを無効にする

swagger-uiの環境を作る

swager-ui用の階層をプロジェクトのrootに作って下記のファイルを作ります。
ポートは衝突しない何かしらを選んで指定します。

docker-compose.yml
swagger-ui:
  image: swaggerapi/swagger-ui
  container_name: "swagger-ui"
  ports:
    - "8001:8080"
  volumes:
    - ./openapi.yaml:/openapi.yaml
  environment:
    SWAGGER_JSON: /openapi.yaml

SWAGGER_JSON という環境変数で、apiを定義するファイルを指定します。
openapi.yamlは同階層に作成し、マウントしておきます。

swagger-phpを使ってopenapi.yamlを自動生成できるようにする

openapi.yamlを手で書くこともできますが、コントローラに記述したPHPDocから自動生成するツールがあるのでそれを使います。どの道何かしらを追記する必要がありますが、編集対象をphpファイルに統一できるのでこれを使います。

下記の手順でパッケージを追加します。

composer require zircote/swagger-php

これで生成コマンドが使えるようになるので、Makefileに生成コマンドを作っておきます。
実行するとプロジェクト内のPHPDocを見てopenapi.yamlが生成されます。

build: ## openapiの定義ファイルを更新する
    ./vendor/bin/openapi app -o ./swagger-ui/

swagger-phpの記法については下記URLを参考にします。
http://zircote.com/swagger-php

GETリクエストの例

    /**
     * @OA\Get(
     *     path="/sample",
     *     tags={"SampleController"},
     *     @OA\Parameter(name="some_id",
     *         in="query",
     *         description="何かしらのid",
     *         @OA\Schema(type="integer")
     *     ),
     *     @OA\Response(
     *         response="200",
     *         description="レスポンス内容の説明"
     *     )
     * )
     */

POSTリクエストの例

   /**
     * @OA\Post(
     *     path="/sample/register",
     *     tags={"SampleController"},
     *     description="何かしらの登録処理",
     *     @OA\RequestBody(
     *         required=true,
     *         @OA\MediaType(
     *             mediaType="application/x-www-form-urlencoded",
     *             @OA\Schema(
     *                 type="object",
     *                 @OA\Property(
     *                     property="name",
     *                     description="登録する何かの名前",
     *                     type="string"
     *                 ),
     *                 @OA\Property(
     *                     property="user_id",
     *                     description="ユーザid",
     *                     type="integer"
     *                 )
     *             )
     *         )
     *     ),
     *     @OA\Response(
     *         response="200",
     *         description="レスポンス内容の説明",
     *     )
     * )
     */

Cross Originなリクエストをswagger-uiから実行可能にする

異なるhostからのリクエストはデフォルトでは禁じられているため、ローカルでかつswagger-uiからのアクセスのみ許可する設定をします。

下記のページにあるように、許可するhostを指定する必要があります。
https://fetch.spec.whatwg.org/#http-cors-protocol

Middlewareとして下記のクラスを追加します。

Cors.php
<?php

namespace App\Http\Middleware;

use Closure;

class Cors
{
    // from swagger-ui
    const ACCESS_ALLOW_ORIGIN = 'http://localhost:8001';

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $response = $next($request);

        if (app()->isLocal()) {
            $response
                ->header("Access-Control-Allow-Origin" , self::ACCESS_ALLOW_ORIGIN)
                ->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
        }

        return $response;
    }
}

swagger-uiからのリクエストの時はレスポンスをjsonで返す

これでswagger-uiでAPIの一覧を見れるようになりました。
ただし、このままだとレスポンスがhtmlになってしまうので、形式をjsonに変更します。

LaravelはデフォルトでBaseControllerを継承していますが、その間に一つ中間クラスを作ってそこに共通処理を書いていきます。

まずはswagger-uiからのリクエスト判定です。
ローカルからのアクセスであることと、headerの中のアクセスoriginを取得して判定します。

use Illuminate\Routing\Controller as BaseController;
use App\Http\Middleware\Cors;
class Controller extends BaseController
{
    protected function isSwagger() {
        return app()->isLocal() && request()->headers->get('origin') === Cors::ACCESS_ALLOW_ORIGIN;
}

次にこれを使ってresponseの形式を変換します。
実装に手を入れることになり嫌な気持ちがありますが、responseをviewのパラメータを渡す処理をラップする関数を作り、Controllerの記述を書き換えます。

    protected function response($view, $response) {
        if ($this->isSwagger()) {
            return response()->json($response);
        }
        return view($view)->with($response);
    }

これでswagger-uiからのアクセス時にのみresponse形式をjsonにすることができました。
他にうまい方法があればそうしたいところ。

swagger-uiからのリクエストの時は認証を無効にする

何かしらの仕組みで認証を使っている場合は無効にしないとswagger-ui上から突破するのは困難です。
これは認証の使い方次第ですが、標準搭載されているMiddlewareを使っている場合は下記のように対応することで無効化できます。

    public function __construct()
    {
        if (!$this->isSwagger()) {
            $this->middleware('auth')->only(['getUpdate', 'postRegister', 'postUpdate']);
        }
    }

swagger-uiからのPOSTリクエスト時のトークンチェックを無効にする

ここまででGETリクエスウトについては動くようになりますが、まだPOSTリクエスト時にのCSRFトークンのチェックに引っかかります。

これもswagger-ui上から適切なtokenを送るのは困難であるため無効化します。
VerifyCsrfToken.phpというファイルがあるので、handleをオーバーライドし、判定条件を変えます。
UnitTest時に無効にしてるのと挙動としては近いですね。

isSwaggerと同じ条件を書いてしまっているのは良くないですが、雑に作るとこうなります。

VerifyCsrfToken.php
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
    /**
     * @return bool
     */
    protected function runningSwaggerUi()
    {
        return app()->isLocal() && request()->headers->get('origin') === Cors::ACCESS_ALLOW_ORIGIN;
    }

    /**
     * @param \Illuminate\Http\Request $request
     * @param Closure $next
     * @return mixed|\Symfony\Component\HttpFoundation\Response
     * @throws TokenMismatchException
     */
    public function handle($request, Closure $next)
    {
        if (
            $this->runningSwaggerUi() ||
            $this->isReading($request) ||
            $this->runningUnitTests() ||
            $this->inExceptArray($request) ||
            $this->tokensMatch($request)
        ) {
            return $this->addCookieToResponse($request, $next($request));
        }

        throw new TokenMismatchException;
    }

最後に

判定処理が共通化できてないのと、Corsクラスの定数に依存してる辺りはなんとかしたいところです。その辺りは次の課題ということで。いい方法知ってる人がいたら教えてほしいです。
とりあえずswagger-uiからAPIを実行してjsonで結果を見る環境はこれでできました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WordPressプラグイン作成の勉強をしてみた

wordpress初心者です。サンプルは調べながら作成しました、間違いなどありましたら指摘お願いします:bow:

この記事の編集履歴

参考

サンプル

Server-Sent Events(SSE)を使用した簡易チャットプラグインを作成してみました

確認バージョン

  • PHP 7.2
  • wordpress 5.2.1
  • Google Chrome 74.0.3729.169

ソースはこちらです

EIrvCHs1Ic.gif

この記事の概要

wordpressの基本的な機能を勉強しながら、簡易チャットプラグインを作成していきます


最終的なコードを見たい方はこちらを見て、本記事は読まなくて良いです。

私が調べながら作っていった手順を一歩ずつ書いていく内容となっています。
間違いやより良いやり方がある場合は教えてください。

対象読者

  • wordpressのplugin作成に壁を感じてる方

    • 初心者の私でもやればできたというのが伝わればと、
  • wordpress初心者 (wordpressを画面から操作したことがあり、PHPの基礎的な知識がある)

    • plugin作成はそこまで難しくなかったので、初心者がいきなりplugin作成してみる、もありだと思いました

1 プラグインをwordpressに認識させる

1-1 phpファイル作成

{root}/wp-content/plugins/simple-sse-chat/simple-sse-chat.php

pluginsディレクトリに作成したいプラグイン用のフォルダと、それと同じ名前のphpファイルを作成します

image.png

1-2 プラグイン名と説明を設定

/wp-content/plugins/simple-sse-chat/simple-sse-chat.php
<?php
/*
    Plugin Name: Simple SSE Chat
    Description: Server Snet Eventsを使用した簡易チャット
*/

// ↓このファイルに直接アクセスされた場合のために必ず処理の先頭につけましょう
if ( ! defined( 'ABSPATH' ) ) {
   exit;
}

この状態で管理画面のプラグインのページをみると、プラグインを確認することができます

Screen Shot 2019-05-26 at 15.16.12.png

1-3 有効化する

有効化を押すと/wp-content/plugins/simple-sse-chat/simple-sse-chat.phpで書いた内容が実行されることになります。
現時点ではまだ何もおきません。

ZeOrdi3X3l.gif

2 管理画面のメニューに追加

2-1 add_action('admin_menu', $callback)

add_actionまたは、add_filterを使って、フックを設定します。
ここでは、管理メニューが読み込まれる前に実行される admin_menuを指定します。

フックについて参考:
【WordPress入門】アクションフックとフィルターフックを使いこなそう | 滋賀/京都/大阪でホームページ制作ならYUKiYURi WEB

/wp-content/plugins/simple-sse-chat/simple-sse-chat.php
<?php

// ...

add_action('admin_menu', function () {
    add_menu_page(
        'Simple Chat Settings', // <title>タグの内容を設定
        'Simple Chat', // 左メニューに表示される名前を設定
        'manage_options', // 権限
        'simple-sse-chat', // スラッグ
        'admin_menu_simple_sse_chat', // メニューを開いたときに実行される関数名
        'dashicons-admin-comments', // アイコン
        200 // メニューの表示順、200と大きい数字にしたので、メニューの一番下に表示される
    );
});
function admin_menu_simple_sse_chat() {
    ?>
        <div class="wrap">
            <h2>Simple Chat Settings</h2>
        </div>
    <?php
}

Screen Shot 2019-05-26 at 15.32.02.png

メニューに追加され、ページを表示することができました。

2-2 get_optionupdate_optionで入力値を保存する

/wp-content/plugins/simple-sse-chat/simple-sse-chat.php
<?php

// ...

add_action('admin_menu', function () {
    add_menu_page(
        'Simple Chat Settings', // <title>タグの内容を設定
        'Simple Chat', // 左メニューに表示される名前を設定
        'manage_options', // 権限
        'simple-sse-chat', // スラッグ
        'admin_menu_simple_sse_chat', // メニューを開いたときに実行される関数名
        'dashicons-admin-comments', // アイコン
        200 // メニューの表示順、200と大きい数字にしたので、メニューの一番下に表示
    );
});
function admin_menu_simple_sse_chat() {
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        check_admin_referer('hoge-fuga-piyo'); // CSRF対策
        update_option('simple_sse_chat_header', $_POST['chat_heder']);
        ?>
        <div id="setting-error-settings-update" class="update-settings-error notice is-dismissible"><strong>Settings have been saved.</strong></div>
        <?php
    }
    $chat_heder = get_option('simple_sse_chat_header', 'Simple Chat'); // 第2引数は指定のoption_nameがない場合の初期値
    ?>
        <div class="wrap">
            <h2>Simple Chat Settings</h2>
            <form method="POST" action="">
                <?php wp_nonce_field( 'hoge-fuga-piyo'); ?>
                <label for="chat_heder">チャットタイトル</label>
                <textarea name="chat_heder" class="large-text"><?= esc_textarea($chat_heder) ?></textarea>
                <input type="submit" name="submit_scripts_update" class="button button-primary" value="UPDATE">
            </form>
        </div>
    <?php
}

sGFRYQa8ze.gif

wp_optionsテーブルに入力値が保存されていることが確認できました。

Screen Shot 2019-05-26 at 16 (2).png

3 ショートコードでチャットのHTMLを表示する

3-1 まず、簡単なHTMLだけを表示

/wp-content/plugins/simple-sse-chat/simple-sse-chat.php
<?php

// ...

add_shortcode('simple_sse_chat', function () {
    return '<span style="color:blue;">ショートコードのテスト</span>';
});

※wordpress 5からの投稿エディタのGutenberg(グーテンベルグ)でのショートコード設定方法↓

lGhEyE8N28.gif

ショートコードがページに表示できたのを確認できました。

3-2 チャット用のHTMLを表示

HTMLを見やすくするためにバッファリングしてHTMLを表示するようにしてみました。
get_option('simple_sse_chat_header', 'Simple Chat');として管理画面から登録した値を出力します。

/wp-content/plugins/simple-sse-chat/simple-sse-chat.php
<?php

// ...

add_shortcode('simple_sse_chat', function () {
    $header = get_option('simple_sse_chat_header', 'Simple Chat');
    ob_start(); // バッファリング開始
    ?>
    <form id="js-simple-sse-chat-form">
        <h2><?= esc_html($header) ?></h2>
        <div class="simple-sse-chat-container">
            <table id="js-simple-sse-chat-body">
                <tbody></tbody>
            </table>
        </div>
        <input type="text" name="chat-content" id="js-simple-sse-chat-input">
        <input type="submit" value="送信">
    </form>
    <style>
        .simple-sse-chat-container {
            height: 200px;
            overflow: scroll;
        }
    </style>
    <?php
    return ob_get_clean(); // バッファからデータを取得してクリア
});

Screen Shot 2019-05-26 at 16.32.53.png

4 チャットデータ登録用のテーブルをDBに作成する

4-1 register_activation_hook()でプラグインがアクティブ化されたときに実行される関数を定義

/wp-content/plugins/simple-sse-chat/simple-sse-chat.php
<?php

// ...

register_activation_hook(__FILE__, function () {
    // プラグインがアクティブ化されたときに実行される
});

4-2 $wpdbを使用してDBを操作する

/wp-content/plugins/simple-sse-chat/simple-sse-chat.php
<?php

// ...

register_activation_hook(__FILE__, function () {
    global $wpdb;
    $sql = sprintf('CREATE TABLE %ssimple_sse_chat (
        `id` INT NOT NULL AUTO_INCREMENT,
        `user_id` INT NOT NULL,
        `content` TEXT NOT NULL,
        PRIMARY KEY (`id`)
    ) %s;', $wpdb->prefix, $wpdb->get_charset_collate());
    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    dbDelta($sql);
});

一度停止して、最有効化しても実行してくれます

RPH5RSjPOU.gif

プラグイン有効化時にテーブルを作成してくれました。

Screen Shot 2019-05-26 at 16.44.50.png

5 チャットの入力値を登録する

5-1 javascriptを読み込む

jsファイルを作成します。

/wp-content/plugins/simple-sse-chat/script.js
console.log(simple_sse_chat_data) // <- PHPから渡される変数

image.png

今回はショートコードのHTMLをjavascriptで操作したいので、

  1. add_action('the_content', $callback)で、投稿や固定ページの内容が表示されたときに、
  2. has_shortcode($content, 'simple_sse_chat') で指定のショートコードが使われていた場合のみ、jsファイルを読み込みます。
/wp-content/plugins/simple-sse-chat/simple-sse-chat.php
<?php

// ...

add_action('the_content', function ($content) {
    // ショートコードが使われているページのみjsを読み込む
    if (has_shortcode($content, 'simple_sse_chat')) {
        // script.jsにsimple_sse_chat_dataという名前のオブジュエクトを定義し、home_urlというプロパティを定義
        wp_enqueue_script('simple_sse_chat', plugin_dir_url(__FILE__) . 'script.js');
        wp_localize_script('simple_sse_chat', 'simple_sse_chat_data', [
            'home_url' => home_url(),
            'nonce' => wp_create_nonce('hoge-fuga-piyo'), // CSRF対策
        ]);
    }
    return $content;
});

指定のjavascriptが読み込まれ、PHPからjavascriptにsimple_sse_chat_dataという変数名で値を渡せていることが確認できました。

Screen Shot 2019-05-28 at 1.41.34.png

5-2 wp_ajax_{action_name}でチャットの内容を登録する用のurlを設定

/wp-content/plugins/simple-sse-chat/simple-sse-chat.php
<?php

// ...

add_action('wp_ajax_chat_post', function () {
    check_ajax_referer('hoge-fuga-piyo', 'security'); // CSRF対策

    global $wpdb;
    $wpdb->insert($wpdb->prefix.'simple_sse_chat', [
        'user_id' => get_current_user_id(),
        'content' => $_POST['chat-content'],
    ]);
});

↑のような書き方ですと/wp-admin/admin-ajax.php?action=chat_postでリクエストすると、コールバック関数を実行してくれます。

5-3 ajaxで登録処理をする

/wp-content/plugins/simple-sse-chat/script.js
const { home_url, nonce } = simple_sse_chat_data;

// 登録
document.getElementById("js-simple-sse-chat-form").addEventListener("submit", async e => {
    e.preventDefault();
    formData = new FormData(e.target)

    // 入力値が空の場合は何もしない
    if (formData.get("chat-content") === "") return;

    formData.append('security', nonce);

    // 登録のリクエスト
    const res = await fetch(`${home_url}/wp-admin/admin-ajax.php?action=chat_post`, {
        method: "POST",
        body: formData,
    })

    // 入力欄を空にしからにしておく
    document.getElementById("js-simple-sse-chat-input").value = ""
});

6i9KVEcoqd.gif

チャットからの入力値をDBに保存することができました。

Screen Shot 2019-05-26 at 17.57.49.png

6 Server-Sent Eventsでチャットの内容を表示する

6-1 Server-Sent Eventsを実行するurlを定義する

/wp-content/plugins/simple-sse-chat/simple-sse-chat.php
<?php

// ...

// ajaxのhookだが、SSEも問題なかったのでwp_ajax_{action_name}を使用しました
add_action('wp_ajax_event_streame', function () {
    if (!wp_verify_nonce($_GET['_wpnonce'], 'hoge-fuga-piyo')) {
      exit; // CSRF対策
    }

    global $wpdb;
    header('Content-Type: text/event-stream');
    header('Cache-Control: no-store');
    while(true) {
        printf("data: %s\n\n", json_encode([
            'chat_data' => $wpdb->get_results(
                "SELECT s.id, s.content, u.user_login
                 FROM {$wpdb->prefix}simple_sse_chat s
                 LEFT JOIN {$wpdb->prefix}users u ON s.user_id = u.id
                 ORDER BY id DESC
                 LIMIT 10"
            ),
        ]));
        ob_end_flush();
        flush();
        sleep(1);
    }
});

6-2 javascriptで表示処理をする

/wp-content/plugins/simple-sse-chat/script.js
const { home_url, nonce } = simple_sse_chat_data;

// ...

const es = new EventSource(`${home_url}/wp-admin/admin-ajax.php?action=event_streame&_wpnonce=${nonce}`);

// 表示
let lastId = 0;
es.addEventListener("message", e => {
    const { chat_data } = JSON.parse(e.data);

    // 更新がなければ何もしない
    if (lastId === (chat_data[0] ? chat_data[0].id : 0)) return;

    // jsonを受け取ってHTMLにして表示
    targetElement = document.getElementById("js-simple-sse-chat-body").querySelector("tbody");
    targetElement.innerHTML = ""
    chat_data.forEach(data => {
        const { user_login, content } = data
        targetElement.insertAdjacentHTML("afterbegin", `
            <tr>
                <td>
                    <small>${user_login}</small>
                    <br>
                    <strong>${content}</strong>
                </td>
            </tr>
        `)
    })

    // 新着があったら下にスクロール
    const scrollElement = document.querySelector(".simple-sse-chat-container")
    scrollElement.scrollTop = scrollElement.scrollHeight

    lastId = (chat_data[0] ? chat_data[0].id : 0);
});

Server-Sent Eventsでチャットの内容を表示することができました。

xMzAQP4EHQ.gif

7 プラグイン削除時に不要なデータを削除する

7-1 register_uninstall_hook()でプラグインが削除された場合に実行される関数を定義

今回の場合だと、wp_simple_sse_chatテーブルの削除、wp_optionsテーブルのoption_namesimple_sse_chat_headerの削除が必要です。

/wp-content/plugins/simple-sse-chat/simple-sse-chat.php
<?php

// ...

// プラグインが削除されたときに実行される
function simple_sse_chat_uninstall () {
    // TODO マルチサイトの考慮をするとより良いものとなる
    //  see also https://github.com/okumurakengo/simple-sse-chat/pull/7

    global $wpdb;
    $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}simple_sse_chat"); // テーブル削除
    delete_option('simple_sse_chat_header'); // wp_optionsテーブルの値を削除
}
register_uninstall_hook(__FILE__, 'simple_sse_chat_uninstall');

管理画面から削除ボタンを押すと、こちらの関数が実行してくれます

Screen Shot 2019-05-28 at 2.png


最後まで読んでいただいてありがとうございました。m(_ _)m

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む