Symfoware

Symfowareについての考察blog

PHP 「@」でエラー抑止していても処理が途中で終了する

例えばこんなコード。


  1. <?php
  2. // とりあえずファイルを開こうとしてみる
  3. // 存在しない場合のエラーは「@」で抑止
  4. $file = @file_get_contents('dummy.txt');
  5. if ($file === false) {
  6.     echo 'ファイルが存在しません'.PHP_EOL;
  7.     
  8. } else {
  9.     echo $file.PHP_EOL;
  10. }



「@」でエラーを抑止し、とりあえずファイルを開いてみる。
戻り値がfalseだったらファイルが存在しないと判断。


$ php sample.php
ファイルが存在しません




このような処理が書いてあるプログラムで、ある環境ではうまく動くのですが、
別の環境ではファイルが存在しない時に処理が止まってしまい悩んでいました。

よくよく調べてみると、動かない環境では事前に読み込んでいるファイルで
「set_error_handler」が設定されており、ここでexitしています。


  1. <?php
  2. // -------------------------------------------
  3. // 事前に読み込まれるファイルで実行されている処理
  4. function _hook_exception_handler($severity, $message, $filepath, $line) {
  5.     echo $message.PHP_EOL;
  6.     exit();
  7. }
  8. set_error_handler('_hook_exception_handler');
  9. // -------------------------------------------
  10. // とりあえずファイルを開こうとしてみる
  11. // 存在しない場合のエラーは「@」で抑止
  12. $file = @file_get_contents('dummy.txt');
  13. if ($file === false) {
  14.     echo 'ファイルが存在しません'.PHP_EOL;
  15.     
  16. } else {
  17.     echo $file.PHP_EOL;
  18. }





$ php sample.php
file_get_contents(dummy.txt): failed to open stream: No such file or directory




なるほど、エラーを抑止しててもエラー発生時の処理でexitされたら
プログラムの実行は止まりますよね。


該当処理の直前で「set_error_handler(null)」とすることで問題を回避しました。


  1. <?php
  2. // -------------------------------------------
  3. // 事前に読み込まれるファイルで実行されている処理
  4. function _hook_exception_handler($severity, $message, $filepath, $line) {
  5.     echo $message.PHP_EOL;
  6.     exit();
  7. }
  8. set_error_handler('_hook_exception_handler');
  9. // -------------------------------------------
  10. // デフォルトのエラー設定を解除
  11. // 戻り値は変更前の設定無いようなので対比しておく
  12. $error_handlers = set_error_handler(null);
  13. // とりあえずファイルを開こうとしてみる
  14. // 存在しない場合のエラーは「@」で抑止
  15. $file = @file_get_contents('dummy.txt');
  16. if ($file === false) {
  17.     echo 'ファイルが存在しません'.PHP_EOL;
  18.     
  19. } else {
  20.     echo $file.PHP_EOL;
  21. }
  22. echo '初期値に復元'.PHP_EOL;
  23. // エラー設定を元に戻す
  24. set_error_handler($error_handlers);
  25. $file = @file_get_contents('dummy.txt');
  26. if ($file === false) {
  27.     echo 'ファイルが存在しません'.PHP_EOL;
  28.     
  29. } else {
  30.     echo $file.PHP_EOL;
  31. }




狙い通り、最初の処理だけ_hook_exception_handlerが実行されないようになりました。


$ php sample.php
ファイルが存在しません
初期値に復元
file_get_contents(dummy.txt): failed to open stream: No such file or directory




これ、半日ぐらいはまりましたよ...



【参考URL】

set_error_handler


テーマ:プログラミング - ジャンル:コンピュータ

  1. 2016/12/04(日) 16:55:56|
  2. PHP
  3. | トラックバック:0
  4. | コメント:0
  5. | 編集

CodeIgniter 大量データの更新を高速に行う方法

CodeIgniterで大量データを一括登録する方法について調べました。
CodeIgniterで大量データを高速に登録する方法の検討

今回は一括更新の方法について調べてみます。


下準備



CodeIgniter 3.1.0、MariaDB 10.0.26でテストしてみます。

データは前回同様郵便番号の情報を使用します。
http://www.post.japanpost.jp/zipcode/dl/kogaki-zip.html

更新するにあたり、郵便番号をプライマリキーとして使用したかったので、
こんなPythonスクリプトで重複のない住所データを作成しました。


  1. # -*- coding:utf-8 *-
  2. import codecs
  3. def sunit(s, zip_codes):
  4.     
  5.     s = s.strip()
  6.     data = [s.strip('"') for s in line.split(u',')]
  7.     zip_code = data[2]
  8.     if (zip_code in zip_codes):
  9.         return None
  10.     
  11.     zip_codes.add(zip_code)
  12.     address = data[6] + data[7] + data[8]
  13.     
  14.     return '%s,%s\n' % (zip_code, address)
  15. kenall = codecs.open('KEN_ALL.CSV', 'r', 'ms932')
  16. out = codecs.open('all.csv', 'w', 'utf-8')
  17. zip_codes = set()
  18. with kenall, out:
  19.     for line in kenall:
  20.         data = sunit(line, zip_codes)
  21.         if not data:
  22.             continue
  23.         out.write(data)




119,777件のテストデータとなりました。
以下のテーブルを作成し、事前にデータの登録を行っておきます。


  1. create table zip (
  2.     zip_code char(7) not null primary key,
  3.     address varchar(100) not null
  4. );







