A Day In The Boy's Life -24ページ目

A Day In The Boy's Life

とあるエンジニアのとある1日のつぶやき。

システムの規模が大きくなってくるといろんなクラスファイルが出来上がり、必要なものをincludeやrequireするのがかなり面倒になったりプログラムの可読性が悪くなったりします。

こういうときにオートローダーがあれば便利なわけですが、PHP5からは標準でクラスのオートロードの機能が実装されています。



PHPでクラスのオートローダーを作る


PHP5からは、__autoload() という関数があるのですが、現在推奨されておらず変わりにspl_autoload_register() 関数を用いることが推奨されています。


今回は、オートローダー機能がついた基底クラスを作り、サブクラスでは特に意識せずに所定のライブラリを利用できるような構成のプログラムを書いてみます。


<?php

class baseClass {

    function __construct() {
        define ('LIB_DIRPATH', '/path/to/lib/');
        spl_autoload_register(array($this, 'classAutoLoad'));
    }

    /**
     * オートローダー
     * @param string $className : ロードするクラス名
     */
    public function classAutoLoad($className) {
        $path = LIB_DIRPATH . $className . '.php';
        if (is_readable($path)) {
            require_once $path;
        }
    }
}

spl_autoload_register()関数によって、不明なクラスオブジェクトを生成しようとした際に呼び出す関数を定義しておきます(ここではclassAutoLoad()関数)。

コンストラクタとして定義しているので、サブクラス側では意識せずに自動的に呼び出されます。


classAutoLoad()関数は、呼び出したクラス名が自動的に引数として渡されるので、それをライブラリが置かれているパスの中からrequireするようなロジックを組み立てておきます。

クラス名とそれを定義しているプログラム名は同名になるようにしておきます。


利用する方は下記のような書き方になります。


<?php

require_once 'baseClass.class.php';

class subClass extends baseClass {
    function main() {
        $class = new fooClass();
        $class->checkString("xyz"));
    }
}

$obj = new subClass();
$obj->main();


subClass側のmain関数で呼び出しているfooClassは特にincludeやrequireはしていませんが、基底クラスのほうで定義しているclassAutoLoad()によって自動的にrequireされます。
これで、多くのクラスファイルを利用するプログラムでも先頭に長大なrequireを書く必要はありません。


spl_autoload_register()は、クラスをオートロードする役目というよりは認識できないクラスオブジェクトが生成されたときの処理を定義するという感じで、結局はその時にクラスファイルを読み込むためのロジックをclassAutoLoad()で書いておくということをしています。


名前空間に対応させたい場合、classAutoLoad()に渡される引数に名前空間付のクラス名が渡されるので一工夫が必要です。

また、ライブラリのパスがphp.iniに定義されているinclude_path内に必ずあるというならstream_resolve_include_path() を使ったほうが便利かもしれません。


    /**
     * オートローダー
     * @param string $className : ロードするクラス名
     */
    public function classAutoLoad($className) {
        // 名前空間付の場合は分解してクラス名だけを取り出す
        $className = (array_pop(explode('\\', $className)));
        // php.iniのinclude_pathからファイルを探してパスを返してくれる
        $path = stream_resolve_include_path($className . ".php");
        if ($path !== FALSE && is_readable($path)) {
            require_once $path;
        }
    }


呼び出し側は、下記のように名前空間付で呼び出しても勝手にロードしてくれます。


    function main() {
        $logic = new \hoge\FooBar();
        var_dump($logic->checkString("abc"));
    }


名前空間の扱いは自動で分解するなど、もっとうまいことやってくれたらいいのになぁとか思ったりはしますが。





PHPのcURL関数 を使えば、外部アクセスが容易にできてデータを送信したり、スクレイピングするのが容易になりますが、特に社内環境などプロキシを通さないといけない環境でcURL関数を使う場合にはまったことのメモです。



cURL関数でプロキシを通す


一般的に、プロキシを使って外部にアクセスする場合のプログラムは下記のようになります。


<?php

// アクセスするURL
$url = "https://www.google.co.jp";
// プロキシを通すかどうかのフラグ
$proxyFlg = TRUE;

