PHP の学習: 第 3 回 認証、オブジェクト、例外、ストリーム処理

このチュートリアルは、単純なワークフロー・アプリケーションの構築プロセスを通して PHP の使用方法を説明する 3 部構成のシリーズ「PHP の学習」の第 3 回です。今回のチュートリアルでは、HTTP 認証の使用方法、ファイルをストリーム処理する方法、そしてオブジェクトと例外の作成方法を学びます。

Nicholas Chase, Founder, NoTooMi

Nicholas Chase は、NoTooMi の創設者です。彼は大手企業の技術文書作成の他、Lucent Technologies、Sun Microsystems、Oracle、Tampa Bay Buccaneers などの企業の Web サイト開発に従事してきました。高校の物理教師、低レベル放射性廃棄物施設のマネージャー、オンライン SF 雑誌の編集者、マルチメディア・エンジニア、Oracle インストラクター、そして双方向通信の会社での最高技術責任者としての経歴があります。『XML Primer Plus』(Sams、2002年) をはじめ、複数の本の著者でもあります。



2013年 12月 12日

始める前に

このチュートリアルでは、HTTP 認証の使用方法、ファイルをストリーム処理する方法、オブジェクトおよび例外の作成方法を学びます。

このチュートリアルについて

このチュートリアルでは、PHP について学習するこのシリーズの第 1 回から構築している単純なワークフロー・アプリケーションを完成させます。今回は、HTTP 認証、Web でアクセスできない場所にある文書をストリーム処理する機能、そして例外処理を追加します。また、アプリケーションの一部をオブジェクトにして整理します。

全体としては、管理者がユーザーに一般公開するファイルを承認できるようにします。このプロセスを通して、以下のトピックを取り上げます。

  • ブラウザー・ベースの HTTP 認証を有効にして使用する方法
  • ファイルのデータをストリーム処理する方法
  • クラスおよびオブジェクトを作成する方法
  • オブジェクトのメソッドおよびプロパティーを使用する方法
  • 例外を作成および処理する方法
  • どのページからリクエストされたかによってデータへのアクセスを制御する方法

このチュートリアルの対象読者

このチュートリアルは、単純なワークフロー・アプリケーションの構築プロセスを通して、PHP プログラミングの基本を説明することを目的とした 3 部構成のシリーズの第 3 回です。今回は、オブジェクト指向プログラミングに PHP を使用する方法など、高度なトピックについて詳しく学びたいと思っている開発者を対象としています。HTTP 認証、ストリーム処理、クラスとオブジェクト、例外処理についても取り上げます。

このチュートリアルでは、読者が構文やフォームの処理、データベースへのアクセスなどといった PHP の基本概念を理解していることを前提とします。「PHP の学習: 第 1 回」と「PHP の学習: 第 2 回」を読み、さらに「参考文献」を調べることで、このチュートリアルを理解するために必要なすべての情報を入手できます。

前提条件

Web サーバー、PHP、そしてインストールされていて使用できる状態のデータベースが必要です。ホスティング・アカウントを使用している場合は、サーバーに PHP V5 がインストール済みで、MySQL データベースにアクセスできる限り、そのアカウントを使用することができます。そうでない場合は、以下のパッケージをダウンロードしてインストールしてください。

XAMPP
Windows、Linux、あるいは Mac のどれを使用しているかに関わらず、このチュートリアルに必要なすべてのソフトウェアを取得するのに最も簡単な方法は、Web サーバー、PHP、MySQL データベース・エンジンをまとめてパッケージ化した XAMPP をインストールすることです。この方法を選ぶとしたら、XAMPP をインストールした後、コントロールパネルを実行して Apache および MySQL プロセスを起動すれば、準備は完了です。各種の構成要素を個別にインストールするという選択肢もありますが、その場合には、すべてが連動するように構成する必要があることを念頭に置いてください。XAMPP をインストールする方法では、この構成ステップまでが行われます。
Web サーバー
XAMPP を使用しないことにした場合、Web サーバーにはいくつかの選択肢があります。PHP 5.4 を使用する場合 (このチュートリアルを作成している時点では、XAMPP では PHP 5.3.8 のみが使用されています)、テスト用に組み込み Web サーバーを使用することができます。ただし、本番用には Apache Web サーバーのバージョン 2.x を使用することを前提とします。
PHP 5.x
XAMPP を使用しない場合、PHP 5.x を別途ダウンロードする必要があります。標準ディストリビューションには、このチュートリアルに必要なすべてのものが含まれています。ダウンロードするのはバイナリーで構いません。このチュートリアルには (PHP のコード自体に手を加えたいというのでない限り)、ソースは不要です。このチュートリアルは、PHP 5.3.8 を前提に作成してテストしました。
MySQL
このプロジェクトの一環としてデータをデータベースに保存する必要があるため、データベースも必要です。データベースについても同じく、XAMPP をインストールする場合には、このステップをスキップすることができますが、必要に応じてデータベースを別途インストールすることもできます。このチュートリアルでは、MySQL に焦点を合わせます。PHP では、MySQL が一般的に使用されているためです。MySQL を選択する場合は、Community Server をダウンロードしてインストールすることができます。

これまでの経緯

このセクションでは、このシリーズでこれまで行ってきた作業をおさらいします。その後、ウェルカム・ページを作成し、PHP を使用していくつかの制限を適用します。

現在の状況

これまでのチュートリアルを通して、単純なワークフロー・アプリケーションを構築してきました。このアプリケーションでは、ユーザーがファイルをシステムにアップロードして、これらのアップロードしたファイルを表示したり、管理者によって承認されたファイルを表示したりすることができます。現在までに、以下の構成要素を作成しました。

  • 登録ページ: このページでは、ユーザーが固有のユーザー名、e-メール・アドレス、パスワードを HTML フォームに入力することによって、アカウントを登録することができます。送信されたデータを分析して、ユーザー名が一意であることをデータベースに確認し、登録情報をデータベースに保存する PHP ページも作成しました。
  • ログイン・ページ: ユーザー名とパスワードを受け取り、その情報をデータベースに照らし合わせます。ユーザー名とパスワードが有効な場合は、サーバーがそのユーザーにどのファイルを表示すればよいかを認識できるように、サーバー上でセッションを作成します。
  • 単純なインターフェース要素: そのユーザーがログインしているかどうかを検出し、それに応じた適切な項目を表示します。
  • アップロード・ページ: このページで、ユーザーはブラウザーを介してファイルをサーバーに送信することができます。アップロードされたファイルを受け取るページも作成しました。このページは、受け取ったファイルをサーバーに保存した後、ファイルに関する情報を後で取得できるように DOM (Document Object Model) を使用してファイル情報を XML ファイルに追加します。
  • 表示関数: この関数は、データを保存するにも表示するにも、代替フォーマットとして JSON (JavaScript Object Notation) を使用します。

PHP の学習: 第 2 回」の終わりの時点でのアプリケーションを表すファイルは、ダウンロードすることができます。

これから行う作業

今回のチュートリアルを開始するに当たって、完全な、ただし極めて単純なワークフロー・アプリケーションが出来上がっています。このチュートリアルでは、以下の作業を行います。

  • HTTP 認証を追加して、Web サーバーで認証を制御します。さらに、登録プロセスを統合して、新規ユーザーがこの Web サーバーに追加されるようにします。
  • 使用可能なファイルを表示する関数へのリンクを追加し、ユーザーがこれらのリンクを使ってファイルをダウンロードできるようにします。Web でアクセスできない場所に保存されたこれらのファイルをブラウザーに対してストリーム処理するための関数を作成します。
  • ユーザーが適切なページからファイルをダウンロードするようにします。単に HTTP サーバーからファイルを提供するのではなく、ファイルをアプリケーションがストリーム処理しなければならないことを利用して、ユーザーがファイルをダウンロードする状況を制御できるようにします。
  • 文書を表すクラスを作成し、オブジェクト指向の手法を使用してそのクラスにアクセスし、文書をダウンロードします。
  • 問題を特定できるようにするためのカスタム例外を作成し、使用します。
  • 承認プロセスを管理します。

まず始めに、これまでに作成したアプリケーションを公開するためのページを作成します。

ウェルカム・ページ

これまでのところ、アプリケーションの個々の構成要素を作成する作業に集中してきました。ここからは、これらの構成要素を 1 つに組み立てる作業を開始します。まずは、訪問者を目的のページに導くために使用できる単純なウェルカム・ページから取り掛かります。index.php という名前の新規ファイルを作成して、リスト 1 に記載するコードを追加してください。

