Oracle® Database

2日でPHP開発者ガイド

12c リリース1 (12.1)

B71348-02(原本部品番号:E18554-05)

2014年1月

Oracle Database 2 Day + PHP Developer's Guide, 12c Release 1 (12.1)

B71348-02

Copyright © 2010, 2014, Oracle and/or its affiliates.All rights reserved.

原本著者: Simon Watt

原本協力者: Christopher Jones、Simon Law、Glenn Stokol、Ligaya Turmelle、Johannes Schlüter

このソフトウェアおよび関連ドキュメントの使用と開示は、ライセンス契約の制約条件に従うものとし、知的財産に関する法律により保護されています。ライセンス契約で明示的に許諾されている場合もしくは法律によって認められている場合を除き、形式、手段に関係なく、いかなる部分も使用、複写、複製、翻訳、放送、修正、ライセンス供与、送信、配布、発表、実行、公開または表示することはできません。このソフトウェアのリバース・エンジニアリング、逆アセンブル、逆コンパイルは互換性のために法律によって規定されている場合を除き、禁止されています。

ここに記載された情報は予告なしに変更される場合があります。また、誤りが無いことの保証はいたしかねます。誤りを見つけた場合は、オラクル社までご連絡ください。

このソフトウェアまたは関連ドキュメントを、米国政府機関もしくは米国政府機関に代わってこのソフトウェアまたは関連ドキュメントをライセンスされた者に提供する場合は、次の通知が適用されます。

U.S. GOVERNMENT END USERS: Oracle programs, including any operating system, integrated software, any programs installed on the hardware, and/or documentation, delivered to U.S. Government end users are "commercial computer software" pursuant to the applicable Federal Acquisition Regulation and agency-specific supplemental regulations.As such, use, duplication, disclosure, modification, and adaptation of the programs, including any operating system, integrated software, any programs installed on the hardware, and/or documentation, shall be subject to license terms and license restrictions applicable to the programs.No other rights are granted to the U.S. Government.

このソフトウェアもしくはハードウェアは様々な情報管理アプリケーションでの一般的な使用のために開発されたものです。このソフトウェアもしくはハードウェアは、危険が伴うアプリケーション(人的傷害を発生させる可能性があるアプリケーションを含む)への用途を目的として開発されていません。このソフトウェアもしくはハードウェアを危険が伴うアプリケーションで使用する際、安全に使用するために、適切な安全装置、バックアップ、冗長性(redundancy)、その他の対策を講じることは使用者の責任となります。このソフトウェアもしくはハードウェアを危険が伴うアプリケーションで使用したことに起因して損害が発生しても、オラクル社およびその関連会社は一切の責任を負いかねます。

OracleおよびJavaはOracle Corporationおよびその関連企業の登録商標です。その他の名称は、それぞれの所有者の商標または登録商標です。

Intel、Intel Xeonは、Intel Corporationの商標または登録商標です。すべてのSPARCの商標はライセンスをもとに使用し、SPARC International, Inc.の商標または登録商標です。AMD、Opteron、AMDロゴ、AMD Opteronロゴは、Advanced Micro Devices, Inc.の商標または登録商標です。UNIXは、The Open Groupの登録商標です。

このソフトウェアまたはハードウェア、そしてドキュメントは、第三者のコンテンツ、製品、サービスへのアクセス、あるいはそれらに関する情報を提供することがあります。オラクル社およびその関連会社は、第三者のコンテンツ、製品、サービスに関して一切の責任を負わず、いかなる保証もいたしません。オラクル社およびその関連会社は、第三者のコンテンツ、製品、サービスへのアクセスまたは使用によって損失、費用、あるいは損害が発生しても一切の責任を負いかねます。

目次

タイトルおよび著作権情報

はじめに

- 対象読者

- ドキュメントのアクセシビリティについて

- 関連ドキュメント

- 表記規則

1 Oracle DatabaseでのPHPの導入

- 目的

- サンプル・アプリケーションの概要

- リソース

2 事前準備

- 必要なもの

- Oracle Databaseのインストール

- HRユーザーのロック解除

- データベース常駐の接続プール

- DRCPプールの起動

- Apache HTTPサーバーのインストール

- Windows XPでのApacheのインストール

- LinuxでのApacheのインストール

- Apacheインストールのテスト

- PHPのインストール

- WindowsでのPHPのインストール

- LinuxでのPHPのインストール

- WindowsおよびLinuxでのPHPインストール後のタスク

- PHPインストールのテスト

- phpinfo()によるPHP構成の確認

- OracleへのPHP接続のテスト

- NetBeans 7.0 IDEのインストール

- WindowsでのNetBeans IDEのインストール

- LinuxでのNetBeans IDEのインストール

- LinuxおよびWindowsでのNetBeansの構成

- NetBeansの使用

3 データベース・アクセス・クラスの作成

- 接続定数

- Dbクラスの作成

- PHP OCI8でSQLを実行する一般的な例

- Dbクラスを使用したSQLの実行

- Dbクラスのテスト

4 AnyCoアプリケーションの作成

- カスケード・スタイルシート

- セッションのためのアプリケーションのクラス

- PHPセッションを使用したステートフルWebエクスペリエンスの提供

- Pageクラスの追加

- アプリケーション・ログイン・ページ

5 従業員データのページ移動

- 従業員リストの作成

- 従業員リストの実行

6 REF CURSORを使用した備品レコードの表示

- PL/SQLパッケージとパッケージ本体の概要

- PL/SQLストアド・プロシージャの概要

- REF CURSORの概要

- 備品表

- PHPでのREF CURSORのコール

7 エラー処理

- データベース・エラー

- カスタム・エラー・メッセージの表示

8 問合せのパフォーマンスとプリフェッチ

- プリフェッチの概要

- 「Employee Report」ページ

- 備品レポートの実行

- REF CURSORのプリフェッチ

9 データの挿入

- 挿入フォームの作成

- 1件挿入フォームの実行

- ac_add_one.phpでのCSRFの例

10 複数データ値の挿入

- 複数挿入フォームの作成

- 複数挿入フォームの実行

11 JSONの使用とJPEGイメージの生成

- JSONを返す単純なWebサービスの作成

- JPEGイメージの作成

12 BLOBのアップロードと表示

- ロゴを格納する表の作成

- PHP OCI8でのイメージのアップロード

- ロゴのフェッチとイメージの作成

- ロゴの表示

13 アプリケーションのデータベース使用状況の監視

- メタデータの概要

- メタデータの表示

- メタデータのその他の使用

- メタデータと永続接続

14 グローバル・アプリケーションの構築

- OracleとPHP間の環境の確立

- 文字列の操作

- ユーザーのロケールの決定

- ロケール認識の開発

- HTMLページのエンコーディング

- HTMLページ用のページ・エンコーディングの指定

- PHPへのページ・エンコーディングの指定

- 翻訳のためのHTMLページのコンテンツの編成

- PHPの文字列

- 静的ファイル

- データベースのデータ

- ユーザーが考えているとおりの表記規則を使用したデータの表示

- Oracleの日付書式

- Oracleの数値書式

- Oracleの言語ソート

- Oracleのエラー・メッセージ

索引

はじめに

『Oracle Database 2日でPHP開発者ガイド』は、PHPを使用してOracle Databaseにアクセスする方法について説明した開発者向けのマニュアルです。

内容は次のとおりです。

- 対象読者

- ドキュメントのアクセシビリティについて

- 関連ドキュメント

- 表記規則

対象読者

『Oracle Database 2日でPHP開発者ガイド』では、PHPとOracle Databaseを使用したアプリケーション開発について説明します。

このマニュアルを使用するには、SQL、PL/SQLおよびPHPに関する大枠の知識が必要です。

ドキュメントのアクセシビリティについて