通常の更新



1件ずつ郵便番号をキーに住所を更新するプログラムはこうなりました。

・application/controllers/Test.php


  1. <?php
  2. class Test extends CI_Controller {
  3.     public function index() {
  4.         
  5.         $this->load->database();
  6.         
  7.         // csvファイル読み込み準備
  8.         $csv = new SplFileObject('/var/dev/php/all.csv', 'r');
  9.         $csv->setFlags(SplFileObject::READ_CSV);
  10.         
  11.         
  12.         // 時間計測開始
  13.         $start = microtime(true);
  14.         
  15.         $rows = [];
  16.         
  17.         foreach ($csv as $row) {
  18.             if ($row === [null]) {
  19.                 break;
  20.             }
  21.             
  22.             $this->db->where('zip_code', $row[0]);
  23.             $this->db->update(
  24.                 'zip',
  25.                 ['address' => '変更1'.$row[1]]
  26.             );
  27.         }
  28.         
  29.         $stop = microtime(true);
  30.         echo sprintf("処理時間:%.3f 秒\n", ($stop - $start));
  31.         
  32.     }
  33. }



実行してみると、3,947秒でした。


$ php index.php test
処理時間:3947.239 秒



テストは仮想環境で行っているため、ディスクIOが多く発生すると
性能が低下するようです。





一括コミット



コードはほぼそのままに、ループ全体にトランザクションをかけて
一括更新するようにしてみます。


  1. <?php
  2. class Test extends CI_Controller {
  3.     public function index() {
  4.         
  5.         $this->load->database();
  6.         
  7.         // csvファイル読み込み準備
  8.         $csv = new SplFileObject('/var/dev/php/all.csv', 'r');
  9.         $csv->setFlags(SplFileObject::READ_CSV);
  10.         
  11.         
  12.         // 時間計測開始
  13.         $start = microtime(true);
  14.         
  15.         // トランザクションをかけて一括更新
  16.         $this->db->trans_start();
  17.         foreach ($csv as $row) {
  18.             if ($row === [null]) {
  19.                 break;
  20.             }
  21.             
  22.             $this->db->where('zip_code', $row[0]);
  23.             $this->db->update(
  24.                 'zip',
  25.                 ['address' => '変更2'.$row[1]]
  26.             );
  27.         }
  28.         
  29.         $this->db->trans_complete();
  30.         
  31.         $stop = microtime(true);
  32.         echo sprintf("処理時間:%.3f 秒\n", ($stop - $start));
  33.         
  34.     }
  35. }



実行してみると20秒程度。
劇的に性能が向上しました。


$ php index.php test
処理時間:20.908 秒







update_batch



CodeIgniterのドキュメントを見てみると、update_batchという
メソッドが用意されていました。
Query Builder Class

どうやってupdate文をバッチ処理するんだろう?と思い、
実行するクエリを調べてみると、このようにcaseで設定する値を
変更していました。なるほど。


  1. UPDATE `zip` SET `address` = CASE
  2. WHEN `zip_code` = '0600000' THEN '変更3北海道札幌市中央区以下に掲載がない場合'
  3. WHEN `zip_code` = '0640941' THEN '変更3北海道札幌市中央区旭ケ丘'
  4. WHEN `zip_code` = '0600041' THEN '変更3北海道札幌市中央区大通東'
  5. ELSE `address` END
  6. WHERE `zip_code` IN('0600000','0640941','0600041')




update_batchを使用した処理に変更してみます。


  1. <?php
  2. class Test extends CI_Controller {
  3.     public function index() {
  4.         
  5.         $this->load->database();
  6.         
  7.         // csvファイル読み込み準備
  8.         $csv = new SplFileObject('/var/dev/php/all.csv', 'r');
  9.         $csv->setFlags(SplFileObject::READ_CSV);
  10.         
  11.         
  12.         // 時間計測開始
  13.         $start = microtime(true);
  14.         
  15.         $rows = [];
  16.         
  17.         foreach ($csv as $row) {
  18.             if ($row === [null]) {
  19.                 break;
  20.             }
  21.             
  22.             $rows[] = [
  23.                 'zip_code' => $row[0],
  24.                 'address' => '変更3'.$row[1],
  25.             ];
  26.             
  27.         }
  28.         
  29.         // update_batchで一括処理
  30.         // 第三引数に更新のキーとなる値を指定する
  31.         $this->db->update_batch('zip', $rows, 'zip_code');
  32.         
  33.         $stop = microtime(true);
  34.         echo sprintf("処理時間:%.3f 秒\n", ($stop - $start));
  35.         
  36.     }
  37. }




8秒で処理が終了しました。


$ php index.php test
処理時間:8.339 秒