リスト 1. index ページ
<?php

   session_start();

   include ("top.txt");
   include ("scripts.txt");

   display_files();

   include ("bottom.txt");

?>

上記のコードは、セッションを開始して後で使用できるようにした後、最初の include() 関数でページ最上部のインターフェース要素をロードします (ページ最上部にインターフェース要素を配置する場合)。2 番目のインクルード関数では、これまでに作成したすべてのスクリプトをロードします。「PHP の学習: 第 2 回」で作成した、現在のユーザーからアップロードされたファイルと管理者に承認されたファイルのすべてを一覧表示する display_files() 関数も同じくロードされます。最後にHTML ページの最下部がインクルードされます。

このファイルを、これまでに作成した他のファイルと同じディレクトリーに保存します。例えば、他のファイルがサーバーのドキュメント・ルートに保存されているとしたら、そこに保存することになります。その場合、このページを表示するには、HTTP サーバーを起動した後、ブラウザーで http://localhost/index.php にアクセスします。

図 1 に、この単純なページを示します。

図 1. 基本的なファイル一覧ページ
基本的なファイル一覧ページのスクリーン・キャプチャー

ファイル・アクセスを制限する

次のセクションでは、認証によって、誰が何を表示できるのかを制御する方法を学びます。そのためにはまず、何らかの制限を適用しなければなりません。現時点では、ファイルが承認されているかどうかに関わらず、すべてのユーザーにすべてのファイルが表示されますが、これは望んでいる動作ではありません。ユーザー自身がアップロードしたファイルでない限り、display_files() によって表示されるのは承認済みのファイルのみとなるようにしたいものです。

scripts.txt を開いて、リスト 2 のコードを追加してください。

リスト 2. ファイルへのアクセスを制限する
    for ($i = 0; $i < count($workflow["fileInfo"]); $i++) {
        $thisFile = $workflow["fileInfo"][$i];
        if (
            ($thisFile["approvedBy"] != null) ||
            (
                    isset($_SESSION["username"]) &&
                    ($thisFile["submittedBy"] == $_SESSION["username"])
            )
        ) {

            echo "<tr>";
            echo "<td>" . $thisFile["fileName"] . "</td>";
            echo "<td>" . $thisFile["submittedBy"] . "</td>";
            echo "<td>" . $thisFile["size"] . "</td>";
            echo "<td>" . $thisFile["status"] . "<td>";
            echo "</tr>";
        } 
    }

リスト 2 では、3 つの異なる条件を組み合わせることによって、特定のファイルを表示するかどうかを決定します。まず、ファイルが承認されていれば、$thisFile["approvedBy"] に値が格納されるため、最初の条件は真となります。二重のパイプ (||) は「or」を意味するので、この最初のテストが偽の結果となった場合には、条件の後半で 2 度目のチャンスがあります。

条件の後半も 2 つの部分からなりますが、「and」を意味する二重のアンパサンド (&&) を使用しているので、2 つの部分が両方とも真でなければ、後半の条件は真となりません。後半の 2 つの条件のうちの最初の条件では、セッションがユーザー名を既知であるかを調べます。既知であれば、ユーザー名が $thisFile["submittedBy"] の値と突き合わせられます。

条件全体が「真」と評価された場合 (つまり、ファイルが承認されている場合、またはユーザーがログインしている状態で、ファイルを送信した本人である場合)、システムはそのファイルを表示します。そうでなければ、ファイルは表示されません。

ユーザーがログインしていない状態だとしたら (このことを判断するには、ブラウザーを再起動する必要があります)、図 2 のような空のページが表示されます。

図 2. 制限が適用された基本的なファイル一覧ページ
制限が適用された基本的なファイル一覧ページのスクリーン・キャプチャー

ブラウザーを起動した直後はログインしている状態ではないため、「Register (登録)」リンクと「Login (ログイン)」リンクが表示されます。次のセクションでは、このログイン・プロセスを処理する別の方法を見て行きます。


HTTP 認証を使用する

このセクションでは、HTTP 認証用のサーバーをセットアップして、Web サーバーが PHP アプリケーションのログイン・プロセスを制御できるようにします。

HTTP 認証

これまで使用していたログイン・システムでは、ユーザーがユーザー名とパスワードをフォームに入力し、ユーザーがそのフォームを送信すると、その情報が MySQL データベースに対して照合されます。情報が一致していれば、アプリケーションは PHP 内でセッションを作成し、ユーザー名を $_SESSION 配列に格納して、後で使用できるようにします。

このプロセスのままでも有効に機能しますが、他のシステムと統合するとなると、問題に突き当たります。例えば、ワークフロー・アプリケーションがイントラネットに組み込まれるとします。その場合、他のシステムからすでにユーザー名を入力してログインしたユーザーに対し、再度ログインを要求することはしたくありません。ユーザーが他のどこかでログインを済ませている場合には、このアプリケーションにアクセスする時点で、ユーザーをログイン済みの状態にしたいと思うはずです。この仕組みは、シングル・サインオン・システムとして知られています。

このアプリケーションでシングル・サインオンを可能にするには、Web サーバーが実際にログイン・プロセスを制御するというシステムに切り替えることになります。このサーバーは単にページを提供するだけでなく、ブラウザーからのリクエストにユーザー名とパスワードが含まれるかどうかをチェックし、これらの情報が含まれていなければ、ブラウザーにユーザー名とパスワードを入力するためのボックスをポップアップ表示させて、ユーザーがその情報を入力できるようにします。ユーザーは一度ユーザー名とパスワードを入力すれば、その情報を再度入力する必要はありません。以降のリクエストでは、ブラウザーがその情報を送信します。

それでは、サーバーをセットアップするところから始めましょう。

HTTP 認証を有効にする

作業を開始する前に、Apache 2.X 以外のサーバーを使用しているとしたら、HTTP 認証に関する資料で HTTP 認証用のサーバーをセットアップする方法を調べる必要があります。XAMPP は HTTP 認証を使用するので、XAMPP を使用している場合は準備がすべて整っています (あるいは、このセクションをスキップして、いずれかのタイプの認証をアプリケーションが処理するための適切なステップを組み込んでも構いません)。

HTTP 認証は実際にはどのように機能するのでしょうか?何よりもまず、サーバーは、ディレクトリーのそれぞれに対してどのような種類のセキュリティーを適用する必要があるかを把握しています。特定のディレクトリーに対して適用するセキュリティーを変更する 1 つの方法は、それに応じた設定をサーバーのメイン構成の中でセットアップすることです。あるいは、.htaccess ファイルを使用するという方法もあります。このファイルに、それが置かれているディレクトリーについての指示を含めます。

例えば、ユーザー固有のファイルにアクセスするユーザーのすべてが、有効なユーザー名とパスワードを持っていることをサーバーに確認させたいとします。それにはまず、現在ファイルが置かれているディレクトリーの中に、loggedin という名前のディレクトリーを作成します。ファイルが /usr/local/apache2/htdocs に置かれているとしたら、/usr/local/apache2/htdocs/loggedin ディレクトリーを作成することになります。

次に、サーバーに対して、このディレクトリーのセキュリティーを全面的に変更することを指定します。httpd.conf ファイルを開き、リスト 3 のコードを追加してください。

リスト 3. ディレクトリーのセキュリティーを変更する
<Directory /usr/local/apache2/htdocs/loggedin>
 AllowOverride AuthConfig
</Directory>

(皆さんそれぞれのセットアップに応じて、適切なディレクトリーを指定してください。)

次は、実際のディレクトリーを準備します。

認証を設定する

新しいテキスト・ファイルを作成し、そのファイルを .htaccess という名前で loggedin ディレクトリーに保存します。新規ファイルには、リスト 4 のコードを追加します。

リスト 4. .htaccess ファイルを作成する
AuthName "Registered Users Only"
AuthType Basic
AuthUserFile /usr/local/apache2/password/.htpasswd
Require valid-user

リスト 4 に示されている AuthName は、ユーザー名とパスワードの入力ボックスの最上部に表示されるテキストです。AuthType で、Basic 認証を使用することを指定します。これは、ユーザー名とパスワードを平文で送信することを意味します (高いセキュリティーが要求されるアプリケーションを作成する場合には、他の選択肢を調べてください)。AuthUserFile は、許可されるユーザー名とパスワードが含まれるファイルです (このファイルは、この後すぐに作成します)。最後の Require ディレクティブで、このコンテンツを実際に表示する対象ユーザーを指定することができます。ここでは、有効なユーザーのすべてに表示するように指定していますが、特定のユーザーやユーザー・グループを指定することもできます。