$ch = curl_init($url);
$defaultOption = array(CURLOPT_HEADER         => FALSE, // ヘッダを出力しない
                       CURLOPT_RETURNTRANSFER => TRUE, // curl_exec()の戻り値を文字で受取る
                       CURLOPT_FAILONERROR    => TRUE // HTTPステータスコード400 以上の場合に 処理失敗と判断
                      );
curl_setopt_array($ch, $defaultOption);

// プロキシを通さないといけないURLの場合
if ($proxyFlg === TRUE) {
    $proxyOption = array(CURLOPT_PROXY     => "http://proxy.example.com",
                         CURLOPT_PROXYPORT => 8080);
    curl_setopt_array($ch, $proxyOption);
}

// サイトにアクセス、エラーの場合メッセージを出力
if (($response = curl_exec($ch)) === FALSE) {
    echo curl_error($ch) . PHP_EOL;
}

echo $response;

curl_close($ch);

上記のプログラム内ではプロキシを通すかどうかは便宜上、先頭にフラグを立ててしまっていますが、実際にはそのURLがプロキシを経由する必要があるかどうかの判別が必要です。


で、本題のところですが自分の環境では上記のようにプログラム内部においてプロキシを通すかどうかの判別処理を入れていたのですが、どうもサイトからHTTPステータスコードの403が返ってきて悩んでいたわけです。

アクセス先のサイトにACLは切っていたものの、ログを見てもどうもそこまで到達していないらしく事前にアクセスを拒否されているようでした。


で、後述するデバッグ方法で通信情報を見てみるとプロキシが通るようになっており、プロキシ側で403を返されていました。

内部的なプロキシを通す/通さないの判別処理は正しかったのでなぜプロキシ経由になるんだって悩んでたわけですが、原因はApacheの環境変数にありました。

rootユーザーの.bash_profileに下記のように環境変数を埋め込んでおり、それがApache起動時に読み込まれていました。


export HTTP_PROXY=http://proxy.example.com:8080
export HTTPS_PROXY=http://proxy.example.com:8080

phpinfo(またはgetenv())で環境変数をみてみると、確かにプロキシの情報がセットされています。


var_dump(getenv("HTTPS_PROXY"));
string(29) http://proxy.example.com:8080

ってことで、プログラム内の処理以前に環境変数によって強制的にプロキシを通るようになっていたわけです。

コマンドラインでどうプログラムを実行すると実行ユーザーが違うためにこの環境変数の影響を受けないのに、ウェブアクセスだとうまくいかないからかなりはまったりしました。


先のプログラムの中でアクセス先のサンプルとしてGoogleサイトを指定していますが、ここではHTTPSでのアクセスとしています。

これにも理由があって、HTTPSでのアクセスだと環境変数内のHTTPS_PROXYが有効になるのですが、HTTPでのアクセスだと環境変数のHTTP_PROXYは読み取ってくれず、PHP内のCURLOPT_PROXYオプションをcurl_setopt()でセットしなくてはなりません

なんで、HTTPSのときだけなのかわからないんですが、環境変数の影響を受けたくないなら、先のプログラムを下記のように意図的にプロキシを通さない場合はCURLOPT_PROXYオプションをクリアしてしまうほうがよいかもしれません。


if ($proxyFlg === TRUE) {
    $proxyOption = array(CURLOPT_PROXY     => "http://proxy.example.com",
                         CURLOPT_PROXYPORT => 8080);
} else {
    $proxyOption = array(CURLOPT_PROXY     => "",
                         CURLOPT_PROXYPORT => "");
}
curl_setopt_array($ch, $proxyOption);


cURL関数のデバッグ方法


他のサイトにアクセスするので、エラーがあった際にはcurl_error()の内容から受け取ったHTTPステータスコードなどである程度のデバッグができますが、403とか返ってくると今回のようにどこで蹴られているのか(プロキシからのエラーなのか、該当サイトのACLに引っかかっているのかなど)がわかりづらくなります。

ってことで、通信状況なども見たいのであれば、下記のようにデバッグのオプションを入れることで、cURL関数がどういう経路で通信しようとしているのか見ることができます。(先のプログラムの一部抜粋版です)


// 通信内容を保存するためのテンポラリファイル
$fp = tmpfile();