update_batchは第四引数で一括処理するレコード数を指定できます。(デフォルト100)
1,000件、10,000件と一括処理する件数を変更して試してみます。


  1. <?php
  2. class Test extends CI_Controller {
  3.     public function index() {
  4.         
  5.         $this->load->database();
  6.         
  7.         // csvファイル読み込み準備
  8.         $csv = new SplFileObject('/var/dev/php/all.csv', 'r');
  9.         $csv->setFlags(SplFileObject::READ_CSV);
  10.         
  11.         
  12.         // 時間計測開始
  13.         $start = microtime(true);
  14.         
  15.         $rows = [];
  16.         
  17.         foreach ($csv as $row) {
  18.             if ($row === [null]) {
  19.                 break;
  20.             }
  21.             
  22.             $rows[] = [
  23.                 'zip_code' => $row[0],
  24.                 'address' => '変更3'.$row[1],
  25.             ];
  26.             
  27.         }
  28.         
  29.         // update_batchで一括処理
  30.         // 第三引数に更新のキーとなる値を指定する
  31.         // 第四引数で一括処理するレコード数を指定
  32.         $this->db->update_batch('zip', $rows, 'zip_code', 1000);
  33.         
  34.         $stop = microtime(true);
  35.         echo sprintf("処理時間:%.3f 秒\n", ($stop - $start));
  36.         
  37.     }
  38. }




1,000件一括更新

$ php index.php test
処理時間:9.253 秒



10,000件一括更新

$ php index.php test
処理時間:49.161 秒




クエリーの生成やデータベースサーバー側での解析に時間がかかるようで、
性能はデフォルトの100件の時と変わりませんでした。





update_batch + トランザクション



トランザクションをかけた状態で、update_batchを実行してみます。


  1. <?php
  2. class Test extends CI_Controller {
  3.     public function index() {
  4.         
  5.         $this->load->database();
  6.         
  7.         // csvファイル読み込み準備
  8.         $csv = new SplFileObject('/var/dev/php/all.csv', 'r');
  9.         $csv->setFlags(SplFileObject::READ_CSV);
  10.         
  11.         
  12.         // 時間計測開始
  13.         $start = microtime(true);
  14.         
  15.         $rows = [];
  16.         
  17.         foreach ($csv as $row) {
  18.             if ($row === [null]) {
  19.                 break;
  20.             }
  21.             
  22.             $rows[] = [
  23.                 'zip_code' => $row[0],
  24.                 'address' => '変更5'.$row[1],
  25.             ];
  26.             
  27.         }
  28.         
  29.         // トランザクションをかけて一括更新
  30.         $this->db->trans_start();
  31.         
  32.         // update_batchで一括処理
  33.         // 第三引数に更新のキーとなる値を指定する
  34.         // 第四引数で一括処理するレコード数を指定
  35.         $this->db->update_batch('zip', $rows, 'zip_code', 100);
  36.         
  37.         $this->db->trans_complete();
  38.         
  39.         $stop = microtime(true);
  40.         echo sprintf("処理時間:%.3f 秒\n", ($stop - $start));
  41.         
  42.     }
  43. }




100件ずつ一括更新

$ php index.php test
処理時間:9.680 秒



1,000件ずつ一括更新

$ php index.php test
処理時間:9.636 秒



10,000件ずつ一括更新

$ php index.php test
処理時間:51.278 秒




case文の解釈に時間がかかるのでしょうか、トランザクションの有無による
実行時間の差はほとんどありませんでした。




まとめ



各パターン5回実行した時の結果と平均をまとめています。数字はすべて秒です。
通常パターンはあまりに時間がかかるので1回しか測定していません。

内容1回目2回目3回目4回目5回目平均
単件update3947.239----3947.239
一括update20.90819.88719.65220.3619.31520.024
update_batch(100)8.3398.3688.2088.5998.8248.468
update_batch(1,000)9.2538.6118.5368.9688.4518.764
update_batch(10,00)49.16152.61449.04149.27548.85849.79
update_batch(100) + tran9.688.4468.2368.688.3978.688
update_batch(1,000) + tran9.6368.8099.6919.2498.4639.17
update_batch(10,00) + tran51.27850.61448.50550.18549.26449.969


update_batchデフォルトの100件ずつ、トランザクションなしで実行するのが一番速そうです。




テーマ:プログラミング - ジャンル:コンピュータ

  1. 2016/08/06(土) 17:07:19|
  2. PHP
  3. | トラックバック:0
  4. | コメント:0
  5. | 編集

CodeIgniterで大量データを高速に登録する方法の検討

PHPのwebフレームワークCodeIgniter
http://www.codeigniter.com/

このフレームワークを使用しての大量データ投入を
高速に行う方法を考えてみます。

使用したCodeIgniterのバージョンは3.1.0
PHPのバージョンは5.6.24です。


サンプルデータ



郵便番号データ123,929件を登録してみます。
http://www.post.japanpost.jp/zipcode/dl/kogaki-zip.html

こちらの全国一括版を使用しました。
Pythonで文字コードをutf-8に変換し、郵便番号と住所だのcsvファイルに加工しておきます。


  1. # -*- coding:utf-8 *-
  2. def sunit(s):
  3.     
  4.     s = s.strip()
  5.     data = [s.strip('"') for s in line.split(u',')]
  6.     zip_code = data[2]
  7.     address = data[6] + data[7] + data[8]
  8.     
  9.     return '%s,%s\n' % (zip_code, address)
  10. kenall = codecs.open('KEN_ALL.CSV', 'r', 'ms932')
  11. out = codecs.open('all.csv', 'w', 'utf-8')
  12. with kenall, out:
  13.     for line in kenall:
  14.         out.write(sunit(line))




登録するデータベースにはMariaDB 10.0.26を使用します。
Debian 8.5にインストールしました。


郵便番号を格納するテーブルの定義は以下のとおりです。


  1. create table zip (
  2.     zip_code char(7) not null,
  3.     address varchar(100) not null
  4. );