HTTP サーバーを再起動して、変更を有効にします。

(XAMPP では、XAMPP コントロールパネルのメニュー、または (サーバーをサービスとして設定した場合は) 「Services (サービス)」コントロールパネルからサーバーを再起動することができます。すべての Apache V2.0 インストール済み環境では、<APACHE_HOME>/bin/apachectl stop に続けて <APACHE_HOME>/bin/apachectl start を実行することにより、再起動することができます。)

次は、パスワード・ファイルを作成します。

パスワード・ファイルを作成する

以上で行った作業を有効にするには、パスワード・ファイルを用意して、サーバーがそのファイルをチェックできるようにする必要があります。パスワード・ファイルを PHP 内から操作する方法については「パスワード・ファイルに新規ユーザーを追加する」で説明することにして、ここでは、コマンド・ラインを使用できるという前提で、このファイルを直接作成する方法を説明します。

まず、.htpasswd ファイルの格納場所を選ばなければなりません。Web でアクセス可能なディレクトリー内は選択しないでください。このファイルを誰かが簡単にダウンロードして分析できるような場所は、安全ではありません。また、PHP が書き込み可能な場所であることも必要です。例えば、apache2 ディレクトリー内に password ディレクトリーを作成するなどです。どの場所を選択するにしても、.htaccess ファイル内には必ず正確な情報を含めてください。

パスワード・ファイルを作成するには、htpasswd アプリケーションが必要です。これは、Apache に付属しているアプリケーションで、XAMPP を使用している場合には <XAMPP_HOME>/apache/bin 内に配置されています。コマンド htpasswd -c /usr/local/apache2/password/.htpasswd roadnick を、リスト 5 に記載されているように各自のディレクトリーとユーザー名に置き換えて実行します。

このコマンドを実行すると、パスワードの入力を求めるプロンプトに続き、確認パスワードの入力を求めるプロンプトが出されます (リスト 5 を参照)。

リスト 5. .htpasswd ファイルを作成する
htpasswd -c /usr/local/apache2/password/.htpasswd NickChase
New password:
Re-type new password:
Adding password for user NickChase

-c スイッチは、サーバーに新規ファイルを作成するように指示するので、新規ユーザーを追加した後、ファイルは NickChase:IpoRzCGnsQv.Y のような内容になっています。

このバージョンのパスワードは暗号化されるので、アプリケーションからパスワードを追加するときは、この点に留意する必要があります。

早速、実際の動作を見てみましょう。

ログインする

動作を確認するには、保護されたディレクトリー内のファイルにアクセスする必要があります。uploadfile.php ファイルと uploadfile_action.php ファイルを loggedin ディレクトリーに移動させて、index.php を display_files.php という名前で loggedin ディレクトリーにコピーします。

この 3 つのファイルのそれぞれで、include() 文を変更して新しい場所を適用させます (リスト 6 を参照)。

リスト 6. ファイルを表示する
<?php

   session_start();
   include ("../top.txt");
   include ("../scripts.txt");

   echo "Logged in user is ".$_SERVER['PHP_AUTH_USER'];

   display_files();

   include ("../bottom.txt");

?>

この例では、インクルードするファイルへの参照を固定していますが、ブラウザーがユーザー名とパスワードを送信するときに設定される変数も参照しています。ブラウザーで http://localhost/loggedin/display_files.php にアクセスすると、この動作を確認することができます。図 3 を見るとわかるように、ユーザー名とパスワードの入力ボックスが表示されるはずです。

図 3. ユーザー名とパスワードの入力ボックス
ユーザー名とパスワードの入力ボックスのスクリーン・キャプチャー

パスワード・ファイルを作成する」で使用したユーザー名とパスワードを入力して、実際のページを表示してください。

ログイン情報を使用する

ユーザー名を入力してログインした状態になったので、ページを見ることができます。図 4 に示されているように、ユーザーがログイン状態になったことを伝えるメッセージは表示されますが、実際のコンテンツはその状態に対応していません。 「Register (登録)」 リンクと 「Login (ログイン)」 リンクはまだ表示されています。ファイルのリストに表示されるのは、管理者が承認したファイルのみです。現在のユーザーがアップロードしたファイルは表示されていません。図 4 からわかるように、ユーザーがアップロードしたファイルは、保留中の状態のままです。

図 4. ログインしている状態・・・ただし完全ではありません
ある種のログイン状態のスクリーン・キャプチャー

これらの問題を解決するには、2 つの方法があります。まず 1 つは、アプリケーションがユーザー名を参照するインスタンスの 1 つひとつで、$_SESSION["username"] ではなく $_SERVER['PHP_AUTH_USER'] を探すようにコードを作成し直すことです。しかし、優秀なプログラマーというものは本質的に面倒臭がりなので、この方法はそれほど魅力的なものではありません。

もう 1 つの方法は、単純に $_SERVER['PHP_AUTH_USER'] に基づいて $_SESSION["username"] を設定するというものです。その場合、すべては引き続き以前と同じように機能します。この方法を選択する場合は、top.txt で、新しいセッションの開始直後、または既存のセッションへの参加直後に現在のユーザーを設定します (リスト 7 を参照)。

リスト 7. 現在のユーザーを設定する
<?php

  if (isset($_SESSION["username"])){
      //Do nothing
  } elseif (isset($_SERVER['PHP_AUTH_USER'])) {
      $_SESSION["username"] = $_SERVER['PHP_AUTH_USER'];
  }

?>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
  <head>
...

ブラウザーにユーザーが入力したユーザー名とパスワードを「忘れさせる」唯一の方法は、ブラウザーを閉じることなので、$_SESSION["username"] 変数を優先させます。このようにすれば、ユーザーが別のユーザーとしてログインすることも可能になります (ここでは省きますが、この方法も選択できることは確かです)。

$_SESSION 変数と $_SERVER 変数がどちらも設定されていなければ、何の変化も起こりません。ページは (この例の場合では) ユーザーがログインしていないかのように現状を維持します。この単純な 1 つの変更だけで、ログイン問題は解消されるというわけです (図 5 を参照)。

図 5. 修正後のページ
修正後のページのスクリーン・キャプチャー

インターフェースを修正する

新規ユーザーを追加する前に、top.txt が新しい構造に対応するように、このファイルにいくつかの簡単な修正を加える必要があります。その 1 つは、「Login (ログイン)」リンクが古い login.php ページではなく、新しくセキュアにされた display_files.php ファイルを指すようにする必要があります。ユーザーがログイン・ページにアクセスしようとしたときに、ブラウザーが新しいログイン手段を提供するようにします (リスト 8 を参照)。

リスト 8. ナビゲーションを調整する
...
<div id="nav1">
   <ul style='float: left'>
      <li><a href="#" shape="rect">Home</a></li>
      <li><a href="/uploadfile.php" shape="rect">Upload</a></li>
      ><li><a href="/loggedin/display_files.php" shape="rect">Files
</a></li>>
       <?php
          if (isset($_SESSION["username"]) || isset($username)){
             if (isset($_SESSION["username"])){
                $usernameToDisplay =  $_SESSION["username"];
             } else {
                $usernameToDisplay = $username;
             }
      ?>
      ><!--> <li><a href="logout.php" shape="rect">Logout</a>
</li>   -->
            <li><p style='color:white;'>&nbsp;&nbsp;&nbsp;
Welcome,
                   <b><?=$usernameToDisplay?></b>.</p></li>
       <?php
          } else {
       ?>
            <li><a href="/registration.php" shape="rect">
Register</a></li>
            <li><a href=">/loggedin/display_files.php" 
shape="rect">Login</a></li>
       <?php
          }
       ?>
   </ul>
</div>
...

上記のコードでは、ログイン参照を修正して、ファイルのリストを表示するための新しい選択肢を追加している他に、ログアウトに関するメッセージをコメントアウトしていることにも注意してください。ログアウトについては、このチュートリアルでは取り上げません。

これであとは登録プロセスとパスワード・ファイルを統合すればよくなりました。

パスワード・ファイルに新規ユーザーを追加する

最後のステップでは、登録プロセスと .htpasswd ファイルを統合します。そのために必要な作業は、ユーザーをデータベースに保存した後、新しいエントリーを .htpasswd に追加することのみです。registration_action.php を開いて、リスト 9 のコードを追加してください。