$ch = curl_init($url);
$defaultOption = array(CURLOPT_HEADER         => FALSE, // ヘッダを出力しない
                       CURLOPT_RETURNTRANSFER => TRUE, // curl_exec()の戻り値を文字で受取る
                       CURLOPT_VERBOSE        => TRUE, // デバッグを有効化
                       CURLOPT_STDERR         => $fp, // デバッグ情報は標準エラー出力されるためファイルに書き出す
                       CURLOPT_FAILONERROR    => TRUE // HTTPステータスコード400 以上の場合に 処理失敗と判断
                      );
curl_setopt_array( $ch, $defaultOption);

// サイトにアクセス
if (($response = curl_exec($ch)) === FALSE) {
    echo curl_error($ch) . PHP_EOL;
}

// 記録した通信内容を読み出す
fseek($fp, 0);
while (($line = fgets($fp)) !== FALSE) {
    $tmp .= $line . "<br />";
}
// 通信状況を出力
echo $tmp;

主な変更点は、CURLOPT_VERBOSEとCURLOPT_STDERRオプションを追加すること、そしてその内容を一時ファイルに書き出し、最後に出力させるというだけです。

以下のような結果が受け取れたりします(これはプロキシで拒否されて403を返されていることがわかる)


The requested URL returned error: 403 bool(false) * About to connect() to proxy proxy.example.com port 8080 
* Trying 192.168.1.2... * connected 
* Connected to proxy.example.com (192.168.1.2) port 8080 
* Establish HTTP proxy tunnel to www.example.com:443 
> CONNECT www.example.com:443 HTTP/1.0 
Host: www.example.com:443 
Proxy-Connection: Keep-Alive 

< HTTP/1.1 403 Forbidden 
< Cache-Control: no-cache 
< Pragma: no-cache 
< Content-Type: text/html; charset=utf-8 
< Proxy-Connection: close 
< Connection: close 
< Content-Length: 2943 
< 
* The requested URL returned error: 403 
* Received HTTP code 403 from proxy after CONNECT 
* Closing connection #0 




RedHatやCentOSなどでパッケージをインストール/アンインストールするコマンドとしてyumやrpmコマンドがありますが、その辺に関連して保守・運用上で個人的によく利用するコマンドやオプションなどのまとめです。

yumコマンドの基本的な使い方は「yumによるRPMパッケージの更新管理 」を参考にしてみてください。


・ リポジトリに公開されているパッケージを検索する


yumコマンドを使ってインストール可能なパッケージをリポジトリから検索します。


# yum search httpd
Loaded plugins: rhnplugin, security
This system is receiving updates from RHN Classic or RHN Satellite.
Excluding Packages in global exclude list
Finished
Excluding Packages from Red Hat Enterprise Linux (v. 5 for 64-bit x86_64)
Finished
=================================================================== Matched: httpd 

===================================================================
mod_ssl.x86_64 : Apache HTTP Server 〓 SSL/TLS 〓〓〓〓〓
system-config-httpd.noarch : Apache 設定ツール
httpd.x86_64 : Apache HTTP Server

・ インストールされているパッケージを検索する


インストール済みのパッケージを検索します。


# rpm -q httpd
httpd-2.2.3-83.el5_10

または、yumコマンドでも。


# yum info httpd
Loaded plugins: rhnplugin, security
This system is receiving updates from RHN Classic or RHN Satellite.
Excluding Packages in global exclude list
Finished
Excluding Packages from Red Hat Enterprise Linux (v. 5 for 64-bit x86_64)
Finished
Installed Packages
Name       : httpd
Arch       : x86_64
Version    : 2.2.3
Release    : 83.el5_10
Size       : 3.2 M
Repo       : installed
Summary    : Apache HTTP Server
URL        : http://httpd.apache.org/
License    : Apache Software License
Description: The Apache HTTP Server is a powerful, efficient, and extensible
           : web server.

・ インストールされているコマンドがどのパッケージに含まれるか調べる


コマンドはわかるんだけどどのパッケージに含まれてるんだっけって時はrpmコマンドで調べられます。


# rpm -qf /usr/bin/sar
sysstat-7.0.2-13.el5

詳細が知りたければ