csvファイルの読み込み



csvファイルを読み込むだけの処理はこんな感じになります。

・application/controllers/Test.php


  1. <?php
  2. class Test extends CI_Controller {
  3.     public function index() {
  4.         
  5.         $this->load->database();
  6.         
  7.         // 全件削除
  8.         $this->db->empty_table('zip');
  9.         
  10.         // csvファイル読み込み準備
  11.         $csv = new SplFileObject('/var/dev/php/all.csv', 'r');
  12.         $csv->setFlags(SplFileObject::READ_CSV);
  13.         
  14.         
  15.         // 時間計測開始
  16.         $start = microtime(true);
  17.         
  18.         foreach ($csv as $row) {
  19.             if ($row === [null]) {
  20.                 break;
  21.             }
  22.         }
  23.         
  24.         $stop = microtime(true);
  25.         echo sprintf("処理時間:%.3f 秒\n", ($stop - $start));
  26.         
  27.     }
  28. }




コマンドでプログラムを実行します。


$ php index.php test




時間を計測すると、0.3226秒でした。





単件インサート



1件づつ登録するサンプルです。


  1. <?php
  2. class Test extends CI_Controller {
  3.     public function index() {
  4.         
  5.         $this->load->database();
  6.         
  7.         // 全件削除
  8.         $this->db->empty_table('zip');
  9.         
  10.         // csvファイル読み込み準備
  11.         $csv = new SplFileObject('/var/dev/php/all.csv', 'r');
  12.         $csv->setFlags(SplFileObject::READ_CSV);
  13.         $csv->setFlags($csv->getFlags() | SplFileObject::SKIP_EMPTY);
  14.         $csv->setFlags($csv->getFlags() | SplFileObject::DROP_NEW_LINE);
  15.         
  16.         
  17.         // 時間計測開始
  18.         $start = microtime(true);
  19.         
  20.         foreach ($csv as $row) {
  21.             $this->db->insert('zip', [
  22.                 'zip_code' => $row[0],
  23.                 'address' => $row[1],
  24.             ]);
  25.         }
  26.         
  27.         $stop = microtime(true);
  28.         echo sprintf("処理時間:%.3f 秒\n", ($stop - $start));
  29.         
  30.     }
  31. }




結果は4074秒となりました。
なんだか怪しい測定結果ですが、かなり待たされたのは間違いないです。





insert_batch



insert_batchを使用して100件毎に一括登録してみます。


  1. <?php
  2. class Test extends CI_Controller {
  3.     public function index() {
  4.         
  5.         $this->load->database();
  6.         
  7.         // 全件削除
  8.         $this->db->empty_table('zip');
  9.         
  10.         // csvファイル読み込み準備
  11.         $csv = new SplFileObject('/var/dev/php/all.csv', 'r');
  12.         $csv->setFlags(SplFileObject::READ_CSV);
  13.         
  14.         
  15.         // 時間計測開始
  16.         $start = microtime(true);
  17.         
  18.         $rows = [];
  19.         
  20.         foreach ($csv as $row) {
  21.             if ($row === [null]) {
  22.                 break;
  23.             }
  24.             
  25.             $rows[] = [
  26.                 'zip_code' => $row[0],
  27.                 'address' => $row[1],
  28.             ];
  29.             
  30.             if (count($rows) >= 100) {
  31.                 $this->db->insert_batch('zip', $rows);
  32.                 $rows = [];
  33.             }
  34.         }
  35.         
  36.         if (!empty($rows)) {
  37.             $this->db->insert_batch('zip', $rows);
  38.         }
  39.         
  40.         $stop = microtime(true);
  41.         echo sprintf("処理時間:%.3f 秒\n", ($stop - $start));
  42.         
  43.     }
  44. }




結果は40秒程度。
格段に性能が向上しました。


合わせて、一括登録する件数を1,000件、10,000件と増やしてみます。
insert_batchはデフォルトで100件毎に分割してinsertするので、
一括登録する件数を引数で明示してやります。
若干処理を変更しました。


  1. <?php
  2. class Test extends CI_Controller {
  3.     public function index() {
  4.         
  5.         $this->load->database();
  6.         
  7.         // 全件削除
  8.         $this->db->empty_table('zip');
  9.         
  10.         // csvファイル読み込み準備
  11.         $csv = new SplFileObject('/var/dev/php/all.csv', 'r');
  12.         $csv->setFlags(SplFileObject::READ_CSV);
  13.         
  14.         
  15.         // 時間計測開始
  16.         $start = microtime(true);
  17.         
  18.         $rows = [];
  19.         
  20.         foreach ($csv as $row) {
  21.             if ($row === [null]) {
  22.                 break;
  23.             }
  24.             
  25.             $rows[] = [
  26.                 'zip_code' => $row[0],
  27.                 'address' => $row[1],
  28.             ];
  29.         }
  30.         
  31.         // 1,000件区切りでインサート
  32.         $this->db->insert_batch('zip', $rows, null, 1000);
  33.         
  34.         $stop = microtime(true);
  35.         echo sprintf("処理時間:%.3f 秒\n", ($stop - $start));
  36.         
  37.     }
  38. }




1,000件毎インサートで6.7秒
10,000件毎インサートで2.8秒程度でした。
格段に性能が向上しています。