リスト 9. ユーザーを登録時にサーバー上に作成する
...
    if ($checkUserStmt->rowCount() == 0) {

        $stmt = $dbh->prepare("insert into users (username, email, password) ".
                                                                 "values (?, ?, ?)");

        $stmt->bindParam(1, $name);
        $stmt->bindParam(2, $email);
        $stmt->bindParam(3, $pword);

        $name = $_POST["name"];
        $email = $_POST["email"];
        $pword = $passwords[0];

        $stmt->execute();

        $pwdfile = '/usr/local/apache2/password/.htpasswd';
        if (is_file($pwdfile)) {
            $opencode = "a";
        } else {
            $opencode = "w";
        }
        $fp = fopen($pwdfile, $opencode);
        $pword_crypt = crypt($passwords[0]);
        fwrite($fp, $_POST['name'] . ":" . $pword_crypt . "\n");
        fclose($fp);

        echo "<p>Thank you for registering!</p>";

    } else {

        echo "<p>There is already a user with that name: </p>";
...

作業を開始する前に、既存の .htpasswd ファイルがある場合には、Web サーバー上のこのファイルへの書き込みが、Apache を実行しているユーザーに許可されていることを確認してください。許可されていない場合は、ユーザーが該当するディレクトリーに書き込めるようにする必要があります。

最初にファイルの有無をチェックして、それによって新規ファイルを作成するか、それとも既存のファイルに情報を追加するかを決定します。これが決まったら、そのファイルを開いて作業を開始します。

パスワード・ファイルを作成する」で説明したように、パスワードは暗号化された形で保管されるため、crypt() 関数によって、その暗号化された文字列を取得することができます。最後に、ユーザー名とパスワードをファイルに書き込んで、ファイルを閉じます。

以上の作業の成果をテストするには、ブラウザーを終了してキャッシュされたすべてのパスワードを消去してから、http://localhost/index.php を開きます。

「Register (登録)」をクリックして、新規アカウントを作成します。アカウントの作成が完了したら、またブラウザーを終了して、保護されたページにアクセスしてみてください。新しいユーザー名とパスワードが有効に機能するはずです。


ストリームを使用する

system\ をセットアップしたので、使用可能なファイルをユーザーが実際にダウンロードできるようにする準備が整いました。これらのファイルは元々 Web でアクセスできないディレクトリーに格納されているため、ただファイルにリンクするという方法は問題外です。代わりに、このセクションではファイルを現在の場所からブラウザーに対してストリーム処理する関数を作成します。

ストリームとは何か

ファイルなどのリソースに実際にアクセスする方法は、ファイルが保管されている場所とファイルを保管する方法によって決まります。ローカルにあるファイルにアクセスする方法と、リモート・サーバー上にあるファイルに HTTP や FTP によってアクセスする方法は、まったく違います。

幸い、PHP では「ストリーム・ラッパー」を使用できます。たとえどこにあるリソースを呼び出したとしても、PHP で使用可能なラッパーがあれば、そのラッパーが呼び出し方法を見つけ出してくれます。

使用可能なラッパーを調べるには、stream_get_wrappers() 関数から返される配列の中身を出力するという方法があります (リスト 10 を参照)。

リスト 10. 使用可能なストリーム・ラッパーを表示する
<?php

print_r(stream_get_wrappers());

?>

print_r() 関数は、配列の中身を表示するのに極めて重宝します。例えば、システムからリスト 11 のような結果が表示されることがあるとします。

リスト 11. 使用可能なストリーム・ラッパー
Array
(
    [0] => php
    [1] => file
    [2] => http
    [3] => ftp
)

これにより、ファイルをローカル・サーバーに保管する代わりの手段として、簡単にリモート Web サーバーや FTP サーバーに保管することができます。このセクションで使用するコードはそのままの状態で機能します。

さっそく見てみましょう。

ファイルをダウンロードする

ユーザーがファイルを表示するには、ブラウザーがそのファイルを受け取らなければなりません。また、ファイルを正しく表示するには、ブラウザーがそのファイルが何であるかを把握する必要もあります。この 2 つの課題を両方とも片付けるために、download_file.php という名前の新規ファイルを作成して、loggedin ディレクトリーに保存します。そしてこのファイルに、リスト 12 のコードを追加します。

リスト 12. ファイルを送信する
<?php

   include ("../scripts.txt");

   $filetype = $_GET['filetype'];
   $filename = $_GET['file'];
   $filepath = UPLOADEDFILES.$filename;

   if($stream = fopen($filepath, "rb")){
      $file_contents = stream_get_contents($stream);
      header("Content-type: ".$filetype);
      print($file_contents);
   }

?>

このプロセスは、その効果の割には実際のところ至って単純な仕組みです。上記のコードではまず、ファイルを読み取ってバッファーに入れるためにそのファイルを開きます。fopen() 関数で実際に何を処理しているのかというと、ファイルを表すリソースを作成しています。そのリソースを stream_get_contents() に渡すと、この関数がファイル全体を単一の文字列の中に読み込みます。

ファイルの中身を入手できたので、それをブラウザーに送信することができますが、ブラウザーがそれをどう処理すればよいかがわからなければ、おそらくテキストとして表示することになります。テキスト・ファイルの場合はそれで問題ありませんが、ファイルが画像の場合、あるいは HTML ファイルである場合でも、そのような処理では適切でありません。そこで、ファイルの中身をそのまま送信するのではなく、最初に header をブラウザーに送信して、ファイルの Content-type に関する情報 (image/jpeg など) を提供するようにします。

最後に、ファイルの中身をブラウザーに出力します。あらかじめ Content-type ヘッダーを受け取っていれば、ブラウザーはファイルの中身をどのように処理すればよいかがわかります。

実際に使用するファイルとそのタイプを判別するという点に関しては、これらの情報を $_GET 配列から読み取っているので、以下のように URL にそのまま情報を追加することができます。

http://localhost/loggedin/download_file.php?file=NoTooMiLogo.png&filetype=image/png

ブラウザーに上記の URL を (もちろん適切なファイル名とタイプを指定して) 入力すると、図 6 に示す結果が表示されます。

図 6. ファイルをダウンロードする
ファイルをダウンロードする際の画面のスクリーン・キャプチャー

ファイルにリンクを追加する

ダウンロード・ページに必要な情報はすべて URL に追加できるため、ユーザーがファイルをダウンロードできるようにするためのリンクを追加するのはわけありません。使用可能なファイルの表示は display_files() 関数によって作成するので、リスト 13 に示すようにリンクを追加することができます。

リスト 13. リンクを追加する
...
for ($i = 0; $i < count($workflow["fileInfo"]); $i++){
    $thisFile = $workflow["fileInfo"][$i];
    echo "<tr>";
    echo "<td><a href='/loggedin/download_file.php?file="
.$thisFile["fileName"].
            "&filetype=".$thisFile["fileType"]."'>".$thisFile["fileName"]
."</a></td>";
    echo "<td>".$thisFile["submittedBy"]."</td>";
    echo "<td>".$thisFile["size"]."</td>";
    echo "<td>".$thisFile["status"]."<td>";
    echo "</tr>";

}
...

すると、図 7 のような結果になります。

図 7. ファイルへのリンク
ファイルへのリンクのスクリーン・キャプチャー

リンクをクリックして、ファイルを確認してください。

次は、このプロセスをオブジェクトにカプセル化する方法を見て行きます。


オブジェクトを使用する

このセクションでは、オブジェクトの使用方法について探ります。ここまでは、ほとんどすべての作業を手続き型の方法で行ってきました。つまり、用意されたスクリプトのほぼ最初から最後までが実行されました。ここからは、その方法から離れます。

そもそもオブジェクトとは何なのか?

オブジェクト指向プログラミングの中心概念となっているのは、自給自足のバンドルとして「モノ」を表すことができるという考えです。例えば、電気ケトルには、色や最大温度などの諸元があり、さらには水が加熱される、自動的に電源がオフになる、といった機能があります。

この電気ケトルをオブジェクトとして表現したとすると、そのオブジェクトも同じく colormaximumTemperature などの諸元、つまりプロパティーと、heatWater()turnOff() などの機能、つまりメソッドを持つことになります。この電気ケトルとのインターフェースを持つプログラムを作成するとしたら、実際にそれらの機能をどのようにして実現するかを悩むのではなく、単に電気ケトル・オブジェクトのメソッド (例えば heatWater() メソッドなど) を呼び出すことになります。

こうしたことをもっとよくわかるようにするために、これからダウンロード対象となるファイルを表すオブジェクトを作成します。このオブジェクトは、ファイルの名前とタイプなどのプロパティーと、download() などのメソッドを持つことになります。

そうは言っても、実際にオブジェクトを定義するわけではありません。定義するのはオブジェクトではなく、オブジェクトのクラスです。クラスは、特定のタイプのオブジェクトの「テンプレート」のような役割を果たします。そのクラスのインスタンスを作成すると、そのインスタンスがオブジェクトになります。

それでは、実際のクラスを作成するところから始めましょう。

WFDocument クラスを作成する

オブジェクトを扱う最初のステップは、オブジェクトのベースとするクラスを作成することです。クラスの定義は scripts.txt ファイルに追加することができますが、ここでは保守しやすいコードを目指しているので、定義を追加することによってコードの保守がしにくくならないように、WFDocument.php という別のファイルを作成します。このファイルをメイン・ディレクトリーに保存して、リスト 14 に記載するコードを追加します。

リスト 14. 基本のドキュメント・オブジェクト
<?php

include_once("scripts.txt");

class WFDocument {

   function download($filename, $filetype) {

      $filepath = UPLOADEDFILES.$filename;

      if($stream = fopen($filepath, "rb")){
        $file_contents = stream_get_contents($stream);
        header("Content-type: ".$filetype);
        print($file_contents);
      }
   }
}

?>

まず、UPLOADEDFILES 定数が必要なので、scripts.txt ファイルをインクルードします。次に、実際のクラスを作成します。この WFDocument クラスには、唯一のメソッド download() があります。このコードは download_file.php のコードとほぼ同じですが、ファイル名とタイプを $_GET 配列から直接抽出するのではなく、この関数に対する入力として受け取るという点が異なります。

次は、このクラスのインスタンス化について見て行きましょう。

WFDocument 型のオブジェクトを呼び出す

このシリーズの第 2 回で DOM を操作したときに、実際にいくつかのオブジェクトをインスタンス化しましたが、その理由や方法についてはほとんど説明しませんでした。ここで、その不足を補います。

download_file.php ページを開いて、コードを変更し、リスト 15 に示すコードと同じになるようにしてください。

リスト 15. 関数にファイル情報を送信する
<?php

   include ("../WFDocument.php");

   $filetype = $_GET['filetype'];
   $filename = $_GET['file'];

   $wfdocument = new WFDocument();
   $wfdocument->download($filename, $filetype);

?>

このコードでは、scripts.txt ファイルをインクルードするのではなく、WFDocument.php ファイルに追加した WFDocument クラスの定義をインクルードするところから始まっています (一部の開発者は、個々のクラスを至るところでインクルードするよりも、作成するすべてのクラスを含めたページを作成して、そのページをインクルードするほうが有効だと考えています)。

新しいオブジェクトを作成する準備が整ったところで、new キーワードを使用して新規オブジェクトを作成します。この行は、WFDocument 型の新規オブジェクトを作成し、このオブジェクトを $wfdocument 変数に割り当てます。

その新規オブジェクトを参照するための変数が作成されれば、そのオブジェクトのパブリック・メソッドをどれでも呼び出すことができます。この例の場合、メソッドは download() だけなので、-> 演算子を使用してこのメソッドを呼び出すことができます。この記号は基本的に、「このオブジェクトに属するこのメソッド (またはプロパティー) を使用する」ことを表しています。

ファイルを保存して、ページに表示されているリンクのいずれかをクリックしてテストをしてください。リンクをクリックして呼び出されるコードは以前とまったく変わっていません。唯一、呼び出す方法が変わっているだけです。

プロパティーを作成する

メソッドは話の一部でしかありません。オブジェクトの本質は、これがカプセル化されるところにあります。オブジェクトには、そのオブジェクト固有の情報をすべて含めることになるので、download() メソッドにファイルの名前やタイプをフィードするのではなく、これらの情報をオブジェクトのプロパティーとして設定することができます。それにはまず、クラス内にプロパティーを作成する必要があります (リスト 16 を参照)。

リスト 16. オブジェクトのプロパティーを使用する
<?php
include_once("../scripts.txt");

class WFDocument {

   public $filename;
   public $filetype;

   function download() {

      $filepath = UPLOADEDFILES.$this->filename;

      if($stream = fopen($filepath, "rb")){
        $file_contents = stream_get_contents($stream);
        header("Content-type: ".$this->filetype);
        print($file_contents);
      }
   }
}

?>

関数の外部で変数を宣言していることに注目してください。変数は、関数に含まれるのではなく、クラスの一部であるためです。また、クラスの外部から変数にアクセスできるように、変数を public として宣言しています。プロパティーは private として設定することもできます。その場合、プロパティーを使用できるのはクラスの中でのみに限られます。あるいは、protected として設定して、そのクラス内またはそのクラスを基底クラスに持つ任意のクラス内だけで使用できるようにすることもできます (この概念に馴染みがないとしても、もう少し辛抱してください。「カスタム例外の作成」で、この継承という概念について説明します)。

オブジェクト・プロパティーを参照するには、そのプロパティーを所有するオブジェクトを知らなければなりません。オブジェクト内では、キーワード $this を使用するだけで、オブジェクト自身を参照することができます。したがって、$this->filename を使用すれば、このコードを実行するオブジェクトの filename プロパティーを参照することができます。

次は、これらのプロパティーに値を設定する方法を見て行きましょう。

プロパティーを設定する

オブジェクトに情報を渡すのではなく、実際にオブジェクトのプロパティーを設定するようにします (リスト 17 を参照)。

リスト 17. オブジェクトのプロパティーを設定する
<?php

   include ("../WFDocument.php");

   $filetype = $_GET['filetype'];
   $filename = $_GET['file'];

   $wfdocument = new WFDocument();
   $wfdocument->filename = $filename;
   $wfdocument->filetype = $filetype;
   $wfdocument->download();

?>

上記で使用している表記に注意してください。オブジェクト名である $wfdocument-> 演算子、プロパティーの名前の順になっています。これらのプロパティーを設定した後は、オブジェクト内からプロパティーを使用できるため、download() メソッドにプロパティーを渡す必要がなくなります。

以上でプロパティーの設定が完了しましたが、実のところ、この類いの処理にはこれよりも良い方法があります。それでは、その代替手段を見て行きましょう。

プロパティーを隠す

前のセクションで行ったように、確かにプロパティーの値を直接設定することはできますが、それは最善の処理方法とは言えません。一般的な慣例では、実際のプロパティーを公開せずに、ゲッターとセッターによってプロパティーの値を取得および設定します (リスト 18 を参照)。

リスト 18. private プロパティーを使用する
<?php

include_once("../scripts.txt");

class WFDocument {

   private $filename;
   private $filetype;

      function setFilename($newFilename){
      $this->filename = $newFilename;
   }
   function getFilename(){
      return $this->filename;
   }

   function setFiletype($newFiletype){
      $this->filetype = $newFiletype;
   }
   function getFiletype(){
      return $this->filetype;
   }

   function download() {

      $filepath = UPLOADEDFILES.$this->getFilename();

      if($stream = fopen($filepath, "rb")){
        $file_contents = stream_get_contents($stream);
        header("Content-type: ".$this->getFiletype());
        print($file_contents);
      }
   }
}

?>

まず、プロパティーを private として定義します。これは、前のセクションでのようにプロパティーを直接設定しようとすると、エラーになるということです。しかし、プロパティーの値を設定しなければならないことに変わりはありません。そこで、直接設定するのではなく、getFilename()setFilename()getFiletype()、および setFiletype() メソッドを使用します。これらのメソッドは、元のプロパティーを使用したときと同じように download() メソッドの中で使用していることに注目してください。

ゲッターおよびセッターを使用すると重宝するのは、データに対する操作をより柔軟に制御できるからです。例えば、特定の妥当性検査を実行してからでないと、プロパティーに具体的な値を設定できないようにする必要がある場合などです。

隠したプロパティーを呼び出す

プロパティーを隠したので、download_file.php ページに戻って、エラーを受け取らなくなるように変更する必要があります (リスト 19 を参照)。

リスト 19. セッターを使用する
<?php

   include ("../WFDocument.php");

   $filetype = $_GET['filetype'];
   $filename = $_GET['file'];

   $wfdocument = new WFDocument();
   $wfdocument->setFilename($filename);
   $wfdocument->setFiletype($filetype);
   $wfdocument->download();

?>

この手法と同じく重宝する、さらに簡単にオブジェクトにプロパティーを設定する方法があります。

コンストラクターを作成する

オブジェクトにコンストラクターがある場合、その特定のクラスの新規インスタンスを作成するたびに、コンストラクターが呼び出されます。例えば、リスト 20 に示す単純なコンストラクターを作成することができます。

リスト 20. 単純なコンストラクター
...
   function getFiletype(){
      return $this->filetype;
   }

      function __construct(){
      echo "Creating new WFDocument";
   }

   function download() {

      $filepath = UPLOADEDFILES.$this->filename;
...

上記のスクリプトをそのまま実行すると、図 8 のようにエラーとなります。それは、このオブジェクトがヘッダーを出力する前にテキスト (Creating new WFDocument) を出力することが原因です。

図 8. スクリプトを実行した後のエラー
スクリプトを実行した後のエラーのスクリーン・キャプチャー

つまり、明示的に __construct() メソッドを呼び出してはいないものの、アプリケーションはオブジェクトがインスタンス化されると同時にこのメソッドを呼び出していたのです。情報をコンストラクターに追加することで、アプリケーションのこの動作を有効に利用することができます。

情報を使用してオブジェクトを作成する

コンストラクターの最も一般的な使用方法の 1 つは、オブジェクトの作成時にさまざまな値を初期化する手段を提供することです。例えば、オブジェクトを作成するときに filename および filetype プロパティーを設定するように、WFDocument クラスをセットアップすることができます (リスト 21 を参照)。

リスト 21. より込み入ったコンストラクター
...
   function getFiletype(){
      return $this->filetype;
   }

   function __construct($filename = "", $filetype = ""){
      $this->setFilename($filename);
      $this->setFiletype($filetype);
   }

   function download() {

      $filepath = UPLOADEDFILES.$this->filename;

...

オブジェクトを作成するときに、PHP はコンストラクターに含まれる指示を実行してから、以降の処理を続けます。この例の場合、コンストラクターは filenamefiletype を探します。これらの値を提供しなくても、関数の呼び出し時に値が提供されない場合に使用するデフォルト値を指定してあるので、エラーは発生しません。

けれども、どのようにして __construct() 関数を明示的に呼び出すのでしょうか?

オブジェクトを作成する: コンストラクターを呼び出す

実際に、コンストラクター・メソッドを明示的に呼び出すわけではありません。オブジェクトを作成するたびに、暗黙的に呼び出します。つまり、その特定の時点を利用して、コンストラクターに情報を渡します (リスト 22 を参照)。

リスト 22. コンストラクターを使用する
<?php

   include ("../WFDocument.php");

   $filetype = $_GET['filetype'];
   $filename = $_GET['file'];

   $wfdocument = new WFDocument($filename, $filetype);
   $wfdocument->download();

?>

新規オブジェクトを作成するときにクラスに渡す情報は、コンストラクターに渡されます。このように、オブジェクトを作成し、そのオブジェクトを使用してファイルをダウンロードすることは、簡単にできるのです。


例外の処理

例外が発生するのは、プログラムで予期せぬことが発生した場合です。プログラムは一般に、例外が発生した時点で停止するか、エラーを表示するように設計されます。アプリケーションで何かがおかしい場合に例外が発生するため、例外とエラーは混同されがちですが、例外にはエラーよりも遥かに柔軟性があります。このセクションでは、さまざまなタイプの例外を定義して、アプリケーションで何が起こっているのかを判別するためにそれらの例外を使用する方法を説明します。

汎用例外

まずは、WFDocument クラスの定義に含まれる単純な汎用例外から取り掛かります (リスト 23 を参照)。

リスト 23. 例外をスローする
<?php

include_once("../scripts.txt");

class WFDocument {
...
   function download() {

      $filepath = UPLOADEDFILES.$this->filename;

         try {

         if(file_exists($filepath)){
              if ($stream = fopen($filepath, "rb")){
                 $file_contents = stream_get_contents($stream);
                 header("Content-type: ".$this->filetype);
                 print($file_contents);
              }
             } else {
           throw new Exception ("File '".$filepath."' does not exist.");
         }

      } catch (Exception $e) {

         echo "<p style='color: red'>".$e->getMessage()."</p>";

      }
   }
}

?>

例外はただ発生するだけではなく、スローされます。何かがスローされたら、それをキャッチしなければなりません。そのために作成するのが、try-catch 文です。try セクションには、独自のコードを含めます。何か厄介な事態 (この例では、ファイルが存在しないなどの事態) が起こった場合、例外をスローすると、PHP は直ちに catch ブロックに移動して、その例外をキャッチします。

例外には多数のプロパティー (例えば、例外がスローされる原因となった行やファイルなど) と、メッセージが設定されます。上記にも示されているように、通常、アプリケーションは例外をスローするときにメッセージを設定します。その場合、例外自体 ($e) が getMessage() メソッドを使用して、そのテキストを表示することができます。例えば、存在しないファイルをダウンロードしようとすると、「File 'c:/sw/temp/NoTooMiLogoQWSQ.png' does not exist (ファイル c:/sw/temp/NoTooMiLogoQWSQ.png は存在しません)」というメッセージが表示されます (図 9 を参照)。

図 9 . 基本的な例外
基本的な例外メッセージのスクリーン・キャプチャー

ただし、例外の真の力は、独自の例外を作成することによって発揮されます。

カスタム例外を作成する

前のセクションでは、オブジェクトについて検討しましたが、オブジェクトの非常に重要な側面のうちの 1 つについては説明しませんでした。それは、継承です。

クラスを使用する利点の 1 つは、あるクラスを別のクラスのベースとして使用できることです。例えば、元の Exception クラスを継承して、NoFileExistsException という新しい例外タイプを作成することができます (リスト 24 を参照)。

リスト 24. カスタム例外を作成する
class NoFileExistsException extends Exception {

   public function informativeMessage(){
      $message = "The file, '".$this->getMessage()."', called on line ".
           $this->getLine()." of ".$this->getFile().", does not exist.";
      return $message;
   }

}

(簡単のため、私は上記のコードを WFDocument.php ファイルに追加しましたが、必要なときにアクセスできる場所であれば、どこに追加しても構いません。)

上記のコードで新しく作成した NoFileExistsException クラスには informativeMessage() というメソッドしか定義していませんが、実際のところ、このクラスは Exception でもあるため、Exception オブジェクトのパブリック・メソッドとプロパティーのすべてを使用することができます。

例えば、informativeMessage() 関数内を見てみると、getLine() メソッドと getFile() メソッドを呼び出しています。このコードではこれらのメソッドを定義していませんが、基底クラスの Exception で定義されているため、使用できるというわけです。

続いて、実際の動作を確認してみましょう。

カスタム例外をキャッチする

新しい例外タイプを使用するのに最も簡単な方法は、汎用の Exception をスローする場合と同じように、その例外をスローすることです (リスト 25 を参照)。

リスト 25. カスタム例外をスローしてキャッチする
    function download() {

      $filepath = UPLOADEDFILES.$this->filename;

      try {

         if(file_exists($filepath)){
           if ($stream = fopen($filepath, "rb")){
              $file_contents = stream_get_contents($stream);
              header("Content-type: ".$this->filetype);
              print($file_contents);
           } 
         } else {
           throw new NoFileExistsException ($filepath);
         }

      } catch (NoFileExistsException $e) {

         echo "<p style='color: red'>".$e->informativeMessage()."</p>";

      }
   }

例外を作成するときに渡しているのは $filepath だけですが、完全なメッセージ「The file, 'c:/sw/temp/NoTooMiLogoQWSQ.png', called on line 41 of C;\sw\xampp\htdocs\WFDocument.php, does not exist. (C;\sw\xampp\htdocs\WFDocument.php のライン 41 で呼び出されている c:/sw/temp/NoTooMiLogoQWSQ.png ファイルが存在しません。)」が返されます (図 10 を参照)。

図 10. カスタム例外を使用する
カスタム例外が使用されている画面のスクリーン・キャプチャー

複数の例外を扱う

カスタム例外クラスを作成する理由の 1 つは、PHP では複数の例外をそれぞれに区別できることを利用するためです。例えば、1 つの try に対して複数の catch ブロックを作成することも可能です (リスト 26 を参照)。

リスト 26. 例外を区別する
...
   function download() {

      $filepath = UPLOADEDFILES.$this->filename;

      try {

         if(file_exists($filepath)){
           if ($stream = fopen($filepath, "rb")){
              $file_contents = stream_get_contents($stream);
              header("Content-type: ".$this->filetype);
              print($file_contents);
           } else {
              throw new Exception ("Cannot open file ".$filepath);
           }
         } else {
           throw new NoFileExistsException ($filepath);
         }

      } catch (NoFileExistsException $e) {

         echo "<p style='color: red'>".$e->informativeMessage()."</p>";

      } catch (Exception $e){

         echo "<p style='color: red'>".$e->getMessage()."</p>";
      }
   }
}

この例では、問題が発生する前にそれをキャッチするために、ファイルの有無をチェックしてから NoFileExistsException をスローします。このチェックを通ったとしても、他の何かが問題でファイルを開けない場合は、汎用例外をスローします。PHP はスローされた例外のタイプを検出し、それに応じた catch ブロックを実行します。

単にメッセージを出力するだけのために、ここまでするのは少しやり過ぎのように思えるかもしれませんが、可能性を阻むものは何もありません。例外に対処するカスタム・メソッドを作成して、例えば特定のイベントの通知を送信したり、複数のカスタム catch ブロックを作成して、状況ごとに異なるアクションを実行したりすることもできます。

さらに、例外を使用することで、厳密にはエラーであるけれども実際にプログラムを停止するまでのことはない状況をトラップすることも可能です。例えば、画像を処理しようとして処理に失敗したとしても、プログラムを終了するのではなく、そのまま実行を継続することができます。

こうしたさまざまな例外を定義したからといって、1 つひとつの例外をキャッチしなければならないというわけではありません。それが、次に取り上げる話題です。

例外を伝播する

継承には、オブジェクトをその基底クラスのメンバーであるかのように扱えるという便利な機能もあります。例えば、NoFileExistsException をスローして、これを汎用 Exception としてキャッチすることができます (リスト 27 を参照)。

リスト 27. 例外のキャッチを組み合わせる
...
   function download() {

      $filepath = UPLOADEDFILES.$this->filename;

      try {

         if(file_exists($filepath)){
           if ($stream = fopen($filepath, "rb")){
              $file_contents = stream_get_contents($stream);
              header("Content-type: ".$this->filetype);
              print($file_contents);
           } else {
              throw new Exception ("Cannot open file ".$filepath);
           }
         } else {
           throw new NoFileExistsException ($filepath);
         }

      } catch (Exception $e){

         echo "<p style='color: red'>".$e->getMessage()."</p>";
      }
   }
}

上記の例では、例外がスローされると、PHP はその例外に該当する最初のブロックを探して、catch ブロックのリストを上から順番に当たって行きます。この例では catch ブロックは 1 つしかありませんが、図 11 の「File 'c:/sw/temp/NoTooMiLogoQWSQ.png' does not exist. (ファイル c:/sw/temp/NoTooMiLogoQWSQ.png が存在しません。)」メッセージからわかるように、任意の Exception をキャッチすることができます。

図 11. 例外を伝播する
例外が伝播されていることを示す画面のスクリーン・キャプチャー

1 つにまとめる

ファイルのダウンロード・プロセスが整ったので、今度はすべての構成要素を 1 つにまとめてアプリケーションを仕上げる作業に取り掛かります。このセクションでは、まだ完了していない各種作業を行います。

  • 管理者の検出
  • 管理者がファイルを承認するために使用するフォームの作成
  • 別のサーバーから呼び出されていないことを確認するためのダウンロードのチェック

最初に、ファイルを承認する管理者をセットアップする必要があります。

管理者を検出する

当初、データベースに users テーブルを作成したときには、一般ユーザーと管理者を区別しなればならないという点を考慮していませんでした。そこで、今それに対処します。MySQL にログインして、以下のコマンドを実行してください。

alter table users add status varchar(10) default 'USER';
update users set status = 'USER';
update users set status = 'ADMIN' where id=3;

最初のコマンドによって、新しい status という列が users テーブルに追加されます。登録ページにはユーザー・タイプを指定していなかったので、システムに追加されるすべての新規ユーザーに、単純にデフォルト値の USER を指定します。2 番目のコマンドで、このステータスを既存のユーザーに設定します。そして最後のコマンドで、管理者にするユーザーを選択します (皆さんのデータに応じた適切な id 値を使用してください)。

データの用意ができたので、現在のユーザーのステータスを返す関数を作成することができます。リスト 28 に記載する関数を scripts.txt に追加してください。

リスト 28. ユーザー・ステータスを検出する
function getUserStatus()
{
    $dbh = new PDO('mysql:host=localhost;dbname=workflow', 'wfuser', 'wfpass');
    $stmt = $dbh->prepare("select * from users where username= :username");

    $stmt->bindParam("username", $username);

    $username = $_SESSION["username"];

    $stmt->execute();

    $status = "NONE";
    if ($row = $stmt->fetch()) {
        $status = $row["status"];
    }

    $dbh = null;

    return $status;
}

このプロセスがどのように機能するのかを確認するために、該当するデータベースへの接続を作成します。次に、ユーザー名のパラメーターを使用して SQL 文を準備します。そのパラメーターには、$_SESSION 変数に格納されている実際のユーザー名を設定します。続いて、その SQL 文を実行して、最初のデータ行 (おそらく唯一の行) の取得を試みます。

最初は、$statusNONE として定義してデータの取得を試みます。行が存在しなければ、この変数はそのままの状態を維持するだけです。一方、行が存在する場合は、そのステータスを status 列と同じ値に設定します。最後に接続を閉じて、値を返します。

ファイルを承認する: フォーム

ここまでで、フォームに承認機能を追加する準備ができました。この例では、ファイルの一覧を表示しているユーザーが管理者である場合、ファイルを保留状態にするためのチェック・ボックスを表示する必要があります。scripts.txt の display_files() 関数が、その動作を処理します (リスト 29 を参照)。

リスト 29. 管理用の関数を追加する
function display_files()
{

    $userStatus = getUserStatus($_SESSION["username"]);

    if ($userStatus == "ADMIN") {
        echo "<form action='/approve_action.php' method='POST'>";
    }

    $workflow = json_decode(file_get_contents(UPLOADEDFILES . "docinfo.json"), true);

    echo "<table width='100%'>";

    $files = $workflow["fileInfo"];

    echo "<tr><th>File Name</th>";
    echo "<th>Submitted By</th><th>Size</th>";
    echo "<th>Status</th>";
    if ($userStatus == "ADMIN") {
        echo "<th>Approve</th>";
    }
    echo "</tr>";

    for ($i = 0; $i < count($workflow["fileInfo"]); $i++) {
        $thisFile = $workflow["fileInfo"][$i];
        if (
            >($userStatus == "ADMIN") ||>
            ($thisFile["approvedBy"] != null) ||
            (
                    isset($_SESSION["username"]) &&
                    ($thisFile["submittedBy"] == $_SESSION["username"])
            )
        ) {

            echo "<tr>";
            echo "<td><a href='/loggedin/download_file.php?file=" .
                    $thisFile["fileName"] . "&filetype=" . $thisFile["fileType"] .
                    "'>" . $thisFile["fileName"] . "</a></td>";
            echo "<td>" . $thisFile["submittedBy"] . "<lt;/td>";
            echo "<td>" . $thisFile["size"] . "</td>";
            echo "<td>" . $thisFile["status"] . "<td>";
            if ($userStatus == "ADMIN") {
                if ($thisFile["status"] == "pending") {
                    echo "<input type='checkbox' name='toapprove[]' ".
                                   "value='" . $i . "' checked='checked' />";
                }
            }
            echo "</tr>";
        }
    }

    echo "</table>";
    if ($userStatus == "ADMIN") {
        echo "<input type='submit' value='Approve Checked Files' />";
        echo "</form>";
    }

}

先頭から説明すると、まず、ユーザーのステータスを判別します。この処理は、次の 2 つの理由で重要です。1 つは、ユーザーが管理者の場合、承認フォームを表示する必要があるからです。もう 1 つは、ユーザーが管理者である場合には、すべてのファイルを表示することになるからです。それぞれのファイルをどのユーザーがアップロードしたか、アップロードしたユーザーのステータスが何であるかは関係ありません。これに対処するために、if 文にもう 1 つの条件を追加します。

実際のファイルの表示には、ユーザーが管理者であり、かつファイルが保留状態の場合には、事前にチェック・マークを入れたチェック・ボックスを表示します。チェック・ボックスの値は、後で参照できるようにファイルの番号にします。ユーザーには、配列のような名前を指定します (この例では、toapprove[] が Web サーバーに対し、同じフィールド名に複数の値を想定するように指示します)。

図 12 に示されているように、フォームには適切なフィールド (ファイル名、送信者、ファイル・サイズ、ステータス) と、承認する場合に選択するチェック・ボックスが表示されるようになりました。

図 12. 承認フォーム
承認フォームのスクリーン・キャプチャー

ファイルを承認する: JSON の更新

承認チェック・ボックスを受け入れる実際のフォーム・ページ (approve_action.php) は、実に単純なものです (リスト 30 を参照)。

リスト 30. 承認フォームを処理する
<?php

  session_start();

  include "/scripts.txt";

  $allApprovals = $_POST["toapprove"];
  foreach ($allApprovals as $thisFileNumber) {
     approveFile($thisFileNumber);
  }
  echo "Files approved.";

?>

toapprove チェック・ボックスのそれぞれに対して、単純に approveFile() 関数を呼び出します。この関数は scripts.txt 内にあります (リスト 31 を参照)。

リスト 31. フォームを承認する
function approveFile($fileNumber){

    $workflow = json_decode(file_get_contents(UPLOADEDFILES . "docinfo.json"), true);

    $workflow["fileInfo"][$fileNumber]["approvedBy"] = $_SESSION["username"];
    $workflow["fileInfo"][$fileNumber]["status"] = "approved";

    $jsonText = json_encode($workflow);
    file_put_contents(UPLOADEDFILES . "docinfo.json", $jsonText);

}

上記のコードは、ファイルを表示するときと同じように、json_decode() を使用してデータをロードするところから始まっています。この例では、approvedBy 値を設定するために、さまざまな参照を連結します。すべてのデータは、$workflow 変数に取り込まれます。fileInfo プロパティーはすべてのファイルが組み込まれる配列なので、必要なファイルを $fileNumber によって参照します。該当するファイルを取得した後、approvedBy プロパティーを設定することができます。同様に、statusapproved に設定します。

最後にファイルを保存します。

以上は説明を簡単にするための方法であることに注意してください。本番アプリケーションでは、一度だけファイルを開いてロードし、すべての変更を行ってからファイルを保存するほうが効率的です。

いくつかのファイルを承認してからファイルを再表示して、動作をテストしてください。

ダウンロード時のセキュリティー・チェック

最後のステップとして、ダウンロード・プロセスにセキュリティー・チェックを追加する必要があります。ダウンロード・プロセスは完全にアプリケーションによって制御するため、任意のチェックを適用することができます。この例では、ユーザーがローカル・サーバー上のページでファイルへのリンクをクリックしたことをチェックして、誰かが外部サイトからローカル・サーバー上のファイルにリンクすることはもちろん、リンクにブックマークを付けたり、他の誰かにリンクをそのまま送信したりすることを防ぎます。

まずは、こうしたことが起きたときのために、新しい例外を WFDocument.php ファイル内に作成することから始めます (リスト 32 を参照)。

リスト 32. リモートからのダウンロードを無効にする
<?php
   include_once("/scripts.txt");

class NoFileExistsException extends Exception {

   public function informativeMessage(){
      $message = "The file, '".$this->getMessage()."', called on line ".
           $this->getLine()." of ".$this->getFile().", does not exist.";
      return $message;
   }

}

class ImproperRequestException extends Exception {

   public function logDownloadAttempt(){
      //Additional code here
      echo "Notifying administrator ...";
   }

}

class WFDocument {

   private $filename;
   private $filetype;

   function setFilename($newFilename){
      $this->filename = $newFilename;
   }
   function getFilename(){
      return $this->filename;
   }

   function setFiletype($newFiletype){
      $this->filetype = $newFiletype;
   }
   function getFiletype(){
      return $this->filetype;
   }

   function __construct($filename = "", $filetype = ""){
      $this->setFilename($filename);
      $this->setFiletype($filetype);
   }

   function download() {

      $filepath = UPLOADEDFILES.$this->filename;

      try {

         $referer = $_SERVER['HTTP_REFERER'];
         $noprotocol = substr($referer, 7, strlen($referer));
         $host = substr($noprotocol, 0, strpos($noprotocol, "/"));
         if ( $host != 'boxersrevenge' &&
                                $host != 'localhost'){
            throw new ImproperRequestException("Remote access not allowed.
                        Files must be accessed from the intranet.");
         }

         if(file_exists($filepath)){
           if ($stream = fopen($filepath, "rb")){
              $file_contents = stream_get_contents($stream);
              header("Content-type: ".$this->filetype);
              print($file_contents);
           } else {
              throw new Exception ("Cannot open file ".$filepath);
           }
         } else {
           throw new NoFileExistsException ($filepath);
         }
      } catch (ImproperRequestException $e){

         echo "<p style='color: red'>".$e->getMessage()."</p>";
         $e->logDownloadAttempt();

      } catch (Exception $e){

         echo "<p style='color: red'>".$e->getMessage()."</p>";

      }
   }
}

?>

ImproperRequestException 内に作成する logDownloadAttempt() という新しいメソッドでは、e-メールを送信したり、他のアクションを実行したりすることができます。このメソッドをこの例外タイプの catch ブロックで使用します。

実際の download() 関数では、最初に HTTP_REFERER を取得します。このオプションのヘッダーは Web リクエストと一緒に送信されるもので、リクエストがどのページから行われたかがこのヘッダーの値から特定されます。例えば、皆さんが自分のブログから developerWorks にリンクしている場合、そのリンクをクリックすると、IBM ログにそのアクセスの HTTP_REFERER として、皆さんのブログの URL が示されます。

この例では、リクエストがサンプル・アプリケーションから行われていることを確実にする必要があります。そこで、先頭の「http://」文字列を取り除き、最初のスラッシュ (/) までのすべてのテキストを保存します。このテキストが、リクエストのホスト名です。

外部リクエストの場合は boxersrevenge.nicholaschase.com のようなホスト名になりますが、ここで探しているのは内部リクエストだけです。したがって、boxersrevenge または localhost を受け入れます。リクエストがそれ以外の場所から行われているとしたら、ImproperRequestException をスローします。すると該当するブロックによって、この例外がキャッチされます。

この方法は、セキュリティーに関する限り絶対に確実というわけではないことに注意してください。ブラウザーによっては、リファラー情報をサポートしてないか、ユーザーが送信する内容を改変しているために、リファラー情報を正確に送信しない場合もあります。けれども、この例から、コンテンツを制御するために行える処理のタイプを把握できるはずです。


まとめ

今回のチュートリアルでは、3 部構成のシリーズ「PHP の学習」の締めくくりとして、単純なワークフロー・アプリケーションを完成させました。前回までは、構文、フォームの処理、データベースへのアクセス、ファイルのアップロード、XML、JSON などの基本に重点を置きました。今回のチュートリアルでは、それらを一歩進めて 1 つにまとめ、管理者が各種のファイルを承認できるようにするフォームを作成しました。今回取り上げたトピックは以下のとおりです。

  • HTTP 認証を使用する方法
  • ファイルをストリーム処理する方法
  • クラスおよびオブジェクトを作成する方法
  • オブジェクトのプロパティーおよびメソッド
  • オブジェクト・コンストラクターを使用する方法
  • オブジェクトの継承を使用する方法
  • 例外を使用する方法
  • カスタム例外を作成する方法
  • ダウンロードの追加セキュリティー・チェックを実行する方法

ダウンロード

内容ファイル名サイズ
Part 3 source codePart3CodeFiles.zip68KB

参考文献

学ぶために

製品や技術を入手するために

  • XAMPP をダウンロードしてください。
  • PHP 5.x をダウンロードしてください。
  • Apache Web サーバーのバージョン 2.x をダウンロードしてください。
  • MySQL をダウンロードしてください。
  • IBM 試用版ソフトウェアを入手し (ダウンロードまたは DVD で入手できます)、開発者専用のソフトウェアを使用して次のオープンソース開発プロジェクトを革新してください。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Open source, Linux, XML
ArticleID=956689
ArticleTitle=PHP の学習: 第 3 回 認証、オブジェクト、例外、ストリーム処理
publish-date=12122013