PSQL Windows ODBC Linux SQL 接続

要約:
DB MagicのBtrieveファイルをPSQLを介して、LinuxのPHPからODBC経由で読み書きできる
なかなか苦労したので、こんなことやる人少ないだろうけど、記しておく

前置き

DOS時代から聞いたことがあったBtrieveというファイル内DBの仕組み。固定長データベースですからなかなかとっつきにくいし、Windows時代になって長いこと過ぎて、既に使われていないのでは…と思ったら結構あるもんですね。DB Magic(UniPaaSを経て今はMagic XPA)で採用されてるからかな。

で、そのBtrieveファイルをもとにSQLとして扱える仕組みが提供されており、だったらPHPからBtrieve読み書きできたりしちゃうんじゃないのーということで、そういう話が始まりまして、結構苦労したので記します。日本語のソースもあんまりなかったし。

Btrieveを使ったDBMSとしてActian PSQLというのがあります(以前はPervasive PSQLでした)。このPSQLエンジンがあれば、バックエンドはBtrieveですが、SQLを使って問い合わせができます。
そしてさらにPSQLはODBCインターフェースを提供しているので、Windowsはもちろん、LinuxでもODBCドライバが存在しています。

ということでWindows側でBtrieve、PSQL、ODBC、Linux側でPSQLドライバ、unixODBC、php-odbcと重ねていくことで、PHPからWindows上のPSQLエンジンを利用することができるわけです。

Windows側の準備

Actian PSQLをセットアップすると大体入ってます。Magic XPA付属のPSQLでも入ってました。適当にBtrieveファイルをもとにPSQLが利用できるようです。

Btrieveデータファイルだけがある状態ではSQLは利用できません。
PSQL Control Centerから「データベースの作成」を行い、データパスにBtrieveデータがあるフォルダを指定してやります。
さらにPSQL DDF Builderを起動し、作ったデータベースのデータパス内から使用するデータファイルを探し出します。そこで右クリック「テーブル定義の作成」です。

Btrieveファイルは固定長データベースですが、デリミタがありませんので、あらかじめ「Xバイト目からはYというフィールドで型がINTEGERだから4バイトです」という定義が必要なのです。データファイルがあるだけではPSQLはこれを知りませんので、定義を別途作ってPSQLに読ませます。この作業をするのがDDF Builderです。きっとData Definition Fileかなんかの略です。

DDFができたらPSQL上でテーブルが扱えるようになっています。初回開くとインデックスが作成されたりしてRDBMSっぽいです。スキーマをエクスポートしておくと、他の環境にデータファイルを持っていった際にDDFをインポートするだけで動くようになります。
ちなみにスキーマの頭には CREATE TABLE “hoge” USING “hoge.dat” なんて書かれていて、ああファイルに対するテーブル定義なんだなぁと確認できます。

WindowsのODBCデータソースからPervasive ODBC Unicode Interfaceなんかのドライバを使ってDSNを作れます。フリーソフトのcse(Common SQL Environment)などで接続すると、DB内にDDFで定義されたテーブルが存在しているように見えるのがわかります。

PSQLはTCPポート1583で動くらしいので、他のマシンからアクセスさせるためにファイヤウォールの解除をしておきます。いまどきはポート指定じゃなくてPSQLエンジンのプログラムを指定したほうがいいんだろうけど、やってないからわかりません。

Linux側の準備

今回はCentOS8上でApache+php-fpmを用意してましたのでこれでやります。
CentOS8上にあるApache2.4はmod_phpじゃなくてphp-fpmを使うようになってますのでそのへんセットアップしておきます。
Apache2.4はデフォルトで全体がdenyされているのではじめてセットアップするとforbiddenの嵐で大抵ハマります。

とりあえずパッケージとして php-odbc unixODBC あと使う人は php-pdo 他使うモノを入れておきます。

Linux用のPSQL Clientをダウンロードしてきます。
PSQL v12 SP1 インストール用ファイル(ダウンロード情報)
64bit Linux rpmをインストールします。あっさり終わりますが、環境設定は自前でやる必要があります。