Oracleのアクセシビリティについての詳細情報は、Oracle Accessibility ProgramのWebサイト(http://www.oracle.com/pls/topic/lookup?ctx=acc&id=docacc)を参照してください。

Oracle Supportへのアクセス

Oracleサポート・サービスでは、My Oracle Supportを通して電子支援サービスを提供しています。詳細情報は(http://www.oracle.com/pls/topic/lookup?ctx=acc&id=info)か、聴覚に障害のあるお客様は(http://www.oracle.com/pls/topic/lookup?ctx=acc&id=trs)を参照してください。

関連ドキュメント

詳細は、次のOracleドキュメントを参照してください。

- 『Oracle Database Express Edition 2日で開発者ガイド』

- 『Oracle Database SQL言語リファレンス』

- 『Oracle Database PL/SQL言語リファレンス』

- 『SQL*Plusユーザーズ・ガイドおよびリファレンス』

- 『Oracle Databaseグローバリゼーション・サポート・ガイド』

表記規則

このマニュアルでは次の表記規則を使用します。

規則

意味

太字

太字は、操作に関連するGraphical User Interface要素、または本文中で定義されている用語および用語集に記載されている用語を示します。

イタリック体

イタリックは、ユーザーが特定の値を指定するプレースホルダ変数を示します。

固定幅フォント

固定幅フォントは、段落内のコマンド、URL、サンプル内のコード、画面に表示されるテキスト、または入力するテキストを示します。

1 Oracle DatabaseでのPHPの導入

PHPは、HTMLに埋め込むことができることから、特にWebの開発で有効な人気の高いスクリプト言語です。

この章のトピックは、次のとおりです。

- 目的

- サンプル・アプリケーションの概要

- リソース

目的

このマニュアルでは、PHPスクリプト言語とOracle Databaseを使用してWebアプリケーションを作成する方法について説明します。

サンプル・アプリケーションの概要

このマニュアルでは、AnyCo Corpという架空の企業が備品管理に使用するサンプル・アプリケーションの開発方法を説明します。このマニュアルではPHP言語とOracle DatabaseにアクセスするPHP OCI8拡張モジュールについて説明しますが、PHPフレームワークまたは抽象レイヤーは使用しません。しかし、フレームワークは一般に広く利用されており、アプリケーションを構築するときには使用を検討するとよいでしょう。これにはAnyCoアプリケーションへの手動実装が必要なタスクを実行する機能が含まれているほか、優れたアプリケーション設計パラダイムが提供されます。

AnyCoアプリケーションでは、サンプルHRスキーマ(Oracle Databaseに同梱)に含まれているEMPLOYEES表内の従業員データを使用します。このスキーマの詳細は、『Oracle Databaseサンプル・スキーマ』を参照してください。各従業員に支給された備品の詳細を格納するための表を、このアプリケーション用に新しく作成します。

図1-1に、サンプル・アプリケーションの概要を示します。

図1-1 サンプル・アプリケーションの概要

このアプリケーションには次の機能が含まれています。

- PHP OCI8拡張モジュールを使用して、データベースへの接続を確立します。Oracle接続プールを使用してアプリケーションをスケーラブルにする方法を示します。

- データベースに従業員データと備品データを問い合せます。

- データの表示およびデータ内のナビゲートを行います。

- PL/SQL REF CURSORを使用したフェッチなど、レコードの様々な挿入方法およびフェッチ方法を示します。

- SQL問合せからデータをフェッチするPHPの調整方法を示します。

- Webサービスの作成および使用方法を示します。

- イメージをアップロードおよび表示します。

- アプリケーションによるデータベース・リソースの使用状況を監視します。

リソース

次のOTN(Oracle Technology Network)のWebサイトでは、ユーザーにとって有効な追加情報が提供されています。

- Oracle Technology NetworkのOracle Databaseホーム・ページ
http://www.oracle.com/technetwork/database/database-technologies/express-edition/downloads/index.html

- Oracle Databaseドキュメント・ライブラリ
http://www.oracle.com/pls/xe112/homepage

- 無料のマニュアル、「Underground PHP and Oracle Manual」には、Oracle DatabaseでのPHPの使用に関する詳細が記載されています。
http://www.oracle.com/technetwork/topics/php/underground-php-oracle-manual-098250.html

- PHP開発者センター(リソースとPHPフォーラムを含む)
http://www.oracle.com/technetwork/topics/php/whatsnew/index.html

- PHP開発者向けのNetBeans IDE学習コース
http://netbeans.org/kb/trails/php.html

- Oracle Databaseドキュメント・ライブラリ
http://www.oracle.com/technetwork/indexes/documentation/index.html

- PHPのスケーラビリティおよび高可用性に関するホワイトペーパ
http://www.oracle.com/technetwork/topics/php/whatsnew/php-scalability-ha-twp-128842.pdf

- Oracle Databaseを使用したPHPのWeb監査、認可および監視
http://www.oracle.com/technetwork/articles/dsl/php-web-auditing-171451.html

- PHPオンライン・マニュアル
http://www.php.net/manual/en/

2 事前準備

この章では、Oracle DatabaseおよびPHP環境をインストールし、テストする方法について説明します。

この章のトピックは、次のとおりです。

- 必要なもの

- Oracle Databaseのインストール

- Apache HTTPサーバーのインストール

- Apacheインストールのテスト

- PHPのインストール

- PHPインストールのテスト

- NetBeans 7.0 IDEのインストール

必要なもの

Oracle DatabaseおよびPHP環境をインストールするには、次のものが必要です。

- Oracle Database

- Apache Webサーバー。Linuxの場合、通常これはパッケージ・リポジトリで入手できます。

- PHP 5.3。最近の一部のLinuxディストリビューションには、このバージョンがパッケージとして含まれています。

- PHPファイルを編集するためのテキスト・エディタ。デバッガを備えているNetBeans PHPエディションなどのコード・エディタが理想的ですが、必須ではありません。

Oracle Databaseのインストール

Oracle Databaseをコンピュータにインストールする必要があります。このチュートリアルで使用するサンプル・データは、デフォルトでインストールされます。これがサンプル・スキーマのHRコンポーネントとなります。

Windowsに64ビット版のPHPはないため、Windowsの場合は32ビット版のOracle Databaseをインストールします。Oracle Databaseのインストール後、正しいPATH環境変数が設定されるように再起動します。

この項で説明する項目は、次のとおりです。

- HRユーザーのロック解除

- データベース常駐の接続プール

- DRCPプールの起動

関連項目:

- HRサンプル・スキーマの詳細は、『Oracle Databaseサンプル・スキーマ』を参照してください。

- Oracle SQL DeveloperのWebページ
http://www.oracle.com/technetwork/developer-tools/sql-developer/overview/index.html

HRユーザーのロック解除

PHPアプリケーションは、HRユーザーとしてデータベースに接続します。HRアカウントを使用する前に、ロック解除が必要になる場合があります。SQL*PlusまはたSQL Developerを使用して、HRユーザーのロックを解除します。

この項で説明する項目は、次のとおりです。

- コマンドラインを使用したHRユーザーのロック解除

- SQL Developerを使用したHRユーザーのロック解除

コマンドラインを使用したHRユーザーのロック解除

次のコマンドラインを使用してHRユーザーのロックを解除します。

SQL> source /u01/app/oracle/product/12.1/bin/oracle_env.sh
SQL> sqlplus system/system_password
SQL> alter user hr identified by welcome account unlock;

ここで、system_passwordは、データベース構成時に入力したパスワードです。

SQL Developerを使用したHRユーザーのロック解除

SQL Developerを使用してHRユーザーのロックを解除する手順は、次のとおりです。

1. SQL Developerを起動し、Oracle Databaseへの接続をオープンします。

2. systemユーザーとしてOracle Databaseにログインします。

3. SQLワークシートまたはSQL*Plusを開き、次のSQL文を実行します。

alter user hr identified by welcome account unlock;

Oracle Databaseアカウントのロック解除の詳細は、『Oracle Database Express Edition 2日でデータベース管理者』の第7章「ユーザーおよびセキュリティの管理」を参照してください。

関連項目:

- Oracle Databaseのドキュメント
http://www.oracle.com/technetwork/indexes/documentation/index.html

データベース常駐の接続プール

AnyCoサンプル・アプリケーションでは、データベース常駐の接続プール(DRCP)を使用して、多数のユーザーをサポートするためのPHPアプリケーションの拡張方法を示します。

PHPがスレッド・セーフであるとの想定はできないため、PHPはApache Webサーバーのpre-forkモデルやFastCGIなどのマルチプロセス・モードで実行されるのが一般的です。中間層にはプロセス間通信が存在しないため、アクティブPHPプロセスとアイドルPHPプロセスとの間でOracle接続を共有することはできません。DRCPが機能するのは、共有処理がデータベース・ホスト・コンピュータで行われるためです。そのため、複数の中間層ホストによって作成された接続リソースを共有することも可能になります。

最大のパフォーマンスを得るため、一般にPHP OCI8アプリケーションでは永続データベース接続が使用されます。PHPがアプリケーション・スクリプトの実行を終了し、その出力をWebユーザーのブラウザに送信するとき、スクリプトの背後にあるDB接続はクローズされません。接続は、まだ実行中で現在はアイドルのPHP/Apacheプロセスにキャッシュされたままになります。このPHPプロセスによる接続の再利用は、同じデータベース資格証明を使用して接続する後続のPHPスクリプト内で可能です。これはパフォーマンス面で大きな利点があります。DRCPがなかったとすると、PHPプロセスが多数存在する場合に、オープン状態のデータベース接続によってデータベース・ホストのメモリーが大量に消費される恐れがあります。各Webページ・リクエスト間に発生するユーザーの思考時間や、PHPスクリプトが非データベース操作を実行している時間など、多くの接続がアイドル状態になっている場合でも、そのような状況に陥ります。

DRCPを使用することでPHPアプリケーションは、データベース・ホストのメモリーを効率的に使用して多数のWebユーザーをサポートできます。DRCPを使用することで、データベース・リソースの使用ユーザーを、現在データベース操作を実行しているWebユーザーに限定できます。ベンチマークでは、小規模の商用Linuxデータベース・ホストを使用した場合、DRCPで何万ものWebユーザーをサポートできることが確認されました。

図2-1 DRCPなしの場合、PHPからのアイドル永続接続によってデータベース・リソースが消費される

DRCPは、データベース・ホスト上でデータベース・サーバー・プロセスの小規模なプールを維持することで、そのホストのメモリー不足を克服します。これらは、すべてのPHPプロセスおよび中間層サーバーにおけるすべてのPHPデータベース接続で、必要に応じて共用できます。

図2-2 DRCPのアーキテクチャ

PHPスクリプトがデータベースに接続する際、使用可能なサーバー・プロセスがプールになければ、スクリプトはいずれかのプロセスが空くのを待機します。そのため、データベースが過負荷になるのを回避でき、アプリケーションも実行し続けることができます。

DRCPプールを起動すると、プールを使用するかどうかをアプリケーションで実行時に選択できるようになります。これはPHP OCI8接続文字列で指定します。通常は、存続時間が短く、似たような種類のタスクのみがDRCPを使用するようにします。バッチ・プロセスではプールは使用しないようにします。

DRCPプールは2通りの方法で使用できます。基本的な使用方法は、プロセスのみを再利用する方法です。もう1つの方法では各プロセスに関連付けられたセッション・メモリーも再利用するため、パフォーマンスがさらに向上します。PHPでは永続接続のみが後者の方法を使用します。PHPのように、各スクリプトが1つのアプリケーションの構成物であるWebアプリケーションでは、このセッション・メモリー共有で問題が発生することはほとんどありません。ただし、セッション設定(日付形式など)が予期せず保持されないよう、また、そうしたことから情報漏えいが生じないよう注意を払う必要があります。DRCPでは、プールを仮想的にサブ・パーティション可することにより、このような問題を軽減できます。

PHP OCI8アプリケーションでは、oci_pconnect()コールを使用して、永続データベース接続を作成します。oci_connect()またはoci_new_connect()を使用してOracleにアクセスすることもできます。この方法では非永続接続が作成されます。oci_new_connect()関数はコールされるたびに、トランザクション上の依存がない新規の接続リソースを返します。実行中のスクリプトが同じ接続資格証明を使用してoci_connect()関数またはoci_pconnect()関数を複数回コールした場合、これらの関数はそれぞれが同一のPHPリソースを返します。いずれの接続方法でも、各スクリプトの後に必要に応じてロールバックが実行されます。

これら3つの関数は、DRCPの使用有無によってもその動作に違いが生じます。

DRCPなしの場合、PHPスクリプトが完了しても永続接続はオープンの状態を維持します。同じ資格証明を使用して接続する後続のスクリプトは、その接続をすぐに再利用できます。これは速度は期待できますが、アイドル状態のプロセスも含め各PHPプロセスからの接続を維持し続ける必要があるため、データベース・ホストに十分なメモリーが必要になります。oci_connect()関数およびoci_new_connect()関数は、PHPスクリプトの終了後、その背後にあるデータベース接続を維持しません。この場合、接続確立までの時間が長くなりますが、データベース・ホストのメモリー使用の上限をアクティブのWebユーザー数に抑えることができます。

PHP接続でDRCPを使用する場合、DRCPプールにある確立済DRCPサーバー・プロセスを使用することで、3つのOCI8接続関数はいずれも恩恵を受けます。各スクリプトが終了したとき(さもなくば、それより前に)、データベース・プール・サーバーが、再利用に備えてDRCPプールに戻されます。DRCPブローカへの軽量接続が維持され、再接続のパフォーマンス向上を支援します。oci_pconnect()関数は、プロセス・セッション・メモリーを再利用するため、効率性と拡張性がさらに向上します。oci_connect()およびoci_new_connect()の各コールでは、再利用されるDRCPプール・プロセス内にOracleセッション・メモリーが再作成されます。

DRCPおよびPHPの詳細は、ホワイトペーパを参照してください。

http://www.oracle.com/technetwork/topics/php/whatsnew/php-scalability-ha-twp-128842.pdf

DRCPプールの起動

DRCPプールは、あらかじめ提供されているPL/SQL DBMS_CONNECTION_POOLパッケージを使用してSQL*Plusで管理できます。

Oracle Linuxでプールを起動するには、端末ウィンドウを開き、rootユーザーとして接続します。

# su -

次に、suでOracleアカウントに切り替えます。

# su - oracle
$ source /u01/app/oracle/product/12.1/bin/oracle_env.sh

SQL*PlusをSYSDBAシステム権限で実行し、DBMS_CONNECTION_POOL.START_POOL()プロシージャを起動します。

$ sqlplus / as sysdba
SQL> execute dbms_connection_pool.start_pool()

プールがデフォルト・パラメータを使用して実行されます。

プールを停止するには、次を実行します。

SQL> execute dbms_connection_pool.stop_pool()

データベースの再起動時にDRCPが実行中であった場合、プールは自動的に再起動されます。

プールのパラメータは、次のようにDBMS_CONNECTION_POOL.ALTER_PARAM()で変更できます。

SQL> execute dbms_connection_pool.alter_param(null, 'MAXSIZE', '10');

パラメータの変更後はプールを再起動する必要があります。

現在のプール設定は、Oracleのデータ・ディクショナリに問い合せることで表示できます。

select * from DBA_CPOOL_INFO;

DRCPプール全体の統計は、次で表示できます。

select * from V$CPOOL_STATS;

統計の監視を継続することで、プールのパラメータの調整方法を決定できます。

各DRCPデータベース接続では、任意の接続クラスを指定できます。PHPの接続クラスは、PHPのphp.ini初期化ファイルで構成するか、または実行時に設定します。「WindowsおよびLinuxでのPHPインストール後のタスク」を参照してください。接続クラスは、DRCPプールを様々なユース・ケースごとにパーティション化するときに役立ちます。

各接続クラスの統計を表示するには、次を使用します。

# select * from V$CPOOL_CC_STATS;

接続クラスを設定しなかった場合、DRCPプール・リソースでの共有利用が少なくなります。一般的なアプリケーションの場合、V$CPOOL_CC_STATSで、システム生成の接続クラス名が多数表示される場合は、各中間層サーバー上のPHP構成ファイルで接続クラスが正しく設定されていることを確認してください。

DRCPプールは、PHP、PerlおよびPythonで記述されたものを含め、すべての対応アプリケーション間で共有できます。SQL*Plusなど、一部のツールはDRCP対応ではありません。DRCP接続をSQL*Plusで使用した場合、V$CPOOL_CC_STATSでSHAREDというクラス名のエントリが表示されます。SQL*PlusはDRCPプールのプロセスを再利用しますが、各プロセスのセッション・メモリーを再作成する必要があります。

AnyCoアプリケーションを作成して実行すると、動作中のDRCPを監視ビューで確認できます。

Apache HTTPサーバーのインストール

Apache Webサーバーは、受信したユーザー・ページ・リクエストを処理し、PHPを起動してアプリケーションのHTMLマークアップを生成します。

この項で説明する項目は、次のとおりです。

- Windows XPでのApacheのインストール

- LinuxでのApacheのインストール

Windows XPでのApacheのインストール

PHP 5.3.6はWindowsのFastCGIモデルを使用してインストールします。Windows用のApache HTTPサーバーを入手するには、次の手順を実行します。

1. Webブラウザに次のURLを入力します。

http://httpd.apache.org/download.cgi

2. httpd-2.2.17-win32-x86-no_ssl.msiをクリックします。

3. ダウンロードしたファイルを一時ディレクトリ(c:\tmpなど)に保存し、ダブルクリックしてインストールします。
ソフトウェアが、C:\Program Files\Apache Software Foundation\Apache2.2などのディレクトリにインストールされます。

ファイル名および抽出ディレクトリは、現行バージョンに基づいて決定されます。この手順では、インストールしているバージョンのディレクトリ名を使用するようにしてください。

4. 次のURLからApache mod_fcgid FastCGIコンポーネントをダウンロードします。
http://httpd.apache.org/download.cgi#mod_fcgid

5. これを、インストールしたApache 2.2のディレクトリに解凍します。

6. C:\Program Files\Apache Software Foundation\Apache2.2\conf\httpd.confを編集し、次を追加します。

LoadModule fcgid_module modules/mod_fcgid.so

httpd.confで、htdocsの<Directory>セクションを探し、OptionsにExecCGIを追加します。

<Directory "C:/Program Files/Apache Software Foundation/Apache2.2/htdocs">
...
Options Indexes FollowSymLinks ExecCGI
...
</Directory>

「スタート」メニュー・オプションを使用して、Apacheを起動できます。コンソール・ウィンドウが開き、エラー・メッセージがあれば表示されます。エラー・メッセージはC:\Program Files\Apache Software Foundation\Apache2.2\logs\error.logにも書き込まれる場合があります。

ApacheMonitorユーティリティを使用してApacheを起動することもできます。Apacheをすべてのユーザーに対するサービスとしてインストールすることを選択した場合、システム・トレイにApacheがアイコンとして表示されます。

エラーが発生した場合は、httpd.confファイルを再確認します。

LinuxでのApacheのインストール

この項では、LinuxにApache HTTPサーバーをインストールする方法について説明します。

ファイル名および抽出ディレクトリは、現行バージョンに基づいて決定されます。この手順では、インストールしているバージョンのディレクトリ名を使用するようにしてください。

Apacheは、通常すでにLinuxにインストールされているか、パッケージ・リポジトリから直接入手できます。

この項で説明する項目は、次のとおりです。

- Oracle LinuxでのデフォルトHTTPDパッケージの使用

- LinuxでのApacheの手動インストール

- Linux上のApacheのためのOracle環境の設定

- Linuxでのサンプル・プロジェクトのためのユーザー・ディレクトリの設定

Oracle LinuxでのデフォルトHTTPDパッケージの使用

1. Oracle Linuxで、httpdパッケージを次のようにインストールします。

# yum install httpd

または

# up2date httpd

2. PHPを手動でコンパイルする(後述)場合、httpd-develパッケージもインストールします。

# yum install httpd-devel

または

# up2date httpd-devel

3. 次を使用して、rootとしてApacheを停止します。

# service httpd stop

4. Apacheを起動するには、次を実行します。

# service httpd start

LinuxでのApacheの手動インストール

この項では、LinuxにApache HTTPサーバーを手動でインストールする方法について説明します。ファイル名および抽出ディレクトリは、現行バージョンに基づいて決定されます。この手順では、インストールしているバージョンのディレクトリ名を使用するようにしてください。

Apache HTTPサーバーをインストールするには、次の手順を実行します。

1. httpd-2.2.17.tar.bz2など、httpdサーバーをapache.orgからダウンロードします。

2. httpd-2.2.17.tar.bz2ファイルをダウンロードしたディレクトリに移動します。

3. rootユーザーとしてログインし、次のコマンドを実行します。

# tar -jxvf httpd-2.2.17.tar.bz2
# cd httpd-2.2.17
# export ORACLE_HOME=/usr/lib/oracle/app/oracle/product/11.2.0/server
# ./configure \
--prefix=/usr/local/apache \
--enable-module=so
# make
# make install

--enable-module=soオプションは、PHPをDynamic Shared Object(DSO)としてコンパイルすることを許可します。--prefixオプションは、make installコマンドで使用されるApacheインストール・ディレクトリを設定します。

Apacheを権限のあるユーザーとしてインストールおよび実行しないようにするには、--prefixを$HOME/apacheなどのディレクトリに設定します。権限のないユーザーはデフォルト・ポート80を使用できないため、インストール完了後に、httpd.confを編集しListenパラメータをApacheがリスニングするポートに変更する必要があります。

Apacheは次のapachectlスクリプトで起動できます。

# /usr/local/apache/bin/apachectl start

次を使用してApacheを停止します。

# /usr/local/apache/bin/apachectl stop

Linux上のApacheのためのOracle環境の設定

PHP OCI8を適切に動作させるため、Apacheの起動前にOracle環境を正しく設定する必要があります。一般的には、$ORACLE_HOME/bin/oracle_env.shスクリプトで設定されるものと同じ変数を設定する必要があります。必要な環境変数はApacheの環境構成ファイルで設定できます。

デフォルトhttpdパッケージを使用したOracle Linuxでは、これは/etc/sysconfig/httpdです。前の項に記載されている方法で独自でApacheをインストールした場合、/usr/local/bin/envvarsです。このファイルを編集し、次の行を追加します。

export ORACLE_HOME=/u01/app/oracle/product/12.1
export LD_LIBRARY_PATH=$ORACLE_HOME/lib:$LD_LIBRARY_PATH

環境変数が適用されるように、Apacheを停止して再起動します。

Linuxでのサンプル・プロジェクトのためのユーザー・ディレクトリの設定

これから作成するPHPファイルはApacheがアクセスできるディレクトリに格納する必要があります。この場所の候補としては、Apacheのドキュメント・ルート・ディレクトリである/var/www/html (または、Apacheを手動でインストールした場合は/usr/local/apache/htdocs)が考えられます。より簡単な方法として、Apacheにホーム・ディレクトリのサブディレクトリへのアクセスを許可する方法もあります。

1. 通常のユーザーとしてログインし、作業ディレクトリを作成します。

mkdir $HOME/public_html

Apacheプロセスがファイルにアクセスできるようにする必要があります。たとえば、次を使用します。

chmod 755 $HOME $HOME/public_html

1. Oracle Linuxの場合は、/etc/httpd/conf/httpd.confを編集し、mod_userdir.cセクションを探します。これを次のように変更します。

<IfModule mod_userdir.c>
#
# UserDir is disabled by default since it can confirm the presence
# of a user name on the system (depending on home directory
# permissions).
#
#UserDir disable
#
# To enable requests to /~user/ to serve the user's public_html
# directory, remove the "UserDir disable" line above, and uncomment
# the following line instead:
#
UserDir public_html
</IfModule>

2. Apacheを手動でインストールした場合は、/usr/local/apache/conf/httpd.confを編集し、次の行を探します。

Include conf/extra/httpd-userdir.conf

先頭にシャープ記号(#)がある場合は、削除して非コメント化します。

3. Apacheを再起動します。
これにより、Webブラウザはユーザーの$HOME/public_htmlディレクトリからファイルを提供できるようになります。たとえば、'chris'としてログインして$HOME/public_htmlに作成したPHPファイルには、URL http://localhost/~chris/でアクセスできるようになります。

PHPプロジェクト・ファイルをpublic_htmlに作成することにした場合は、これより先、マニュアルに記載されているURLを読み替える必要があります。たとえば、マニュアルでhttp://localhost/を使用するように指定されている場合は、http://localhost/~user/を使用することになります。

Apacheインストールのテスト

Apache HTTPサーバーのインストールをテストするには、次の手順を実行します。

1. ApacheをインストールしたコンピュータでWebブラウザを起動します。

2. 次のURLを入力します。

http://localhost/

Webブラウザに、次のようなページが表示されます。

このページが表示されない場合は、Apache構成を確認します。一般的な問題としては、Apacheが実行されていない、Apacheがデフォルト以外のポートをリスニングしているなどの問題が考えられます。ポート番号は、Apacheの構成ファイル/etc/httpd/conf/httpd.conf (あるいは、/usr/local/apache/conf/httpd.confまたはC:\Program Files\Apache Software Foundation\Apache2.2\conf\httpd.conf)のListenパラメータで設定します。

サイトでデフォルト以外のポート番号を使用する場合は、これより先、マニュアルに記載されているURLを読み替える必要があります。たとえば、Apacheがポート8888をリスニングする場合は、マニュアルでhttp://localhost/を使用するように指定されている場合に、http://localhost:8888/を使用することになります。

PHPのインストール

このマニュアル内のアプリケーションでは、Oracle Database用のOCI8 1.4拡張モジュールが含まれたPHP 5.3を使用します。PHP 5.3およびOCI8 1.4の新機能が使用されています。PHPのGD拡張モジュールは、第11章「JSONの使用とJPEGイメージの生成」で使用されます。

この項で説明する項目は、次のとおりです。

- WindowsでのPHPのインストール

- LinuxでのPHPのインストール

- WindowsおよびLinuxでのPHPインストール後のタスク

WindowsでのPHPのインストール

この項では、WindowsにPHPをインストールする方法について説明します。

ファイル名および抽出ディレクトリは、現行バージョンに基づいて決定されます。この手順では、インストールしているバージョンのディレクトリ名を使用するようにしてください。

PHPをインストールするには、管理者ユーザーである必要があります。PHPをインストールするには、次の手順を実行します。

1. PHP 5.3.6 zipファイルを次のWebサイトからダウンロードします。
http://windows.php.net/download/

FastCGIモードでインストールするので、非スレッドセーフ・バンドルを使用します。

2. Windowsのエクスプローラで、PHP 5.3.6 zipファイルをダウンロードしたディレクトリに移動します。

3. C:\php-5.3.6というディレクトリにPHPパッケージを解凍します。

4. php.ini-developmentをC:\php-5.3.6\php.iniにコピーします。

5. php.iniを編集して次の変更を加えます。

- 行extension_dir = "C:\php-5.3.6\ext"を追加します。
これはPHP拡張モジュールが含まれているディレクトリです。

- 行頭のセミコロンを削除します。
extension=php_oci8_11g.dll

6. C:\Program Files\Apache Software Foundation\Apache2.2\conf\httpd.confを編集し、次の行を追加します。必ず、バックスラッシュ(\)ではなくスラッシュ(/)を使用してください。

FcgidInitialEnv PHPRC "c:/php-5.3.6"
AddHandler fcgid-script .php
FcgidWrapper "c:/php-5.3.6/php-cgi.exe" .php

mod_fcgid.soがロードされ、項「Windows XPでのApacheのインストール」の説明のとおり、ExecCGIオプションが設定されていることを確認します。

7. Apacheサーバーを再起動すると、PHPインストールのテストが可能になります。
エラーが発生した場合は、httpd.confおよびphp.iniファイルを再確認します。PATH環境変数にOracleライブラリが含まれるようにするため、Oracle Databaseのインストール後、必ずコンピュータを再起動します。

LinuxでのPHPのインストール

LinuxディストリビューションにPHP 5.3パッケージが含まれている場合は、それを使用するのが最も簡単な方法です。ソース・コードからPHPをビルドすることも可能です。

この項で説明する項目は、次のとおりです。

- Oracle LinuxへのPHPおよびOCI8のインストール

- Linux上の既存のPHPへのOCI8拡張モジュールの追加

- LinuxでのPHPとOCI8の手動ビルド

Oracle LinuxへのPHPおよびOCI8のインストール

Oracle Linuxで、次を使用してPHP 5.3をインストールします。

# yum install php53 php53-gd

または

# up2date php53 php53-gd

Oracle Unbreakable Linux Network (ULN)に登録していない場合は、次の項「Linux上の既存のPHPへのOCI8拡張モジュールの追加」」の説明のとおり、OCI8を手動でインストールする必要があります。

ULNの登録者である場合は、Oracle Software for Enterprise Linux 5チャネルでOCI8 RPMにアクセスできます。このチャネルを追加し、次を実行します。

# yum install php53-oci8-11gR2

または

# up2date php53-oci8-11gR2

php53-oci8-11gR2パッケージをインストールすると、Oracleインスタント・クライアント・ライブラリもインストールされます。

Apacheを再起動します。エラーが発生した場合は、画面に表示されます。これらは/var/log/httpd/error_logにも書き込まれる場合があります。問題があった場合は、httpd.confおよびphp.iniを再確認します。

Linux上の既存のPHPへのOCI8拡張モジュールの追加

OCI8なしのPHP 5.3がすでにインストールされている場合は、PHPのPECLライブラリ、http://pecl.php.net/oci8を使用して最新のPHP OCI8拡張モジュールを追加できます。通常、このライブラリを使用してOCI8をPHP 4.3.9以降に追加できます。このマニュアルのコード例ではPHP 5.3が必要です。

phpizeコマンドなどのPHP開発ファイルが必要になります。Oracle Linuxでは、これはphp53-develパッケージに含まれています。

OCI8をインストールする手順は、次のとおりです。

1. http://pecl.php.net/get/oci8-1.4.5.tgzをダウンロードして展開します。

2. 次のコマンドを実行します。

# cd oci8-1.4.5
# phpize
# export ORACLE_HOME=/u01/app/oracle/product/12.1
# ./configure --with-oci8
# make install

PHP OCI8は共有ライブラリとしてビルドされます。そのため、他のPHPインストール内容に干渉することなく簡単にアップグレードできます。

PHPにライブラリをロードするには、/etc/php.iniを編集し、次を追加します。

extension=oci8.so

LinuxでのPHPとOCI8の手動ビルド

この項では、Linux上でソース・コードからPHPをビルドする方法について説明します。ファイル名および抽出ディレクトリは、現行バージョンに基づいて決定されます。この手順では、インストールしているバージョンのディレクトリ名を使用するようにしてください。

この手順を実行すると、OCI8とともにPHPバイナリが静的に組み込まれます。代替手段として考えられるのは、前項の説明のとおり、OCI8なしでPHPをビルドした後、PECLからOCI8を追加する方法です。

PHPをインストールするには、次の手順を実行します。

1. 次からPHP 5.3.6をダウンロードします。
http://www.php.net/downloads.php

2. rootユーザーとしてログインし、次のコマンドを実行します。

# tar -jxvf php-5.3.6.tar.bz2
# cd php-5.3.6
# export ORACLE_HOME=/u01/app/oracle/product/12.1
# ./configure \
--with-oci8 \
--with-apxs2=/usr/local/apache/bin/apxs \
--with-config-file-path=/usr/local/apache/conf

/usr/local/apache以外にApacheがインストールされている場合は、使用しているシステムでの適切なパスを使用します。PHPを標準外の場所(ホーム・ディレクトリの下位など)にインストールする場合は、configureを実行するときに--prefixオプションを追加します。たとえば、--prefix=$HOME/php53のようにします。

3. PHPをビルドしてインストールします。

# make
# make install

4. PHPで提供される初期化開発ファイルをコピーします。

# cp php.ini-development /usr/local/apache/conf/php.ini

5. Apacheの構成ファイル/usr/local/apache/conf/httpd.confを編集します。
PHPのインストールでLoadModule行が挿入されなかった場合は、次のように追加します。

LoadModule php5_module modules/libphp5.so

次の行をhttpd.confに追加します。

#
# This next section will call PHP for .php files
#
AddType application/x-httpd-php .php

6. Apacheサーバーを再起動します。

/usr/local/apache/bin/apachectl stop
/usr/local/apache/bin/apachectl start

エラーが発生した場合は、画面に表示されます。エラーは、/usr/local/apache/logs/error_logにも書き込まれる場合があります。問題があった場合は、httpd.confおよびphp.iniを再確認します。

WindowsおよびLinuxでのPHPインストール後のタスク

PHPのインストール後、php.ini構成ファイルに次の設定が含まれていることを確認します。

1. タイムゾーンの設定は、PHP 5.3の新しい要件です。

date.timezone = America/Los_Angeles

2. メッセージをApacheのログ・ファイルのみに出力するのではなく、PHPのdisplay_errorsを設定して出力テキストに表示されるようにしておくと、テストの際に便利です。php.iniでdisplay_errors設定を探し、必要に応じてOnに変更します。このディレクティブが存在しない場合は、次の行を追加します。

display_errors = On

3. DRCP接続クラスがphp.iniに構成されている必要があります。この名前はユーザーが選択した文字列であり、このアプリケーションとDRCPを使用する他のアプリケーションとの区別に使用されます。クラスが設定されていない場合は、OracleでDRCPプールを効率よく共有できません。
次のようなクラス名エントリを追加します。

oci8.connection_class = ACXE

構成ファイルを保存します。

Linuxを使用している場合は、前の項「Linux上のApacheのためのOracle環境の設定」に記載されている手順も完了していることを確認します。

4. Apacheを再起動します。

PHPインストールのテスト

この項で説明する項目は、次のとおりです。

- phpinfo()によるPHP構成の確認

- OracleへのPHP接続のテスト

phpinfo()によるPHP構成の確認

最初にApacheのエラー・ファイルerror_log (/var/log/httpd、/usr/local/apache/logsまたはC:\Program Files\Apache Software Foundation\Apache2.2\logs内)で、ApacheまたはPHPの起動エラーがないことを確認します。

PHPプロジェクト・ファイルを作成する場所を決定します。このディレクトリはApacheがアクセス可能なディレクトリである必要があります。たとえばLinuxでは、Apacheのドキュメント・ルート(/var/www/html or /usr/local/apache/htdocs)または$HOME/public_html(ユーザー・ディレクトリを構成した場合)を使用します。Windowsの場合は、Apacheのドキュメント・ルートC:\Program Files\Apache Software Foundation\Apache2.2\htdocsを使用します。

エディタで、新規のPHPファイルpi.phpを作成し、次を含めます。

<?php
phpinfo();
?>

このファイルをブラウザにロードします。

http://localhost/pi.php

次のことを確認します。

- デフォルト以外のポート(8888など)を使用している場合は、ポートが含まれるようにURLを変更します。たとえば、http://localhost:8888/pi.phpのようにします。

- ファイルを$HOME/public_htmlに置いた場合は、ユーザー名が含まれるようにURLを変更します。たとえば、http://localhost/~chris/pi.phpのようにします。

- または、http://localhost:8888/~chris/pi.phpのように両方を使用します。

次のようなページが表示されることを確認します。

エコー・バックされたファイルのテキストが表示された場合は、ApacheがPHPファイルをPHPに送信する構成になっていません。Apacheのhttp.confファイルに、行AddType application/x-httpd-php .php(Linuxの場合)またはAddHandler fcgid-script .php(Windowsの場合)が含まれている必要があります。

phpinfo()が正常に出力された場合は、php.iniの場所とこのファイルがロードされたかどうかが表示されます。ロード済のphp.iniが表示されない場合は、これまでの手順を再度確認し、sample php.iniファイルを適切な場所にコピーし、「WindowsおよびLinuxでのPHPインストール後のタスク」の項の手順に従います。

「OCI8」セクションまでスクロールします。次のように表示されることを確認します。

「OCI8」セクションがない場合は、OCI8をインストールしていることを確認する必要があります。共有ライブラリとしてインストールした場合は、php.iniにextension=oci8.soまたはextension=php_oci8_11g.dll(Windowsの場合)があることを確認し、さらにphpinfo()の出力で、extension_dirがOCI8ライブラリがインストールされたディレクトリに設定されていることを確認します。

OracleへのPHP接続のテスト

OCI8拡張モジュールが正常に動作していることを確認するには、新規のPHPファイルtestoci8.phpを作成し、次を含めます。

<?php

$c = oci_connect('hr', 'welcome', 'localhost');
if (!$c) {
$m = oci_error();
trigger_error('Could not connect to database: '. $m['message'], E_USER_ERROR);
}
$s = oci_parse($c, "SELECT * FROM employees");
if (!$s) {
$m = oci_error($c);
trigger_error('Could not parse statement: '. $m['message'], E_USER_ERROR);
}
$r = oci_execute($s);
if (!$r) {
$m = oci_error($s);
trigger_error('Could not execute statement: '. $m['message'], E_USER_ERROR);
}
$r = oci_fetch_all($s, $res);
if (!$r) {
$m = oci_error($s);
trigger_error('Could not fetch rows: '. $m['message'], E_USER_ERROR);
}
echo "<table border='1'>\n";
foreach ($res as $row) {
echo "<tr>\n";
foreach ($row as $item) {
echo "  <td>".($item!==null?htmlentities($item,
ENT_QUOTES):"&nbsp;")."</td>\n";
}
echo "</tr>\n";
}
echo "</table>\n";

?>

<?php ?>タグ内のコードがすべてPHPによって処理され、出力がユーザーのブラウザに送信されます。タグの外側のテキストはユーザーのブラウザにそのまま送信されます。これには先行および後続の空白も含まれます。ファイルには複数のタグ・セットを含めることができます。これを利用してHTMLコンテンツ内にPHPのスニペットを埋め込むアプリケーションもあります。しかし、PHPを中心に据えたアプリケーションでは、通常PHPのechoまたはprint文を使用して、必要なHTMLタグを出力します。

OCI8関数のコールがどのように動作するかの詳細は、「PHP OCI8でSQLを実行する一般的な例」の項およびこのマニュアル内の残りの各所で説明します。

次のファイルをブラウザにロードします。

http://localhost/testoci8.php

Apacheをどのようにインストールしたかに応じて、次のいずれかを使用する必要があります。

- http://localhost:8888/testoci8.php

- http://localhost/~chris/testoci8.php

- http://localhost:8888/~chris/testoci8.php

次のように表示されることを確認します。

画面に何も表示されない場合は、php.iniにdisplay_errors = Onが含まれていることを確認します。ページを再ロードして、エラーが表示されるかどうかを確認します。

エラー「ORA-28000: アカウントがロックされています。」が表示された場合は、「HRユーザーのロック解除」の項の手順のとおり、HRアカウントのロックを解除します。

Linuxでの次のような起動エラー

Warning: oci_connect():OciEnvNlsCreate() failedがあった場合、ORACLE_HOMEを正しく設定したかどうかを確認してください。

「ORA-12541: TNS: リスナーがありません」のようなエラーが表示された場合は、Oracle Databaseのインストール・ログで、Oracle Databaseリスナーをインストールしたときに使用したポートを確認します。たとえば1522を使用した場合は、testoci8.php内の接続コールを次のように変更します。

$c = oci_connect('hr', 'welcome', 'localhost:1522');

ORA-01017: 無効なユーザー名/パスワードのエラーが表示された場合は、oci_connect()コールを変更し、「HRユーザーのロック解除」の項でHRに割り当てたパスワードを使用します。

NetBeans 7.0 IDEのインストール

NetBeansはPHP Webプロジェクト用のIDEとして非常に人気が高く、優れたコーディング機能を備えています。HTML、JavaScript、CSSを含むいくつかのプログラミング言語およびマークアップ言語の編集機能がサポートされています。NetBeansの最新バージョンはPHP 5.3の言語構造に対応しています。NetBeansは、PHPフレームワークをサポートしており、PHPUnitなどのテスト用ツールとの統合およびドキュメント生成用のPHPDocumentorとの統合が可能です。PHPの開発に役立つオプションのPHPデバッガ拡張モジュールとともに構成することもできます。NetBeansにはOracle Databaseと一緒に使用できるSQLエディタも含まれています。

詳細なインストール手順は、次のURLで入手できます。

http://netbeans.org/community/releases/70/install.html

この項で説明する項目は、次のとおりです。

- WindowsでのNetBeans IDEのインストール

- LinuxでのNetBeans IDEのインストール

- LinuxおよびWindowsでのNetBeansの構成

- NetBeansの使用

WindowsでのNetBeans IDEのインストール

NetBeans IDEをWindowsにインストールする手順は、次のとおりです。

1. NetBeans 7.0とJava SE Development Kitを次の場所からダウンロードします。
http://www.oracle.com/technetwork/java/javase/downloads/jdk-netbeans-jsp-142931.html

2. Windowsのエクスプローラで、ダウンロードしたファイルjdk-6u25-nb-7_0-windows-ml.exeを探し、ダブルクリックして実行します。

3. インストール後、「スタート」、「すべてのプログラム」、「NetBeans」、「NetBeans IDE 7.0」の順にナビゲートして、NetBeansを起動します。

4. 「Run Tools」から「Plugins」を選択します。

5. 「Available Plugins」タブに移動し、「PHP」カテゴリ内のすべてのオプションを選択します。

6. 「Install」をクリックし、使用許諾条項および場所に同意します。

7. 要求された場合は、IDEを再起動します。

LinuxでのNetBeans IDEのインストール

NetBeans IDEをLinuxにインストールする手順は、次のとおりです。

1. 次のWebサイトからNetBeans 7.0 PHPバンドルをダウンロードします。
http://netbeans.org/downloads/

2. Linuxでターミナル・ウィンドウを開き、インストーラを実行します。

# sh netbeans-7.0-ml-php-linux.sh

3. JDKがデフォルトの場所にインストールされていない場合は、次のようにインストール時にパスを指定します。

# sh netbeans-6.9-ml-php-linux.sh --javahome $HOME/jdk1.6.0_24

4. 使用許諾条項に同意します。

5. インストール・ディレクトリを確認します。

6. JDKの場所を確認します。

NetBeansがインストールされます。

JDKがコンピュータにまだインストールされていない場合は、JDKが含まれているNetBeans 7.0バンドルを次の場所からダウンロードします。

http://www.oracle.com/technetwork/java/javase/downloads/jdk-netbeans-jsp-142931.html

このバンドルはPHP対応になっていません。PHP対応にするには、「Tools」に移動し、「Plugins」を実行します。「Available Plugins」タブに移動し、「PHP」カテゴリ内のすべてのオプションを選択します。「Install」をクリックし、使用許諾条項および場所に同意します。要求された場合は、IDEを再起動します。

LinuxおよびWindowsでのNetBeansの構成

LinuxおよびWindowsでNetBeansを構成するには、次の手順を実行します。

1. デスクトップ・アイコンまたはメニュー項目を使用して、NetBeansを起動します。「Tools」、「Options」の順にナビゲートします。

2. 「General」オプションで、使用するブラウザを選択します。

3. 「PHP」オプションで、「General」タブに移動します。

4. PHP 5インタプリタをPHPコマンドライン実行可能ファイルに設定します。たとえば、/usr/bin/php、/usr/local/bin/phpまたはC:\php-5.3.6\php.exeなどです。

NetBeansの使用

新規のPHPプロジェクトを次のように作成します。

1. 「File」、「New Project」の順にナビゲートするか、「New Project」アイコンをクリックするか、[Ctrl]、[Shift]、[N]を同時に押します。

2. 「PHP」と「PHP Application」を選択します。

3. 「Project name」をACXEに変更します。

4. ソース・ファイルを保存するフォルダを選択します。/home/chris/public_html/ACXEなど、Apacheがアクセスできるディレクトリを使用すると、インストール作業が簡単になります。

5. 「PHP Version」をPHP 5.3に設定します。

6. プロジェクトURLをソースが配置されるディレクトリにマップするように設定します。たとえば、デフォルト以外のApacheポートを使用する場合は、http://localhost:8888/~chris/ACXE/のようにします。フレームワークは使用しないでください。

7. プロジェクトが作成され、index.phpファイルがデフォルトNetBeansテンプレートに設定されます。

8. 実行できるかどうかを確認するため、index.phpにコンテンツを配置します。<?php ?>タグ内のコメント行// put your code hereを、次と置き換えます。

echo "hi";

9. 「Run」から「Run Project」を選択するか、[F6]を押してファイルを実行します。ブラウザ・ウィンドウが開き、新しく追加したメッセージが表示されます。

10. URLが間違っていて、ディスク上のファイルにマップされない場合は、「Project navigator」でプロジェクト名を右クリックし、「Properties」を選択します。「Run Configuration」を選択し、プロジェクトURLを変更します。

このマニュアルの残りの部分では、「Project navigator」でプロジェクト名を選択してから、「File」、「New File」の順にナビゲートする方法、または「New File」アイコンもしくは[Ctrl]と[N]を同時に押す方法で、必要に応じてACXEプロジェクトにファイルを追加します。

3 データベース・アクセス・クラスの作成

AnyCoアプリケーションのOracle Database機能は、すべてのPHP OCI8アクセスを処理する1つのクラスに抽象化します。

この章のトピックは、次のとおりです。

- 接続定数

- Dbクラスの作成

- PHP OCI8でSQLを実行する一般的な例

- Dbクラスを使用したSQLの実行

- Dbクラスのテスト

接続定数

PHPファイルac_cred.inc.phpを作成します。'.inc'は慣習的に名前に含められるもので、このファイルに関数やクラスなどの定義しか含まれていないことを表しています。末尾に付いた拡張子.phpは、ユーザーがこのファイルに対してなんらかのHTTPリクエストを発行したときに、Webサーバーからユーザーにテキストが送信される(セキュリティ・リスク)のではなく、そのファイルがPHPスクリプトとして実行されることを意味しています。ファイルには定義しか含まれていないので、ユーザーに送信される出力はありません。これにより、望ましくない結果やコードの露呈を防止できます。

ac_cred.inc.phpファイルの初期の状態は次のようになります。

<?php

/**
* ac_cred.inc.php: Secret Connection Credentials for a database class
* @package Oracle
*/

/**
* DB user name
*/
define('SCHEMA', 'hr');

/**
* DB Password.
*
* Note: In practice keep database credentials out of directories
* accessible to the web server.
*/
define('PASSWORD', 'welcome');

/**
* DB connection identifier
*/
define('DATABASE', 'localhost:pooled');

/**
* DB character set for returned data
*/
define('CHARSET', 'UTF8');

/**
* Client Information text for DB tracing
*/
define('CLIENT_INFO', 'AnyCo Corp.');

?>

Oracle DBに接続するには、ユーザー名、パスワードおよび接続先DBを識別するための文字列が必要になります。これらは、PHPのdefine()コマンドを使用し、定数SCHEMA、PASSWORDおよびDATABASEとして設定します。キャラクタ・セットはオプションですが、推奨されている接続パラメータです。ここでは、CHARSET定数にUTF8が選択されています。

ほとんどのPHPアプリケーションは、常時1つのデータベース・アカウントを使用してDBに接続します。この例では、データベース・ユーザーはHRです。このことは、軽視できないセキュリティ上の意味合いを含んでいます。ファイルの拡張子が.phpだとしても、実際には、資格証明やその他のセキュリティ情報を記述したファイルはApacheがアクセスするディレクトリとは別の場所に格納することが推奨されており、これらのファイルはPHPのrequire()コマンドを使用してロードします。ファイルに資格証明をハードコーディングするのを避けるため、サイトによってはアプリケーションは、Apacheの起動前に設定される環境変数から、値を読み取る必要があります。

DATABASEで使用されるデータベース接続構文は簡易接続構文です。これはデータベースが実行されているホスト名を指定し、データベースのサービス名を特定します。ここで指定されているコンピュータはlocalhostで、これはPHPとデータベースを同じコンピュータ上に置く必要があることを意味しています。接尾辞:pooledは、接続でDRCPプールを使用する必要があることを示しています。「WindowsおよびLinuxでのPHPインストール後のタスク」の項でDRCPプールを起動していない場合は、この接尾辞を省略して、DATABASEをlocalhostに変更します。DRCPを使用するかどうかを決定するのに必要なアプリケーション変更は、この箇所のみです。

サイトの規準に応じて、接続識別子をOracle Netのtnsnames.oraの別名にすることもできます。

CLIENT_INFO定数は、データベース内でアプリケーションのエンドツーエンドの追跡を行うために使用されます。これについては、第13章「アプリケーションのデータベース使用状況の監視」で説明します。

Dbクラスの作成

新規のPHPファイルac_db.inc.phpを作成し、データベース・アクセス・クラスを含めます。初期のファイルには次が含まれています。

<?php

/**
* ac_db.inc.php: Database class using the PHP OCI8 extension
* @package Oracle
*/

namespace Oracle;

require('ac_cred.inc.php');

/**
* Oracle Database access methods
* @package Oracle
* @subpackage Db
*/
class Db {

/**
* @var resource The connection resource
* @access protected
*/
protected $conn = null;
/**
* @var resource The statement resource identifier
* @access protected
*/
protected $stid = null;
/**
* @var integer The number of rows to prefetch with queries
* @access protected
*/
protected $prefetch = 100;

}

?>

ac_db.inc.phpファイルではネームスペースをOracleに設定して、ファイルで宣言または使用されるクラスのネームスペースを定義しています。これにより、アプリケーション内で偶然同じ名前のクラスが複数実装された場合の衝突を回避します。

データベース資格証明は、require()を使用してインクルードされます。要求されたファイルが存在しなかった場合、コンパイル・エラーが発生します。PHPには、ファイルが存在しない場合のエラーを表示しないinclude()関数もあります。require_once()およびinclude_once()といった異なる形を使用して、サブ・ファイルが複数回インクルードされるのを防ぐこともできます。

Dbクラスの属性については後述します。

コメントはオープン・ソース・ツールPHPDocumentorが解析できる形式です。たとえば、@packageは、このファイルが所属している全体のパッケージを定義しています。NetBeans 7.0では、これらのタグを使用してアプリケーション・ドキュメントを自動生成できます。

Dbクラスの$prefetch属性と閉じ括弧の間に、次の2つのメソッドを追加します。

/**
* Constructor opens a connection to the database
* @param string $module Module text for End-to-End Application Tracing
* @param string $cid Client Identifier for End-to-End Application Tracing
*/
function __construct($module, $cid) {
$this->conn = @oci_pconnect(SCHEMA, PASSWORD, DATABASE, CHARSET);
if (!$this->conn) {
$m = oci_error();
throw new \Exception('Cannot connect to database: ' . $m['message']);
}
// Record the "name" of the web user, the client info and the module.
// These are used for end-to-end tracing in the DB.
oci_set_client_info($this->conn, CLIENT_INFO);
oci_set_module_name($this->conn, $module);
oci_set_client_identifier($this->conn, $cid);
}

/**
* Destructor closes the statement and connection
*/
function __destruct() {
if ($this->stid)
oci_free_statement($this->stid);
if ($this->conn)
oci_close($this->conn);
}

PHPオブジェクトのインスタンスを作成するときには、__construct()メソッドがコールされます。DbクラスのコンストラクタがOracle Databaseへの接続をオープンし、文の実行時に使用できるように、その接続リソースを$conn属性で保持します。接続に失敗した場合、エラーが生成されます。PHPのphp.iniパラメータdisplay_errorsがOnの場合は、このエラーはユーザーに表示され、log_errorsがOnの場合は、Apacheのログ・ファイルに送信されます。「WindowsおよびLinuxでのPHPインストール後のタスク」の項では、開発の助けとなるようにdisplay_errorsがOnに設定されています。情報漏えいにつながるため、本番アプリケーションではユーザーにエラーが表示されないようにする必要があります。

コンストラクタによって、接続資格証明がoci_pconnect()関数に渡されます。AnyCoアプリケーションでは、「データベース常駐の接続プール」の説明のとおり、oci_pconnect()を使用して永続DRCP接続を作成します。

キャラクタ・セットもoci_pconnect()に渡されます。これはデータがOracleからPHPに返されるときのキャラクタ・セットを指定します。設定はオプションですが、設定することをお薦めします。キャラクタ・セットがoci_pconnect()に渡されなかった場合、PHPは環境設定からキャラクタ・セットを判断します。この場合処理が遅くなり、予期しない値が使用される可能性もあります。

1つのデータベース・ユーザー名を使用するということは、アプリケーション内のすべての文がHRによって実行されたものとしてデータベースに記録されるということです。そのため、分析や追跡が困難になったり、まったくできなくなることもあります。oci_set_client_identifier()関数を使用すると、接続とデータベースで処理された文の詳細とともに、任意の文字列を記録できます。識別子をWebユーザーの名前に設定することで、データベース管理者はエンド・ユーザーとデータベースの使用状況を明示的に関連付けることができます。次のドキュメントは、Oracle Databaseでクライアント識別子を使用できる箇所について説明しています。

http://www.oracle.com/technetwork/articles/dsl/php-web-auditing-171451.html

また、データベース追跡を支援するため各接続には、クライアント情報とモジュール名という2つのメタデータが設定されます。第13章「アプリケーションのデータベース使用状況の監視」では、これらの便利な使用方法について説明します。

接続エラーが発生した場合、例外がスローされます。例外クラスの名前は完全修飾の名前です。先頭の'\'を削除すると、\Oracle\Exceptionのコールが試行されるため、ランタイム・エラーが発生します。これは、ExceptionというクラスがOracleネームスペースで定義されていないためです。PHPのネームスペース・セパレータはバックスラッシュ(\)です。PHP 5.3にネームスペースが導入された際、使用可能であった唯一の文字がバックスラッシュでした。

Dbインスタンスのデストラクタは、すべてのオープン接続を明示的にクローズします。説明したようなDRCPプール永続接続の場合、再利用に備えデータベース・サーバー・プロセスをDRCPプールに戻します。PHP変数は内部で参照カウント・メカニズムを使用しており、接続リソースの参照カウントを増加させる変数はすべて、その背後にあるデータベース接続が物理的にクローズされる前に解放される必要があります。ここでは、これは文リソースをクローズするという意味であり、このマニュアルでは文を実行するためのクラス拡張の箇所でこれを使用します。

PHPの参照カウント・メカニズムのため、説明したデストラクタはオブジェクトのインスタンスが破棄されるときに、単にデフォルトの動作をエミュレートします。文と接続のリソースは、それらを参照している変数が破棄されたときに終了します。そのため、この特殊なデストラクタ実装は省略できます。

PHP OCI8でSQLを実行する一般的な例

PHP OCI8での文の実行には、文のテキストの解析と実行が含まれます。手続き型の手法では、INSERTは次のようになります。

$c = oci_pconnect($un, $pw, $db, $cs);
$sql = "INSERT INTO mytable (c1, c2) VALUES (1, 'abc')";
$s = oci_parse($c, $sql);
oci_execute($s);

データベース・システム内で、ある文が別のデータ値で再実行される場合、バインド変数を使用します。

$c = oci_pconnect($un, $pw, $db, $cs);
$sql = "INSERT INTO mytable (c1, c2) VALUES (:c1_bv, :c2_bv)";
$s = oci_parse($c, $sql);
$c1 = 1;
$c2 = 'abc';
oci_bind_by_name($s, ":c1_bv", $c1, -1);
oci_bind_by_name($s, ":c2_bv", $c2, -1);
oci_execute($s);

バインディングはPHP変数を、SQL文内のバインド識別子プレースホルダに関連付けます。バインド長は-1に設定されます。これは、内部バッファ・サイズをPHP値の長さから推測するようPHPに指示します。oci_bind_by_name()を使用してデータベースからデータを取得する場合は(PL/SQL関数の戻り値をバインド変数に代入するような場合)、実際に予測されるデータ長を指定して、PHP変数に十分な内部スペースが割り当てられるようにする必要があります。

バインド変数はパフォーマンスとセキュリティの面で重要です。これによりデータベースは、同じ文を異なる変数値を使用して繰り返し実行するときに、文メタデータを再利用できます。このかわりに使用できるPHPのコーディング形式では、PHP変数値を連結してSQL文のテキストを生成します。DBはこれらの文をそれぞれ一意の文として認識するため、キャッシュの利用が少なくなります。これはDBのパフォーマンスに大きな影響を与えます。連結には、悪質なユーザー入力による連結によってSQL文のセマンティクスが変更されるといった、SQLインジェクションのセキュリティ・リスクもあります。

PHP内でSQL問合せは実行とほぼ同じですが、その後にフェッチ・コール(このPHPにはいくつかの異形があります)が続きます。たとえば、すべての行を一度にフェッチするには次のようにします。

$c = oci_pconnect($un, $pw, $db, $cs);
$sql = "SELECT * FROM mytable WHERE c1 = :c1_bv AND c2 = :c2_bv";
$s = oci_parse($c, $sql);
$c1 = 1;
$c2 = 'abc';
oci_bind_by_name($s, ":c1_bv", $c1, -1);
oci_bind_by_name($s, ":c2_bv", $c2, -1);
oci_execute($s);
oci_fetch_all($s, $res, 0, -1, OCI_FETCHSTATEMENT_BY_ROW);

問合せ結果は$resに格納されます。OCI_FETCHSTATEMENT_BY_ROW定数は、結果が各行のエントリを含む配列になることを示しています。行自体はサブ配列で表現されます。

問合せから大量の行が返される場合、メモリーが大量に使用される恐れがあります。oci_fetch_array()などのPHP OCI8関数をかわりにコールできます。この関数は結果セットの1行のみを返します。スクリプトが1行の処理を終えた後、oci_fetch_array()を再びコールして、次の行をフェッチできます。

注意:
oci_bind_by_name()コールのバインド変数の名前引数に、コロンの接頭辞を付ける必要はありませんが、含めておくと視覚的なコード点検に役立ちます。

Dbクラスを使用したSQLの実行

ac_db.inc.phpのDbクラスの使い勝手をよくするため、次の2つのメソッドをクラスに追加します。

/**
* Run a SQL or PL/SQL statement
*
* Call like:
*     Db::execute("insert into mytab values (:c1, :c2)",
*                 "Insert data", array(array(":c1", $c1, -1),
*                                      array(":c2", $c2, -1)))
*
* For returned bind values:
*     Db::execute("begin :r := myfunc(:p); end",
*                 "Call func", array(array(":r", &$r, 20),
*                                    array(":p", $p, -1)))
*
* Note: this performs a commit.
*
* @param string $sql The statement to run
* @param string $action Action text for End-to-End Application Tracing
* @param array $bindvars Binds. An array of (bv_name, php_variable, length)
*/
public function execute($sql, $action, $bindvars = array()) {
$this->stid = oci_parse($this->conn, $sql);
if ($this->prefetch >= 0) {
oci_set_prefetch($this->stid, $this->prefetch);
}
foreach ($bindvars as $bv) {
// oci_bind_by_name(resource, bv_name, php_variable, length)
oci_bind_by_name($this->stid, $bv[0], $bv[1], $bv[2]);
}
oci_set_action($this->conn, $action);
oci_execute($this->stid);              // will auto commit
}

/**
* Run a query and return all rows.
*
* @param string $sql A query to run and return all rows
* @param string $action Action text for End-to-End Application Tracing
* @param array $bindvars Binds. An array of (bv_name, php_variable, length)
* @return array An array of rows
*/
public function execFetchAll($sql, $action, $bindvars = array()) {
$this->execute($sql, $action, $bindvars);
oci_fetch_all($this->stid, $res, 0, -1, OCI_FETCHSTATEMENT_BY_ROW);
$this->stid = null;  // free the statement resource
return($res);
}

これらのメソッドの機能は前述の手続き型のサンプルと同じですが、Actionというデータベース追跡メタデータと、問合せのパフォーマンスを調整するプリフェッチと呼ばれる方法がさらに追加されています。プリフェッチについては、第8章「問合せのパフォーマンスとプリフェッチ」で後述します。

Dbクラスに設定する追跡メタデータはすべてオプションですが、後付けするよりも設計に含めておくほうが簡単です。これがないと、本番アプリケーションのパフォーマンス問題やアクセス問題のトラブルシューティングが非常に困難になります。

文識別子リソース$this->stidをnullに設定すると、oci_free_statement() (デストラクタで使用)と同様の内部クリーン・アップが開始され、その後のメソッドの正当性テストのため、属性がnullに設定されます。

このDb::execute()メソッドでは、次のようにINSERT文を記述できます。

$db = new \Oracle\Db("Test Example", "Chris");
$sql = "INSERT INTO mytable (c1, c2) VALUES (:c1_bv, :c2_bv)";
$c1 = 1;
$c2 = 'abc';
$db->execute($sql, "Insert Example", array(array(":c1_bv", $c1, -1),
array(":c2_bv", $c2, -1)));

次に問合せの例を示します。

$db = new \Oracle\Db("Test Example", "Chris");
$sql = "SELECT * FROM mytable WHERE c1 = :c1_bv AND c2 = :c2_bv";
$c1 = 1;
$c2 = 'abc';
$res = $db->execFetchAll($sql, "Query Example",
array(array(":c1_bv", $c1, -1),
array(":c2_bv", $c2, -1)));

Dbインスタンスの作成では、完全修飾ネームスペース記述を使用します。

バインド変数は配列の配列内にカプセル化されます。各サブ配列が1つのバインド変数を表します。

コードのとおり、Dbクラスは、oci_execute()がコールされるたび自動的にコミットされます。これは、このクラスを将来のアプリケーションで再利用する場合に、パフォーマンスおよびトランザクションの整合性の面で意味を持ちます。より汎用的なDbにするには、次を実行するようにDb::execute()を変更することを検討します。

...
oci_execute($this->stid, OCI_NO_AUTO_COMMIT);
...

この場合、oci_commit()をコールするcommitメソッドとoci_rollback()をコールするrollbackメソッドを、Dbクラスに追加する必要があります。このマニュアルの例では、これらの変更は必要ありません。PHPにおいて、同じ接続資格証明が使用されるoci_connect()またはoci_pconnect()のコールでは、その背後で同一のデータベース接続が再利用されます。そのため、アプリケーションがDbインスタンスを2つ作成した場合、それらのトランザクション状態は同じになります。インスタンスをロールバックまたはコミットすると、もう一方のトランザクションにも影響があります。oci_new_connect()関数はこれと異なり、コールされるたびに専用の新規接続を作成します。

Dbクラスのテスト

test_db.phpという新規のPHPファイルを作成して、Dbクラスをテストします。

<?php

// test_db.php

require('ac_db.inc.php');

$db = new \Oracle\Db("test_db", "Chris");
$sql = "SELECT first_name, phone_number FROM employees ORDER BY employee_id";
$res = $db->execFetchAll($sql, "Query Example");
// echo "<pre>"; var_dump($res); echo "</pre>\n";

echo "<table border='1'>\n";
echo "<tr><th>Name</th><th>Phone Number</th></tr>\n";
foreach ($res as $row) {
$name = htmlspecialchars($row['FIRST_NAME'], ENT_NOQUOTES, 'UTF-8');
$pn   = htmlspecialchars($row['PHONE_NUMBER'], ENT_NOQUOTES, 'UTF-8');
echo "<tr><td>$name</td><td>$pn</td></tr>\n";
}
echo "</table>";

?>

require()コマンドでac_db.inc.phpのコンテンツを含め、Dbクラスへのスクリプト・アクセスを付与しています。

Dbインスタンス作成のモジュール名パラメータは、ファイル名ベースtest_dbに設定します。これにより、データベース追跡で接続の開始元を特定できるようになります。接続識別子は架空のユーザー名に自由に設定します。$db->execFetchAll()へのActionパラメータは、ファイル内の操作に設定します。

この例ではバインド変数は渡さないため、$db->execFetchAll()メソッド・コールでオプションのバインド変数は指定しません。Db::execFetchAll()の定義で、最後の引数がないときにバインド変数リストを空の配列に設定しているため、データのバインドは行われません。

問合せ結果は行データの配列として$resに代入されます。var_dump()関数を非コメント化すると配列構造を確認できるので、単純なPHPのデバッグに有用です。$res配列がforeach()ループで反復処理され、各行が順番に処理されます。各行のサブ配列に入っている2つの列には、$row['FIRST_NAME']と$row['PHONE_NUMBER']でアクセスできます。Oracle Databaseの表の列は、デフォルトで大文字と小文字が区別されません。これらは大文字の配列索引としてPHPに返されます。次のように大文字と小文字を区別する列名を使用して、Oracleで表が作成されているとします。

CREATE TABLE mytab ("MyCol" NUMBER);

この場合、PHPで大文字と小文字を区別する配列索引$row['MyCol']を使用する必要があります。

test_db.phpでは、戻りデータをhtmlspecialchars()で処理し、HTMLに解釈されてしまう可能性を含むテキストを、HTMLマークアップではなく表示可能なテキストとして処理されるようにしています。このような出力のエスケープ処理は、Webアプリケーションからクロスサイト・スクリプティング(XSS)のセキュリティ問題を排除するためのセキュリティ対策として、非常に重要です。

使用するhtmlspecialchars()オプションはコンテキストによって異なります。PHPのhtmlentities()関数も便利な関数です。キャラクタ・セットはHTMLページのキャラクタ・セットと一致している必要があります。AnyCoアプリケーションではこれを行います。

test_db.phpをブラウザにロードします(http://localhost/test_db.php)。または、NetBeansで「Projects」ナビゲータ内のファイルを右クリックし、「Run」を選択します。

次のように表示されます。

接続で問題が発生した場合は、PHPインタプリタ・エラーをすべて解決します。すべてのメソッドがクラス定義の括弧内に配置されていることを確認します。その他の一般的な問題については、項「OracleへのPHP接続のテスト」を参照してください。

4 AnyCoアプリケーションの作成

この章のトピックは、次のとおりです。

- カスケード・スタイルシート

- セッションのためのアプリケーションのクラス

- PHPセッションを使用したステートフルWebエクスペリエンスの提供

- Pageクラスの追加

- アプリケーション・ログイン・ページ

カスケード・スタイルシート

AnyCoアプリケーションの作成前に、カスケード・スタイルシート・ファイルstyle.cssを作成します。次を含めます。

/* style.css */

body {
background: #FFFFFF;
color:      #000000;
font-family: Arial, sans-serif;
}

table {
border-collapse: collapse;
margin: 5px;
}

tr:nth-child(even) {background-color: #FFFFFF}
tr:nth-child(odd) {background-color: #EDF3FE}

td, th {
border: solid #000000 1px;
text-align: left;
padding: 5px;
}

#header {
font-weight: bold;
font-size: 160%;
text-align: center;
border-bottom: solid #334B66 4px;
margin-bottom: 10px;
}

#menu {
position: absolute;
left: 5px;
width: 180px;
display: block;
background-color: #dddddd;
}
#user {
font-size: 90%;
font-style:italic;
padding: 3px;
}

#content {
margin-left: 200px;
}

これはアプリケーションに単純なスタイルを提供し、メイン・コンテンツの左側にメニューを配置します。表の行は1行置きに色を変えます。第1章「Oracle DatabaseでのPHPの導入」の図1-1を参照してください。

セッションのためのアプリケーションのクラス

AnyCoアプリケーションに再利用可能なコンポーネントを提供するため、SessionおよびPageという2つのクラスを作成します。

Sessionクラスは、Webユーザー認証が追加されるクラスです。これは、中間層上にあるWebユーザーのセッション情報を保存および取得するコンポーネントも提供するため、これによりアプリケーションはステートフルになります。PHPセッションは、DRCPの概要で説明したOracleセッションとは直接関係しません。PHPセッションには、問合せ結果の現在の表示ページの開始行番号などのデータを格納できます。後続のHTTPリクエストはこの値をセッション・ストレージから取得し、結果の次のページを表示できます。

ac_equip.inc.phpという名前の新規のPHPファイルを作成し、初期の状態として次を含めます。

<?php

/**
* ac_equip.inc.php: PHP classes for the employee equipment example
* @package Equipment
*/
namespace Equipment;

/**
* URL of the company logo
*/
//define('LOGO_URL', 'http://localhost/ac_logo_img.php');

/**
* @package Equipment
* @subpackage Session
*/
class Session {
/**
*
* @var string Web user's name
*/
public $username = null;
/**
*
* @var integer  current record number for paged employee results
*/
public $empstartrow = 1;
/**
*
* @var string CSRF token for HTML forms
*/
public $csrftoken = null;

}

?>

このファイルはネームスペース宣言(ここではEquipment)から始まっています。

コメント・アウトされているLOGO_URL定数については、第12章「BLOBのアップロードと表示」で後述します。

$username属性にはWebユーザーの名前が格納されます。$empstartrow属性には、現在表示されている従業員セットの最初の行番号が格納されます。これにより、図1-1「サンプル・アプリケーションの概要」のように、「Next」と「Previous」ボタンを使用して従業員データのページ移動が可能になります。$csrftoken値については、第9章「データの挿入」で説明します。

Sessionクラスに2つの認証メソッドを追加します。

/**
* Simple authentication of the web end-user
*
* @param string $username
* @return boolean True if the user is allowed to use the application
*/
public function authenticateUser($username) {
switch ($username) {
case 'admin':
case 'simon':
$this->username = $username;
return(true);  // OK to login
default:
$this->username = null;
return(false); // Not OK
}
}

/**
* Check if the current user is allowed to do administrator tasks
*
* @return boolean
*/
public function isPrivilegedUser() {
if ($this->username === 'admin')
return(true);
else
return(false);
}

authenticateUser()メソッドで実装されるユーザー認証は、洗練性に欠け、安全性も高くありません。通常、PHP Webアプリケーションはそのアプリケーション専用のユーザー認証を実行します。ここでは、adminとsimonにのみアプリケーションの使用を許可します。認証の詳細は、次を参照してください。

http://www.oracle.com/technetwork/articles/mclaughlin-phpid1-091467.html

isPrivilegedUser()メソッドは、現在のユーザーに権限があるかどうかを示すブール値を返します。AnyCoアプリケーションでは、これを使用して、ユーザーにさらに別のレポートの閲覧と新規データのアップロードを許可するかどうかを判断します。権限の必要なこれらの操作を、AnyCoのadminにのみ許可します。

PHPセッションを使用したステートフルWebエクスペリエンスの提供

PHPでは、ユーザーがHTMLページ間を移動するときに、永続的と考えられるセッション値を格納できます。デフォルトでセッション・データはPHPサーバーのディスク上のファイルに保存されます。セッション・データは一意のcookie値によって識別され、ユーザーがcookieをオフにしている場合はURLで渡される値で識別されます。cookieの使用によりPHPは、ローカル・セッション・ストレージを適切なWebユーザーに関連付けることができます。

PHPセッションを使用すると、中間層Apacheプロセスをランダムに使用して、ユーザーのHTTPページ・リクエストをシームレスに処理することができ、この処理中には各ユーザーの現在のセッション・データにアクセスできます。PHPでは、セッション失効を処理する方法やセッション・データをデータベースへ格納する方法など、セッション処理について広範囲のカスタマイズを加えることができます。詳細は、PHPドキュメントを参照してください。

AnyCoアプリケーションのセッション値を格納、フェッチおよびクリアするため、次の3つのメソッドをSessionクラスに追加します。

/**
* Store the session data to provide a stateful web experience
*/
public function setSession() {
$_SESSION['username']    = $this->username;
$_SESSION['empstartrow'] = (int)$this->empstartrow;
$_SESSION['csrftoken']   = $this->csrftoken;
}

/**
* Get the session data to provide a stateful web experience
*/
public function getSession() {
$this->username = isset($_SESSION['username']) ?
$_SESSION['username'] : null;
$this->empstartrow = isset($_SESSION['empstartrow']) ?
(int)$_SESSION['empstartrow'] : 1;
$this->csrftoken = isset($_SESSION['csrftoken']) ?
$_SESSION['csrftoken'] : null;
}

/**
* Logout the current user
*/
public function clearSession() {
$_SESSION = array();
$this->username = null;
$this->empstartrow = 1;
$this->csrftoken = null;
}

これらは、PHPのセッション・データへのアクセスを可能にする、スーパーグローバル連想配列$_SESSIONを参照します。Session属性が変更されるとき、AnyCoアプリケーションはsetSession()をコールし、変更後の状態を記録します。その後、別のアプリケーション・リクエストが処理を開始する際、そのスクリプトはgetSession()メソッドをコールして、保存された属性値を取得します。三項演算子"?:"のテストでは、セッション値があればその値が使用され、ない場合はハードコードされたデフォルトが使用されます。

最後に、HTMLフォームのCSRF保護のため、次のメソッドをSessionクラスに追加します。これについては、第9章「データの挿入」の「ac_add_one.phpでのCSRFの例」の項で説明します。

/**
* Records a token to check that any submitted form was generated
* by the application.
*
* For real systems the CSRF token should be securely,
* randomly generated so it cannot be guessed by a hacker
* mt_rand() is not sufficient for production systems.
*/
public function setCsrfToken() {
$this->csrftoken = mt_rand();
$this->setSession();
}

Pageクラスの追加

Pageクラスには、HTML出力のブロックを出力するメソッドを実装し、アプリケーションの各Webページが同じ外観になるようにします。

ac_equip.inc.phpファイルで、Sessionクラスの閉じ括弧とPHPの閉じタグ?>の間に、新規のPageクラスを追加します。クラスの初期の状態は次のようになります。

/**
* @package Equipment
* @subpackage Page
*/
class Page {
/**
* Print the top section of each HTML page
* @param string $title The page title
*/
public function printHeader($title) {
$title = htmlspecialchars($title, ENT_NOQUOTES, 'UTF-8');
echo <<<EOF
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type"
content="text/html; charset=utf-8">
<link rel="stylesheet" type="text/css" href="style.css">
<title>$title</title>
</head>
<body>
<div id="header">
EOF;
// Important: do not put white space on the 'EOF;' line before or after the tag

if (defined('LOGO_URL')) {
echo '<img src="' . LOGO_URL . '" alt="Company Icon">&nbsp;';
}
echo "$title</div>";
}
/**
* Print the bottom of each HTML page
*/
public function printFooter() {
echo "</body></html>\n";
}

}

printHeader()メソッドは、HTMLページの導入部(スタイルシートなど)を出力し、ページ・タイトルを出力します。

PHPの'heredoc'は、大きなHTMLコンテンツ・ブロックを出力します。テキスト内の変数$titleは展開され、その値が表示されます。閉じタグ'EOF;'は行の先頭に配置し、その後に空白が含まれないようにします。このようにしなかった場合、PHPパーサーがファイルの残りの部分を文字列テキストと見なすため、PHP変数と思われるものが出現したときに、解析エラーがランダムに生成されます。

後述の例でLOGO_URLを定義した場合、ヘッダーにはロゴも表示されます。これは現在ac_equip.inc.phpの上部でコメント・アウトされています。

printFooter()メソッドは単にHTMLページの本文を終了させています。一般的なアプリケーションではこれを拡張して、サイトの著作権情報など各ページの下部に含めるコンテンツを表示するのに使用されます。

AnyCoアプリケーションには左側にナビゲーション・メニューがあります。これを出力するためPageクラスにメソッドを追加します。

/**
* Print the navigation menu for each HTML page
*
* @param string $username The current web user
* @param type $isprivilegeduser True if the web user is privileged
*/
public function printMenu($username, $isprivilegeduser) {
$username = htmlspecialchars($username, ENT_NOQUOTES, 'UTF-8');
echo <<<EOF
<div id='menu'>
<div id='user'>Logged in as: $username </div>
<ul>
<li><a href='ac_emp_list.php'>Employee List</a></li>
EOF;
if ($isprivilegeduser) {
echo <<<EOF
<li><a href='ac_report.php'>Equipment Report</a></li>
<li><a href='ac_graph_page.php'>Equipment Graph</a></li>
<li><a href='ac_logo_upload.php'>Upload Logo</a></li>
EOF;
}
echo <<<EOF
<li><a href="index.php">Logout</a></li>
</ul>
</div>
EOF;
}

ユーザーごとにメニューをカスタマイズするため、ユーザー名とユーザーの権限ステータスが渡されます。これらの値はSessionクラスから渡されます。

このマニュアルの以降の章で、リンクの中で参照されるPHPファイルを作成します。これらのファイルを作成しないでリンクをクリックすると、所定のエラーが発生します。

これで、Db、SessionおよびPageというAnyCoアプリケーションで使用される3つのクラスが完成しました。

アプリケーション・ログイン・ページ

AnyCoアプリケーションの開始ページはログイン・ページです。index.phpという新規のPHPファイルを作成します。NetBeansの場合は、このファイルの既存のコンテンツを置き換えます。index.phpファイルには次を含める必要があります。

<?php

/**
* index.php: Start page for the AnyCo Equipment application
*
* @package Application
*/

session_start();
require('ac_equip.inc.php');

$sess = new \Equipment\Session;
$sess->clearSession();

if (!isset($_POST['username'])) {
$page = new \Equipment\Page;
$page->printHeader("Welcome to AnyCo Corp.");
echo <<< EOF
<div id="content">
<h3>Select User</h3>
<form method="post" action="index.php">
<div>
<input type="radio" name="username" value="admin">Administrator<br>
<input type="radio" name="username" value="simon">Simon<br>
<input type="submit" value="Login">
</div>
</form>
</div>
EOF;
// Important: do not put white space on the 'EOF;' line before or after the tag
$page->printFooter();
} else {
if ($sess->authenticateUser($_POST['username'])) {
$sess->setSession();
header('Location: ac_emp_list.php');
} else {
header('Location: index.php');
}
}

?>

index.phpファイルはsession_start()コールから始まります。コードで$_SESSIONスーパーグローバルを使用する場合は、これをコードに必ず含め、出力の作成前にコールする必要があります。

Sessionクラスのインスタンスが作成され、既存のセッション・データは$sess->clearSession()コールによって破棄されます。これにより、このファイルをログアウト・ページに使用できるようになります。index.phpがロードされると、Webユーザーがアプリケーションからログアウトさせられます。

ファイルは大まかに2つのパートに分類できます。HTMLフォームを作成する部分と、それを処理する部分です。実行パスはPHPスーパーグローバル$_POSTで決定されます。このファイルを初めて実行するときには、$_POST['username']が設定されないため、HTMLフォームがページ・ヘッダーとフッターとともに表示されます。このフォームを使用してWebユーザーはAdministratorまたはSimonとしてログインします。

フォームの発行アクションのターゲットは、このindex.phpです。そのため、ユーザーがブラウザでフォームを発行すると、同じPHPファイルが実行されます。発行メソッドが"post"であるため、PHPはスーパーグローバル$_POSTにフォーム値を移入します。今回は'if'文の2つ目の分岐が実行されます。

そしてユーザー認証が行われます。ラジオ・ボタンの入力値'admin'と'simon'が、$sess->authenticateUser()に渡される値です。有効なユーザーがセッション・データに記録されます。次にPHPはHTTPヘッダーを送り返し、ブラウザをac_emp_list.phpにリダイレクトさせます。このファイルは次の項で作成します。

ユーザーが$sess->authenticateUser()で認証されなかった場合、ログイン・フォームが再び表示されます。

header()コールの実行前にスクリプトでテキストが表示されないようにします。

この時点でのアプリケーションを実行するには、ブラウザにindex.phpをロードします。NetBeansの場合は、「Run」→「Run Project」を使用するか、[F6]を押します。ブラウザに次のように表示されます。

5 従業員データのページ間の移動

この章では、図1-1「サンプル・アプリケーションの概要」に示す、AnyCoアプリケーションのメインの表示ページを作成します。一度に5件の従業員レコードを表示し、従業員リストのページ間を移動できるようにします。

この章のトピックは、次のとおりです。

- 従業員リストの作成

- 従業員リストの実行

従業員リストの作成

新規のPHPファイルac_emp_list.phpを作成し、初期状態で次を含めます。

<?php

/**
* ac_emp_list.php: list of employees
* @package Employee
*/

define('NUMRECORDSPERPAGE', 5);

session_start();
require('ac_db.inc.php');
require('ac_equip.inc.php');

$sess = new \Equipment\Session;
$sess->getSession();
if (!isset($sess->username) || empty($sess->username)) {
header('Location: index.php');
exit;
}

$page = new \Equipment\Page;
$page->printHeader("AnyCo Corp. Employees List");
$page->printMenu($sess->username, $sess->isPrivilegedUser());
printcontent($sess, calcstartrow($sess));
$page->printFooter();

// Functions

?>

NUMRECORDSPERPAGE定数は、表示する従業員レコードの件数を決定します。

$sess->getSession()コールを使用して保存されているセッション・データを取得した後、ユーザーにページの表示権限があるかどうかを確認するための基本的な検証を行います。ここでの要件は、usernameが設定されていることです。これが設定されていない場合、ブラウザはログイン・ページindex.phpにリダイレクトされます。本番アプリケーションではここでさらに検証を追加して、タイムスタンプの確認を行い、一定のアイドル時間後にユーザーに再ログインを要求するなどします。ハッカーがセッション・データを見ることやセッションを偽装することを困難にするため、ユーザー名を暗号化することもできます。

ファイルの本文は、HTMLページのヘッダー、メニュー、コンテンツおよびフッターを出力します。

このファイルでは、前に使用したオブジェクト指向の手法は続けずに、PHPの通常の手続き型の手法を示します。Functionsコメントの後に、ページ・コンテンツを出力する関数を追加します。

/**
* Print the main body of the page
*
* @param Session $sess
* @param integer $startrow The first row of the table to be printed
*/
function printcontent($sess, $startrow) {
echo "<div id='content'>";

$db = new \Oracle\Db("Equipment", $sess->username);
$sql = "SELECT employee_id, first_name || ' ' || last_name AS name,
phone_number FROM employees ORDER BY employee_id";
$res = $db->execFetchPage($sql, "Equipment Query", $startrow,
NUMRECORDSPERPAGE);
if ($res) {
printrecords($sess, ($startrow === 1), $res);
} else {
printnorecords();
}

echo "</div>";  // content
// Save the session, including the current data row number
$sess->empstartrow = $startrow;
$sess->setSession();
}

これはEMPLOYEES表で問合せを実行します。Db::execFetchPage()メソッドはDb::execFetchAll()とほぼ同じで、これについては後述します。表示するレコードがある場合、printrecords()によってレコードが表示され、ない場合は、printnorecords()によって表示するレコードがないことを示すメッセージが表示されます。コンテンツ出力の最終段階は、新しい開始行番号を使用してセッションを更新することです。

トップ・レベルのprintcontent()コールでは、calcstartrow()を使用して開始行番号を決定しています。次の関数をac_emp_list.phpに追加します。

/**
* Return the row number of the first record to display.
*
* The calculation is based on the current position
* and whether the Next or Previous buttons were clicked
*
* @param Session $sess
* @return integer The row number that the page should start at
*/
function calcstartrow($sess) {
if (empty($sess->empstartrow)) {
$startrow = 1;
} else {
$startrow = $sess->empstartrow;
if (isset($_POST['prevemps'])) {
$startrow -= NUMRECORDSPERPAGE;
if ($startrow < 1) {
$startrow = 1;
}
} else if (isset($_POST['nextemps'])) {
$startrow += NUMRECORDSPERPAGE;
}
}
return($startrow);
}

「Next」ボタンと「Previous」ボタンを含むフォームとともに行が表示されます。開始行は、どのボタンがクリックされたかと、ユーザーが到達しているデータ・セット内の位置を基に計算されます。

printrecords()をac_emp_list.phpに追加し、フェッチしたレコードを表示します。

/**
* Print the Employee records
*
* @param Session $sess
* @param boolean $atfirstrow True if the first array entry is the first table row
* @param array $res Array of rows to print
*/
function printrecords($sess, $atfirstrow, $res) {
echo <<< EOF
<table border='1'>
<tr><th>Name</th><th>Phone Number</th><th>Equipment</th></tr>
EOF;
foreach ($res as $row) {
$name = htmlspecialchars($row['NAME'], ENT_NOQUOTES, 'UTF-8');
$pn   = htmlspecialchars($row['PHONE_NUMBER'], ENT_NOQUOTES, 'UTF-8');
$eid  = (int)$row['EMPLOYEE_ID'];
echo "<tr><td>$name</td>";
echo "<td>$pn</td>";
echo "<td><a href='ac_show_equip.php?empid=$eid'>Show</a> ";
if ($sess->isPrivilegedUser()) {
echo "<a href='ac_add_one.php?empid=$eid'>Add One</a>";
echo "<a href='ac_add_multi.php?empid=$eid'> Add Multiple</a>\n";
}
echo "</td></tr>\n";
}
echo "</table>";
printnextprev($atfirstrow, count($res));
}

この関数のロジックは、test_db.phpで説明したものとほぼ同じです。'EOF;'トークンは行の先頭に配置し、その後に空白が含まれないようにします。

権限のあるユーザーには、各従業員に備品を割り当てるためのリンクがさらに表示されます。HTML表の最後の部分では、printnextprev()をコールして「Next」ボタンと「Previous」ボタンを表示します。

printnextprev()をac_emp_list.phpに追加します。

/**
* Print Next/Previous buttons as needed to page through the records
*
* @param boolean $atfirstrow True if the first array entry is the first table row
* @param integer $numrows Number of rows the current query retrieved
*/
function printnextprev($atfirstrow, $numrows) {
if (!$atfirstrow || $numrows == NUMRECORDSPERPAGE) {
echo "<form method='post' action='ac_emp_list.php'><div>";
if (!$atfirstrow)
echo "<input type='submit' value='< Previous' name='prevemps'>";
if ($numrows == NUMRECORDSPERPAGE)
echo "<input type='submit' value='Next >' name='nextemps'>";
echo "</div></form>\n";
}
}

printnextprev()のロジックは、次のような境界ケースも処理します。

- 最初のページには「Previous」ボタンを表示しません。

- ページ全体が表示されなかった場合は「Next」ボタンを表示しません。

最後に、printnorecords()をac_emp_list.phpに追加して、表示するレコードがなくなったときにメッセージを表示します。

/**
* Print a message that there are no records
*
* This can be because the table is empty or the final page of results
* returned no more records
*/
function printnorecords() {
if (!isset($_POST['nextemps'])) {
echo "<p>No Records Found</p>";
} else {
echo <<<EOF
<p>No More Records</p>
<form method='post' action='ac_emp_list.php'>
<input type='submit' value='< Previous' name='prevemps'></form>
EOF;
}
}

注意:
EOF;トークンは行の先頭に配置し、その後に空白が含まれないようにします。

ここで2つのケースが考えられます。表に行が含まれていなかった場合と、ユーザーが表のページ間を移動していて「Next」をクリックしたときに表示するデータがなかった場合です。後者のケースは、表内の行数がNUMRECORDSPERPAGEの倍数のときに発生します。

アプリケーションを実行する前に、Db::execFetchPage()メソッドを作成する必要があります。ファイルac_db.inc.phpで、Dbクラスに新しいメソッドを追加します。

/**
* Run a query and return a subset of records.  Used for paging through
* a resultset.
*
* The query is used as an embedded subquery.  Do not permit user
* generated content in $sql because of the SQL Injection security issue
*
* @param string $sql The query to run
* @param string $action Action text for End-to-End Application Tracing
* @param integer $firstrow The first row number of the data set to return
* @param integer $numrows The number of rows to return
* @param array $bindvars Binds. An array of (bv_name, php_variable, length)
* @return array Returns an array of rows
*/
public function execFetchPage($sql, $action, $firstrow = 1, $numrows = 1,
$bindvars = array()) {
//
$query = 'SELECT *
FROM (SELECT a.*, ROWNUM AS rnum
FROM (' . $sql . ') a
WHERE ROWNUM <= :sq_last)
WHERE :sq_first <= RNUM';

// Set up bind variables.
array_push($bindvars, array(':sq_first', $firstrow, -1));
array_push($bindvars, array(':sq_last', $firstrow + $numrows - 1, -1));
$res = $this->execFetchAll($query, $action, $bindvars);
return($res);
}

Oracle Databaseデータベースには行のサブセットを返すためのLIMIT句がないため、コール元の問合せをネストする必要があります。PHPのarray_push()関数は、外側の問合せの開始行番号と終了行番号で使用される追加のバインド変数を、コール元の問合せのバインド変数に追加します。

SQLテキストは連結されるため、SQLインジェクションのリスクに注意してください。ユーザー入力はこの関数に絶対に渡さないようにします。

従業員リストの実行

すべてのファイルを保存し、アプリケーションを実行します。最初にSimonとしてログインします。次のように表示されます。

「Logout」をクリックします。Administratorとして再ログインします。次のように表示されます。

「Next」ボタンと「Previous」ボタンを使用して、データ・ページ間を移動します。

NUMRECORDSPERPAGEを変更して、ページへの影響を確認します。

6 REF CURSORを使用した備品レコードの表示

この章では、前の章で作成した「AnyCo Corp. Employees List」ページで従業員名の横にある「Show」リンクをクリックしたときに実行されるレポートを作成します。

前の章ではSQL問合せからデータをフェッチする方法を説明しました。この章では、PHPでREF CURSORを使用する方法について説明します。従業員に支給された備品の名前をREF CURSORでフェッチします。

この章のトピックは、次のとおりです。

- PL/SQLパッケージとパッケージ本体の概要

- PL/SQLストアド・プロシージャの概要

- REF CURSORの概要

- 備品表

- PHPでのREF CURSORのコール

PL/SQLパッケージとパッケージ本体の概要

PL/SQLパッケージには、関連する項目が単一の論理エンティティとして格納されます。パッケージは、次の2つの部分で構成されます。

- パッケージ仕様部は、パッケージの内容物を定義します。C++言語などのヘッダー・ファイルに似ています。仕様部はすべてのパブリック項目を定義します。仕様部はパッケージへの公開インタフェースです。

- パッケージ本体には、仕様部で定義したプロシージャおよびファンクションのコードと、仕様部で宣言していないプライベート・プロシージャおよびファンクションのコードが含まれます。プライベート・コードはパッケージ本体内でのみ確認できます。

パッケージ仕様部とパッケージ本体は、データ・ディクショナリに別々のオブジェクトとして格納され、user_sourceビューで確認できます。仕様部はPACKAGE型として格納され、本体はPACKAGE BODY型として格納されます。

一連のパブリック定数を宣言する場合と同様、本体のない仕様部を保持することはできますが、仕様部のない本体を保持することはできません。

PL/SQLストアド・プロシージャの概要

ストアド・プロシージャとは、ある操作を実行するために設計された一連のPL/SQL文を1つにまとめて名前を付けたものです。ストアド・プロシージャはデータベース内に格納されます。これは、クライアント・アプリケーションがデータベース・オブジェクトと直接対話できるようにするものではなく、データベースのプログラミング・インタフェースを定義するものです。一般的にストアド・プロシージャを使用するのは、データを検証する場合や、複数のSQL問合せを組み合せた大規模で複雑な処理の指示をカプセル化する場合です。

ストアド・ファンクションは、戻り値パラメータを1つとります。ファンクションとは異なり、プロシージャは値を戻す場合と戻さない場合があります。

REF CURSORの概要

REF CURSORを使用することは、Oracle Databaseからの問合せ結果をクライアント・アプリケーションに戻す最も強力かつ柔軟で、拡張性のある方法の1つです。

REF CURSORはPL/SQLデータ型であり、この値はデータベース上の問合せ作業領域のメモリー・アドレスです。つまり、REF CURSORは、データベース上にある結果セットへのポインタまたはハンドルとなります。

REF CURSORには次の特性があります。

- REF CURSORは、データベース上のメモリー・アドレスを参照します。したがって、クライアントがデータベースにアクセスするには、REF CURSORの存続中はデータベースに接続している必要があります。

- REF CURSORにより、追加のデータベース・ラウンドトリップが発生します。REF CURSORがクライアントに戻されても、クライアントがREF CURSORをオープンしてデータをリクエストするまで、実際のデータは戻されません。ユーザーがその読取りを試行するまで、データは取得されません。

- REF CURSORは更新できません。REF CURSORで表される結果セットは読取り専用です。REF CURSORを使用してデータベースを更新することはできません。

- REF CURSORは後方にスクロールできません。REF CURSORで表される結果セットは、前進専用で順次アクセスされます。結果セット内のレコードをランダムにポイントするためにREF CURSOR内にレコード・ポインタを配置することはできません。

- REF CURSORはPL/SQLデータ型です。PL/SQLコード・ブロック内でREF CURSORを作成して戻すことができます。

備品表

このマニュアルの例では、AnyCo Corpが各従業員に職務に必要な備品を支給するというシナリオを使用します。EQUIPMENT表には、備品名と支給先の従業員が格納されます。

SQL*Plusで、HRユーザーとして接続し、次のスクリプトを実行します。

sqlplus hr/welcome@localhost

CREATE TABLE equipment(
id          NUMBER PRIMARY KEY,
employee_id REFERENCES employees(employee_id) ON DELETE CASCADE,
equip_name  VARCHAR2(20) NOT NULL);

CREATE SEQUENCE equipment_seq;
CREATE TRIGGER equipment_trig BEFORE INSERT ON equipment FOR EACH ROW
BEGIN
:NEW.id := equipment_seq.NEXTVAL;
END;
/

PL/SQLシーケンスおよびトリガーによって、各新規従業員レコードが挿入される際、レコードに一意のキーが割り当てられます。

これらの文をNetBeansなどのSQLエディタで実行する場合は、CREATE TRIGGER文の末尾のスラッシュ('/')を省略します。SQL*Plus内でスラッシュは文の終わりを示すインジケータであり、データベースで実行する文の一部ではありません。

サンプル・データを作成します。

-- Sample Data
INSERT INTO equipment (employee_id, equip_name) VALUES (100, 'pen');
INSERT INTO equipment (employee_id, equip_name) VALUES (100, 'telephone');
INSERT INTO equipment (employee_id, equip_name) VALUES (101, 'pen');
INSERT INTO equipment (employee_id, equip_name) VALUES (101, 'paper');
INSERT INTO equipment (employee_id, equip_name) VALUES (101, 'car');
INSERT INTO equipment (employee_id, equip_name) VALUES (102, 'pen');
INSERT INTO equipment (employee_id, equip_name) VALUES (102, 'paper');
INSERT INTO equipment (employee_id, equip_name) VALUES (102, 'telephone');
INSERT INTO equipment (employee_id, equip_name) VALUES (103, 'telephone');
INSERT INTO equipment (employee_id, equip_name) VALUES (103, 'computer');
INSERT INTO equipment (employee_id, equip_name) VALUES (121, 'computer');
INSERT INTO equipment (employee_id, equip_name) VALUES (180, 'pen');
INSERT INTO equipment (employee_id, equip_name) VALUES (180, 'paper');
INSERT INTO equipment (employee_id, equip_name) VALUES (180, 'cardboard box');
COMMIT;

SQL*Plusで、HRとしてプロシージャを作成します。

CREATE OR REPLACE PROCEDURE get_equip(eid_p IN NUMBER, RC OUT SYS_REFCURSOR) AS
BEGIN
OPEN rc FOR SELECT   equip_name
FROM     equipment
WHERE    employee_id = eid_p
ORDER BY equip_name;
END;
/

PHPでは、匿名PL/SQLブロックを実行することで、このプロシージャをコールできます。

BEGIN get_equip(:id, :rc); END;

:idバインド変数は前述のバインドと同じように使用されます。get_equip()のWHERE句に使用するため、PHP変数からの値をデータベースに渡します。バインド変数:rcはこれと異なり、後述するとおりequip_name()から返された問合せ結果を格納します。

PHPでのREF CURSORのコール

従業員の備品リストを表示するため、新規のPHPファイルac_show_equip.phpを作成します。

<?php

/**
* ac_show_equip.php: Show an employee's equipment
* @package ShowEquipment
*/

session_start();
require('ac_db.inc.php');
require('ac_equip.inc.php');

$sess = new \Equipment\Session;
$sess->getSession();
if (!isset($sess->username) || empty($sess->username)
|| !isset($_GET['empid'])) {
header('Location: index.php');
exit;
}
$empid = (int) $_GET['empid'];

$page = new \Equipment\Page;
$page->printHeader("AnyCo Corp. Show Equipment");
$page->printMenu($sess->username, $sess->isPrivilegedUser());
printcontent($sess, $empid);
$page->printFooter();

// Functions

?>

これはac_emp_list.phpとほぼ同じ構造です。今回は、$sess->getSession()の後の検証テストでも従業員識別子をチェックします。この値は、ac_emp_list.phpのprintrecords()関数からURLパラメータとして渡されます。

...
<a href='ac_show_equip.php?empid=$eid'>Show</a>
...

識別子の値はac_show_equip.phpで、PHPの$_GETスーパーグローバル配列をとおしてアクセスされます。配列エントリが設定されていない場合、ac_show_equip.phpが不適切にコールされたものと見なされ、ユーザーはログイン・ページindex.phpにリダイレクトされます。

$_GET['empid']値を整数にキャストし、SQLインジェクションの危険性を極力少なくしています。この値をバインドしますが、すべてのユーザー入力を一貫してフィルタすることをお薦めします。$_GET['empid']になんらかの理由でアルファベットのテキストが格納された場合、PHPのキャスト・ルールの結果、$empidに数字0が格納されます。テキストの先頭が数字であった場合、$empidはその数字になりますが、少なくともそれに続くテキストは破棄されます。

ファイルのメイン・コンテンツに入る前に、ac_show_equip.phpのFunctionsセクションに、小さいヘルパー関数getempname()を追加します。

/**
* Get an Employee Name
*
* @param Db $db
* @param integer $empid
* @return string An employee name
*/
function getempname($db, $empid) {
$sql = "SELECT first_name || ' ' || last_name AS emp_name
FROM employees
WHERE employee_id = :id";
$res = $db->execFetchAll($sql, "Get EName", array(array(":id", $empid, -1)));
$empname = $res[0]['EMP_NAME'];
return($empname);
}

これはスクリプトを実行する従業員識別子を受け取り、一致する従業員名をルック・アップします。演習として、問合せで行が返されなかった場合の処置を加えてください。

メインのprintcontent()関数をac_show_equip.phpに追加します。

/**
* Print the main body of the page
*
* @param Session $sess
* @param integer $empid Employee identifier
*/
function printcontent($sess, $empid) {
echo "<div id='content'>\n";
$db = new \Oracle\Db("Equipment", $sess->username);
$empname = htmlspecialchars(getempname($db, $empid), ENT_NOQUOTES, 'UTF-8');
echo "$empname has: ";

$sql = "BEGIN get_equip(:id, :rc); END;";
$res = $db->refcurExecFetchAll($sql, "Get Equipment List",
"rc", array(array(":id", $empid, -1)));
if (empty($res['EQUIP_NAME'])) {
echo "no equipment";
} else {
echo "<table border='1'>\n";
foreach ($res['EQUIP_NAME'] as $item) {
$item = htmlspecialchars($item, ENT_NOQUOTES, 'UTF-8');
echo "<tr><td>$item</td></tr>\n";
}
echo "</table>\n";
}
echo "</div>";  // content
}

これは、新規のメソッドDb::refcurExecFetchAll()をコールします。レコードの配列が返され、慣習的なループの方法で出力されます。

REF CURSOR バインド・パラメータ:rcは、特別にバインドする必要があります。バインド変数名は文テキストの任意の場所で任意に選択または配置できるため、その名前は単独でrefcurExecFetchAll()に渡され、通常のバインド変数の配列には格納されません。

ac_db.inc.phpを編集しrefcurExecFetchAll()メソッドを作成し、これをDbクラスに追加します。

/**
* Run a call to a stored procedure that returns a REF CURSOR data
* set in a bind variable.  The data set is fetched and returned.
*
* Call like Db::refcurexecfetchall("begin myproc(:rc, :p); end",
*                            "Fetch data", ":rc", array(array(":p", $p, -1)))
* The assumption that there is only one refcursor is an artificial
* limitation of refcurexecfetchall()
*
* @param string $sql A SQL string calling a PL/SQL stored procedure
* @param string $action Action text for End-to-End Application Tracing
* @param string $rcname the name of the REF CURSOR bind variable
* @param array  $otherbindvars Binds. Array (bv_name, php_variable, length)
* @return array Returns an array of tuples
*/
public function refcurExecFetchAll($sql, $action, $rcname,
$otherbindvars = array()) {
$this->stid = oci_parse($this->conn, $sql);
$rc = oci_new_cursor($this->conn);
oci_bind_by_name($this->stid, $rcname, $rc, -1, OCI_B_CURSOR);
foreach ($otherbindvars as $bv) {
// oci_bind_by_name(resource, bv_name, php_variable, length)
oci_bind_by_name($this->stid, $bv[0], $bv[1], $bv[2]);
}
oci_set_action($this->conn, $action);
oci_execute($this->stid);
oci_execute($rc); // run the ref cursor as if it were a statement id
oci_fetch_all($rc, $res);
$this->stid = null;
return($res);
}

$rcname内のREF CURSORバインド・パラメータは、通常のPHP変数ではなくoci_new_cursor()で作成されたカーソルにバインドされます。型OCI_B_CURSORを指定する必要があります。

追跡するアクションのテキストを設定した後、PL/SQL文が実行されます。この例では、get_equip()がコールされ、問合せのカーソルをオープンして返します。これで、$rc内のREF CURSORをoci_parse()コールから返された識別子のように、PHP文の識別子として扱うことが可能になりました。フェッチはここから行われます。問合せ結果は、$resで関数のコール元に返されます。

すべてのファイルを保存し、ブラウザでアプリケーションを実行します。SimonまたはAdministratorとしてログインします。「Steven King」の横の「Show」リンクをクリックします。その個人が所有する備品が表示されます。

7 エラー処理

エラー処理を様々なレベルで実施し、不正なユーザー入力からデータベース・エラーまで、あらゆるものからWebアプリケーションを保護する必要があります。ユーザー・エクスペリエンスをスムーズにするため、PHPのエラーはWebユーザーに表示されないようにする必要があります。これらは中間層ログ・ファイルで捕捉し、ユーザーにはタスクの再試行または別のタスクの実行の機会のみを与えるようにします。本番システムでは、php.iniのdisplay_errors設定をOffにする必要があります。

この章のトピックは、次のとおりです。

- データベース・エラー

- カスタム・エラー・メッセージの表示

データベース・エラー

データベース・レベルでは、すべてのPHP OCI8エラーをチェックすることをお薦めします。

現状のac_db.inc.phpでは、エラー・チェックは接続時にのみ__construct()で行われています。

...
if (!$this->conn) {
$m = oci_error();
throw new \Exception('Cannot connect to database: ' . $m['message']);
}
...

oci_error()関数は連想配列を返します。その要素の1つに、Oracleエラー・メッセージのテキストが含まれています。

さらなる演習として、Dbクラスのエラー処理を向上させてください。このような変更を加えても、この演習の残りの部分に影響はありません。各PHP OCI8コールを評価し、戻り値をチェックする場所を決定します。oci_error()をコールし、メッセージのテキストを取得します。接続エラーについては、前述のように引数をoci_error()に渡さないでください。接続エラーではoci_error()は引数を受け取りませんが、oci_parse()からのエラーをチェックする場合は、接続リソースをoci_error()に渡します。

$stid = oci_parse($conn, $sql);
if (!$stid) {
$m = oci_error($conn)
...
}

oci_execute()エラーの場合は、文ハンドルを渡します。

$r = oci_execute($stid);
if (!$r) {
$m = oci_error($stid)
...
}

カスタム・エラー・メッセージの表示

getempname()を編集しprintcontent()で例外をスローして、ac_show_equip.phpでエラーをシミュレートします。このコールに達すると、PHPでランタイム・エラーが発生します。

function printcontent($sess, $empid) {
echo "<div id='content'>\n";
$db = new \Oracle\Db("Equipment", $sess->username);
$empname = htmlspecialchars(getempname($db, $empid), ENT_NOQUOTES, 'UTF-8');
echo "$empname has: ";

throw new Exception;

$sql = "BEGIN get_equip(:id, :rc); END;";

...

ブラウザでアプリケーションを実行し、「Steven King」の「Show」リンクをクリックします。開発作業用にdisplay_errorsをOnに設定しているため、コンテンツ領域にエラーが表示されます。

エラーが、ユーザー名を表示するページの最初の部分に続いて出力されます。display_errorsをOffに設定した本番サイトでは、この部分的なセクション・コンテンツが表示されます。これは望ましくありません。これを回避するには、PHPの出力バッファを使用します。

ac_show_equip.phpを編集し、printcontent()をコールする箇所を変更します。PHPのtry-catchブロックでコールを囲み、次のように変更します。

...
$page->printMenu($sess->username, $sess->isPrivilegedUser());
ob_start();
try {
printcontent($sess, $empid);
} catch (Exception $e) {
ob_end_clean();
echo "<div id='content'>\n";
echo "Sorry, an error occurred";
echo "</div>";
}
ob_end_flush();
$page->printFooter();
...

ob_start()関数は、以降生成される出力をすべてバッファに捕捉します。その他のPHP ob_*関数は、バッファの破棄やブラウザへのフラッシュを実行するためのものです。このコード内の例外ハンドラのob_end_clean()コールで「Steven King has:」というメッセージが破棄されるため、カスタム・エラー・メッセージを出力できます。

アプリケーションを再度実行すると、次のエラーが表示されます。

オブジェクト指向コードを使用しないで、例外をスローおよび捕捉するには、printcontent()からブール値を返し、エラーを手動で処理します。実行を停止するには、PHPのtrigger_error()を使用します。

ac_show_equip.phpのprintcontent()関数を編集し、一時的な行を変更します。

throw new Exception;

変更後

trigger_error('Whoops!', E_USER_ERROR);

E_USER_ERRORのようなPHPエラーを捕捉して処理するには、エラー・ハンドラ関数を登録できるPHPのset_error_handler()関数を使用します。

ac_show_equip.phpの冒頭にset_error_handler()へのコールを追加します。

...
session_start();
set_error_handler("ac_error_handler");

require('ac_db.inc.php');
require('ac_equip.inc.php');
...

コールされる関数も追加します。

/**
* Error Handler
*
* @param integer $errno Error level raised
* @param string $errstr Error text
* @param string $errfile File name that the error occurred in
* @param integer $errline File line where the error occurred
*/
function ac_error_handler($errno, $errstr, $errfile, $errline) {
error_log(sprintf("PHP AnyCo Corp.: %d:  %s in %s on line %d",
$errno, $errstr, $errfile, $errline));
header('Location: ac_error.html');
exit;
}

これはApacheログ・ファイルへのメッセージの記録と、エラー・ページへのリダイレクトを実行します。このエラー・ページを新規のHTMLファイルac_error.htmlで作成します。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<!-- ac_error.html: a catch-all error page -->
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title></title>
</head>

<body bgcolor="#ffffff">
<h1>AnyCo Corp. Error Page</h1>
<p>We're sorry an error occurred.<p>
<p><a href="index.php">Login</a></p>

</body>
</html>

アプリケーションを実行してログインします。「Show」をクリックして、従業員の備品を表示します。エラー・ページが表示されます。

システムでApacheエラー・ログを探します。たとえばOracle Linuxの場合は、/var/log/httpd/error_log内にあります。ログにはPHPで生成されたメッセージが含まれています。

[Wed Apr 27 13:06:09 2011] [error] [client 127.0.0.1] PHP AnyCo Corp.: 256:
Whoops! in /home/chris/public_html/ACXE/ac_show_equip.php on line 71, referer:
http://localhost/~chris/ACXE/ac_emp_list.php

次の章に進む前に、printcontent()内の臨時のtrigger_error()コールを削除するかコメント・アウトします。

...
// trigger_error('Whoops!', E_USER_ERROR);
...

8 問合せのパフォーマンスとプリフェッチ

この章のトピックは、次のとおりです。

- プリフェッチの概要

- 「Employee Report」ページ

- 備品レポートの実行

- REF CURSORのプリフェッチ

プリフェッチの概要

この項では、問合せ行のフェッチのパフォーマンスをPHPで調整する方法について説明します。

PHP OCI8では問合せ結果をフェッチするときに、プリフェッチという方法でデータベースへのネットワーク・ラウンドトリップを減らすことができます。行をバッチとして取得すると、データベースとネットワークの効率が上がります。

PHP OCI8ではデフォルトでプリフェッチが有効になっています。データベースから最初の行を取得するときに、設定した上限までの行(デフォルトで100)も一緒に返され、PHPプロセスから見てローカルにある内部バッファに格納されます。その後スクリプトでコールされるPHP OCI8 oci_fetch_*関数は、バッファからのデータを内部的に使用し続け、データがすべて消費されると、データベースへのラウンドトリップが新たに開始され、新しい行のバッチが返されます。oci_fetch_*関数がコール元にデータを返す方法は、有効なプリフェッチ値にかかわらず同じです。

デフォルトのプリフェッチ値はphp.ini構成ファイルのoci8.default_prefetchで設定するか、実行時にoci_set_prefetch()で設定できます。

現状のAnyCoアプリケーションでは、oci_fetch_all()を使用しています。この章では、よく使用される別の関数oci_fetch_array()について説明します。これがループでコールされると、問合せ結果セット内のすべての行が反復処理されます。データ・セットが大きい場合は、一度に1行フェッチする方法をとることで、結果セット全体を維持するための大量のメモリーを確保する必要がなくなります。

oci_fetch_all()を使用した場合でも、プリフェッチの動作と利点に変わりはありません。プリフェッチの処理は、PHPの下層にあるOracleクライアント・ライブラリで行われます。

「Employee Report」ページ

すべての従業員と支給備品のレポートを生成する新規のPHPファイルac_report.phpを作成します。ファイルの初期の状態は次のようになります。

<?php

/**
* ac_report.php: Full report of all employees and their equipment
* @package Report
*/

session_start();
require('ac_db.inc.php');
require('ac_equip.inc.php');

$sess = new \Equipment\Session;
$sess->getSession();
if (!isset($sess->username) || empty($sess->username)
|| !$sess->isPrivilegedUser()) {
header('Location: index.php');
exit;
}

$page = new \Equipment\Page;
$page->printHeader("AnyCo Corp. Equipment Report");
$page->printMenu($sess->username, $sess->isPrivilegedUser());
printcontent($sess);
$page->printFooter();

// Functions

?>

Functionsセクションにprintcontent()関数を追加します。

/**
* Print the main body of the page
*
* @param Session $sess
*/
function printcontent($sess) {
echo "<div id='content'>";
$db = new \Oracle\Db("Equipment", $sess->username);

$sql = "select first_name || ' ' || last_name as emp_name, equip_name
from employees left outer join equipment
on employees.employee_id = equipment.employee_id
order by emp_name, equip_name";

// Change the prefetch value to compare performance.
// Zero will be slowest. The system default is 100
$db->setPrefetch(200);

$time = microtime(true);
$db->execute($sql, "Equipment Report");
echo "<table>";
while (($row = $db->fetchRow()) != false) {
$empname = htmlspecialchars($row['EMP_NAME'], ENT_NOQUOTES, 'UTF-8');
$equipname = htmlspecialchars($row['EQUIP_NAME'], ENT_NOQUOTES, 'UTF-8');
echo "<tr><td>$empname</td><td>$equipname</td></tr>";
}
echo "</table>";
$time = microtime(true) - $time;
echo "<p>Report generated in " . round($time, 3) . " seconds\n";
echo "</div>";  // content
}

基本的にこの構造は前の章のレイアウトと同じです。

$db->setPrefetch()コールを使用して、プリフェッチ値を設定します。microtime()コールは、レポートの生成時間を示すものです。

新しいDb::fetchRow()メソッドを使用して、一度に1行ずつ取得しています。これは問合せが実行された後、ループ内でコールされます。

ac_db.inc.phpを編集し、setPrefetch()メソッドとfetchRow()メソッドをDbクラスに追加します。

/**
* Set the query prefetch row count to tune performance by reducing the
* number of round trips to the database.  Zero means there will be no
* prefetching and will be slowest.  A negative value will use the php.ini
* default value.  Some queries such as those using LOBS will not have
* rows prefetched.
*
* @param integer $pf The number of rows that queries should prefetch.
*/
public function setPrefetch($pf) {
$this->prefetch = $pf;
}

/**
* Fetch a row of data.  Call this in a loop after calling Db::execute()
*
* @return array An array of data for one row of the query
*/
public function fetchRow() {
$row = oci_fetch_array($this->stid, OCI_ASSOC + OCI_RETURN_NULLS);
return($row);
}

OCI_ASSOCフラグは、列名を配列キーとする連想配列に結果を返すようPHPに指示しています。OCI_RETURN_NULLSフラグは、nullのデータ値について配列エントリを返すようPHPに指示しています。値は空の文字列になります。これにより、各行の配列に含まれるエントリの数が同じになります。

備品レポートの実行

すべてのファイルを保存し、アプリケーションをAdministratorとして実行します。左側のナビゲーション・メニューから「Equipment Report」を選択します。すべての従業員と支給備品が表示されます。

下部に問合せの出力の生成にかかった時間が表示されています。使用したデータの量と、PHPとデータベースがネットワークで分断されていないという状況から、所要時間は少なくなっています。

プリフェッチをオフにしたときの効果を確認するため、ac_report.phpを編集してプリフェッチ設定を0に変更します。

$db->setPrefetch(0);

これは、PHP OCI8がOracleクライアント・ライブラリからデータを1行取得するたびに、データベース・サーバーへのラウンドトリップ・リクエストが開始されるという意味です。行が余計にプリフェッチされることはありません。

レポートを再実行します。経過時間が長くなります。

このような小規模のシステムでは、テストによりばらつきが生じる可能性があり、値が小さすぎて信頼性に欠けることもあります。このような場合は、何度か実行するか、多くの行が返されるように問合せを変更します。

REF CURSORのプリフェッチ

プリフェッチはREF CURSORSからレコードをフェッチするするときにも使用できます。Dbクラス内でREF CURSORプリフェッチ値を変更できるようにするには、ac_db.inc.phpを編集し、次の行をDb::refcurExecFetchAll()のREF CURSOR実行の前に追加します。

if ($this->prefetch >= 0) {
oci_set_prefetch($rc, $this->prefetch);  // set on the REFCURSOR
}

このプリフェッチ・サイズは、トップ・レベルの文ではなくREF CURSORで設定します。関数は次のようになります。

public function refcurExecFetchAll($sql, $action, $rcname,
$otherbindvars = array()) {
$this->stid = oci_parse($this->conn, $sql);
$rc = oci_new_cursor($this->conn);
oci_bind_by_name($this->stid, $rcname, $rc, -1, OCI_B_CURSOR);
foreach ($otherbindvars as $bv) {
// oci_bind_by_name(resource, bv_name, php_variable, length)
oci_bind_by_name($this->stid, $bv[0], $bv[1], $bv[2]);
}
oci_set_action($this->conn, $action);
oci_execute($this->stid);
if ($this->prefetch >= 0) {
oci_set_prefetch($rc, $this->prefetch);  // set on the REFCURSOR
}
oci_execute($rc); // run the ref cursor as if it were a statement id
oci_fetch_all($rc, $res);
return($res);
}

アプリケーションでテストを行うことで、問合せの最適なプリフェッチ・サイズが明らかになります。あまり大きい値を使用しても利点はありません。Oracleでは動的にスペースが割り当てられるため、逆に値を小さくしすぎても得られるものは多くありません。

プリフェッチを完全にオフにしたほうがよい状況はあまりありません。考えられるケースとしては、PHPコードがREF CURSORを取得し、そこから一部のデータをフェッチした後、残りのデータをフェッチするPL/SQLプロシージャにカーソルを戻すような場合です。PHPがREF CURSORからレコードをフェッチするときにプリフェッチが行われたのに、それらのプリフェッチされた行がoci_fetch_*コールをとおしてスクリプトに戻されなかった場合、それらの行は失われ、次のPL/SQLプロシージャで使用できません。

注意:
REF CURSORからプリフェッチを行うには、PHPをOracle Database 11gR2ライブラリにリンクする必要があります。以前のバージョンを使用している場合、リクエストした各REF CURSOR行でデータベースへのラウンドトリップが必要になるため、システムのパフォーマンスが低下します。

9 データの挿入

備品を従業員に割り当てるため、AnyCoアプリケーションにHTMLフォームを用意する必要があります。管理者はフォームを使用して備品を特定の従業員に割り当てることができます。

この章のトピックは、次のとおりです。

- 挿入フォームの作成

- 1件挿入フォームの実行

- ac_add_one.phpでのCSRFの例

挿入フォームの作成

新規のPHPファイルac_add_one.phpを作成します。ファイルの初期の状態は次のようになります。

<?php

/**
* ac_add_one.php: Add one piece of equipment to an employee
* @package Application
*/
session_start();
require('ac_db.inc.php');
require('ac_equip.inc.php');

$sess = new \Equipment\Session;
$sess->getSession();
if (!isset($sess->username) || empty($sess->username)
|| !$sess->isPrivilegedUser()
|| (!isset($_GET['empid']) && !isset($_POST['empid']))) {
header('Location: index.php');
exit;
}
$empid = (int) (isset($_GET['empid']) ? $_GET['empid'] : $_POST['empid']);

$page = new \Equipment\Page;
$page->printHeader("AnyCo Corp. Add Equipment");
$page->printMenu($sess->username, $sess->isPrivilegedUser());
printcontent($sess, $empid);
$page->printFooter();

// Functions

?>

操作のプロセス・フローはindex.phpとほぼ同じです。最初にac_add_one.phpを実行したときにはHTML入力フォームが表示されます。ユーザーがフォームを発行すると、ac_add_one.phpが再び起動されて、データがデータベースに挿入されます。

この関数で必要となる権限には、$_GET['empid']または$_POST['empid']スーパーグローバルに従業員IDが設定されているかどうかのチェックが含まれます。ac_add_one.phpが最初にコールされたとき(ac_emp_list.phpのprintrecords()を参照)に、従業員IDがURLパラメータとして渡され、$_GETスーパーグローバルに格納されます。ac_add_one.php内のフォーム(後述します)が発行されたときに、従業員識別子が$_POSTに格納されます。

printcontent()関数をac_add_one.phpに追加します。

/**
* Print the main body of the page
*
* @param Session $sess
* @param integer $empid Employee identifier
*/
function printcontent($sess, $empid) {
echo "<div id='content'>\n";
$db = new \Oracle\Db("Equipment", $sess->username);
if (!isset($_POST['equip']) || empty($_POST['equip'])) {
printform($sess, $db, $empid);
} else {
/* if (!isset($_POST['csrftoken'])
|| $_POST['csrftoken'] != $sess->csrftoken) {
// the CSRF token they submitted does not match the one sent
header('Location: index.php');
exit;
} */
$equip = getcleanequip();
if (empty($equip)) {
printform($sess, $db, $empid);
} else {
doinsert($db, $equip, $empid);
echo "<p>Added new equipment</p>";
echo '<a href="ac_show_equip.php?empid='
. $empid . '">Show Equipment</a>' . "\n";
}
}
echo "</div>";  // content
}

printcontent()関数にはロジックが含まれており、HTMLフォームを出力するのか、それともユーザーが入力したデータを挿入するのかを判断します。コメントアウトされたCSRFトークン・コードについては後述します。

さらに、ac_add_one.phpにprintform()関数を追加します。

/**
* Print the HTML form for entering new equipment
*
* @param Session $sess
* @param Db $db
* @param integer $empid Employee identifier
*/
function printform($sess, $db, $empid) {
$empname = htmlspecialchars(getempname($db, $empid), ENT_NOQUOTES, 'UTF-8');
$empid = (int) $empid;
$sess->setCsrfToken();
echo <<<EOF
Add equipment for $empname
<form method='post' action='${_SERVER["PHP_SELF"]}'>
<div>
Equipment name <input type="text" name="equip"><br>
<input type="hidden" name="empid" value="$empid">
<input type="hidden" name="csrftoken" value="$sess->csrftoken">
<input type="submit" value="Submit">
</div>
</form>
EOF;
}

注意:
EOF;トークンは行の先頭に配置し、その後に空白が含まれないようにします。

この単純なフォームはユーザーに値の入力を要求します。CSRFトークンについては後述します。

getcleanequip()関数をac_add_one.phpに追加します。

/**
* Perform validation and data cleaning so empty strings are not inserted
*
* @return string The new data to enter
*/
function getcleanequip() {
if (!isset($_POST['equip'])) {
return null;
} else {
$equip = $_POST['equip'];
return(trim($equip));
}
}

この実装では、入力されたデータの先頭または末尾の空白はすべて除去されます。

基本的なWebアプリケーションのセキュリティとして、入力をフィルタして出力をエスケープするということが一般的に言われています。getcleanequip()関数は入力をフィルタします。ここで、他の方法でデータをサニタイズすることも可能です。HTMLタグを受け入れないようにすることもできます。このようなタグはPHPの入力フィルタを使用して除去します。たとえば、次のように変更できます。

$equip = $_POST['equip'];

変更後

$equip = filter_input(INPUT_POST, 'equip', FILTER_SANITIZE_STRING);

これによりHTMLタグが削除され、他のテキストが残ります。

ac_add_one.phpで、有効なデータがdoinsert()で挿入されます。この関数のコードをファイルに追加します。

/**
* Insert a piece of equipment for an employee
*
* @param Db $db
* @param string $equip Name of equipment to insert
* @param string $empid Employee identifier
*/
function doinsert($db, $equip, $empid) {
$sql = "INSERT INTO equipment (employee_id, equip_name) VALUES (:ei, :nm)";
$db->execute($sql, "Insert Equipment", array(array("ei", $empid, -1),
array("nm", $equip, -1)));
}

これは一般的なバインド変数構文を使用して、ac_db.inc.php内の既存のDb::execute()メソッドを使用します。「Dbクラスを使用したSQLの実行」の説明のとおり、oci_execute()がコールされるたびにDbクラスは自動的にコミットされます。

最後に、ヘルパー関数getempname()を追加してac_add_one.phpを完成させます。

/**
* Get an Employee Name
*
* @param Db $db
* @param integer $empid
* @return string An employee name
*/
function getempname($db, $empid) {
$sql = "SELECT first_name || ' ' || last_name AS emp_name
FROM employees
WHERE employee_id = :id";
$res = $db->execFetchAll($sql, "Get EName", array(array("id", $empid, -1)));
$empname = $res[0]['EMP_NAME'];
return($empname);
}

これは、ac_show_equip.php内の同名の関数と同じです。

ac_show_equip.phpフォームと同様の機能を使用して、レコードの削除や更新を実行できます。ただし、行をあるHTMLページでロックして別のページで変更できないという、ステートレスWebアーキテクチャの制限があります。

1件挿入フォームの実行

AnyCoアプリケーションを実行し、Administratorとしてログインします。「Steven King」の横の「Add One」リンクをクリックします。備品入力フォームが表示されます。

新規の備品paperを入力し、「Submit」をクリックします。新規データが挿入されます。更新されたリストは、「Steven King」の横にある「Show」リンクをクリックすると表示されます。

ac_add_one.phpでのCSRFの例

現状のフォームはCSRF攻撃を受けやすい状態にあり、ログイン中のユーザーを他のサイトが悪用して、データの発行や、権限が必要な操作をユーザーに実行させる可能性があります。

これを再現するため、hack.htmlという新規のHTMLページを作成します。

<html>
<!-- hack.html: Show issues with CSRF -->
<body>
<h1>Make Millions!</h1>
<form method='post' action='http://localhost/ac_add_one.php'>
<div>
Do you dream of being rich?<br>
<input type="hidden" name="equip" value="fish">
<input type="hidden" name="empid" value="100">
<input type="submit" value="Win">
</div>
</form>

</body>
</html>

使用するシステムに合わせて、HTMLフォームのactionのURLを変更します。

ブラウザでAnyCoアプリケーションを実行し、Administratorとしてログインします。新しいブラウザ・タブまたはブラウザ・ウィンドウで、次のファイルを開きます。

http://localhost/hack.html

表面的に、ページを閲覧するユーザーにとって、これはAnyCoアプリケーションと何の関係もありません。

「Win」ボタンをクリックします。AnyCoアプリケーションがコールされ、架空の備品名fishが、従業員100(Steven King)の備品リストに挿入されます。挿入された値は、後続の「Show Equipment」ページに表示されます。

ac_add_one.phpを編集し、printcontent()のチェックのコメントを削除してCSRF保護を有効にします。

...
} else {
if (!isset($_POST['csrftoken'])
|| $_POST['csrftoken'] != $sess->csrftoken) {
// the CSRF token they submitted does not match the one sent
header('Location: index.php');
exit;
}
$equip = getcleanequip();
...

ac_add_one.phpのフォームには、生成されたクロスサイト・リクエスト・フォージェリ・トークンが非表示フィールドとして含まれています。この値もユーザー・セッションに格納されます。printcontent()のCSRFチェックは、発行されたフォーム内のトークンがPHPに保存されたセッション値と一致するかどうかをチェックします。

ファイルを保存しAnyCoアプリケーションを再度実行して、Administratorとしてログインします。新しいブラウザ・タブまたはブラウザ・ウィンドウで、次のファイルを開きます。

http://localhost/hack.html

「Win」をクリックします。

今回は、printcontent()内のCSRF保護が、発行されたフォーム内でCSRFトークンを検出しません。ログイン・ページindex.phpにリダイレクト後、ログアウトされます。AnyCoアプリケーションに再度ログインし、Steven Kingの備品リストが変更されておらず、fishにかわる第2のエントリがないことを確認します。hack.htmlが正常に動作するには、ac_add_one.phpで正規の入力フォームが生成された際PHPセッション内に保存される、csrftokenフィールドの値が必要になります。

CSRF保護は、Webアプリケーションに施す必要のある様々のセキュリティ制限の1つにすぎません。Webにデプロイするコードは徹底的なセキュリティ評価を実施する必要があります。

広く使用されているPHPフレームワークの多くには、安全なアプリケーションの作成作業を省力化する機能が備わっています。たとえば、AnyCoのSessionクラスよりも安全なCSRFトークン生成の実装が用意されています。

10 複数データ値の挿入

PHP OCI8では、文字や整数の配列を1回のコールで挿入できます。そのため、複数の値を表に挿入する際の、ネットワーク・トラフィックおよびデータベース・システムのオーバーヘッドを削減できます。

この章のトピックは、次のとおりです。

- 複数挿入フォームの作成

- 複数挿入フォームの実行

複数挿入フォームの作成

この章の例では、3つのデータ値を1回の操作で挿入できるフォームについて説明します。

配列の挿入には、PL/SQLのバルクFORALLコマンドを使用します。SQL*PlusにHRとしてログインし、PL/SQLパッケージを作成します。

CREATE OR REPLACE PACKAGE equip_pkg AS
TYPE arrtype IS TABLE OF VARCHAR2(20) INDEX BY PLS_INTEGER;
PROCEDURE insert_equip(eid_p IN NUMBER, eqa_p IN arrtype);
END equip_pkg;
/

CREATE OR REPLACE PACKAGE BODY equip_pkg AS
PROCEDURE insert_equip(eid_p IN NUMBER, eqa_p IN arrtype) IS
BEGIN
FORALL i IN INDICES OF eqa_p
INSERT INTO equipment (employee_id, equip_name)
VALUES (eid_p, eqa_p(i));
END insert_equip;
END equip_pkg;
/

insert_equip()プロシージャは従業員名の配列を受け入れて、EQUIPMENT表に挿入します。

新規のPHPファイルac_add_multi.phpを作成し、ここにac_add_one.phpのコンテンツをコピーします。値の配列を処理できるようにするため、ファイルに次の変更を加えます。

ac_add_multi.phpのHTMLフォームで、次の1件入力フィールドを変更します。

<div>
Equipment name <input type="text" name="equip"><br>
<input type="hidden" name="empid" value="$empid">
...

3件入力フィールドへの変更

...
<div>
Equipment name <input type="text" name="equip[]"><br>
Equipment name <input type="text" name="equip[]"><br>
Equipment name <input type="text" name="equip[]"><br>
<input type="hidden" name="empid" value="$empid">
...

[]トークンは配列を返します。これはac_add_one.phpでは必要ありませんでした。

返されたフォームの値を処理できるようにするため、ac_add_multi.phpのgetcleanequip()関数を置き換えます。

/**
* Perform validation and data cleaning so empty strings are not inserted
*
* @return array The array of new data to enter
*/
function getcleanequip() {
if (!isset($_POST['equip'])) {
return array();
} else {
$equiparr = array();
foreach ($_POST['equip'] as $v) {     // Strip out unset values
$v = trim($v);
if (!empty($v))
$equiparr[] = $v;
}
return($equiparr);
}
}

配列の各エントリをループ処理し、空でない文字列のみを返します。

最後に、ac_add_multi.phpのdoinsert()を次に置き換えます。

/**
* Insert an array of equipment values for an employee
*
* @param Db $db
* @param array $equiparr array of string values to be inserted
* @param string $empid Employee identifier
*/
function doinsert($db, $equiparr, $empid) {
$arraybinds = array(array("eqa", $equiparr, SQLT_CHR));
$otherbinds = array(array("eid", $empid, -1));
$sql = "BEGIN equip_pkg.insert_equip(:eid, :eqa); END;";
$db->arrayInsert($sql, "Insert Equipment List", $arraybinds, $otherbinds);
}

これは、Dbクラスで新規のarrayInsert()メソッドを使用して、PL/SQL insert_equip()プロシージャをコールしています。データ値配列のバインドは通常のスカラーPHP OCI8バインドとは異なる方法で行う必要があるため、arrayInsert()へのバインド・パラメータは2種類に分かれています。

ac_db.inc.phpを編集して、新規のメソッドを追加します。

/**
* Insert an array of values by calling a PL/SQL procedure
*
* Call like Db::arrayinsert("begin myproc(:arn, :p); end",
*                               "Insert stuff",
*                               array(array(":arn", $dataarray, SQLT_CHR)),
*                               array(array(":p", $p, -1)))
*
* @param string $sql PL/SQL anonymous block
* @param string $action Action text for End-to-End Application Tracing
* @param array $arraybindvars Bind variables. An array of tuples
* @param array $otherbindvars  Bind variables. An array of tuples
*/
public function arrayInsert($sql, $action, $arraybindvars,
$otherbindvars = array()) {
$this->stid = oci_parse($this->conn, $sql);
foreach ($arraybindvars as $a) {
// oci_bind_array_by_name(resource, bv_name,
// php_array, php_array_length, max_item_length, datatype)
oci_bind_array_by_name($this->stid, $a[0], $a[1],
count($a[1]), -1, $a[2]);
}
foreach ($otherbindvars as $bv) {
// oci_bind_by_name(resource, bv_name, php_variable, length)
oci_bind_by_name($this->stid, $bv[0], $bv[1], $bv[2]);
}
oci_set_action($this->conn, $action);
oci_execute($this->stid);              // will auto commit
$this->stid = null;
}

Db::arrayInsert()でのバインドは、このマニュアルの前述の例とほぼ同じです。データ配列の要素数を渡す必要があるため、oci_bind_array_by_name()関数が受け取る引数はこれとは少し異なります。AnyCoアプリケーションのoci_bind_array_by_nameでは、PHPからのデータを挿入するだけなので、最大データ長パラメータは-1として渡すことができます。これは実際のデータ長を使用するようにPHPに指示しています。1回のoci_execute()コールですべてのデータ項目がデータベースに挿入されます。

複数挿入フォームの実行

ファイルを保存し、ブラウザでAnyCoアプリケーションを実行します。Administratorとしてログインし、Steven Kingの「Add Multiple」リンクをクリックします。

Computer、Monitor、Keyboardなどのデータ項目を追加します。

「Submit」をクリックし、Steven Kingの横の「Show」をクリックしてデータ項目が挿入されたことを確認します。

配列バインディングはデータのフェッチにも機能します。効率的なBULK COLLECT構文を使用したPL/SQLプロシージャでは、1回のOCI8 oci_execute()コールでデータをPHPに返すことができます。Oracleからデータを取得する際、PHPがメモリーを正しく割り当てるためには、oci_bind_array_by_name()コールが項目数と最大データ・サイズを認識している必要があります。

11 JSONの使用とJPEGイメージの生成

この章では、JSONシリアライズ形式を使用して単純なWebサービスからデータを転送する方法について説明します。このWebサービスは、PHPのGD拡張モジュールを使用してイメージを作成するクライアントからコールされます。

この章のトピックは、次のとおりです。

- JSONを返す単純なWebサービスの作成

- JPEGイメージの作成

JSONを返す単純なWebサービスの作成

新規のPHPファイルac_get_json.phpを作成し、次を含めます。

<?php

/**
* ac_get_json.php: Service returning equipment counts in JSON
* @package WebService
*/

require('ac_db.inc.php');

if (!isset($_POST['username'])) {
header('Location: index.php');
exit;
}

$db = new \Oracle\Db("Equipment", $_POST['username']);

$sql = "select equip_name, count(equip_name) as cn
from equipment
group by equip_name";
$res = $db->execFetchAll($sql, "Get Equipment Counts");

$mydata = array();
foreach ($res as $row) {
$mydata[$row['EQUIP_NAME']] = (int) $row['CN'];
}

echo json_encode($mydata);

?>

このWebサービスに認証機能はありません。これはAnyCoアプリケーションから見て外部になります。必要になるのは、POSTデータへのusernameの入力のみです。

このファイルはAnyCo Corpの備品割当を問い合せ、PHPのjson_encode()を使用して、統計をJSON形式で返します。EQUIPMENT表に現在格納されているデータに応じて、Webサービスから次のような出力が返されます。

{"cardboard box":1,"pen":4,"computer":2,"telephone":3,"paper":3,"car":1}

JPEGイメージの作成

Webサービスをコールしグラフを作成する新規のPHPファイルac_graph_img.phpを作成します。初期のファイルには次が含まれています。

<?php

/**
* ac_graph_img.php: Create a JPEG image of the equipment allocation statistics
*
* Do not put any text or white space before the "<?php" tag because it will
* be incorporated into the image stream and corrupt the picture.
*
* @package Graph
*/

define('WEB_SERVICE_URL', "http://localhost/ac_get_json.php");

session_start();
require('ac_equip.inc.php');

$sess = new \Equipment\Session;
$sess->getSession();
if (!isset($sess->username) || empty($sess->username)
|| !$sess->isPrivilegedUser()) {
header('Location: index.php');
exit;
}
$data = callservice($sess);
do_graph("Equipment Count", 600, $data);

// Functions

?>

使用しているシステムに合わせて、WebサービスのURLを変更します。

このファイルにcallservice()関数を追加します。

/**
* Call the service and return its results
*
* @param Session $sess
* @return array Equipment name/count array
*/
function callservice($sess) {
// Call the web "service" to get the Equipment statistics
// Change the URL to match your system configuration
$calldata = array('username' => $sess->username);
$options = array(
'http' => array(
'method'  => 'POST',
'header'  => 'Content-type: application/x-www-form-urlencoded',
'content' => http_build_query($calldata)
)
);
$ctx = stream_context_create($options);
$result = file_get_contents(WEB_SERVICE_URL, false, $ctx);
if (!$result) {
$data = null;
} else {
$data = json_decode($result, true);

// Sort an array by keys using an anonymous function
uksort($data, function($a, $b) {
if ($a == $b)
return 0;
else
return ($a < $b) ? -1 : 1;
});
}
return($data);
}

これはPHPのストリーム機能を使用して、URLをリクエストし統計を取得します。ストリーム・コンテキストには、サービスで必要になるpost変数としてusernameが含まれています。

JSON形式からデータがデコードされ、配列は名前順でソートされます。PHPのuksort()関数への第2引数は、データ比較を行う匿名関数です。

ac_graph_img.phpを編集し、イメージを作成する関数を追加します。

/**
* Draw a bar graph, with bars projecting horizontally
*
* @param string $title The Graph's title
* @param type $width Desired image width in pixels
* @param array $items Array of (caption, value) tuples
*/
function do_graph($title, $width, $items) {
$border = 50;             // border space around bars
$caption_gap = 4;         // space between bar and its caption
$bar_width = 20;          // width of each bar
$bar_gap = 40;            // space between each bar
$title_font_id = 5;       // font id for the main title
$bar_caption_font_id = 5; // font id for each bar's title

// Image height depends on the number of items
$height = (2 * $border) + (count($items) * $bar_width) +
((count($items) - 1) * $bar_gap);

// Find the horizontal distance unit for one item
$unit = ($width - (2 * $border)) / max($items);

// Create the image and add the title
$im = ImageCreate($width, $height);
if (!$im) {
trigger_error("Cannot create image<br>\n", E_USER_ERROR);
}
$background_col = ImageColorAllocate($im, 255, 255, 255); // white
$bar_col = ImageColorAllocate($im, 0, 64, 128);           // blue
$letter_col = ImageColorAllocate($im, 0, 0, 0);           // black
ImageFilledRectangle($im, 0, 0, $width, $height, $background_col);
ImageString($im, $title_font_id, $border, 4, $title, $letter_col);

// Draw each bar and add a caption
$start_y = $border;
foreach ($items as $caption => $value) {
$end_x = $border + ($value * $unit);
$end_y = $start_y + $bar_width;
ImageFilledRectangle($im, $border, $start_y, $end_x, $end_y, $bar_col);
ImageString($im, $bar_caption_font_id, $border,
$start_y + $bar_width + $caption_gap, $caption, $letter_col);
$start_y = $start_y + ($bar_width + $bar_gap);
}

// Output the complete image.
// Any text, error message or even white space that appears before this
// (including any white space before the "<?php" tag) will corrupt the
// image data.  Comment out the "header" line to debug any issues.
header("Content-type: image/jpg");
ImageJpeg($im);
ImageDestroy($im);
}

この関数はPHPのGD拡張モジュールを使用してグラフを作成します。デフォルトのGDフォントは若干洗練性に欠けますが、新しいフォントの追加が可能です。出力はJPEGストリームであるため、WebページのHTMLコードにイメージ・ファイルを含めるかわりに、任意の場所でPHPファイルをコールできます。

AnyCoアプリケーションでは、新規ファイルac_graph_page.phpを作成してイメージを統合します。

<?php

/**
* ac_graph_page.php: Display a page containing the equipment graph
* @package Graph
*/

session_start();
require('ac_equip.inc.php');

$sess = new \Equipment\Session;
$sess->getSession();
if (!isset($sess->username) || empty($sess->username)
|| !$sess->isPrivilegedUser()) {
header('Location: index.php');
exit;
}

$page = new \Equipment\Page;
$page->printHeader("AnyCo Corp. Equipment Graph");
$page->printMenu($sess->username, $sess->isPrivilegedUser());

echo <<<EOF
<div id='content'>
<img src='ac_graph_img.php' alt='Graph of office equipment'>
</div>
EOF;

$page->printFooter();

?>

注意:
'EOF;'トークンは行の先頭に配置し、その後に空白が含まれないようにします。

イメージはHTMLの通常のimgタグ内に配置されます。

ブラウザにAnyCoアプリケーションをロードし、Administratorとしてログインします。左側のナビゲーション・メニューから「Equipment Graph」リンクをクリックします。グラフが表示されます。

イメージが表示されない場合は、エラー・メッセージなどのテキストや<?phpタグの前の空白など、ac_graph_img.phpに問題があることが考えられます。このテキストがイメージ・ストリームに含められ、その結果画像が無効になります。この種の問題をデバッグするためには、ac_graph_img.phpで$sessionチェックおよびheader()コールをコメント・アウトすると役に立ちます。次に、イメージ・ストリームの生データを表示させるため、ブラウザで次のリンクをロードします。

http://localhost/ac_graph_img.php

JSON形式は、ブラウザとPHPサーバー間のデータ転送効率を上げる目的でよく利用されます。Webサービスac_get_json.phpは、使用可能なJSONグラフィック・ライブラリの多くで直接使用できます。

12 BLOBのアップロードと表示

この章のトピックは、次のとおりです。

- ロゴを格納する表の作成

- PHP OCI8でのイメージのアップロード

- ロゴのフェッチとイメージの作成

- ロゴの表示

ロゴを格納する表の作成

PHP OCI8拡張モジュールを使用するとLOBデータを簡単に操作できます。AnyCoアプリケーションではBLOBを使用して会社のロゴを格納し、これを各Webページに表示します。

SQL*Plusで、ロゴを格納する表PICTURESを作成します。

CREATE TABLE pictures (id NUMBER, pic BLOB);

CREATE SEQUENCE pictures_seq;
CREATE TRIGGER pictures_trig BEFORE INSERT ON pictures FOR EACH ROW
BEGIN
:NEW.id := pictures_seq.NEXTVAL;
END;
/

PHP OCI8でのイメージのアップロード

新規のPHPファイルac_logo_upload.phpを作成します。初期のコンテンツは次のようになります。

<?php

/**
* ac_logo_upload.php: Upload a new company logo
* @package Logo
*/

session_start();
require('ac_db.inc.php');
require('ac_equip.inc.php');

$sess = new \Equipment\Session;
$sess->getSession();
if (!isset($sess->username) || empty($sess->username)
|| !$sess->isPrivilegedUser()) {
header('Location: index.php');
exit;
}

$page = new \Equipment\Page;
$page->printHeader("AnyCo Corp. Upload Logo");
$page->printMenu($sess->username, $sess->isPrivilegedUser());
printcontent($sess);
$page->printFooter();

// Functions

?>

printcontent()関数を追加します。

/**
* Print the main body of the page
*
* @param Session $sess
*/
function printcontent($sess) {
echo "<div id='content'>";
if (!isset($_FILES['lob_upload'])) {
printform();
} else {
$blobdata = file_get_contents($_FILES['lob_upload']['tmp_name']);
if (!$blobdata) {
// N.b. this test could be enhanced to confirm the image is a JPEG
printform();
} else {
$db = new \Oracle\Db("Equipment", $sess->username);
$sql = 'INSERT INTO pictures (pic)
VALUES(EMPTY_BLOB()) RETURNING pic INTO :blobbind';
$db->insertBlob($sql, 'Insert Logo BLOB', 'blobbind', $blobdata);
echo '<p>New logo was uploaded</p>';
}
}
echo "</div>";  // content
}

これで、HTMLフォームとフォームハンドラという2つの部分からなる一般的な構造になりました。INSERT文は、バインド値を使用してBLOBを表しています。新規DbクラスのinsertBlob()は、BLOBデータをバインド変数に関連付けて、レコードをコミットします。アップロードされたイメージがPICTURES表に追加されます。

フォーム関数printform()を追加して、ac_logo_upload.phpを完成させます。

/**
* Print the HTML form to upload the image
*
* Adding CSRF protection is an exercise for the reader
*/
function printform() {
echo <<<EOF
Upload new company logo:
<form action="ac_logo_upload.php" method="POST" enctype="multipart/form-data">
<div>
Image file name: <input type="file" name="lob_upload">
<input type="submit" value="Upload"
</div>
<form
EOF;
}

注意:
'EOF;'トークンは行の先頭に配置し、その後に空白が含まれないようにします。

このフォームが発行されると、printcontent()で見られるように、PHP Webサーバーが一時ファイル$_FILES['lob_upload']['tmp_name']にアップロードされたBLOBデータにアクセスできるようになります。

PHPにはファイルの場所や上限サイズを制御する様々なオプションがあります。詳細は、PHPのドキュメントを参照してください。AnyCoアプリケーションではデフォルト値を使用します。

ac_db.inc.phpを編集し、insertBlob()メソッドをDbクラスに追加します。

/**
* Insert a BLOB
*
* $sql = 'INSERT INTO BTAB (BLOBID, BLOBDATA)
*        VALUES(:MYBLOBID, EMPTY_BLOB()) RETURNING BLOBDATA INTO :BLOBDATA'
* Db::insertblob($sql, 'do insert for X', myblobid',
* $blobdata, array(array(":p", $p, -1)));
*
* $sql = 'UPDATE MYBTAB SET blobdata = EMPTY_BLOB()
*        RETURNING blobdata INTO :blobdata'
* Db::insertblob($sql, 'do insert for X', 'blobdata', $blobdata);
*
* @param string $sql An INSERT or UPDATE statement that returns a LOB locator
* @param string $action Action text for End-to-End Application Tracing
* @param string $blobbindname Bind variable name of the BLOB in the statement
* @param string $blob BLOB data to be inserted
* @param array $otherbindvars Bind variables. An array of tuples
*/
public function insertBlob($sql, $action, $blobbindname, $blob,
$otherbindvars = array()) {
$this->stid = oci_parse($this->conn, $sql);
$dlob = oci_new_descriptor($this->conn, OCI_D_LOB);
oci_bind_by_name($this->stid, $blobbindname, $dlob, -1, OCI_B_BLOB);
foreach ($otherbindvars as $bv) {
// oci_bind_by_name(resource, bv_name, php_variable, length)
oci_bind_by_name($this->stid, $bv[0], $bv[1], $bv[2]);
}
oci_set_action($this->conn, $action);
oci_execute($this->stid, OCI_NO_AUTO_COMMIT);
if ($dlob->save($blob)) {
oci_commit($this->conn);
}
}

insertBlob()メソッドは、最後のオプション・パラメータを通常のバインド変数として受け取ります。ac_logo_upload.phpのprintcontent()でコールするときには、これは使用されません。

第6章「REF CURSORを使用した備品レコードの表示」で説明したREF CURSORのバインドと同様に、BLOBは特別な型としてバインドされます。PHP OCI8にはOCI_B_CLOB定数もあり、CLOBのバインドに使用できます。LOBディスクリプタはPHP OCI8のOCI-Lobクラスのインスタンスで、データのアップロードと読取りに使用できる様々なメソッドがあります。oci_execute()がSQL INSERT文で処理されるとき、OCI_NO_AUTO_COMMITフラグが使用されます。これは、$dlob->save()メソッドでデータが挿入されるまで、データベース・トランザクションをオープンにしておく必要があるためです。最終的に、明示的なoci_commit()でBLOBをコミットします。

ブラウザでAnyCoアプリケーションを実行し、Administratorにログインします。左側のメニューで「Upload Logo」リンクをクリックします。コンピュータ上でJPEGイメージを探して選択します。この章の次の項に示すとおり、タイトルの入ったページ・ヘッダー内にイメージを表示するため、15から20ピクセルの高さのイメージを選択します。

「アップロード」ボタンをクリックします。

ロゴのフェッチとイメージの作成

ロゴの表示は、概念的には前の章で説明したグラフ・イメージの表示とほぼ同じです。ただし、BLOBはすでにJPEG形式になっているため、GD拡張モジュールは必要ありません。

新規のPHPファイルac_logo_img.phpを作成します。ファイルには、次が含まれています。

<?php

/**
* ac_logo_img.php: Create a JPEG image of the company logo
*
* Do not put any text or white space before the "<?php" tag because it will
* be incorporated into the image stream and corrupt the picture.
*
* @package Logo
*/

session_start();
require('ac_db.inc.php');
require('ac_equip.inc.php');

$sess = new \Equipment\Session;
$sess->getSession();
if (isset($sess->username) && !empty($sess->username)) {
$username = $sess->username;
} else { // index.php during normal execution, or other external caller
$username = "unknown-logo";
}

$db = new \Oracle\Db("Equipment", $username);
$sql = 'SELECT pic FROM pictures WHERE id = (SELECT MAX(id) FROM pictures)';
$img = $db->fetchOneLob($sql, "Get Logo", "pic");

header("Content-type: image/jpg");
echo $img;

?>

これは最新のロゴを問い合せて、JPEGストリームとして返します。イメージが破損しているようであれば、header()関数とecho関数のコールをコメント・アウトし、スクリプトによってテキストや空白が送出されていないかどうかを確認します。

ユーザー名チェックは、前の項で使用したものと異なります。Webユーザー名を認識する前のログイン・ページを含め、ロゴはすべてのページに表示されます。Dbはエンドツーエンドの追跡用にユーザー名を受け取るため、ac_logo_img.phpではブートストラップ・ユーザー名unknown-logoを使用します。

ac_db.inc.phpを編集し、fetchOneLob()メソッドをDbクラスに追加します。

/**
* Runs a query that fetches a LOB column
* @param string $sql A query that include a LOB column in the select list
* @param string $action Action text for End-to-End Application Tracing
* @param string $lobcolname The column name of the LOB in the query
* @param array $bindvars Bind variables. An array of tuples
* @return string The LOB data
*/
public function fetchOneLob($sql, $action, $lobcolname, $bindvars = array()) {
$col = strtoupper($lobcolname);
$this->stid = oci_parse($this->conn, $sql);
foreach ($bindvars as $bv) {
// oci_bind_by_name(resource, bv_name, php_variable, length)
oci_bind_by_name($this->stid, $bv[0], $bv[1], $bv[2]);
}
oci_set_action($this->conn, $action);
oci_execute($this->stid);
$row = oci_fetch_array($this->stid, OCI_RETURN_NULLS);
$lob = null;
if (is_object($row[$col])) {
$lob = $row[$col]->load();
$row[$col]->free();
}
$this->stid = null;
return($lob);
}

oci_fetch_array()オプションにOCI_RETURN_LOBSフラグを含めて、データをPHP文字列として返すことを指定することもできます。このコードでは、そうではなくロケータとして列が返されます。これは、ロケータに対する操作方法を示しています。ここではload()を使用してすべてのデータを読み取り、free()メソッドを使用してリソースを解放しています。大きなデータを扱うアプリケーションでは、ロケータread()メソッドを使用してLOBをチャンクで処理すると、メモリーを効率よく使用して大きなデータ・ストリームを処理できます。

OCI_B_BLOB型を使用してバインドすることからBLOBにしか使用できないinsertBlob()とは異なり、fetchOneLob()はBLOBデータとCLOBデータの両方に使用できます。

アプリケーションで複数のイメージ(またはイメージのチャンク)を、次のようにループで順次処理するとします。

while (($img = $db->fetchOneLob($sql, "Get Logo", "pic")) != null ) {
dosomething($img);
}

この場合、ループの下で$imgを明示的に設定解除するとPHPのピークのメモリー使用を少なくすることができます。

dosomething($img);
$unset($img);

これにより、現在の$imgに割り当てられているメモリーが、次のイメージ・データ・ストリームで再利用されます。そうしなかった場合、PHPによって2件目のイメージが構築され、$imgに割当てできる状態になるまで、元のイメージ・メモリーは解放されません。この最適化はAnyCoアプリケーションでは必要ありません。

ロゴの表示

アップロードされたロゴをAnyCoアプリケーションに表示するには、ac_equip.inc.phpを編集し、LOGO_URL定義を非コメント化します。

define('LOGO_URL', 'http://localhost/ac_logo_img.php');

URLが使用環境に対して適切であることを確認します。

ロゴはPage::printHeader()で表示されます。アプリケーションのすべての標準ページにロゴが表示されます。アプリケーションを再実行して、次を確認します。

データベースにイメージを保存しておくと、アプリケーション・データ全体をバックアップし、すべてのアプリケーションで共有できます。ただし、パフォーマンスについて考慮するのであれば、ロゴをディスクに書き込むキャッシュ技術を実装し、データベース・アクセスのオーバーヘッドを回避できる直接ストリームも検討します。新しいイメージがアップロードされるたびにアップロード・フォームでディスク・ファイルが再生成されます。

13 アプリケーションのデータベース使用状況の監視

この章のトピックは、次のとおりです。

- メタデータの概要

- メタデータの表示

- メタデータのその他の使用

- メタデータと永続接続

メタデータの概要

AnyCoアプリケーションの実装では、Oracleのエンドツーエンドのアプリケーション追跡機能のため、メタデータ値を使用してきました。設定された値は次のとおりです。

- クライアント識別子

- クライアント情報

- モジュール

- アクション

クライアント識別子には、各エンド・ユーザーに一意に関連付けられる値が格納されています。他の3つの値は実質的に、アプリケーションのタスクに関する降順のデータ階層です。

メタデータ値は半任意のテキスト文字列です。Oracleはメタデータ値を記録しておき、現在オープン中の接続のリスト内など、特定のデータベース機能でそれらを使用できるようにしています。これらの機能をデータベース管理者がどのように使用するのか、および、アプリケーションがどのように設計されているかによって、アプリケーションに設定する値は変わってきます。

AnyCoアプリケーションでは、どのWebユーザーがアプリケーションにログインしたかによって、クライアント識別子をsimonまたはadminに設定しています。クライアント情報は常に'AnyCo Corp.'に設定されます。ac_db.inc.phpのCLIENT_INFOを参照してください。モジュールは各ページでDbクラスのインスタンスが作成されるたびに設定されます。選択によっては、モジュール名Equipmentが常に使用されます。これは、このファイル・セットが従業員の備品の操作に関連することを示しています。アクションは実行されたSQL文によって異なります。

メタデータの表示

メタデータ値が使用される箇所を確認するため、AnyCoアプリケーションにログインし、いくつかのページ間をナビゲートします。

Windowsの場合、「スタート」メニューから「プログラム」(または「すべてのプログラム」)、「Oracle Database 12c」、データベース管理ページに移動の順に選択します。

Linuxの場合は、「Application」メニュー(Gnomeの場合)または「K」メニュー(KDEの場合)をクリックし、「Oracle Database 12c」、データベース管理ページに移動の順にポイントします。セッションページにナビゲートし、SYSTEMユーザーとしてログインします。次が表示されます。

複数のエントリが含まれています。Apacheによっていくつかのプロセスが作成され、どのプロセスも何らかのHTTPリクエストを処理している可能性があります。アプリケーション・ページをナビゲートすると、そのページ・リクエストが別のApacheプロセスによって処理されます。

いずれかのAnyCoセッションのSID番号をクリックしてドリルダウンします。

このページではクライアントとアプリケーションの情報も確認できます。

Oracle Database管理ツールでは、SQL文とその実行についての統計を確認できます。調整が不十分な文や、頻繁に使用されている文が見つかった場合は、エンドツーエンドの追跡メタデータでソースのPHPファイルを特定できるので、確認と再設計を簡単に行うことができます。文の場合は、各文の最初の実行時の有効なメタデータが、記録されるデータです。

メタデータの正確性は、データベースに接続するアプリケーション内での使用の一貫性に依存します。

クライアント識別子の使用の詳細は、次の技術ドキュメントを参照してください。

http://www.oracle.com/technetwork/articles/dsl/php-web-auditing-171451.html

メタデータのその他の使用

Oracle Databaseの管理ペインは、接続と文の実行に関してOracle Databaseが記録したすべての情報の簡易ビューです。V$SESSIONやV$SQLAREAなど、様々な標準データベース管理ビューにも、これらの追跡メタデータが含まれています。問合せを作成したりその他のツールを使用して、この情報を提供できます。

クライアント識別子メタデータはデータ・アクセスを制限するときに使用できます。Oracle Databaseでは、各SQL文を手動で強化して、クライアント識別子をチェックする方法でアクセスを制限できます。

select * from equipment
where sys_context('userenv', 'client_identifier') = 'admin';

別のクライアント識別子のWebユーザーがログインした場合、WHERE条件がfalseに評価され、行は返されません。Administratorのみがデータを表示できます。

Oracle DatabaseのEnterprise Editionでは、データ・アクセスを自動的に制限するポリシー・ルールの作成が仮想プライベート・データベース機能でサポートされています。そのため、個々のSQL文を変更する必要はありません。監査を有効にした場合は、クライアント識別子は監査ログにも記録されます。

クライアント識別子を使用してセキュリティを施す場合は、アプリケーション・コードの整合性を保つことが重要です。クライアント識別子の欠落や偽装を回避することが不可欠です。

メタデータと永続接続

AnyCoアプリケーションでは、PHPプロセスがスクリプトを処理していなくてもオープン状態を保つPHP OCI8永続接続を使用します。メタデータ値がユーザー・スクリプトの後でリセットされないため、PHPプロセスとそのデータベース接続がアイドルのときでも、セッションスクリーンでメタデータを確認できます。また、そのPHPプロセスで処理される後続のoci_pconnect()で明示的に再設定されないかぎり、現在の値が有効な値として維持されます。

非永続の接続を使用してoci_connect()またはoci_new_connect()をコールした場合、PHPスクリプトが完了するたびに接続がクローズされてしまうため、このような動作にはなりません。

アイドル時間がほとんどないビジー・システムで、永続接続のメタデータが残ることは通常問題ではありません。データベース管理者にとっての問題領域は、アイドルではないビジーの接続です。メタデータの設定解除を行わないことは、PHPとデータベース間のラウンドトリップを回避できるため、パフォーマンスを最大限に高めることができます。これはシステム全体の速度を下げるため、お薦めしません。しかし、Dbデストラクタに次を追加して、メタデータを強制的にクリアできます。

$this->stid = oci_parse($this->conn,
"begin
dbms_session.clear_identifier;
dbms_application_info.set_client_info('');
dbms_application_info.set_module('', '');
end;");
oci_execute($this->stid);

これを実行する場合は、最初にWebサーバーを再起動して、既存のPHP永続データベース接続をすべてクローズします。

完璧な解決策というものは存在しません。PHPプロセスがクラッシュした場合、値をクリアしたり、データベースに非永続接続をクローズするよう通知したりできません。

14 グローバル・アプリケーションの構築

この章では、PHPおよびOracle Database環境でのグローバル・アプリケーションの開発について説明します。グローバルなインターネット・アプリケーションの開発およびデプロイに関連付けられている基本的なタスク(ロケール認識の開発、ユーザー選択言語でのHTMLコンテンツの構築、ユーザーのロケールの表記規則に従ったデータの表示など)について説明します。

様々なロケールをサポートするグローバル・インターネット・アプリケーションを構築するには、適切な開発の実施が必要です。ロケールは、各国語とその言語が使用されている地域を指します。アプリケーション自体がユーザーのロケール・プリファレンスを認識し、ユーザーが考えているとおりの表記規則に従ってコンテンツを表示できる必要があります。正しい日付書式や数値書式など、該当するロケールの特性を使用してデータを表示することが重要です。Oracle Databaseは完全に国際化されており、グローバル・アプリケーションを開発およびデプロイするためのグローバル・プラットフォームを提供します。

この章のトピックは、次のとおりです。

- OracleとPHP間の環境の確立

- 文字列の操作

- ユーザーのロケールの決定

- ロケール認識の開発

- HTMLページのエンコーディング

- 翻訳のためのHTMLページのコンテンツの編成

- ユーザーが考えているとおりの表記規則を使用したデータの表示

OracleとPHP間の環境の確立

グローバル・アプリケーションを構築するための最初のステップは、PHPエンジンとOracle Database間の接続を正しく設定することです。これにより、すべての層にわたるデータの整合性が保証されます。ほとんどのインターネット・ベースの標準では、文字エンコーディングとしてUnicodeがサポートされています。この章では、データ交換のキャラクタ・セットとしてUnicodeを使用することに焦点を絞って説明します。

PHPはオラクル社のC言語OCIインタフェースと、OCIおよびPHPに適用されるルールを使用します。Oracleロケールの動作(OCIアプリケーションで使用されるクライアント・キャラクタ・セットを含む)は、NLS_LANG環境変数で定義します。この環境変数の形式は次のとおりです。

<language>_<territory>.<character set>

たとえば、Unicodeでアプリケーションを実行するブラジル国内のポルトガル語ユーザーの場合、NLS_LANGは次のように設定されます。

BRAZILIAN PORTUGUESE_BRAZIL.AL32UTF8

言語および地域の設定によって、Oracle日付書式、エラー・メッセージ言語、ソート順序に使用されるルールなどのOracleの動作が制御されます。キャラクタ・セットAL32UTF8は、UTF-8のOracle名です。

NLS_LANG環境変数の詳細は、Oracle Databaseのインストレーション・ガイドを参照してください。

Oracle LinuxのApacheにPHPをインストールするときに、/etc/sysconfig/httpdでNLS_LANGを設定できます。

export NLS_LANG='BRAZILIAN PORTUGUESE_BRAZIL.AL32UTF8'

この変更を実装するには、Webリスナーを再起動する必要があります。

文字列の操作

PHPは、ISO-8859-1キャラクタ・セットで動作するように設計されています。それ以外のキャラクタ・セット(特にマルチバイト・キャラクタ・セット)を処理する場合は、一連のマルチバイト文字列関数を使用できます。これらの機能を有効にするには、PHPのmbstring拡張モジュールを有効にする必要があります。

アプリケーション・コードでmb_strlen()などの関数を使用して、文字列内の文字数を計算する必要があります。これによって、文字列内のバイト数を戻すstrlen()とは異なる値が戻される場合があります。

mbstring拡張モジュールを有効にし、Webサーバーを再起動すると、いくつかの構成オプションを使用できるようになります。mbstring.func_overloadをOverload設定のいずれかに設定して、標準のPHP文字列関数の動作を変更できます。

詳細は、PHPのmbstringに関する次のリファレンス・マニュアルを参照してください。

http://www.php.net/mbstring

ICUライブラリをラップするPHP intl拡張モジュールもまた、文字列の操作用として広く使用されています。次を参照してください。

http://www.php.net/intl

ユーザーのロケールの決定

グローバル環境のアプリケーションでは、ロケール・プリファレンスが異なるユーザーに対応する必要があります。ユーザーの優先ロケールを決定した後は、そのロケールの言語でHTMLコンテンツを構築し、そのロケールの表記規則に従います。

ユーザーのロケールを決定する一般的な方法として、ブラウザのデフォルトのISOロケール設定を使用する方法があります。通常、ブラウザは、Accept Language HTTPヘッダーでHTTPサーバーにそのロケール・プリファレンス設定を送信します。Accept LanguageヘッダーがNULLの場合は、使用可能なロケール・プリファレンス情報がなく、事前定義のデフォルトのロケールにフォールバックされます。

次のPHPコードは、$_SERVERサーバー変数を使用して、Accept Language HTTPヘッダーからISOロケールを取り出します。

$s = $_SERVER["HTTP_ACCEPT_LANGUAGE"]

ロケール認識の開発

ユーザーのロケール・プリファレンスを決定すると、日付、時間、通貨の書式などのロケール依存の関数をコールして、ロケールの表記規則に従ってHTMLページを書式設定できます。

様々なプログラミング環境で実装されるグローバル・アプリケーションを構築する場合は、異なる環境間でユーザー・ロケール設定を同期できるようにする必要があります。たとえば、PL/SQLプロシージャをコールするPHPアプリケーションでは、PL/SQLプロシージャをコールする前に、ISOロケールを対応するNLS_LANGUAGE値およびNLS_TERRITORY値にマップし、ユーザーのロケールに合わせてパラメータ値を変更する必要があります。PL/SQL UTL_I18Nパッケージには、ISOロケールとOracleロケール間でマップできるマッピング関数が含まれています。

表14-1に、よく使用されるロケールをISO環境およびOracle環境に定義する方法を示します。

表 14-1: ISO、SQL、PL/SQLの各プログラミング環境でのロケールの表現

ロケール

ロケールID

NLS_LANGUAGE

NLS_TERRITORY

中国語(中華人民共和国)

zh-CN

SIMPLIFIED CHINESE

CHINA

中国語(台湾)

zh-TW

TRADITIONAL CHINESE

TAIWAN

英語(アメリカ合衆国)

en-US

AMERICAN

AMERICA

英語(イギリス)

en-GB

ENGLISH

UNITED KINGDOM

フランス語(カナダ)

fr-CA

CANADIAN FRENCH

CANADA

フランス語(フランス)

fr-FR

FRENCH

FRANCE

ドイツ語

de

GERMAN

GERMANY

イタリア語

it

ITALIAN

ITALY

日本語

ja

JAPANESE

JAPAN

韓国語

ko

KOREAN

KOREA

ポルトガル語(ブラジル)

pt-BR

BRAZILIAN PORTUGUESE

BRAZIL

ポルトガル語

pt

PORTUGUESE

PORTUGAL

スペイン語

es

SPANISH

SPAIN

HTMLページのエンコーディング

HTMLページのエンコーディングは、ブラウザおよびインターネット・アプリケーションに関する重要な情報です。ページ・エンコーディングは、インターネット・アプリケーションがサービスを提供するロケールに使用されるキャラクタ・セットです。ブラウザは、正しいフォントおよびキャラクタ・セットのマッピング表を使用してHTMLページを表示できるように、ページ・エンコーディングを認識している必要があります。インターネット・アプリケーションは、HTMLフォームに入力されたデータを処理できるように、HTMLページ・エンコーディングを認識している必要があります。

ロケールごとに異なるネイティブ・エンコーディングを使用するのではなく、すべてのページ・エンコードでUTF-8(Unicodeエンコーディング)を使用することをお薦めします。このエンコーディングを使用すると、グローバル・アプリケーションのコーディングを簡略化できるのみでなく、単一ページに多言語コンテンツを配置できます。

HTMLページ用のページ・エンコーディングの指定

HTMLページのエンコーディングをHTTPヘッダーまたはHTMLページ・ヘッダーに指定できます。

HTTPヘッダーでのエンコーディングの指定

HTMLページ・エンコーディングをHTTPヘッダーに指定するには、HTTP仕様にContent-Type HTTPヘッダーを含めます。このヘッダーで、コンテンツ・タイプおよびキャラクタ・セットを指定します。Content-Type HTTPヘッダーの形式は次のとおりです。

Content-Type: text/html; charset=utf-8

charsetパラメータで、HTMLページのエンコーディングを指定します。charsetパラメータに指定可能な値は、ブラウザでサポートされている文字エンコーディングのIANA名です。

HTMLページ・ヘッダーでのエンコーディングの指定

この方法は、主に静的なHTMLページに使用します。HTMLページ・エンコーディングをHTMLページ・ヘッダーに指定するには、文字エンコーディングを次のようにHTMLヘッダーに指定します。

<meta http-equiv="Content-Type" content="text/html;charset=utf-8">

charsetパラメータで、HTMLページのエンコーディングを指定します。Content-Type HTTPの場合と同様に、charsetパラメータに指定可能な値は、ブラウザでサポートされている文字エンコーディングのIANA名です。

PHPへのページ・エンコーディングの指定

PHP構成変数を次のように設定することで、Content-Type HTTPヘッダーでHTMLページのエンコーディングを指定できます。

default_charset = UTF-8

この設定によって送信ページは変換されません。アプリケーションでは、サーバー生成のページをUTF-8でエンコードする必要があります。

翻訳のためのHTMLページのコンテンツの編成

ユーザー・インタフェースをユーザーのローカル言語で使用できるようにすることは、アプリケーションのグローバル化の基本的なタスクです。HTMLページのコンテンツの翻訳可能なソースは、次のカテゴリに分類されます。

- アプリケーション・コードに含まれているテキスト文字列

- 静的なHTMLファイル、イメージ・ファイル、およびCSSなどのテンプレート・ファイル

- データベースに格納された動的データ

PHPの文字列

PHPアプリケーション・ロジック内の翻訳可能な文字列を外部化して、そのテキストを簡単に翻訳できるようにする必要があります。これらのテキスト・メッセージは、翻訳対象データのタイプおよびボリュームに応じて、フラット・ファイルまたはデータベース表に格納できます。

静的ファイル

HTMLファイルなどの静的ファイルは簡単に翻訳できます。これらのファイルを翻訳するときは、UTF-8をファイル・エンコーディングとして、対応する言語に翻訳する必要があります。翻訳したファイルの言語を区別するには、静的ファイルを言語ごとに異なるディレクトリにステージングするか、または異なるファイル名でステージングします。

データベースのデータ

通常、製品名や製品説明などの動的な情報がデータベースに格納されています。様々な翻訳を区別するには、この情報を保持しているデータベース・スキーマに言語を示すための列を含める必要があります。目的の言語を選択するには、問合せにWHERE句を含める必要があります。

ユーザーが考えているとおりの表記規則を使用したデータの表示

アプリケーションのデータは、ユーザーが考えているとおりに表示される必要があります。そうでない場合、データの意味が誤って解釈される可能性があります。たとえば、12/11/05という日付は、アメリカ合衆国では2005年12月11日を意味するのに対して、イギリスでは2005年11月12日を意味します。ユーザーの数値書式および通貨書式にも同様の違いが存在します。たとえば、.という記号はアメリカ合衆国では小数点セパレータですが、ドイツでは千単位セパレータです。

各言語に独自のソート・ルールがあります。アルファベットの文字の順序に従う言語、文字の画数に従う言語、単語の発音に従う言語などがあります。ユーザーが慣れている言語順序でソートされていないデータを表示すると、情報の検索が難しくなり、時間がかかることがあります。

アプリケーションのロジックおよびデータベースから取得されるデータ量によっては、アプリケーション・レベルではなくデータベース・レベルでデータを書式設定する方が適切な場合があります。Oracle Databaseには、ユーザーのロケール・プリファレンスがわかっている場合にデータの表示を調整するのに役立つ機能が多く用意されています。以降の項では、SQLでのロケール依存の操作の例を示します。

Oracleの日付書式

Oracle Databaseの日付表示書式には、標準の日付、短い日付、長い日付の3種類があります。次の例では、アメリカ合衆国およびブラジルのポルトガル語ユーザーでの短い日付書式と長い日付書式の違いを示します。

SQL> alter session set nls_territory=america nls_language=american;

Session altered.

SQL> select employee_id EmpID,
2  substr(first_name,1,1)||'.'||last_name "EmpName",
3  to_char(hire_date,'DS') "Hiredate",
4  to_char(hire_date,'DL') "Long HireDate"
5  from employees
6* where employee_id <105;

EMPID EmpName                     Hiredate   Long HireDate
---------- --------------------------- ---------- -----------------------------
100 S.King                      06/17/1987 Wednesday, June 17, 1987
101 N.Kochhar                   09/21/1989 Thursday, September 21, 1989
102 L.De Haan                   01/13/1993 Wednesday, January 13, 1993
103 A.Hunold                    01/03/1990 Wednesday, January 3, 1990
104 B.Ernst                     05/21/1991 Tuesday, May 21, 1991

SQL> alter session set nls_language = 'BRAZILIAN PORTUGUESE' nls_territory = 'BRAZIL';

Sessão alterada.

SQL> select employee_id EmpID,
2  substr(first_name,1,1)||'.'||last_name "EmpName",
3  to_char(hire_date,'DS') "Hiredate",
4  to_char(hire_date,'DL') "Long HireDate"
5  from employees
6* where employee_id <105;

EMPID EmpName      Hiredate    Long HireDate
----- --------     ---------   -------------------------------
100   S.King       17/6/2003   terça-feira, 17 de junho de 2003
101   N.Kochhar    21/9/2005   quarta-feira, 21 de setembro de 2005
102   L.De Haan    13/1/2001   sábado, 13 de janeiro de 2001
103   A.Hunold     3/1/2006    terça-feira, 3 de janeiro de 2006
104   B.Ernst      21/5/2007   segunda-feira, 21 de maio de 2007

Oracleの数値書式

次の例では、アメリカ合衆国とブラジルのポルトガル語ユーザーでの小数点文字およびグループ・セパレータの違いを示します。

SQL> alter session set nls_territory=america;

Session altered.

SQL> select employee_id EmpID,
2  substr(first_name,1,1)||'.'||last_name "EmpName",
3  to_char(salary, '99G999D99') "Salary"
4  from employees
5* where employee_id <105

EMPID EmpName                     Salary
---------- --------------------------- ----------
100 S.King                       24,000.00
101 N.Kochhar                    17,000.00
102 L.De Haan                    17,000.00
103 A.Hunold                      9,000.00
104 B.Ernst                       6,000.00

SQL> alter session set nls_territory=brazil;

Session altered.

SQL> select employee_id EmpID,
2  substr(first_name,1,1)||'.'||last_name "EmpName",
3  to_char(salary, '99G999D99') "Salary"
4  from employees
5* where employee_id <105

EMPID EmpName                     Salary
---------- --------------------------- ----------
100 S.King                       24.000,00
101 N.Kochhar                    17.000,00
102 L.De Haan                    17.000,00
103 A.Hunold                      9.000,00
104 B.Ernst                       6.000,00

注意:
Oracleで使用されている小数点セパレータおよび千単位セパレータがそれぞれ'.'と','でない場合、返されたデータ値で計算を実行したときにPHPエラーが発生します。これは、PHPではセパレータをPHP形式で解析できない場合に、数字を含んだ文字列変数を整数または浮動小数の変数に正しく変換できないためです。この問題を回避するには、次を使用して書式を明示的に設定します。

alter session set nls_numeric_characters = '.,'

Oracleの言語ソート

スペイン語では、伝統的にch、llおよびñが一意の文字として扱われ、それぞれc、lおよびnの後に順序付けられます。次の例では、ChenとChungという従業員名にスペイン語のソートを使用した場合の結果を示します。

SQL> alter session set nls_sort=binary;

Session altered.

SQL> select employee_id EmpID,
2         last_name "Last Name"
3  from employees
4  where last_name like 'C%'
5* order by last_name

EMPID Last Name
---------- -------------------------
187 Cabrio
148 Cambrault
154 Cambrault
110 Chen
188 Chung
119 Colmenares

6 rows selected.

SQL> alter session set nls_sort=spanish_m;

Session altered.

SQL> select employee_id EmpID,
2         last_name "Last Name"
3  from employees
4  where last_name like 'C%'
5* order by last_name

EMPID Last Name
---------- -------------------------
187 Cabrio
148 Cambrault
154 Cambrault
119 Colmenares
110 Chen
188 Chung

6 rows selected.

Oracleのエラー・メッセージ

NLS_LANGUAGE パラメータは、データベースから戻されるデータベース・エラー・メッセージの言語も制御します。SQL文を発行する前にこのパラメータを設定すると、言語固有のデータベース・エラー・メッセージがアプリケーションに戻されるようになります。

次のサーバー・メッセージを考えてみます。

ORA-00942: table or view does not exist

NLS_LANGUAGEパラメータをBRAZILIAN PORTUGUESEに設定すると、このサーバー・メッセージは次のように表示されます。

ORA-00942: a tabela ou view não existe

Oracle Databaseのグローバリゼーション・サポート機能の詳細は、『Oracle Database Express Edition 2日で開発者ガイド』のグローバル環境での作業に関する項を参照してください。

索引
A  B  C  D  E  F  G  H  I  L  M  N  O  P  R  S  T  U  V  W

記号

$_GETスーパーグローバル配列, 6.5

$actionパラメータ, 5.1, 6.5

$atfirstrowパラメータ, 5.1, 5.1

$bindvarsパラメータ, 5.1

$conn属性, 3.2

$firstrowパラメータ, 5.1

$numrowsパラメータ, 5.1, 5.1

$otherbindvarsパラメータ, 6.5

$rcnameパラメータ, 6.5

$resパラメータ, 5.1

$sessパラメータ, 5.1, 5.1

$sess->getSession()コール, 5.1

$sqlパラメータ, 5.1, 6.5

$startrowパラメータ, 5.1

__construct()メソッド, 3.2

A

ac_cred.inc.php, 3.1

作成, 3.1

ac_db.inc.php, 3.2

作成, 3.2

AL32UTF8キャラクタ・セット, 14.1 AnyCoサンプル・アプリケーション, 2.2.2 Apache

Windowsでのインストールのテスト2.4

Apache HTTPサーバー

インストール, 2.3

Linuxでの手動, 2.3.2.2

Linuxでの, 2.3.2

Windowsでの, 2.3.1

テスト, 2.4

Linuxでのインストール, 2.3.2 環境の設定

Linuxでの, 2.3.2.3

ユーザー・ディレクトリの設定, 2.3.2.4 インストールのテスト, 2.4 アプリケーション

ロケール固有の関数のコール, 14.4

翻訳可能な文字列の外部化, 14.6.1

グローバル化, 14

HTMLおよびGIFの翻訳, 14.6.2

ユーザー・インタフェースの翻訳, 14.6

UTF-8ページ・エンコーディング, 14.5

array_push()関数, 5.1

B

バインド・パラメータ

rc, 6.5

バインド変数

id, 6.4

rc, 6.4

C

calcstartrow()関数, 5.1

コール

oci_bind_by_name(), 3.3

キャラクタ・セット

AL32UTF8, 14.1

グローバリゼーション設定, 14.2

UTF-8, 14.1

charsetパラメータ, 14.5.1.1 接続

HRユーザー, 2.2.1

定数

NUMRECORDSPERPAGE, 5.1, 5.1, 5.2

表記規則

データの表示, 14.7

D

データベース, 2.2

動的な情報, 14.6.3

インストール, 2.2

HRユーザーのロック解除, 2.2.1

データベース・アクセス・クラス, 3.2

作成, 3.2

データベース常駐の接続プール 2.2.2 Oracleの日付書式, 14.7.1 Dbクラス, 5.1, 6.5 Db::execFetchAll()メソッド, 5.1 Db::execFetchPage()メソッド, 5.1, 5.1 Db::refcurExecFetchAll()メソッド, 6.5 DRCP

アーキテクチャ, 2.2.2

プール, 2.2.2

プールの起動, 2.2.3

E

環境変数

NLS_LANG, 14.1

NLS_LANGUAGE, 14.4, 14.7.4

NLS_TERRITORY, 14.4

エラー

NLS_LANGUAGE, 14.7.4

F

ファイル

HTMLおよびGIFの翻訳, 14.6.2

関数

array_push(), 5.1

calcstartrow(), 5.1

getempname(), 6.5

htmlentities(), 3.5

htmlspecialchars(), 3.5

oci_connect(), 2.2.2, 2.2.2, 2.2.2, 3.4

oci_fetch_array(), 3.3

oci_new_connect(), 2.2.2, 2.2.2, 3.4

oci_pconnect(), 2.2.2, 3.2

oci_set_client_identifier(), 3.2

printcontent(), 5.1, 5.1, 6.5

printnextprev(), 5.1, 5.1

printnorecords(), 5.1, 5.1

printrecords(), 5.1, 6.5

var_dump(), 3.5

G

getempname()関数, 6.5

グローバル化

アプリケーション, 14

ロケール固有の関数のコール, 14.4

キャラクタ・セット, 14.2

日付書式, 14.7.1

ユーザー・ロケールの決定, 14.3

動的な情報, 14.6.3

HTMLページ・エンコーディング, 14.5

言語ソート, 14.7.3

NLS_LANGUAGE, 14.7.4

数値書式, 14.7.2

PHPおよびOracle環境, 14.1

データの表示, 14.7

データのソート, 14.7

ユーザー・インタフェースの翻訳, 14.6

H

HRユーザー, 2.2.1

HTML

ページ・エンコーディング, 14.5, 14.5.1.1, 14.5.1.2, 14.5.2

ページ・ヘッダー, 14.5.1.2

htmlentities()関数, 3.5 htmlspecialchars()関数, 3.5 HTTPヘッダー

ページ・エンコーディング, 14.5.1.1

I

idバインド変数, 6.4

インストール

Apache HTTPサーバー, 2.3

LinuxでのApache HTTPサーバー, 2.3.2

WindowsでのApache HTTPサーバー, 2.3.1

Apache httpdパッケージ, 2.3.2.1

LinuxでのApache, 手動, 2.3.2.2

NetBeans 7.0 IDE, 2.7

LinuxでのNetBeans IDE, 2.7.2

WindowsでのNetBeans IDE, 2.7.1

OCI8, 2.5.2.2

Oracle Database, 2, 2.2, 2.2

PHP, 2.5

Oracle LinuxでのPHPおよびOCI8, 2.5.2.1

LinuxでのPHP, 2.5.2

WindowsでのPHP, 2.5.1, 2.5.1

L

言語ソート, 14.7.3

ロケール, 14.3

M

メソッド

__construct(), 3.2

Db::execFetchAll(), 5.1

Db::execFetchPage(), 5.1

Db::refcurExecFetchAll(), 6.5

refcurExecFetchAll(), 6.5

N

NetBeans

構成, 2.7.3

インストール, 2.7

Linuxでの, 2.7.2

Windowsでの, 2.7.1

使用, 2.7.4 NLS_LANG環境変数, 14.1 NLS_LANGUAGE環境変数, 14.4, 14.7.4 NLS_TERRITORY環境変数, 14.4 Oracleの数値書式, 14.7.2 NUMRECORDSPERPAGE定数, 5.1, 5.1, 5.2

O

取得

Oracle Database, 2.2

oci_bind_by_name()コール, 3.3 oci_connect()関数, 2.2.2, 2.2.2, 2.2.2, 3.4 oci_fetch_array()関数, 3.3 OCI_FETCHSTATEMENT_BY_ROW定数, 3.3 oci_new_connect()関数, 2.2.2, 2.2.2, 3.4 oci_pconnect()関数, 2.2.2, 3.2 oci_set_client_identifier()関数, 3.2 Oracle

日付書式, 14.7.1

環境の確立, 14.1

数値書式, 14.7.2

Oracle Database, 2.2

インストール, 2, 2.2

取得およびインストール, 2.2

前提条件, 2.1

P

パッケージ本体, 6.1

PACKAGE BODY型, 6.1

パッケージ仕様部, 6.1

PACKAGE型, 6.1

パラメータ

$action, 5.1, 6.5

$atfirstrow, 5.1, 5.1

$bindvars, 5.1

$firstrow, 5.1

$numrows, 5.1, 5.1

$otherbindvars, 6.5

$rcname, 6.5

$res, 5.1

$sess, 5.1, 5.1

$sql, 5.1, 6.5

$startrow, 5.1

charset, 14.5.1.1

PHP

アクティブ・プロセス, 2.2.2

OCI8拡張モジュールの追加, 2.5.2.2

キャラクタ・セット, 14.2

ユーザー・ロケールの決定, 14.3

環境の確立, 14.1

翻訳可能な文字列の外部化, 14.6.1

アプリケーションのグローバル化, 14

HTMLページ・エンコーディング, 14.5.2

アイドル・プロセス, 2.2.2

インストール, 2.5

Linuxでの, 2.5.2

Windowsでの, 2.5.1

Linuxでのインストール, 2.5.2 Windowsでのインストール, 2.5.1 OCI8拡張モジュール, 1.2 PHPDocumentor, 3.2 インストール後タスク, 2.5.3 テスト, 2.6.2 インストールのテスト, 2.6 HTMLおよびGIFファイルの翻訳, 14.6.2 PHP(PHP Hypertext Preprocessor), 1 PHPコール

oci_bind_by_name(), 3.3

PHPコマンド

define(), 3.1

include(), 3.2

include_once(), 3.2

require(), 3.1, 3.2

require_once(), 3.2

PHP関数

array_push(), 5.1

calcstartrow(), 5.1

getempname(), 6.5

htmlentities(), 3.5

htmlspecialchars(), 3.5

oci_connect(), 2.2.2, 2.2.2, 2.2.2, 3.4

oci_fetch_array(), 3.3

oci_new_connect(), 2.2.2, 2.2.2, 3.4

oci_pconnect(), 2.2.2, 3.2

oci_set_client_identifier(), 3.2

printcontent(), 5.1, 5.1, 6.5

printnextprev(), 5.1, 5.1

printnorecords(), 5.1, 5.1

printrecords(), 5.1, 6.5

var_dump(), 3.5

PHP OCI8, 2.5.2.2 PL/SQL

UTL_I18Nパッケージ, 14.4

PL/SQLデータ型

REF CURSOR, 6.3

Oracle Databaseの前提条件, 2.1 printcontent()関数, 5.1, 5.1, 6.5 printnextprev()関数, 5.1, 5.1 printnorecords()関数, 5.1, 5.1 printrecords()関数, 5.1, 6.5

R

rcバインド・パラメータ, 6.5

rcバインド変数, 6.4

REF CURSOR, 6.3

refcurExecFetchAll()メソッド, 6.5

S

ShowEquipmentパッケージ, 6.5

ソート, 14.7, 14.7.3

T

テスト

WindowsでのApacheインストール, 2.4

OracleへのPHP接続, 2.6.2

タイプ

PACKAGE, 6.1

PACKAGE BODY, 6.1

U

HRアカウントのロック解除, 2.2.1

ユーザー・インタフェース

翻訳可能な文字列の外部化, 14.6.1

翻訳, 14.6

UTF-8

キャラクタ・セット, 14.1

HTMLページ・エンコーディング, 14.5

UTL_I18Nパッケージ, 14.4

V

V$CPOOL_CC_STATS, 2.2.3

var_dump()関数, 3.5

W

Webブラウザ

WindowsでのApacheインストールのテスト, 2.4

オラクル社の法律上の注意点について

コピーライトについて

Copyright © 1994-2014, Oracle and/or its affiliates.All rights reserved.

商標と登録商標について

OracleおよびJavaはOracle Corporationおよびその関連企業の登録商標です。その他の名称は、それぞれの所有者の商標または登録商標です。

Intel、Intel Xeonは、Intel Corporationの商標または登録商標です。すべてのSPARCの商標はライセンスをもとに使用し、SPARC International, Inc.の商標または登録商標です。AMD、Opteron、AMDロゴ、AMD Opteronロゴは、Advanced Micro Devices, Inc.の商標または登録商標です。UNIXは、The Open Groupの登録商標です。

ライセンス制約の保証と結果的に生じる損害の免責

このソフトウェアおよび関連ドキュメントの使用と開示は、ライセンス契約の制約条件に従うものとし、知的財産に関する法律により保護されています。ライセンス契約で明示的に許諾されている場合もしくは法律によって認められている場合を除き、形式、手段に関係なく、いかなる部分も使用、複写、複製、翻訳、放送、修正、ライセンス供与、送信、配布、発表、実行、公開または表示することはできません。このソフトウェアのリバース・エンジニアリング、逆アセンブル、逆コンパイルは互換性のために法律によって規定されている場合を除き、禁止されています。

保証免責

ここに記載された情報は予告なしに変更される場合があります。また、誤りが無いことの保証はいたしかねます。誤りを見つけた場合は、オラクル社までご連絡ください。

制限付権利

このソフトウェアまたは関連ドキュメントを、米国政府機関もしくは米国政府機関に代わってこのソフトウェアまたは関連ドキュメントをライセンスされた者に提供する場合は、次の通知が適用されます。

U.S. GOVERNMENT END USERS: Oracle programs, including any operating system, integrated software, any programs installed on the hardware, and/or documentation, delivered to U.S. Government end users are "commercial computer software" pursuant to the applicable Federal Acquisition Regulation and agency-specific supplemental regulations.As such, use, duplication, disclosure, modification, and adaptation of the programs, including any operating system, integrated software, any programs installed on the hardware, and/or documentation, shall be subject to license terms and license restrictions applicable to the programs.No other rights are granted to the U.S. Government.

危険な用途への使用について

このソフトウェアもしくはハードウェアは様々な情報管理アプリケーションでの一般的な使用のために開発されたものです。このソフトウェアもしくはハードウェアは、危険が伴うアプリケーション（人的傷害を発生させる可能性があるアプリケーションを含む）への用途を目的として開発されていません。このソフトウェアもしくはハードウェアを危険が伴うアプリケーションで使用する際、安全に使用するために、適切な安全装置、バックアップ、冗長性（redundancy）、その他の対策を講じることは使用者の責任となります。このソフトウェアもしくはハードウェアを危険が伴うアプリケーションで使用したことに起因して損害が発生しても、オラクル社およびその関連会社は一切の責任を負いかねます。

第三者のコンテンツ、製品、サービスに対する免責事項

このソフトウェアまたはハードウェア、そしてドキュメントは、第三者のコンテンツ、製品、サービスへのアクセス、あるいはそれらに関する情報を提供することがあります。オラクル社およびその関連会社は、第三者のコンテンツ、製品、サービスに関して一切の責任を負わず、いかなる保証もいたしません。オラクル社およびその関連会社は、第三者のコンテンツ、製品、サービスへのアクセスまたは使用によって損失、費用、あるいは損害が発生しても一切の責任を負いかねます。

ドラフトのアルファ版およびベータ版ドキュメントについて

このドキュメントが試作版の場合:

このドキュメントは試作版であり、デモおよび暫定使用のみを目的としたものです。このソフトウェアを使用するハードウェアに限定するものではありません。オラクル社およびその関連会社は、このドキュメントに関して一切の責任を負わず、いかなる保証もいたしません。また、このドキュメントを使用したことによって損失、費用、あるいは損害が発生しても、一切の責任を負いかねます。