# rpm -qi httpd
Name        : httpd                        Relocations: (not relocatable)
Version     : 2.2.3                             Vendor: Red Hat, Inc.
Release     : 83.el5_10                     Build Date: 2013年09月27日 20時41分07秒
Install Date: 2013年10月28日 13時56分32秒      Build Host: x86-001.build.bos.redhat.com
Group       : System Environment/Daemons    Source RPM: httpd-2.2.3-83.el5_10.src.rpm
Size        : 3324015                          License: Apache Software License
Signature   : DSA/SHA1, 2013年10月11日 23時26分24秒, Key ID 5326810137017186
Packager    : Red Hat, Inc. 
URL         : http://httpd.apache.org/
Summary     : Apache HTTP Server
Description :
The Apache HTTP Server is a powerful, efficient, and extensible
web server.

※ オプション「-qfi」として、コマンドから詳細を調べることも可能。 これは、該当のライブラリがどのパッケージに含まれているものか(どこからインストールされたか)を調べることもできて便利です。


# rpm -qf /usr/lib/libssl.so
openssl-devel-0.9.8e-32.el5_11

・ パッケージの依存関係を調べる


yumコマンドでインストールする際には自動的に依存関係を調べて関連するパッケージもまとめてインストールしてくれたり、エラーがあった場合でもどのパッケ ージと依存関係にあるのか表示してくれたりはしますが、rpmコマンドで調べることもできます。


# rpm -q --whatrequires httpd
system-config-httpd-1.3.3.3-1.el5
mod_ssl-2.2.3-83.el5_10
httpd-manual-2.2.3-83.el5_10
httpd-devel-2.2.3-83.el5_10
httpd-devel-2.2.3-83.el5_10

・ ログから調べる


(コマンドではないですが)現在インストールされているパッケージの一覧は、「/var/log/rpmpkgs」に出力されています。(これは、コマンドから「rpm -qa」を実行したのと同じ)


Deployment_Guide-en-US-5.8-1.el5.noarch.rpm
Deployment_Guide-ja-JP-5.8-1.el5.noarch.rpm
GConf2-2.14.0-9.el5.i386.rpm
GConf2-2.14.0-9.el5.x86_64.rpm
GConf2-devel-2.14.0-9.el5.x86_64.rpm
MAKEDEV-3.23-1.2.x86_64.rpm
- snip -

いつ、そのパッケージがインストールされたのかは「/var/log/yum.log」を参照します。(「rpm -qi」で出力される「Install Date」を見てもわかりますがその日・週のログとしてまとまるので見やすい)


Jan 25 08:00:35 Updated: openssl-0.9.8e-32.el5_11.x86_64
Jan 25 08:00:35 Updated: 1:cups-libs-1.3.7-32.el5_11.x86_64
Jan 25 08:00:36 Updated: openssl-0.9.8e-32.el5_11.i686

・ パッケージの更新を通知する


バグ/セキュリティフィックスのパッチが提供された場合にいち早く情報を取得できるように更新通知をメールで受け取ったり、自動適用したりすることができます。

設定は、/etc/yum/yum-updatesd.confにて行います。


[main]
# how often to check for new updates (in seconds)
run_interval = 86400
# how often to allow checking on request (in seconds)
updaterefresh = 6000

# how to send notifications (valid: dbus, email, syslog)
emit_via = email
email_to=webmaster@example.com
email_from=yum-updatesd@localhost
smtp_server=localhost:25
# should we listen via dbus to give out update information/check for
# new updates
dbus_listener = yes

# automatically install updates
do_update = no
# automatically download updates
do_download = yes
# automatically download deps of updates
do_download_deps = yes


説明が書かれているのでなんとなく項目の意味はわかると思いますが、run_intervalでチェック間隔を調整したり、email_xxxx関連でメール通知の設定、db_updateなどでは自動更新するかどうかの設定ができます(本番環境とかで自動適用はしないほうがいいでしょうけど)


設定が終わったらデーモンを再起動。


/etc/init.d/yum-updatesd restart

・ パッケージをダウンロードする


yumdownloaderコマンドを利用すれば、サーバーにインストールすることなくRPMパッケージだけをダウンロードすることができます。(yumdownloaderコマンドはyum-utilsに含まれています)


# yumdownloader --destdir=/tmp httpd
Loaded plugins: rhnplugin
This system is receiving updates from RHN Classic or RHN Satellite.
httpd-2.2.3-91.el5.x86_64.rpm

ちなみに、yumコマンドでもdownloadonlyオプションを使えばダウンロードすることができます。


# yum install --downloadonly --downloaddir=/tmp httpd