psqlユーザーが作られているので、~psql/.bashrc に書いてある環境変数をコピーしておけばrootでも動きます。なんかごちゃごちゃ書いてありますが、元のPATHが消えたりしてて困るので必要そうな部分だけコピーして書き直します。

export PVSW_ROOT=/usr/local/psql
export PATH=$PVSW_ROOT/bin:$PATH
export LD_LIBRARY_PATH=$PVSW_ROOT/lib64:$PVSW_ROOT/bin:$LD_LIBRARY_PATH
export MANPATH=$PVSW_ROOT/man:$MANPATH
export BREQ=$PVSW_ROOT/lib
export LD_BIND_NOW=1

PSQL ClientにDSN追加を命じます(内部的に何をしているのかわからんけど、少なくともodbc.iniファイルを置くだけではダメなのでコマンド実行は必須のようです)
# dsnadd -dsn=MYDSNNAME -db=DBNAME -host=WindowsHostIP
/usr/local/psql/etc/odbc.ini が作成されるので、これを /etc/odbc.ini に追記しておきます。
また /usr/local/psql/etc/odbcinst.ini を同様に /etc/odbcinst.ini に追記しておきます。

お気づきかもしれませんが、PSQL関連の環境変数がないとPSQL ClientおよびODBCドライバは動作しません。なのでphp-fpmに環境変数を渡す必要があります。
CentOS8ではphp-fpmはsystemd経由で動いてますので、 /etc/systemd/system/php-fpm.service.d/ の中に pvsw.conf とでもファイルを作成して、環境変数を渡してやります。

[Service]
Environment=LD_LIBRARY_PATH=/usr/local/psql/lib64:/usr/local/psql/bin:/usr/lib Environment=PATH=/usr/local/psql/bin:/bin:/usr/bin:/usr/local/sbin:/usr/local/bin:/sbin:/usr/sbin:/root/bin
Environment=PVSW_ROOT=/usr/local/psql
Environment=BREQ=/usr/local/psql/lib

設定ファイルの変更ということになるので
# systemctl daemon-reload
# systemctl php-fpm reload
しておきます。

ここまでで準備完了です。適当にコードを書いて走らせてテストしましょう。

$conn = odbc_connect('MYDSNNAME', '', ''); // user/passは空で良い
$odbc_q = odbc_exec($conn, "SELECT * FROM sometable;");
$odbc_r = odbc_fetch_array($odbc_q);

なお、Windows側は文字コードがsjisですので、SQL文は適宜mb_convert_encodingなどしてやる必要があります。文字コードが適切なら日本語カラム名もクォート等なしで通るようです。

WebSocketをPHPで使う、ループ処理も

webブラウザにリアルタイムに変化する情報を表示したいなーと思ったら、既存の頭ではAjax通信をSetIntervalするなりして取得する感じでありましたが、なんかHTML5の世はWebSocketなる仕組みが大体のブラウザでサポートされるに至ったようです。

というわけでWebSocketを使って何か作ろうと思い立ちましたが、サーバー側はNode.jsとかを使うのが主流のようで、もう脳内がPHPになっちゃってる人に新規習得させるのもちょっとコストだな、ということでPHPで作ります。クライアント側はJavaScriptだからjQueryでもなんでも良い。

PHPでWebSocketというとRatchetというのが出てきますので素直にソレを使います。というかRatchetしかないよね。

そんなわけでPHP Ratchetで検索するといろんなQiitaとかblogが出てきます。そのままコピペするとバージョン違いで死ぬので参考程度にしておいて、ちゃんと本家のドキュメントでInstallationとHello Worldまで追いかけましょう(英語だけど)。私は孫引きして2時間ぐらい遠回りしました。
Ratchet – What is a WebSocket?

手順的にはcomposerを用意して、ratchetを取得、使うところでrequireしてやります。

# composerの取得
curl -sS https://getcomposer.org/installer | php