トランザクション



トランザクションをかけて、自動コミットを抑止してみます。


  1. <?php
  2. class Test extends CI_Controller {
  3.     public function index() {
  4.         
  5.         $this->load->database();
  6.         
  7.         // 全件削除
  8.         $this->db->empty_table('zip');
  9.         
  10.         // csvファイル読み込み準備
  11.         $csv = new SplFileObject('/var/dev/php/all.csv', 'r');
  12.         $csv->setFlags(SplFileObject::READ_CSV);
  13.         
  14.         
  15.         // 時間計測開始
  16.         $start = microtime(true);
  17.         
  18.         $rows = [];
  19.         
  20.         foreach ($csv as $row) {
  21.             if ($row === [null]) {
  22.                 break;
  23.             }
  24.             
  25.             $rows[] = [
  26.                 'zip_code' => $row[0],
  27.                 'address' => $row[1],
  28.             ];
  29.         }
  30.         
  31.         // トランザクションをかけて一括更新
  32.         $this->db->trans_start();
  33.         $this->db->insert_batch('zip', $rows, null, 100);
  34.         $this->db->trans_complete();
  35.         
  36.         $stop = microtime(true);
  37.         echo sprintf("処理時間:%.3f 秒\n", ($stop - $start));
  38.         
  39.     }
  40. }




100件毎が4.9秒
1,000件毎が2.4秒
10,000件毎が2.2秒
という結果になりました。





まとめ



結果をまとめると以下のようになります。

5回試行して平均を求めています。(単件登録はあまりに時間がかかるので測定は1回でやめました)
単位はすべて秒です。

内容1回目2回目3回目4回目5回目平均
ループのみ0.3260.3230.3210.3240.3190.323
単件insert4074.385----4074.385
insert_batch(100)41.09640.55241.67940.43940.33640.82
insert_batch(1,000)6.5716.5716.7566.6497.0346.716
insert_batch(10,000)2.6982.9252.8312.6982.7062.772
insert_batch(100)+tran4.7945.0064.8375.0495.0294.943
insert_batch(1,000)+tran2.1432.3632.5342.4342.6032.415
insert_batch(10,000)+tran2.1182.2282.3062.2222.2192.219


・可能な限り1回のbatch_size(登録件数)を増やす。
・ロールバックする気がなくてもトランザクションをかける。

この2つでかなりの高速化が可能です。

テーマ:プログラミング - ジャンル:コンピュータ

  1. 2016/08/05(金) 01:12:39|
  2. PHP
  3. | トラックバック:0
  4. | コメント:0
  5. | 編集

PHP pharに同梱したプログラムにautoloadを適用する

PHPのソースファイルをpharにまとめる方法を調べました。
PHP pharで複数のソースファイルを1つのファイルにまとめる

また、autoloadを自前で実装する方法を調べました。
PHP spl_autoload_registerでautoloadを自前で実装


これらを組み合わせて、pharで固めたソースファイルに対し、
autoloadが適用されるようにしてみます。



サンプルプログラム



pharにするサンプルプログラムは、
PHP spl_autoload_registerでautoloadを自前で実装
こちらで作成したソースにしました。

702_02.png


・classes/Fuga.php


  1. <?php
  2. // classes/Fuga.php
  3. class Fuga {
  4.     
  5.     public function call() {
  6.         echo 'Fuga!'.PHP_EOL;
  7.     }
  8.     
  9. }




・classes/Hoge.php


  1. <?php
  2. // classes/Hoge.php
  3. class Hoge {
  4.     
  5.     public function call() {
  6.         echo 'Hoge!'.PHP_EOL;
  7.     }
  8.     
  9. }




・classes/SomeOne.php


  1. <?php
  2. // classes/SomeOne.php
  3. class SomeBody {
  4.     
  5.     public function call() {
  6.         echo 'SomeBody!'.PHP_EOL;
  7.     }
  8.     
  9. }




・classes/SpecialPiyo.php


  1. <?php
  2. // classes/SpecialPiyo.php
  3. class Piyo {
  4.     
  5.     public function call() {
  6.         echo 'Piyo!'.PHP_EOL;
  7.     }
  8.     
  9. }





目標は
・これらのソースを「my.phar」というファイルにまとめる。
・require_once('phar://my.phar')するだけで、すべての同梱されているクラスがnewできるようにする





pharの作成



こちらを見ながらpharの作成を思い出してみます。
PHP pharで複数のソースファイルを1つのファイルにまとめる


単純にpharにするだけなら、このようなプログラムを用意すればOKです。

・makePhar.php


  1. <?php
  2. // my.pharという名前でpharを作成
  3. $phar = new Phar('my.phar');
  4. // ファイルを追加
  5. $phar->addFile('classes/Fuga.php');
  6. $phar->addFile('classes/Hoge.php');
  7. $phar->addFile('classes/SomeOne.php');
  8. $phar->addFile('classes/SpecialPiyo.php');




実行時に指定するiniファイル
・phar_make_php.ini


[Phar]

phar.readonly = Off
phar.require_hash = Off




以下のコマンドを実行して、pharを作成します。


$ php -c phar_make_php.ini makePear.php




出来上がったmy.pharが使えるかテスト

・sample.php


  1. <?php
  2. require_once('phar://my.phar/classes/Hoge.php');
  3. $hoge = new Hoge();
  4. $hoge->call();