# composerの設定
# vi composer.json
{
"autoload": {
"psr-4": {
"MyApp\": "src"
}
},
"require": {
"cboden/ratchet": "^0.4"
}
}
# 0.4にしなくても * 指定で最新指定のようです

# ratchet取得
php ~/composer.phar require cboden/ratchet

あとはとりあえずHelloWorldからコピペして作ります。
telnetで通信するバージョンと、WebSocketで通信するバージョンができますので、違いを学ぶと良いです。
シリアルポートやSocketの通信をしたことがある方はあっさりと理解できると思います。要はSocket通信がWeb上でできるということですので。

server.php側は手続き型のPHPなのにイベントドリブンな書き方になるのでちょっと戸惑いますが、監視ループで処理していく以上はそうなります。

で、HelloWorldではメッセージの送受信が発生して、それに伴う処理を行うところまではできましたが、サーバー側で何かを監視して、サーバー側発でメッセージを送りたい場合はどう書くのか。ググってもなかなか出てきません。

Hello World的には無名関数を即時生成しています

<?php
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use MyApp\Chat;

require dirname(__DIR__) . '/vendor/autoload.php';
$server = IoServer::factory(
new HttpServer(
new WsServer(
new Chat()
)
),
8080
);
$server->run();

onほげほげメソッド以外に常に何か動かしたい場合、ループ処理の中で実行するコードをChatクラスの中に定義し、$serverに定期的に実行させれば良いのです。

この辺は日本語ソース見つからず、stackoverflowで見ました。
web services – How do I access the ratchet php periodic loop and client sending inside app? – Stack Overflow

<?php
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use MyApp\Chat;

require dirname(__DIR__) . '/vendor/autoload.php';
$chat = new Chat();
$server = IoServer::factory(
new HttpServer(
new WsServer(
$chat
)
),
8080
);
$server->loop->addPeriodicTimer(5, function () use ($chat) {
$chat->someMethod();
});
$server->run();

$chatを複数箇所で指定する必要があるので、事前にnewしておきます。
$serverのloopにaddPeriodicTimerを使って繰り返し処理を作成、別途Chatの中に呼び出すメソッドを作っておけばOKです。
addPeriodicTimerは小数も設定できるので、ミリ秒単位で実行することもできるみたいですが、正確にミリ秒にはならないかも。
とりあえず0.01秒ごとにmicrotime()の結果をクライアントにだらだと流してみましたが、飛び飛びな感じでした。実機・VMなどの環境にもよるだろうなぁ。

今回、PHPは7.0で実行しました。

POST遷移をやり直したときにiOSのsafariが固まる

PHPで制作中に気づいた。

フォームからPOST遷移で次のページに行ったあと、
そこでリロードしたり、もしくはさらに次のページから戻ってきた場合
「フォームの内容の再送信をするか?」と聞かれます。

再送で同じページが出てくるならそれで良いのですが、
場合によっては再送させたくないので、別ページにリダイレクトしたりする場合があります。

今回は「リロード時のみ別ページに転送したい」ということでやってみたんですが
iOSのsafariだと、「再送しますか?」の画面で固まって、それ以上の操作を受け付けませんでした。

form.php

<?php
session_start();
if($_SERVER['REQUEST_METHOD']=='POST'){
  if(isset($_SESSION['token'])){
    // トークンがあれば正常、トークンは削除
    echo 'success';
    session_destroy();
    exit;
  }else{
    // トークンがなければエラーページへリダイレクト
    header('Location: errer.html'); // デフォルトでは302でリダイレクト
    exit;
  }
}else{
  // 通常アクセスの際にトークンを発行
  $_SESSION['token']=1;
}
?>
<html><body>
<form action="" method="POST"><input type="submit" value="post"></form>
</body></html>

ところで、リダイレクトは
301 Moved Permanently
302 Moved Temporarily
の他にも
303 See Other
307 Temporary Redirect
というのがあるんですね。