ここまではうまく行きました。


$ php sample.php
Hoge!








pharをrequireした時に呼び出される関数



こちらが参考になりました。
Pharに自作スタブをセットする

・何も指定しないとデフォルトのスタブが設定される
・setStubで独自の内容に書き換えられる

動作を確認してみます。
先ほどのpharを作成するプログラムを変更。


・makePhar.php


  1. <?php
  2. // my.pharという名前でpharを作成
  3. $phar = new Phar('my.phar');
  4. // ファイルを追加
  5. $phar->addFile('classes/Fuga.php');
  6. $phar->addFile('classes/Hoge.php');
  7. $phar->addFile('classes/SomeOne.php');
  8. $phar->addFile('classes/SpecialPiyo.php');
  9. // 独自のstubを設定
  10. $phar->setStub('<?php
  11. echo "stubが呼び出されました。\n";
  12. __HALT_COMPILER();
  13. ');




pharを再作成


$ php -c phar_make_php.ini makePear.php




pharファイルをrequireしてみます。

・sample.php


  1. <?php
  2. require_once('phar://my.phar');





実行すると、setStubしたソースが実行されます。


$ php sample.php
stubが呼び出されました。




setStubにautoloadの設定プログラムを記載すれば、

・require_once('phar://my.phar')するだけで、すべての同梱されているクラスがnewできるようにする

という目標が達成できますね。


ここで作成したローダーをstubに指定します。
PHP spl_autoload_registerでautoloadを自前で実装


・makePear.php


  1. <?php
  2. // my.pharという名前でpharを作成
  3. $phar = new Phar('my.phar');
  4. // ファイルを追加
  5. $phar->addFile('classes/Fuga.php');
  6. $phar->addFile('classes/Hoge.php');
  7. $phar->addFile('classes/SomeOne.php');
  8. $phar->addFile('classes/SpecialPiyo.php');
  9. // 独自のstubを設定
  10. $phar->setStub('<?php
  11. class MyClassLoader {
  12.     private static $_classMap = null;
  13.     
  14.     public static function initialize() {
  15.         // コストの高い対応表の作成処理
  16.         self::$_classMap = array(
  17.             "Hoge" => "Hoge.php",
  18.             "Fuga" => "Fuga.php",
  19.             "Piyo" => "SpecialPiyo.php",
  20.             "SomeBody" => "SomeOne.php",
  21.         );
  22.     }
  23.     
  24.     // spl_autoload_registerに登録する関数
  25.     public static function loadClass($className) {
  26.         echo "load:".$className.PHP_EOL;
  27.         
  28.         $fileName = self::$_classMap[$className];
  29.         
  30.         // ファイルの存在チェック
  31.         $path = "phar://" . __FILE__ . "/classes/".$fileName;
  32.         if (!is_file($path)) {
  33.             echo "load:".$className." error".PHP_EOL;
  34.             return;
  35.         }
  36.         require_once($path);
  37.     }
  38.     
  39. }
  40. // 事前に対応表を作成
  41. MyClassLoader::initialize();
  42. spl_autoload_register(array("MyClassLoader", "loadClass"));
  43. __HALT_COMPILER();
  44. ');




注意点として、ファイルの存在チェックはpharからのパスで行うことになります。
pharのファイル名は「__FILE__」で取得できるので、


  1. "phar://" . __FILE__ . "/classes/".$fileName;



のようにしてis_fileすればファイルの存在チェックが行えます。
pharを再作成してサンプルプログラムを実行。


・sample.php


  1. <?php
  2. require_once('phar://my.phar');
  3. $hoge = new Hoge();
  4. $hoge->call();




狙いの動作になりました。


$ php sample.php
load:Hoge
Hoge!







stubを別ファイルで準備する



setStubでプログラムのソースを文字列で指定するのは、
コードのハイライトも行われず作成が面倒です。

この部分だけ別ファイルに切り出しましょう。


・stub.php


  1. <?php
  2. class MyClassLoader {
  3.     private static $_classMap = null;
  4.     
  5.     public static function initialize() {
  6.         // コストの高い対応表の作成処理
  7.         self::$_classMap = array(
  8.             "Hoge" => "Hoge.php",
  9.             "Fuga" => "Fuga.php",
  10.             "Piyo" => "SpecialPiyo.php",
  11.             "SomeBody" => "SomeOne.php",
  12.         );
  13.     }
  14.     
  15.     // spl_autoload_registerに登録する関数
  16.     public static function loadClass($className) {
  17.         echo "load:".$className.PHP_EOL;
  18.         
  19.         $fileName = self::$_classMap[$className];
  20.         
  21.         // ファイルの存在チェック
  22.         $path = "phar://" . __FILE__ . "/classes/".$fileName;
  23.         if (!is_file($path)) {
  24.             echo "load:".$className." error".PHP_EOL;
  25.             return;
  26.         }
  27.         require_once($path);
  28.     }
  29.     
  30. }
  31. // 事前に対応表を作成
  32. MyClassLoader::initialize();
  33. spl_autoload_register(array("MyClassLoader", "loadClass"));
  34. __HALT_COMPILER();




pharを作る際は、file_get_contentsで取得してやるとOKです。

・makePear.php


  1. <?php
  2. // my.pharという名前でpharを作成
  3. $phar = new Phar('my.phar');
  4. // ファイルを追加
  5. $phar->addFile('classes/Fuga.php');
  6. $phar->addFile('classes/Hoge.php');
  7. $phar->addFile('classes/SomeOne.php');
  8. $phar->addFile('classes/SpecialPiyo.php');
  9. // 独自のstubを設定
  10. $phar->setStub(file_get_contents('stub.php'));



テーマ:プログラミング - ジャンル:コンピュータ

  1. 2016/08/03(水) 22:19:10|
  2. PHP
  3. | トラックバック:0
  4. | コメント:0
  5. | 編集

PHP spl_autoload_registerでautoloadを自前で実装

過去に作成されたこんなPHPのプログラムがあるとします。

702_01.png

・sample.php


  1. <?php
  2. require_once('classes/Hoge.php');
  3. require_once('classes/Fuga.php');
  4. // ... 以下、使いそうなクラスを事前にrequire_once
  5. // 呼び出し部分だけinclude.phpのようなファイル名で分けている場合もあり
  6. $hoge = new Hoge();
  7. $hoge->call();
  8. // 結局、Fugaは使わなかった...




・classes/Hoge.php


  1. <?php
  2. // classes/Hoge.php
  3. class Hoge {
  4.     
  5.     public function call() {
  6.         echo 'Hoge!'.PHP_EOL;
  7.     }
  8.     
  9. }




・classes/Fuga.php


  1. <?php
  2. // classes/Fuga.php
  3. class Fuga {
  4.     
  5.     public function call() {
  6.         echo 'Fuga!'.PHP_EOL;
  7.     }
  8.     
  9. }





autoloadなこの時代、なんとかならないか調べてみると
「spl_autoload_register」が解決してくれそうです。

PHP で、spl_autoload_register を使って、require_once 地獄を脱出しよう





spl_autoload_register



未知のクラスがnewされた時、spl_autoload_registerに登録しておいた関数が呼び出される。
呼び出しクラス名が引数で渡されるので、そのクラスが存在するファイルを
requireしてやればよいようです。

動作を確認するため、最初のsample.phpを変更してみます。


  1. <?php
  2. function myClassLoader($className) {
  3.     echo '$className:'.$className.PHP_EOL;    
  4. }
  5. spl_autoload_register('myClassLoader');
  6. /* 無名関数で登録してもOK
  7. spl_autoload_register(function ($className) {
  8.     echo $className.PHP_EOL;
  9. });
  10. */
  11. $hoge = new Hoge();
  12. $hoge->call();




実行してみると、「Hoge」というクラス名を引数に渡してくれていますね。
プログラムは実行エラーで終了します。


# php sample.php
$className:Hoge
PHP Fatal error: Class 'Hoge' not found in /var/dev/php/splblog/sample.php on line 15




HogeやFugaクラスの呼び出しに応じて、phpファイルをrequireするようにしてみます。


  1. <?php
  2. function myClassLoader($className) {
  3.     echo '$className:'.$className.PHP_EOL;
  4.     
  5.     // classes/[クラス名].phpファイルを読み込み
  6.     require_once('classes/'.$className.'.php');
  7. }
  8. spl_autoload_register('myClassLoader');
  9. $hoge = new Hoge();
  10. $hoge->call();




ちゃんとHogeクラスのロードと実行が行えました。


# php sample.php
$className:Hoge
Hoge!




Fugaクラスの呼び出しも試してみます。


  1. <?php
  2. function myClassLoader($className) {
  3.     echo '$className:'.$className.PHP_EOL;
  4.     
  5.     // classes/[クラス名].phpファイルを読み込み
  6.     require_once('classes/'.$className.'.php');
  7. }
  8. spl_autoload_register('myClassLoader');
  9. $hoge = new Hoge();
  10. $hoge->call();
  11. $fuga = new Fuga();
  12. $fuga->call();
  13. // 2回目のHoge呼び出し
  14. $hoge2 = new Hoge();
  15. $hoge2->call();




Fugaも呼び出せました。
また、一度呼び出しに成功したクラス名はキャッシュされ、2回めのnewの時は、
spl_autoload_registerで登録した関数の呼び出しは行われませんでした。


# php sample.php
$className:Hoge
Hoge!
$className:Fuga
Fuga!
Hoge!    # $className:Hogeのメッセージ無しに実行結果が表示される







例外クラス名への対応



例のように一律「クラス名.php」というルールでファイル名が生成されていれば良いのですが、
例えば「Piyo」クラスだけ「SpecialPiyo.php」だった場合の対応法を考えます。

・classes/SpecialPiyo.php


  1. <?php
  2. // classes/SpecialPiyo.php
  3. class Piyo {
  4.     
  5.     public function call() {
  6.         echo 'Piyo!'.PHP_EOL;
  7.     }
  8.     
  9. }





最初に考えられるのは「Piyo」の呼び出しの時だけ分岐する案。


  1. <?php
  2. function myClassLoader($className) {
  3.     echo '$className:'.$className.PHP_EOL;
  4.     
  5.     // classes/[クラス名].phpファイルを読み込み
  6.     // Piyoの時は例外
  7.     if ($className == 'Piyo') {
  8.         require_once('classes/SpecialPiyo.php');
  9.     } else {
  10.         require_once('classes/'.$className.'.php');
  11.     }
  12. }
  13. spl_autoload_register('myClassLoader');
  14. $hoge = new Hoge();
  15. $hoge->call();
  16. $piyo = new Piyo();
  17. $piyo->call();




実行結果


# php sample.php
$className:Hoge
Hoge!
$className:Piyo
Piyo!





その他の案は、2つのloaderを作るでしょうか。


  1. <?php
  2. // 通常ケース
  3. function myClassLoader($className) {
  4.     echo 'normal load:'.$className.PHP_EOL;
  5.     
  6.     // ファイルの存在チェック
  7.     $path = 'classes/'.$className.'.php';
  8.     if (!is_file($path)) {
  9.         echo 'normal load:'.$className.' error'.PHP_EOL;
  10.         return;
  11.     }
  12.     require_once($path);
  13. }
  14. // 例外ケース
  15. function mySpecialClassLoader($className) {
  16.     echo 'special load:'.$className.PHP_EOL;
  17.     
  18.     // ファイルの存在チェック
  19.     $path = 'classes/Special'.$className.'.php';
  20.     if (!is_file($path)) {
  21.         echo 'special load:'.$className.' error'.PHP_EOL;
  22.         return;
  23.     }
  24.     require_once($path);
  25. }
  26. // 2つの関数を登録
  27. spl_autoload_register('myClassLoader');
  28. spl_autoload_register('mySpecialClassLoader');
  29. $hoge = new Hoge();
  30. $hoge->call();
  31. $piyo = new Piyo();
  32. $piyo->call();




実行結果


# php sample.php
normal load:Hoge
Hoge!
normal load:Piyo
normal load:Piyo error
special load:Piyo
Piyo!







更に複雑な呼び出し



さらにこんなクラスが出てきました。

・classes/SomeOne.php


  1. <?php
  2. // classes/SomeOne.php
  3. class SomeBody {
  4.     
  5.     public function call() {
  6.         echo 'SomeBody!'.PHP_EOL;
  7.     }
  8.     
  9. }




クラス名とファイル名にある程度規則性があればよいのですが、
全く規則性がない場合は、クラス名ごとにマッピングすることになると思います。


  1. <?php
  2. function myClassLoader($className) {
  3.     
  4.     $classMap = array(
  5.         'Hoge' => 'Hoge.php',
  6.         'Fuga' => 'Fuga.php',
  7.         'Piyo' => 'SpecialPiyo.php',
  8.         'SomeBody' => 'SomeOne.php',
  9.     );
  10.     
  11.     echo 'load:'.$className.PHP_EOL;
  12.     
  13.     $fileName = $classMap[$className];
  14.     
  15.     // ファイルの存在チェック
  16.     $path = 'classes/'.$fileName;
  17.     if (!is_file($path)) {
  18.         echo 'load:'.$className.' error'.PHP_EOL;
  19.         return;
  20.     }
  21.     require_once($path);
  22. }
  23. // 関数を登録
  24. spl_autoload_register('myClassLoader');
  25. $hoge = new Hoge();
  26. $hoge->call();
  27. $piyo = new Piyo();
  28. $piyo->call();
  29. $body = new SomeBody();
  30. $body->call();





これは簡単な例なので良いですが、「クラス名」と「実体のパス」の対応表を作るのに
それなりにコストがかかる場合は、staticにしたいと思うのが人情だと思います。

spl_autoload_registerにはクラスメソッドも登録できるので、
クラスメソッド登録方式に変更してみます。


  1. <?php
  2. class MyClassLoader {
  3.     private static $_classMap = null;
  4.     
  5.     public static function initialize() {
  6.         // コストの高い対応表の作成処理
  7.         self::$_classMap = array(
  8.             'Hoge' => 'Hoge.php',
  9.             'Fuga' => 'Fuga.php',
  10.             'Piyo' => 'SpecialPiyo.php',
  11.             'SomeBody' => 'SomeOne.php',
  12.         );
  13.     }
  14.     
  15.     // spl_autoload_registerに登録する関数
  16.     public static function loadClass($className) {
  17.         echo 'load:'.$className.PHP_EOL;
  18.         
  19.         $fileName = self::$_classMap[$className];
  20.         
  21.         // ファイルの存在チェック
  22.         $path = 'classes/'.$fileName;
  23.         if (!is_file($path)) {
  24.             echo 'load:'.$className.' error'.PHP_EOL;
  25.             return;
  26.         }
  27.         require_once($path);
  28.     }
  29.     
  30. }
  31. // 事前に対応表を作成
  32. MyClassLoader::initialize();
  33. // 関数を登録 array('クラス名', 'メソッド名')が引数
  34. spl_autoload_register(array('MyClassLoader', 'loadClass'));
  35. $hoge = new Hoge();
  36. $hoge->call();
  37. $piyo = new Piyo();
  38. $piyo->call();
  39. $body = new SomeBody();
  40. $body->call();




うまく行きました。


# php sample.php
load:Hoge
Hoge!
load:Piyo
Piyo!
load:SomeBody
SomeBody!





これで昔から使用されているライブラリなどの呼び出しも楽ができそうです。


【参考URL】

PHP で、spl_autoload_register を使って、require_once 地獄を脱出しよう

spl_autoload_register

テーマ:プログラミング - ジャンル:コンピュータ

  1. 2016/08/02(火) 22:50:35|
  2. PHP
  3. | トラックバック:0
  4. | コメント:0
  5. | 編集
前のページ 次のページ