本来ならPOSTして302リダイレクトが帰ってきたら、
リダイレクト先にもPOSTしないといけない(らしい)。

302の場合は
「POSTしましたけどー」「うちじゃないよ、転送先を当たってくれ」
なので、転送先にもあらためてPOSTすることを想定してる。

303の場合はPOST後にGETで見せたいなど、
「POSTしましたけどー」「処理は完了した、次にこっちを見てくれ」
という場合に使うんですね。

ブラウザのPOSTリクエストは、リダイレクトさせるとGETに化ける? ::ハブろぐ

なので、もしかしたら303でリダイレクトしてやったらsafariも固まらないのか?と思いまして

header("HTTP/1.1 303 See Other");
header('Location: error.html');
exit;

としてみましたが、やっぱりsafariは固まりました。

となると、本来であればPOST後のページをform.phpで出すのではなく、
その時点でリダイレクトするというPRGパターン(POST-Redirect-GET)にしておけば良いんでしょうね。
(戻って固まるのは同じだが、頻度は減る?)

あとはそもそもリダイレクトしないことだな。

古いWordPressサイトをStaticPressで静的に書き出してしまおう

WordPressで作ってた古いサイトを静的サイトに変換しておけば、
今後バージョンアップだとかセキュリティアップデートだとかに手間取らない。

ということで久々に触ったサイトのWordpressを最新版にバージョンアップして
StaticPressプラグインを導入。

有効化するぞーと思ったらエラーが。
class-static_press.phpの965行目でsyntax error。unexpected T_SL…

$regex = <<<'END'
/
(中略)
/x
END;

PHPのヒアドキュメントの開始でエラー。
シングルクォートで囲む ‘xxx’ という指定法はNowdoc形式といって、PHP5.3以降でないと使えない。
今回使ってたサーバーはさすが古いだけあって5.2.xでした。

幸い、(中略)の中にシングルクォートや変数を含む部分は無かったので

$regex = '
/
(中略)
/x
';

として解決。
直接プラグインを書き換えるのは一時しのぎだけど、
どうせこのサイトは静的に書き出してしまうのだからこれでも良かろう。

「良かろう」なんて普段使わない言葉遣いしたら亘理町の「よかろう寿司」に行きたくなったわね!はらこめし!
(↓元祖はらこめし味くらべのときの様子)

で、有効化したらメニューのStaticPressから設定して書き出し開始。
設定の「静的サイト URL」は、出力したファイルを後で運用するURLを指定しておく。
「出力先ディレクトリ (ドキュメントルート)」は既存のwordpressファイルととカブると面倒なので
独自のディレクトリを指定しておいた方がラクだと思う(無ければ作ってくれる)。

あとはできあがったファイルを適当に移設して公開すればオッケーよ!

更新しなくなったWordPressサイトを静的にしておく

WordPressはPHPで動的にサイトを出してます。
当然サーバーのCPUパワーを食うので、更新しなくなったサイトは静的に変換して置いておきたい。
StaticPress等のプラグインを入れて出力させるのが常套手段なのですが、現段階の最新版Wordpress4.7.2に入れたらエラーが出たりして(使っているテーマ、環境にもよると思います)。

ならば無理矢理クロールして取ってしまえ!ということでwget
$ wget -nc -x -r http://example.com

出来上がったファイルは拡張子がhtmlじゃないものも多いので、
設置先サーバーのほうでMIME-typeをいじります。

今回はnginx上に置いたので

server {
    server_name example.com;
    location / {
        default_type text/html;
    }
}


これだけ。
たぶんApacheでもDefaultType text/htmlとか書いておけば拡張子ナシでもhtmlとして出してくれるでしょう(未確認)

あとはアクセスしてみて、微妙にリンク先が無かったりするものは個別に手直し。
Wordpressに限らず動的サイトを静的にするのに使えるなぁ。

2017-04-19 追記: wgetによる収集では一部欠けているようで、
やはりなんとかアップデートした後にStaticPressするのが良さそうです。