実際に手を動かして、PHPを学んでいきます。架空のテニスサークルで利用するサイトを例に、Webアプリケーションを作成していきます。作成前に行う企画設計やデザインについても触れ完成形を考えながら開発を進めていきます。さらに機能を追加するにはどうしたら良いか、セキュリティーに対する知識など、発展的な内容についてもサポートしています。では、一緒にWebアプリケーションを作っていきましょう。
PHPの開発環境がまだ整っていない方は、【PHP入門】環境構築でつまずかない! 初心者でも安心!を参考になさってください。
Webアプリケーションを作るにあたって、最初にすべきこと
① Webアプリケーションを企画する
まずは、日常における問題や不便な点について考え、要望を洗い出してWebアプリケーションを企画します。ここでは、次のような企画を考えました。
<企画>
あなたは、大学のテニスサークルの広報・連絡担当になりました。去年まではメンバーが少なく、メールや電話で連絡を取り合っていましたが、今年は新入部員が30人も集まりました。
人数が多くなったので、ミーティングにメンバーを集めるのも、練習日の連絡をするのも、メールだけでは対応しきれなくなってしまいました。記録係が合宿で撮った写真をメンバーに見せるときも不便があり、紙の写真を印刷するのも手間でした。
このような経緯から、メンバーのためのサークルサイトを作ることにしました。
企画が決まったら、この Webアプリケーションを誰が使うのかという想定ユーザ(エンドユーザ)を決めましょう。
今回はテニスサークルのメンバーが使うため、想定ユーザは学生です。また、メンバーは広報や記録係などの役員として活動する人と、一般のサークル参加者という2種類のグループがあります。
②機能の洗い出しをする
では、この企画から機能の洗い出しをしましょう。大きなWebアプリケーションを作るのははじめてなので、まずは最低限、しかし今後の拡張性を考えて機能を考えましょう。
機能1:お知らせ
ミーティングや練習日の日程をWebサイト上に表示する機能。
お知らせを記載するのは広報係。
一般メンバーはお知らせ内容の確認ができる。
機能2:アルバム
アップロードした写真を見ることができる機能。
サークルメンバーなら誰でもアップロード・閲覧できる。
機能3:掲示板
ミーティングの日に全員が集まれないことを考慮した、意見交換のできる掲
示板機能。
サークルメンバーなら誰でも書き込みができる。
③画面遷移を考える
機能が決まったら、画面遷移を考えましょう。画面遷移とは、ボタンをクリックすると次の画面へ移動するという、Webサイト内での画面の動きのことです。
画面単位で全体の作成する分量を把握しておくと、どこまで開発が進んだのかつかみやすくなります。
④デザインを考える
画面遷移を考えたら、ざっくりした画面の内容を決めましょう。画面には、フォーム(入力欄や選択項目といったHTMLの部品)や見出しなどを配置します。例えばログイン画面なら、ユーザ名とパスワードを入力するためのフォームが必要ですね。
こうして検討すると、実際の Webアプリケーションへの具体的なイメージがわきますね。開発を進めていくうちに、画面遷移図に過不足が出ますが、慣れないうちはよくあることです。何度も企画、機能の洗い出し、画面遷移を考えるという作業をすることで、Webアプリケーションの定石のようなものが見つかります。
Bootstrap を使おう
個人で技術の練習をするときに、とても大事なものがなにかおわかりでしょうか。そう、モチベーションです。そっけない画面のためにプログラミングするよりも、美しい画面のほうがやる気が出てきませんか?
実際の開発現場においても、企画と設計が終わると、Webデザイナーからデザインのテンプレートをもらって、それを組み込みながらプログラムを書くことがよくあります。
WebデザインとWebアプリケーションは切っても切れない関係です。出来上がったものの善し悪しは、プログラムの速度や上手さだけでなく、使いやすく美しいデザインであることも関係してきます。
というわけで、今回は、世界でも人気のある Bootstrap という CSS フレームワークを使って画面を作っていきましょう。CSSとは HTMLを美しくスタイリングするための技術で、フレームワークというのは枠組みという意味です。
Bootstrapの枠組みにそってHTMLタグを書くと、とてもきれいな見た目のサイトを作ることができます。
では、サークルサイト開発の下準備をしていきましょう。まず、ドキュメントルートに新しいフォルダを作成します。ドキュメントルートとは、公開フォルダのことです。WebサイトのHTMLや画像ファイルなどを置くフォルダのことを指します。
XAMPPを「C:\xampp」にインストールしたので、ドキュメントルートは「C:\xampp\htdocs」です。このような、あるフォルダやファイルへの場所を指し示す文字列のことをパスといいます。
ドキュメントルート配下のフォルダ名は、URLの一部になります。サークルサイトのURLを「http://localhost/tennis/」にするため、ドキュメントルートである「C:\xampp\htdocs」の中に、「tennis」というフォルダを作成しましょう。
絶対パス
ドキュメントルートにindex.html というファイルがあるとき、これは「http://localhost/index.html」と指定できます。この httpなどから書き始める指定方法のことを絶対パスと呼び、インターネット上での場所を指しています。
フォルダはスラッシュ( / ) で区切ります。もしドキュメントルート に tennisというフォルダがあれば、URLは「http://localhost/tennis/」となります。
特に末尾のスラッシュの後にファイル名を指定しなければ、サーバの設定により、一般的に index.html や index.php といったファイルが表示されます。
末尾にスラッシュがない場合は、そのフォルダ名のファイルを探そうとします。「 http://localhost/tennis」の場合は、ドキュメントルートから tennis というファイルを探そうとします。ない場合はサーバの設定により、一般的には tennis フォルダのindex.htmlを探します。
相対パス
index.html から見ると、画像ファイルは「images/image.gif」と指定することができます。httpから指定しない、相対的なファイルの場所のことを相対パスといいます。
現在いる場所を示すのは「. 」です。サーバOS としてよく採用される Linuxではフォルダのことをディレクトリといい、現在のディレクトリをカレントディレクトリといいます。
画像ファイルのことを単純に「images/image.gif」と指定することもできますが、もっと丁寧に「./images/image.gif」とします。
逆に、images.gif から1つ上の階層である tennis フォルダの index.html を指定する場合は、「../index.html」というように、「 ../ 」で上の階層を表すことができます。2つ階層を戻る場合は「…/」というように、戻りたい分だけ続けます。
なぜ相対パスを使うかというと、絶対パスはドメイン(URLの~.comなどの部分)が変更になり、リンク切れになる可能性があるためです。他のサイトへのリンクでない限り、相対パスを使いましょう。
トップページの作成
ドキュメントルートに tennisフォルダを作ったら、Webサイト作成の下準備としてトップページを作りましょう。内容は次の通りです。tennisフォルダ直下に、それぞれ「index.php」と「navbar.php」とファイル名を付けて保存してください。保存したら、「http://localhost/tennis/」にアクセスし、正しく表示されるか確認しましょう。
トップページ (index.php)
<!DOCTYPE html>
<html lang="ja" >
<head>
<title>サークルサイト</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">
</head>
<body>
<!ナビゲーションバー(上の黒い帯)を読み込む部分>
<?php include('navbar.php'); ?>
<main role="main" class="container" style="padding: 60px 15px 0">
<div>
<h1>サークルサイト</h1>
</div>
</main>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" crossorigin="anonymous"></script>
<script>window.jQuery || document.write('<script src=“/docs/4.5/assets/js/vendor/jquery-slim.min.js”><\/script>')</script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.bundle.min.js"></script>
</body>
</html>
(navbar.php)
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
<a class="navbar-brand" href="./index.php"> サークルサイト</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
<ul class="navbar-nav mr-auto">
<li class="nav-item"><a class="nav-link" href="#">お知らせ</a></li>
<li class="nav-item"><a class="nav-link" href="#">アルバム</a></li>
<li class="nav-item"><a class="nav-link" href="#">掲示板</a></li>
</ul>
</div>
</nav>
実行結果

index.phpの内容を確認しましょう。1行目はドキュメントタイプ宣言で、HTMLのお決まりのコードです。2行目の<html>タグから末尾の</html>までがHTMLであることを意味しています。<head>~</head> タグは、このHTMLのヘッダ情報です。titleタグが、ブラウザのタイトルバーに表示されるタイトル名です。
linkから始まるタグは、CSSの場所を指定しています。今回、見た目のよいサイトを作るために Bootstrapを使うことを前の節で説明しましたが、そのCSSの場所を指定します。
<body>以降がブラウザ上に表示される部分です。
今回のデザインでは、ページの上部に黒い帯のようなナビゲーションバーを設置し、そこにお知らせ機能や掲示板へジャンプできるリンクを設置します。各ページでも同じメニューを使いたいので、メニュー部分だけ navbar.php として切り出しましょう。
この index.php と navbar.php がサークルサイトの基本のファイルになります。どこかに別名で保存して、必要なときにコピーできるようにすると、今後の開発が楽になります。
お知らせ機能を作る
この章では、サークルサイトにミーティングのお知らせなどを掲載する機能を作成しながら、ファイル操作について学びましょう。
お知らせのテキストファイルをサーバにアップロードし、サークルサイト上では PHPからファイルを読み取って表示をさせましょう。
ファイル読み込みの下準備
まず、ファイルを読み込むための下準備をします。次のお知らせファイルをエディタで作成し、ドキュメントルート内の tennis フォルダの中に info.txt という名前で保存してください。
お知らせ (info.txt)
〇月×日のミーティングは3時からです。
場所は102教室です。
開発時はローカル環境でプログラムの作成とテストを行うため、ファイルをサーバにアップロードする代わりにドキュメントルートに保存します。
また、ここでは説明の簡略化のため tennis フォルダに info.txt を置いていますが、Webアプリケーションを外部に公開する場合には、ファイルの意図しない漏えい(URL指定でファイルに直接アクセスするなど)を防ぐため、ドキュメントルートの外にファイルを置き、URLを直接指定しても見えないようにしましょう。
ファイルの読み込みと表示では、ファイルを読み込んで表示してみましょう。
ファイルの読み込みには、file_get_contents 関数を使います。
この関数は、その名が示す通り、引数で渡されたファイルの中身をすべて読み込み、文字列を返り値にする関数です。
では、file_get_contents 関数を使って、index.phpにファイルを読み込む機能を追加します。
ファイルを読み込む機能を追加(index.php)
<?php $info = file_get_contents("info.txt"); ?> ←追加
<!DOCTYPE html>
(略)
<h1>サークルサイト</h1>
<h1> お知らせ</h1> ←追加
<p><?php echo $info; ?></p> ←追加
はじめのコードは、info.txt を読み込み、返り値を $info に代入しています。本文のコードは、$infoの中身を表示しています。では、「http://localhost:tennis/index.php」を表示して動作確認してみましょう。
実行結果

お知らせファイルの2行が読み込まれています。しかし、ファイルでは
「3時からです。」の後に改行していたのに、ブラウザでは改行されていません。改行付きで表示するブラウザ上で改行するには、改行を示す<br>タグを使う必要があります。
お知らせファイルにあった改行は、目には見えませんが改行コードというコードで表現されています。この改行コードを<br>タグに置き換える必要があります。
改行コードを<br>タグに置き換えるには、nl2br (エヌエルツービーアール)関数を使います。第1引数は元の文字列、返り値は改行コードが置き換わった文字列です。
改行タグは、第2引数になにも指定しないとXHTML準拠の<br />タグになり、第2引数にfalse を指定すると<br>タグになります。
では、nl2br関数を使って本文のコードを修正しましょう。
<h1>お知らせ </h1>
<p><?php echo nl2br($info, false); ?></p>
実行結果

読み込み内容に対してnl2br関数を使うように修正したため、うまく改行が表示されましたね!
ファイルを1行ずつ読み込む
お知らせタイトルと詳細
お知らせをファイルから読み込んでトップページに表示できましたが、もしお知らせが長い文章だったとき、すべてがトップページにずらずらと並ぶのは見づらいですよね。
そこで、トップページである index.phpにはお知らせのタイトルだけを表示し、タイトルのリンクをクリックすると詳細画面の info.phpにジャンプするように修正しましょう。
お知らせ修正の下準備
お知らせのテキストファイルを、1行目をタイトル、2行目以降が本文になるように修正しましょう。
お知らせの修正(info.txt)
ミーティングの日程について ←タイトル
日時:○月×日△時 ←お知らせ本文
場所:102教室
議題:コートの使用曜日について
筆記用具を持参してください
また、お知らせの詳細画面はトップページのデザインを踏襲するため、 index.phpをコピーして、info.phpという名前のファイルを作りましょう。
ファイル読み込みの流れ
さっそくお知らせファイルの1行目だけを読み込むようにトップページを修正したいところですが、file_get_contents 関数はファイル全文を読み込む関数です。1行目だけ読み込むにはどうしたらよいでしょうか?
ファイルから1行だけ読み込むには、次のような手順を踏む必要があります。
ファイルを開く
fopen
ファイルから1行読み込む
fgets
ファイルを閉じる
fclose
fopen関数でファイルを開き、fgets 関数で1行読み取り、fclose 関数でファイルを閉じます。この流れは私たちがワープロソフトで作ったファイルを開いて、中身を読み、閉じるボタンを押してファイルを閉じるという動作に似ています。
fopen 関数
ファイルを開くfopen関数は次のように書きます。
引数で指定された「ファイル名」を、指定された「モード」で開きます。
正しく開けたらファイルポインタを返り値として返します。
ファイルポインタとは、どのファイルのどの位置にいるかを表したもので、ワープロソフトでいうカーソルのことです。私たちがマウスとキーボードを使って行う「カーソルを移動させて3文字目から5文字目をコピーする」というような動きを、読み込み内容に対して実現できます。index.phpでは、ファイルの先頭から1行分カーソルを動かして文字列を取得します。
ファイルを開くモード
fopen関数の第2引数であるモードは、どのような状態でファイルを開くか指定するためのものです。次のようなモードがあり、文字列で指定します。ファイルサイズ0は、白紙の状態を表しています。
r
読み込み専用 |読み込みだけできるモード。
・ファイルポインタ位置:ファイルの先頭
・ファイルサイズ 開くだけなのでそのまま
・ファイルがない場合:開けない(戻り値 false)
r+
読み・書き
読むことも書くこともできるモード。
・ファイルポインタ位置:ファイルの先頭
・ファイルサイズ :そのまま
・ファイルがない場合 :開けない(戻り値 false)
w
書き込み専用
書き込みだけできるモード。
・ファイルポインタ位置:ファイルの先頭
・ファイルサイズ :0になる
・ファイルがない場合:作成する
w+
読み・書き
読むことも書くこともできるモード。
・ファイルポインタ位置:ファイルの先頭
・ファイルサイズ :0になる
・ファイルがない場合 :作成する
a
追記(書き足す)
追記するモード。
・ファイルポインタ位置:ファイルの末尾
・ファイルサイズ :そのまま
・ファイルがない場合 :作成する
a+
読み・追記
読むことも追記することもできるモード。
・ファイルポインタ位置:ファイルの末尾
・ファイルサイズ :そのまま
・ファイルがない場合:作成する
例えば、ファイルを開いて読み込むだけの場合は r モードを使い、次のように書きます。
$fp = fopen(“info.txt”, “r”);
ファイルへ書き込みができる wモードで開くと、ファイルサイズが0、つまり白紙の状態になるので、あるファイルをまるっきり書き換えたいときに有効です。「ファイルがない場合に作成する」という動作は、エディタで新規作成したファイルに文字を書き込み、名前を付けて保存する動作によく似ています。
すでに存在するファイルに追記したいときはaモードを使います。ファイル
ポインタがファイルの末尾にくるので、続けて書き込めます。
index.phpでは、お知らせファイルを読み込むだけなので、rモードで開きましょう。
開いたファイルから1行読み込むには、fgets関数を使います。
fgets 関数
fgets(ファイルポインタ);
引数のファイルポインタは、fopen関数の返り値であるファイルポインタです。ファイルポインタを渡すことで、読み込み開始位置がわかるのです。
$fp = fopen (“info.txt”, “r”);
$title = fgets($fp);
fgets 関数の返り値は、取得した1行の文字列です。ファイルの中身が空の場合や、なんらかのエラーがあって読み込めない場合、返り値はFALSEになります。
while構文と fgets 関数を使って、file_get_contents 関数のようにファイル全体を文字列として読み込むこともできます。while構文の条件式は、fgets 関数を使って$lineに1行読み込むというものです。終端にくると読み込むデータがなくなってFALSEを返すので、while構文を抜けます。
$fp=fopen("info.txt", "r"); // 読み込み専用で開く
// 1行読み込めれば表示処理、読めなければFALSEになる
while ($line = fgets($fp)) {
(
echo $line ."<br>";
}
index.phpではタイトルの1行だけを読み込みたいので、fgets 関数を一度だけ使うことにします。
fclose 関数
ファイルを利用し終えたら閉じる必要があります。ファイルを閉じる fclose関数は次のように書きます。
fclose(ファイルポインタ);
fclose関数の返り値は、正常に閉じることができればTRUE、失敗時はFALSEです。
$fp = fopen(“info.txt”, “r”);
fclose($fp);
トップページ index.php の修正
ファイルを1行だけ読み込んでタイトルとして表示するよう、index.phpの冒頭と本文部分を修正しましょう。
<?php $fp fopen("info.txt", "r"); // ファイル読み込み ?>
<!DOCTYPE html>
<html lang="ja" >
(省略)
<h1> お知らせ</h1>
<?php if ($fp){
// ファイルが正しく開けたとき
$title = fgets($fp); // ファイルから1行読み込む
if ($title){
// 1行読み込めたときはタイトル文字列をリンクにする
echo '<p><a href="info.php">'.$title.'</a></p>';
} else {
// ファイルの中身が空だったとき
echo '<p> お知らせはありません。</p>';
}
fclose($fp); // ファイルを閉じる
} else {
// ファイルが開けなかったとき
echo '<p> お知らせはありません。</p>';
}
?>
実行結果

冒頭のfopen関数でお知らせファイルを開きます。モードは読み込み専用の「モードです。開いたファイルのファイルポインタは、変数$fp (file pointer:ファイルポインタの意味)へ代入します。
はじめのif構文は、ファイルがうまく開けたかどうか判別しています。正しく開けていればファイルポインタが返り値になっているので次の処理へ進みます。
正しく開けなかった場合は、返り値がFALSE となり、「お知らせはありません」と表示します。
正しく開けた場合、fgets 関数で1行読み込みます。rモードで fopen したので、ファイルポインタはファイルの先頭にあります。先頭から1行取得し、変数$title に代入しています。
次のif構文では、1行読み込めたかどうかを判別しています。正しく読み込めていれば、変数$titleには文字列が入っているので、タイトル文字列をリンクにします。
aタグはアンカータグと呼ばれ、リンクを示すHTMLタグです。aタグで挟んだ文字列をクリックすると、hrefで指定したリンク先へジャンプします。
この場合、タイトルをクリックすると、本文へとぶようにしたいのです。
ファイルは正しく開けたものの中身が空だった場合、fgets 関数は FALSEを返り値にします。この場合も「お知らせはありません」と表示しましょう。
ファイルを開いたら閉じる必要があるので、1行読めた場合・中身が空だった場合のどちらでも、fclose関数で閉じます。
詳細画面の作成
では次に、タイトル文字列をクリックしたジャンプ先となる、お知らせ詳細の info.phpを作成しましょう。下準備でコピーした info.phpを修正していきます。
詳細画面では、info.txtの内容を読み込んで、1行目を見出し、2行目以降をお知らせ本文として表示します。
お知らせ詳細画面 (info.php冒頭部分)
<?php
$fp = fopen("info.txt", "r"); // ファイルを開く
$line = array(); // ファイル内容を1行ずつ要素に格納するための配列を用意
$body = ‘ ‘; // 本文を格納するための変数
// ファイルが正しく開けたとき
if ($fp){
while(!feof($fp)) {
$line[] = fgets($fp);
}
fclose($fp);
}
?>
<!DOCTYPE html>
<html lang="ja" >
お知らせ詳細画面 (info.php本文部分)
<h1> お知らせ</h1>
<?php
// お知らせがあるとき
if (count($line) > 0){
for ($i = 0 ; $i < count($line); $i++){
if ($i == 0){
// 1行目(=0番目の要素)はタイトル
echo '<h2>'.$line[0].'</h2>';
} else {
// $1行目に改行タグを付けて本文変数に代入
$body . = $line[$i].'<br>';
}
}
} else {
$body ‘お知らせはありません。’ ;
}
echo ‘<p>'.$body.'</p>';
?>
//navbar.phpリンク部分
<li class="nav-item"><a class="nav-link" href="info.php">
お知らせ</a></li>
<li class="nav-item"><a class="nav-link" href="#">アルバム</a></li>
<li class="nav-item"><a class="nav-link" href="#">掲示板</a></li>
実行結果

info.phpの冒頭部分では、まずファイルを開き、ファイルポインタを取得します。$lineはファイル内容を1行ずつ格納するための配列で、タイトルは$line[0]、2行目以降を$line[1] 以降に格納します。$bodyは本文を格納するための配列です。
ファイルが正しく開けたら、while構文を使って配列$line に fgetsした内容を1行ずつ格納していきます。feof関数は後に詳しく説明しますが、ファイルの終端かどうかをチェックする関数です。TRUE の場合終端を示します。
つまり、このwhile関数はファイルが終端でない間、$lineに1行ずつ格納する処理をしています。読み込みが終わったら fclose 関数でファイルを閉じます。
本文部分の処理では、count 関数を使って$lineの行数を数えます。
正しくファイルが開けて、かつ正しくファイルの中身が配列に格納されていると1行以上内容があるはずですので、count 関数の返り値は0以上になるはずです。ファイルがうまく開けていない場合は、「else{ body= 」の処理に移ります。配列は作成時のままで要素数は0個となるので、本文を格納する $body 変数には「お知らせはありません」と代入しています。
さて、ファイルがうまく開けていたら、for構文の処理に移ります。このfor構文では、1行目から最後の行まで1行ずつ処理していきます。
もし $1が0だったら、お知らせファイルの1行目はタイトルなので、<h2>タグで文字列を囲みます。h2 タグは見出しタグで、h1に次ぐ小見出しの意味があります。
2行目以降は、末尾に改行タグ<br>を付けて$body変数に追加していきます。代入ではなく.=なので、前の行にどんどん追加していきます。
すべての処理が終わったら、<p>タグに挟んで$body変数を表示します。
テキストファイルなどには、通常は目に見えないいくつかのコードが含まれています。改行コードもその1つです。同様に、ファイルの末尾にも終端を示すEOFというコードが付いています。
feof 関数は次のように使います。
feof ファイルポインタ
引数は fopen関数の返り値であるファイルポインタで、正しくファイルが開けていることが条件です。返り値は、ファイルの終端のとき TRUE、終端でないときに FALSE です。
ファイルが正しく開けていなかったり、fclose関数ですでにファイルが閉じられていたりするとうまく動作しません。先ほどのプログラムでも、ファイルが正常に開けているか確認した後に feof関数を使っていますね。
配列に含まれる要素数を調べるには、count関数を使います。
count 配列の変数名
引数は配列、返り値は数字です。配列の要素数を返します。連想配列も数えることができます。
いろいろな配列のループ
先ほどの info.phpでは for構文を使って、はじめの行をタイトル、以降を本文としました。while構文や foreach構文を使うこともできます。次のプログラムは、foreach構文を使った書き換え例です。
// キー$iが0だったら、はじめの行なのでタイトルにする
if ($i == 0){
echo ‘<h2>’ . $text.’ </h2>’;
} else {
$body .= $text.’ <br>’
}
}
file関数
指定したファイルの中身を配列にすべて格納します。この際、fopen や fclose をする必要はありません。
file(ファイル名)
file_get_contentsの返り値は読み込んだファイル名の中身全体の文字列でしたが、file関数の返り値は配列になります。info.phpのように読み込むだけであれば、file関数を使うのも楽ですね。
このように、同じ動作をするプログラムでも、いろいろな書き方をすることができます。大きいプログラムになると、書き方によって動作速度に差が出ることもあります。まずは見やすくわかりやすいプログラムを書くよう心がけましょう!
書き込みの流れ – fwrite 関数
読み込みができれば、もちろん書き込みもできます! サークルサイトには書き込み機能を実装しませんが、書き込みの方法について学んでおきましょう。
ファイルの書き込みは、fgets 関数を使ったファイルの読み込みととてもよく似ています。 fopen関数でファイルを開き、fwrite 関数で書き込み、fclose関数でファイルを閉じます。
ファイル読み込みの処理と違って注意すべき点は次の2点です。
fopen関数でファイルを開くとき、書き込みモードか追記モードで開く
fgets 関数でなくfwrite関数で書き込む
fwrite関数は次のように書きます。
fwrite(ファイルポインタ, 書き込み内容)
第1引数の「ファイルポインタ」の位置に、第2引数の「書き込み内容」を書き込みます。ちょうど、ワープロソフトでカーソルの位置に文字を入力するのと似ていますね。
テキストファイルへの書き込み
では、テキストファイルに文字列を書き込んで試してみましょう。次のプログラムを test フォルダに置いて実行してください。
ファイル書き込みプログラム(write.php)
$fp = fopen(“test.txt”, “w”);
if ($fp) {
fwrite($fp, “書き込みテスト”);
fclose($fp);
echo ‘書き込みました。’;
} else {
echo ‘エラーが起きました。’;
}
?>
実行結果
書き込みました。
書き込まれた test.txt
書き込みテスト
このプログラムでは、ファイルを書き込みモード「w」で開きます。ファイルが存在しなければ作成されます。
ファイルポインタが正常に取得できた場合、ファイルに書き込みをし、ファイルを閉じます。
ファイルのアクセス権限
ローカルのWindows環境で動作確認をしているときにはあまり気にする必要はありませんが、実際にインターネット上にプログラムを公開するときにはファイルのアクセス権限を考慮しなくてはいけません。
ファイルを右クリックして[プロパティ]を選択し、ファイルのプロパティを開くと、「セキュリティ]タブのユーザの権限欄に、[読み取り]というチェックボックスがあります。ここにチェックが入っていると、ファイルの書き込みができません。
一般的にWebサーバのOSとして選択される Linuxでは、ファイルの属性が細かくなっており、ファイルの持ち主である「所有者」と、誰にどの程度ファイルを利用する権利があるかという「アクセス権」を設定する必要があります。
アクセス権には書き込み、読み取り、実行の3種類があり、それぞれについて、所有者、グループ、その他のユーザが持てる権限を設定します。
インターネット上のサーバにファイル書き込みプログラムを公開する際、「ローカル環境ではファイルへの書き込みができていたのに、サーバ上ではできない」という場合には、まず書き込みをするファイルのアクセス権を確認しましょう。読み取り専用になっているとプログラムから書き込みができずエラーになります。
なお、インターネット上のWebサーバにファイルをアップロードする場合、FTPソフトや、よりセキュリティの強いSFTPソフトというファイル転送ソフトを使用します。たいていのソフトでは、アップロードしたファイルのアクセス権を簡単に設定できます。
書き込みと改行コード
先ほどの書き込みプログラムを修正し、複数行の書き込みをしてみましょう
複数行ファイル書き込みプログラム(multi_write.php)
$fp = fopen(“test.txt”, “w”);
if ($fp){
fwrite($fp, “書き込みテスト1行目 \n”);
fwrite($fp, “書き込みテスト2行目”);
fclose($fp);
echo ‘書き込みました。’;
} else {
echo ‘エラーが起きました。’;
}
?>
実行結果
書き込みテスト1行目
書き込みテスト2行目
改行は、目に見えない改行コードというものによって表現されていることは説明しましたが、ファイルに書き込みする場合にも、改行の表現には改行コードを入れる必要があります。
書き込みテスト1行目の末尾に「\n」があります。これが改行コードです。
文字コードやフォントの違いによって「¥n」と表示されることもあります。これは特別なコードなので、必ずダブルクォーテーション(“)で囲む必要があります。シングルクォーテーション (‘)だと、「\n」という文字列がそのまま出力されてしまいます。
file_put_contents ですべて書き込む
fopenでファイルを開き、fwriteで書き込み、fcloseでファイルを閉じる。これが基本の流れですが、それらをまとめて行うことのできる関数が file_put_contents 関数です。
file_put_contents(ファイル名 , 書き込み内容)
先ほどの複数行の書き込みも、このようにまとめて行うことができます。
<?php
$contents= "書き込みテスト1行目 \n書き込みテスト2行目";
file_put_contents( "test.txt", $contents);
?>
GETメソッド
リクエストの種類
ブログで「次のページ」というリンクをクリックすると、URLの末尾に「page=2」といったパラメータが付くことがあります。また、ネットショッピングやお問い合わせフォームで名前や住所を入力して送信したことがある方も多いはずです。
Webアプリケーションでは、Webページを表示するだけでなく、そのページを見ている人と双方向でデータのやりとりをしたい場合があります。クライアント(端末、ブラウザ)からサーバへのリクエストを送信する方法には、GETメソッドとPOSTメソッドの2種類があります。
GET メソッドはURLにデータを載せる方式で、少量のデータ送信に向きます。
POSTメソッドはHTMLフォームなどから送信する方式で、大量のデータ送信に向きます。送りたいデータの性質によってどちらを利用するか決めます。
まずは、GET メソッドから勉強していきましょう。
GET メソッドの形式と特徴
GET メソッドでは、URLの末尾に「?」を付け、「パラメータ名=値」という形式でデータを付与します。
ブログの次ページへのリンクや、カテゴリー別ページへのリンクを見ると、このような GET メソッドを使ったリンクになっていることがあります。
プログラムではパラメータ名が要素名となる連想配列の一要素としてデータを受け取り、処理することができます。
ただし、URLには文字数制限があるので、大量のデータは送信できません。
また、URLにパラメータを含めるので、送ったデータを隠すことができません。
このことから、ブログの次のページを表示させるような、個人を特定しない簡単なデータのやりとりに向いています。
GET メソッドのリクエストと受け取り
では、実際に GET メソッドのリクエストを処理してみましょう。
次のプログラムをget.phpというファイル名でドキュメントルートに保存してください。
get.php
<?php
$page = $_GET['page'];
echo 'リクエストされたページは' . $page . 'です';
?>
プログラムができたら、ブラウザから「http://localhost/get.php?page=2」にアクセスしてみましょう。
次のように、URLに付与した値が表示されていれば成功です!
実行結果
リクエストされたページは2です
スーパーグローバル変数
先ほどのプログラムに、見たことのない変数が使われていましたね。
$_GET['page']
これはPHPが備える変数で、スーパーグローバル変数といいます。プログラムのどこからでもアクセスできる特別な変数です。
GET メソッドのスーパーグローバル変数は、内部では連想配列として展開されます。URLに付与されたパラメータ名が連想配列のキーになり、= 以降の値は、連想配列の値になります。
get.phpを次のように修正してみましょう。
get.phpを修正(get-multi.php)
<?php
foreach ($_GET as $key => $value) {
echo ‘ キー: ' . $key . '<br>';
'echo ‘ 値: ' . $value . '<br><br>';
}
?>
作成した get.phpに対し、複数のパラメータを送信してみましょう。ブラウザ
にアクセスしてください。キーと値の組が3つ表示されれば成功です。
実行結果
キー:param1
値:1
キー:param2
値:2
キー:param3
値:3
このように、複数のパラメータを送信する場合は、URLの?以降に「パラメータ=値」の組を&でつないで書きます。
ここではGET メソッドの働きについて確認するため、URLに記載されたキーと値の組み合わせを表示しました。
しかし、GET メソッドの値をそのまま表示するのは、予期せぬパラメータが
送られるなどセキュリティとして問題があります。
POSTメソッド
GET メソッドと違って少し複雑なリクエスト方法です。HTMLフォームを使って送信し、大容量データを送信するのに適しています。画像のアップロードや長い文章の送信などは、POST メソッドで行います。
POST メソッドの受け取り
POST メソッドはHTMLフォームから送信しますが、GET メソッドと同様に、スーパーグローバル変数で次のように受け取ることができます。
$_POST[‘キー名’]
では、どのようにしてPOST メソッドでデータのリクエストを行えばよいのでしょうか? それにはまず、HTMLフォームを理解する必要があります。
HTML フォーム
HTMLフォームとは、テキストボックスやテキストエリア、セレクトボックスからなる Webページの部品群です。単にフォームともいいます。フォームに入力された内容をPOST メソッドで送信し、PHPで受け取ります。
では、基本的なフォーム部品を押さえておきましょう。
form
どこからどこまでが送信する内容なのか示すためのタグです。formタグに
挟まれた部分のデータを送信します。
送信先のURLには、データの送り先のファイル名が入ります。method は、POST か GETのどちらで送信するかを書きます。一般的にはフォームを使った送信時は post と書き、POST メソッドで送信します。
input(テキストボックス) )
入力フォームを作成するためのタグです。type属性を変えることで、ブラウザ上での見た目と送信されるデータ内容を変えることができます。
<input type=”text” name=” キー名”>
type が textだと、1行のテキストボックスになります。name属性の値が送信後のキー名になり、入力内容が$_POST変数に格納されています。
input(ラジオボタン)
type が radioだと、複数の選択肢の中から1つだけを選ぶラジオボタンになります。name 属性の値が送信後のキー名になり、value 属性が送信される値になります。
ラジオボタンは「男性/女性/その他」「はい/いいえ」のように、複数の回答から1つを選ぶためのものです。複数の inputタグに対して同じname属性を与えることで同じグループの選択肢となります。
<input type="radio" value="man" name="gender">男性
<input type="radio" value="woman" name="gender">女性
$_POST[‘gender’]という構文で受け取り、manかwomanが返ってきます。
input (送信ボタン)
type が submitだと送信ボタンになります。valueの値がボタンの上に書かれる文字になり、ボタンが押されると form タグのaction属性に指定されたURLにデータを送信します。
<input type=”submit” value=” ラベル名”>
例
<input type="submit" value="送信">
valueは、ボタンの上に書かれる文字列を表します。
textarea
複数行入力できるテキストエリアを作るタグです。
<textarea name=” キー名”></textarea>
name属性の値が送信後のキー名になります。
<textarea name=”address”></textarea>
$_POST[‘address’]の構文で受け取ります。
ネットショップの備考欄などでは、「その他ご要望をお書きください」とすでにテキストエリアに文章が入力されている場合があります。このように、あらかじめテキストが入力された状態にしたいときは、textareaのタグ内に文字列を書きます。
<textarea name="other"> その他ご要望をお書きください</textarea>
select
selectはセレクトボックス(プルダウンメニュー)を作るためのタグです。
selectタグに挟まれた部分がグループとなり、optionタグが1つ1つの選択肢になります。selectタグのname属性は、データ送信後のキー名です。変数の値には、選択された optionのvalue属性が格納されています。
<select name=“area”>
<option value=“Tokyo”>東京</option>
<option value=“kanagawa”>神奈川</kanagawa>
</select>
HTML フォームから PHP へデータの送受信
では、簡単なアンケートフォームを作成し、POST メソッドの送受信を試し
てみましょう。
ドキュメントルートに POST メソッドをテストするためのフォルダ「post」を作成し、次のHTML を enquete.html として保存しましょう。
送信側 HTML (enquete.html)
<html>
<head>
<meta charset="UTF-8">
<title> アンケート送信テスト</title>
</head>
<body>
<h1> アンケートフォーム</h1>
<form action="post.php" method="post">
<p>お名前:<input type="text" name="name"></p>
<p> 性別:
<input type="radio" name="gender" value="man">
<input type="radio" name="gender" value="woman">
</p>
<p> 評価:
<select name="star">
<option value="1">*</option>
<option value="2">**</option>
<option value="3">***</option>
<option value="4">****</option>
<option value="5">*****</option>
</select>
</p>
<p>ご意見</p>
<p><textarea name="other"></textarea></p>
<input type="submit" value=“送信“>
</form>
</body>
</html>
受信側プログラム
次に、HTMLからデータを受け取る PHPプログラムを作成します。enquete.htmlのform タグのactionにある通り、このファイル名は post.phpにし、同じくpost フォルダに保存してください。
受信側プログラム(post.php)
<?php
// お名前
$name = $_POST['name'];
// 性別
$gender = $_POST['gender'];
if ($gender == "man"){
$gender = "男性";
} else if ($gender == "woman") {
$gender = "女性";
}
// 評価
$tmp_star = $_POST['star'];
$star =‘ ‘; // 画面へ出力する用の文字列
for ($i = 0; $i < $tmp_star; $i++) {
$star .= ‘ * ’ ;// 送信された評価の数だけを追加
}
// ご意見
$other = $_POST['other'];
?>
<html>
<head>
<meta charset="UTF-8">
<title>アンケート結果</title>
</head>
<body>
<h1>アンケート結果</h1>
<p> お名前: <?php echo $name; ?></p>
<p>性別 : <?php echo $gender; ?></p>
<p>評価: <?php echo $star; ?></p>
<p>ご意見:<?php echo nl2br($other); ?></p>
</body>
</html>
プログラムを作成したら、「http://localhost/post/enquete.html」からアンケートを送信しましょう。
実行結果


enquete.htmlで評価の星の数を選択すると、PHPには数字が送信されます。
出力文字列を作成する部分では、受け取った数字をもとに、* の個数を調
整していきます。
はじめのfor構文では、$iが0から受け取った数字になるまで、画面出力用文字列の$starに*を追加していきます。
HTML は簡単に改ざんできてしまう
(HTMLに詳しい人やブラウザの扱いに慣れている人は、HTMLを直接書き換えてしまう可能性があります。このプログラムでは、送られてきたデータが本当に前の画面から送信されたものなのかということがわかりません。evant例えばGoogle Chromeでは、ページを右クリックして[要素を検証]をすることで、HTMLの内容を書き換えることができます。それにより、性別のvalue属性を man でも womanでもないものに変えたり、評価の数字を1から5でなく他の数字や文字列に変えたりして、プログラムを破綻させることもできてしまいます。
性別の受け取り方法を変える
送信される性別は「man」と「woman」のどちらかだと想定していましたが、簡単にHTMLを改ざんできてしまうのであれば、それ以外の値が送信されてきたとき、$genderにはなにも代入されずにアンケート結果表示画面では空欄になってしまいます。あらかじめ決めた値以外受け付けないのであれば、性別の受け取り部分を次のように書き換えるとよいでしょう。
// 性別
$gender = $_POST['gender'];
['
if ($gender == "man") {
(
$gender = "男性":
} else if ($gender == "woman") {
$gender = "女性";
} else {
$gender = "不正な値です";
}
評価の受け取り方法を変える
評価の数字を受け取る部分では、100のような大きい数字が送信された場合、*が100個出力されてしまいます。また数字でなく文字列が送信された場合、*を出力する1つ目のfor構文では、$iを0からいくつまで加算すればいいのかわかりません。
このことから、評価の数字は「数字であること」と「1~5であること」のどちらも満たす必要があります。まず数字であることを確かめるために、intval関数を使いましょう。
intval(変数名)
この関数は、引数に指定された変数に代入された値を、整数として返します。文字列などの整数以外のデータのときは、整数に変換できないため0を返します。
これを利用して、評価受け取り部分を修正しましょう。
// 評価
$tmp_star = intval($_POST['star']); // 整数として受け取る
$star = ‘ '; // 画面へ出力する用の文字列
if ($tmp_star < 1 || $tmp_star > 5){ // 1~5であるかのチェック
$star = "不正な値です";
} else {
for ($i = 0; $i < $tmp_star; $i++){
$star .= ‘ * '; // 送信された評価の数だけ*を追加
}
}
intval関数で $_POST[‘star’]を整数として受け取り、$tmp_star が1より小さいか5より大きい場合には「不正な値です」と表示しています。
このように、Webプログラミングにおいては、必ずしも期待したデータが送信されてくるとは限りません。送信されるデータは常に疑って受け取ることが大切です。
このプログラムは POST メソッドと外部入力への対応について説明するもの
であり、セキュリティの対策はしていません。
画像のアップロード
では、送信メソッドの基礎がわかったところで、サークルサイトに画像のアップロード機能を追加しましょう!
画像アップロード機能の概要
画像アップロード機能には3つのステップがあります。
ステップ1: POST メソッドで画像のアップロード
type が fileの input タグを使い、画像アップロード用の upload.phpを作ります。
ステップ2: 画像の保存
アップロードされた画像を、サーバ内の決められたフォルダに保存します。
ステップ3:画像フォルダ内の画像を一覧表示
album.phpで画像を一覧表示します。[次へ]を押して次の画像を数枚ごとに表示するページング機能も持たせます。
ステップ 1: POST メソッドで画像のアップロード
upload.phpには、POSTされたデータがなければアップロードフォームを表示し、あれば画像を保存する機能を持たせます。そのため、画像アップロードフォームは、actionの指定先が自分自身になります。POSTされたデータの処理についてはステップ2で修正をしていくので、まずはフォームの表示部分を作りましょう。
以前と同じように、index.phpのコピーを upload.phpという名前で tennisフォルダに保存し、本文部分を次のように修正しましょう。
画像アップロードフォーム(upload.php)
<!-- ここから「本文」
<h1>画像アップロード</h1>
<form action="upload.php" method="post" enctype="multipart/form-data">
<div class="form-group">
<label>アップロードファイル</label>
<input type="file" name="image" class="form-control-file">
</div>
<input type="submit" value="アップロードする" class="btn btn-primary">
</form>
<!-- 本文ここまで―>
保存ができたら、「http://localhost/tennis/upload.php」にアクセスして実行結果を確認します。
formタグ内のenctype という属性で、multipart/form-data を指定しています。これは、通常のPOST送信と違って画像ファイルなどをアップロードするときに指定する属性です。
input タグの type属性にfileを指定していると、アップロードするためのファイル選択フォームになり、name属性がPOST されたデータの変数名になります。
次のステップに進む前に、navbar.phpを修正し、ナビ部分に upload.phpと、後ほど作成するアルバムページのalbum.phpへリンクを追加しましょう。
album.phpへのリンクを追加(navbar.php)
<li class="nav-item"><a class="nav-link" href="info.php"> お知らせ</a></li>
<li class="nav-item"><a class="nav-link" href="upload.php">画像アップロード</a></li> 追加
<li class="nav-item"><a class="nav-link" href="album.php">アルバム</a></li> 変更
<li class="nav-item"><a class="nav-link" href="#">掲示板</a></li>
ステップ 2:画像の保存
次に、upload.phpに画像保存機能を付けます。まず、画像がどのようにサーバにアップロードされるのか流れを確認しましょう。
アップロードされた画像は、サーバ内のテンポラリフォルダという場所に自動的に保存されます。テンポラリとは「一時的」という意味で、アップロードされたそのときにしか保存されません。ファイルをアップロードすると、プログラムの開始時に画像がテンポラリフォルダにアップロードされ、プログラムが終わったときにその画像が破棄されます。
このため、テンポラリフォルダという一時的な場所にある画像を、サークルサイト内の画像フォルダに移動する必要があります。
upload.phpの修正
まずは、サークルサイト内に画像を保存するためのフォルダを作成しましょう。サークルサイトの公開フォルダ(C:\xampp\htdocs\tennis)にalbum というフォルダを作成します。
では、upload.phpを修正していきましょう。
upload.phpを修正
<?php
$msg = null; // アップロード状況を表すメッセージ
$alert = null; // メッセージのデザイン用
// アップロード処理
if (isset($_FILES['image']) && is uploaded_file($_FILES
['image']['tmp_name'])){
$ $old_name = $_FILES['image']['tmp_name'];
$new_name =$_FILES['image']['name'];
if (move_uploaded_file($old_name, 'album/'.$new_name)) {
$msg = 'アップロードしました。';
salert = 'success'; // Bootstrapで緑色のボックスにする
} else {
$msg = 'アップロードできませんでした。';
salert = 'danger'; // Bootstrapで赤いボックスにする
}
}
?>
<!DOCTYPPE html>
<html lang="ja" >
(略)
<h1>画像アップロード</h1>
<?php
if ($msg){
echo '<div class="alert alert-'.$alert.'" role="alert">'.$msg.' </div>';
}
?>
<form action="upload.php" method="post" enctype="multipart/form-data">
このページを開いた直後はファイルがなにもアップロードされていませんので、$msgはnull となり、本文に表示されません。画像のアップロード処理
修正後の upload.phpでは、冒頭でメッセージ表示を行うための $msg と、そのメッセージを表示するときのボックスの色を決めるSalertという変数を作成します。どちらも nullを代入しておき、アップロード状況がないことを明示しておきます。画像がアップロードされていれば$msg と salertに文字列が代入されます。
アップロード処理では、はじめのif構文でアップロードされたファイルがあるか、またそのファイルが正しくアップロードされたものであるかを調べています。is_uploaded_file関数は、引数のファイル名を指定すると、正しい手順でアップロードされたものであればTRUEを、そうでなければFALSEを返します。
is_uploaded_file(ファイル名)
upload.php で引数に指定したのは、$_FILESから始まるグローバル変数です。
アップロードしたファイルについての情報を持った連想配列になっています。
1つ目の要素は inputタグで付けたname属性の値です。2つ目の要素は次の
ような各種情報が格納されています。]
$_FILES[‘image’] […]
name アップロードしたオリジナルファイルのファイル名
type ファイルの種類(MIMEタイプ)
tmp_name テンポラリフォルダにアップロードされたときに自動的に付けられた仮の名前
error エラーコード
size ファイルサイズ
type は、MIMEタイプと呼ばれるファイルの種類です。例えばデジカメで撮ったjpegの写真は「image/jpeg」というように、ファイルの種類によってMIMEタイプが決まっています。
error(エラーコード)は、正しくアップロードされてエラーがないときは0、それ以外のときにはエラーの理由を示す数字が入ります。
var_dumpでS_FILES配列を見てみるとイメージがつかみやすいでしょう。
テンポラリフォルダからのファイル移動
次に、$_FILES 配列の tmp_name と name の値を使って、テンポラリフォルダにあるファイル名とオリジナルのファイル名を取得しています。その後、move_uploaded_file関数を使って、テンポラリフォルダからalbum フォルダに画像を移動します。この関数は、正しく移動できればTRUE が返り値になります。
move_uploaded_file(移動元ファイル名, 移動先ファイル名 )
このプログラムでは、テンポラリフォルダのファイルをalbum フォルダ以下に
そして、拡張子を決定する際、画像以外のファイルがアップロードされていたらupload.phpに遷移し直し、アップロードフォームを再表示することで、画像以外のファイルがアップロードされるのを防いでいます。
ステップ 3:画像フォルダ内の画像を一覧表示
アップロードができたら、コピーしていたindex.php を元に album.php というファイルを作り、画像フォルダ内の画像を一覧表示しましょう。
album.php
<?php
$images = array(); // 画像ファイルのリストを格納する配列
//画像フォルダから画像のファイル名を読み込む
if ($handle = opendir('./album')) {
while ($entry = readdir($handle)) {
//「.」および「..」でないとき、ファイル名を配列に追加
if ($entry != "." && $entry != "..") {
$images [ ] = $entry;
}
}
closedir($handle);
}
?>
<!DOCTYPE html>
<html lang="ja" >
(略)
<h1> アルバム</h1>
<?php
if (count($images) > 0){
echo '<div class="row">';
foreach ($images as $img) {
echo '<div class="col-3">';
echo <div class="card">';
echo <a href="./album/'. $img.'" target="_blank">
<img src="./album/'.$img.'" class="img-fluid"></a>';
echo ' </div>';
echo '</div>';
}
echo '</div>';
} else {
echo '<div class="alert alert-dark" role="alert">画像はまだありません。</div>';
}
?>
冒頭のPHPコードでは、画像フォルダ内のファイル名を読み込み、配列に格納しています。本文部分のPHPコードは、読み込んだファイル名を元に、img タグで画像を表示しています。画像はアップロードされたままの状態で表示されると見づらいので、Bootstrapで小さく表示してあります。画像をクリックすると大きな画像が表示されます。
画像の読み込み readdir関数
まずは、冒頭部分である画像の読み込みについて確認していきましょう。opendir関数でフォルダを開きます。ファイルの読み書きと同様、フォルダの一覧を読み込むときにもフォルダを開く必要があります。引数に指定されたフォルダを開き、ディレクトリハンドルを返します。これはどこのフォルダを開いているか示すためのもので、サーバとして使われることが多いOSではフォルダのことをディレクトリと呼ぶため、「ディレクトリを操作するもの」という意味があります。
opendir フォルダ名
正しくフォルダを開くことができたら、フォルダ内のファイルを読み込みます。
readdir関数は、引数のディレクトリハンドルからファイル名を読み込んで返り値にし、取得できなくなるとFALSEを返します。
readdir ディレクトリハンドル
while構文を使ってファイル名を取得し、変数$entryに格納していきます。while構文の式では、readdir関数によりファイル名が取得できている限り条件式がTRUEになるためループし続け、すべてのファイルを読み込んだら FALSE となってループを抜けます。
$images 配列に格納するのは、ファイル名が「.」と「..」以外のときです。
なぜこの処理が必要かというと、サーバ上でファイル名の一覧を取得すると、現在のフォルダを示す「.」と、1つ上の階層のフォルダを示す「..」も取得してしまうためです。今回は画像ファイルだけ取得したいので、if構文でこの2つを除外しましょう。$images[ ] とすることで$images 配列の新しい要素に代入することができます。
ファイルの取得が終わったら、ファイルの読み書きをしたときと同様に、closedir関数を使ってフォルダを閉じましょう。
closedir(ディレクトリハンドル)
読み込んだ画像の表示
本文部分のPHP コードでは、取得した画像ファイル名を元に画像を表示させています。
画像ファイル名が入った $images配列を count関数で数えて、0以上だったら画像表示処理へ移ります。フォルダが正しく開けない場合や、画像がまだアップロードされていない場合は、$images配列が空ですので、「画像はまだありません」と表示します。
画像が1枚以上あれば、foreach構文を使って画像を表示しましょう。画像を表示するのは img タグで、src属性に画像へのパスを指定します。クリックして別のウィンドウで画像を表示させるため、aタグで img タグを挟みましょう。
ページング処理
画像が5枚や10枚なら1ページにすべて表示されてもよいかもしれませんが、増えてくると困りますね。そんなときに必要なのがページングです。ページングとは、ブログの「次のページ」のように、たくさんある記事や写真などをある程度の個数で区切って次のページに表示させることです。album.phpにもページング処理を入れてみましょう。
album.phpにページング処理を追加 (album.php)
<?php
$images = array();
array(); // 画像ファイルのリストを格納する配列
$num 4;//1ページに表示する画像の枚数
// 画像フォルダから画像のファイル名を読み込む
if ($handle = opendir('./album')) {
while ($entry= readdir($handle)) {
// 「.」および「..」でないとき、ファイル名を配列に追加
if ($entry != "." && $entry != "..") {
(!$images [] = $entry;
}
}
closedir($handle);
}
?>
<!DOCTYPE html>
<html lang="ja">
<h1>アルバム</h1>
<?php
if (count($images) > 0){
echo '<div class="row">';
// 指定枚数ごとに画像ファイル名を分割
$images array_chunk($images, $num);
// ページ数指定、基本は1ページ目を指す
$page = 1;
// GETでページ数が指定されていた場合
if (isset($_GET['page']) && is_numeric($_GET['page'])) {
$page = intval($_GET['page']);
// $images[ページ数]は存在するかチェック
if (!isset($images [$page-1])) {
$page =1;
}
}
// 画像表示
foreach ($images [$page-1] as $img) {
echo '<div class="col-3">';
echo <div class="card">';
echo <a href="./album/'.$img.' target="_blank">
<img src="./album/'.$img.'" class="img-fluid"></a>';
echo '</div>';
echo '</div>';
}
echo '</div>';
// ページ数リンクを表示
echo '<nav><ul class="pagination">';
for ($i = 1; $i <= count($images); $i++) {
echo '<li class="page-item"><a class="page-link" href="album.php?page='.$i.' ">'.$i.'</a><li>';
}
echo '</ul></nav>';
} else {
echo '<div class="alert alert-dark" role="alert">画像はまだありません</div>';
}
?>
冒頭に追加したのは、1ページに表示する画像の枚数、$num変数です。本文部分に追加したコードでは、画像ファイルの配列を$num枚ごとに区切り、ページが指定されていれば指定ページを、指定がなければ1ページ目を表示するような処理になっています。では、詳しく動作を見ていきましょう。
配列の分割
array_chunk関数は、第1引数の配列を第2引数の数ごとに分割する関数で、返り値は配列です。
array_chunk(分割したい関数 , 分割数)
基本となるページ数は $page に代入します。人間にわかりやすいよう、指定がない場合の基本のページ数は1ページ目とします。
GETでページ数が指定されていた場合の処理
GET メソッドでページ数が指定されていたときの処理について見ていきましょう。if構文で「GET メソッドで pageという変数が送られたか?」と「page変数が数字か?」という2点について調べます。
isset関数は変数が定義されているかどうかを調べる関数で、定義されていればTRUE、されていなければFALSEを返します。
isset(変数)
このプログラムの場合、2ページ目のリンクが押されて$_GET[‘page’] が送られていれば、スーパーグローバル変数が自動的に定義されているため、isset関数の返り値は TRUEになります。
is_numeric関数は、引数が数値のときにTRUE、そうでないときFALSEを返します。
is_numeric(変数)
GET メソッドはURLに変数と値を指定するため、必ずしも期待する値が送られてくるとは限りません。URLを書き換えれば文字列を送ることもできます。
そのため、送られてきた値がpage という変数名で、値が数字であることを二重にチェックする必要があるのです。
正しく値が送られてきていれば、$_GET[‘page’] の値を intval関数により数値として受け取ります。
次のif構文で、指定されたページに要素があるか確認します。$images[ページ数]の配列を isset 関数で調べます。冒頭に「!」が付いていますので、「もし存在しなければ」$pageに1を代入するという処理になっています。
調べるページ数は、$page-1の要素です。$imagesは配列になっていて、1ページ目は要素番号0だからです。
画像の表示とページング用リンクの出力
画像表示部分では、foreach構文で指定するのが$images[$page-1]です。
以前は $imagesだけでしたが、今回はarray_chunk関数で分割しているので、人間が指定したページ数から-1して、配列用のページ数を指定しましょう。
ページングのリンクは for構文を使って、1ページ目から$images 配列の要素数(ページ数)の分だけ表示します。
データベースの基礎知識
データベースってなんだろう?
PHPでWebアプリケーションを作るときに、必要不可欠なのがデータベース(DB)です。DBとはその名の通り「Data Base = データの基地」で、データを一箇所に集めておくことで、後から使いやすくするのが目的です。
スマホの電話帳には名前、電話番号、メールアドレスなど、決まった形式でデータを書き込んでおきますね。名前を読み順に並べておけば、目的の人の電話番号を探すのも簡単です。
コンピュータにおけるDBもまったく同じ考え方で、ある決まった形式(多くは表形式)でデータをそろえておくことで、後から探しやすく、使いやすいデータの集まりになります。
コンピュータの世界では、電話帳のようなデータの集まりを、データベース管理システム(DBMS: Data Base Management System)が管理しています。
DBMSはクエリという専用の命令文を受け取ると、データの検索や並べ替えをして結果を返してくれます。データの集まりと私たちを仲介してくれる役割があります。
一口にDBといってもたくさんの種類がありますが、ここではPHPと相性がよいMariaDBというDBMSを使うことにします。MariaDBはリレーショナルデータベース(RDB: Relational Data Base)という種類のDBで、広く利用されているオープンソース DBです。高速で動作し、使いやすく、利用料が無料です!
リレーショナルデータベースの要素
リレーショナルデータベースは、いくつかの表からなります。表のことをテーブル(Table)、列をカラム(Column)、行をレコード(Record)といいます。各カラムには型があり、カラムごとにデータの形式を決めます。
Excelのような表計算ソフトの利用経験があるならば、DBは表計算ソフトで作成したファイルそのもの、テーブルはファイル内のシートで、カラム(列)とレコード(行)を持つ、と考えるとイメージが近いです。
サークルサイトに掲示板を作ろう
DBとはなにかわかったところで、作成中のサークルサイトに話を戻します。
サークルにはたくさんのメンバーがいるので、意見をまとめたり雑談をしたりするため、掲示板を設けることにしましょう。
まずは、掲示板の機能概要について決めます。はじめてDBを使ったプログラムを書くので、まずは簡単に、掲示板への書き込みが新しい順に表示されるようにします。書き込み内容は、名前・タイトル・本文の3つで、表示するときに書き込み日時が表示されるといいですね。
また、投稿したコメントを投稿者が削除できるように、簡単な削除パスワー
ドを記入してもらい、投稿時と削除時でパスワードが一致したら削除できるようにしましょう。
データベース設計
DBを使うときには、作りたい機能をもとにデータベース設計をする必要があります。まず保存するデータの種類と型を決めます。サークルサイトで必要なデータは、次の6つです。
・書き込み番号
・名前
・書き込みのタイトル
・本文
・書き込み日時
・削除/パスワード
書き込み番号を付けておくと、後で管理が楽になります。では、これらのデータをどのようなデータ型で保存するか決めましょう。
MariaDBのデータ型
MariaDBのデータ型は、大別して数値型、日付・時刻型、文字列型の3種類があります。どういったデータを保存するかにより、この3つの中から適切な型を選ぶ必要があります。
例えば郵便番号は7桁の数字ですが、0から始まるものもあるため、整数型よりも文字列型のほうが適しています。また、桁数が7桁と決まっているため、文字列型の中でも桁数を指定できる「固定長文字列型(CHAR)」という型が最適。
このように、保存したいデータによって適切な型を選択するのがDB設計の第一歩です。では、MariaDB で利用できる主なデータ型を見てみましょう。
掲示板のデータベース設計
DB設計をする際、はじめに決めるのはDB名です。サークルサイトではテニスサークルの掲示板を作るので、「tennis」というDB名にしましょう。
次に、掲示板のデータを保存するテーブルを設計していきます。テーブル名は「bbs」にしましょう。bbsテーブルには書き込み番号、名前、書き込みのタイトル、本文、書き込み日時が必要なので、次のように設計します。
bbs テーブルの設計
カラム名、型と長さ、NOT NULL制約、説明
id
INT、○ 、書き込みの通し番号。主キー。自動採番にする
name
VARCHAR(255)、○書き込んだ人の名前。長さは255文字まで
title
VARCHAR(255)、書き込みのタイトル。長さは255文字まで。タイトルがないときは、(無題)と表示する
body
TEXT、○、本文
date
DATETIME、○、書き込み日時
pass
CHAR(4)、○、削除パスワード。4桁の数値文字列
主キーの決め方
idは主キーとなります。主キーとは、そのテーブルの中でレコードを識別する一意な値のことです。bbsテーブルの中で、idの数字は「ユニーク(=重複しない)」という性質を持ちます。
例えば、クラス名簿を作ったときはどの項目が主キーになるでしょうか?同姓同名の人がいるかもしれないので、氏名は主キーになりません。しかし、各自に振られた出席番号は、そのクラスの名簿では他人と重複しないので主キーになります。
同様に、bbsテーブルにおいても、idは書き込みの通し番号であり重複しないので、主キーにすることができます。
NOT NULL 制約
NOT NULL 制約とは必須項目、つまり「そのカラムに必ずデータを入れる」という約束を示しています。NOT NULL、つまり NULL ではダメという意味です。
bbsテーブルのテーブル設計を見ると、title以外のNOT NULL制約に○が書いてあり、title以外の項目にはなんらかのデータを入れる必要がある、ということを示しています。
データベースの作成
テーブル設計ができたので、MariaDBに接続してDBを作成しましょう。作成からPHPで利用する準備を終えるまでは、次のような流れになっています。
この利用準備は、XAMPPから起動したコマンドプロンプトで行います。
MariaDBに接続し、コマンドを実行してみましょう。
まず、XAMPP コントロールパネルの[Shell]ボタンをクリックしてコマンドプロンプトを起動します。そして、次のコマンドを実行してMariaDBにログインします。
mysql -u root -p
-uのオプションで、root(管理者権限のユーザ)としてログインします。
-pのオプションを指定しているため、パスワード入力が求められます。設定したパスワードを入力してください。
実行結果
XAMPP for Windows – mysql -u root-p
Betting environment for using XAMPP for Windows.
Duner@DESKTOP-VCFSONA c:¥ ampp
Enter password: ****
Welcome to the MariaDB monitor. Commands end with ; or ¥.
Your MariaDB connection id is 8
Server version: 10.4.17-MariaDB mariadb.org binary distribution
Loovrisht (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type ‘help;’ or ‘h’ for help. Type ‘Wo’ to clear the current input statement.
MariaDB [(none)]
DBの作成
ログインできたら、次のクエリを実行しましょう。
CREATE DATABASE tennis;
CREATE DATABASE文は DBを作成するための構文です。コマンドの末尾にはセミコロンをつけて、命令の終わりを明示します。このような DB用のコマンドのことを、クエリといいます。
クエリはSQLという言語で書かれています。ちょっとややこしいですが、
SQL- 日本語、クエリ = 命令と考えると、日本語で「DBを作って」という命令を出すことは、SQLで「DBを作って」というクエリを実行することに対応しています。
先ほどのクエリを入力して[Enter]キーを押すと、「Query OK, 1 rowaffected」と表示されます。これは、入力されたクエリに文法的な間違いがなく、正しく実行できたことを表しています。エラーがあれば文法を間違えているかもしれません。見直してみましょう。
DROP DATABASE文で、作成したDBを削除できます。DB内に作成されていたテーブルも一緒に削除されます。
DROP DATABASE tennis;
DBの選択
続いて、次のクエリを実行します。
USE tennis;
USE文は、作成したDBを利用するための構文です。ファイルを作成したら開かないと文章が書き込めないように、DBも作った後で USE文を使い、利用する DBを選択する必要があります。
「Database changed」と表示され、MariaDBの後ろに選択したDB名が表示されていれば、正しく DBを選択できています。
実行結果
MariaDB [inone)]> use tennis;
Database changed
WariaDB (tennis]
テーブルの作成
では、tennis データベースに bbsテーブルを作成しましょう。クエリは次のように改行しても、1行にすべて書いてもかまいません。
CREATE TABLE bbs(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
title VARCHAR(255),
body TEXT NOT NULL,
date DATETIME NOT NULL,
pass CHAR(4) NOT NULL
) DEFAULT CHARACTER SET=utf8;
実行結果
B. XAMPP for Windows – mysql -u root-
Variats (tennis! CREATE TABLE bbs
-> id INT NOT NULL AUTO INCREMENT PRIMARY KEY,
name VAPCHAR (255) NOT NULL,
title VARCHAR(255).
-> body TEXT NOT NULL.
date DATETIME NOT NULL.
-> pass CHAR (4) NOT NULL
-> DEFAULT CHARACTER SET=utf8:
Query OK, O rows affected (0.439 sec)
Maria
B (tenis)
「Query OK」の表示が出れば、正しくテーブルが作成できています。CREATE TABLE文は、テーブルを作成するための構文です。USEで選択したDB に対し、テーブルを作成します。カッコの中はそのテーブルのカラムの設定で、カラム名、型、制約、属性の順に書きます。
例えば、idの型は INT、制約は NOT NULL、属性は AUTO_INCREMENT、PRIMARY KEYです。
AUTO_INCREMENTは自動採番機能のことで、レコードが追加されていくに従って、1、2、3……と、自動的に新しい数字を割り当ててくれます。割り当てる数字は MariaDB が管理しています。
PRIMARY KEYは主キーであることを表しています。
その他のカラムも同様に、カラム名と型を指定していきます。制約や属性はオプション項目のため、title カラムなどはカラム名と型だけのシンプルな指定になります。
最後の閉じカッコの後で、DEFAULT CHARACTER SET=utf8 とし、このテーブルで標準的に使う文字コードにUTF-8を指定します。
DROP TABLE文で、作成したテーブルを削除できます。登録されていたレコードは削除されますが、DB自体は残ります。
DROP TABLE bbs;
ユーザの作成
次に、この tennis データベースにアクセスし、操作できるユーザを作成しましょう。MariaDBをインストールしたばかりの状態では、root という管理者権限を持つユーザが作られています。しかし、rootはすべてのDBを操作できるため、セキュリティ上好ましくありません。
使う DBだけを操作できるユーザを作成し、PHPプログラム上でもそのユーザでDBにアクセスできるほうが、他のDBを不正利用されず安心です。
今回は、tennisデータベースを扱うユーザ「tennisuser」を作りましょう。
ユーザを作成するには、GRANT構文を使います。ユーザに権限を与えるための構文です。すでに存在するユーザに対しては権限の付与を、存在しないユーザに対してはユーザの作成と権限付与を同時に行います。GRANT構文は次のような書式です。
構文 ユーザの作成
GRANT 権限 ON データベース名. テーブル名 TO ‘ユーザ名’ @ ‘ホスト名’ IDENTIFIED BY ‘パスワード ‘;
では、実際に tennisデータベースに tennisuserを作成します。
GRANT ALL ON tennis.* TO ‘tennisuser’ @ ‘localhost’ IDENTIFIED BY ‘password’ ;
実行結果
MariaDB (tennis GRANT ALL ON tennis, * O tennisuser’@’localhost’ IDENTIFIED BY ‘password’;
lQuery OK, O rows affected 0.132 sec)
MariaDB (tennis]
上記のクエリは、「localhost 上の tennisuser さんに、tennisデータベースのすべてのテーブルへの権限 ALL を与えます。パスワードは passwordです」という意味になります。
与えることのできる権限はいろいろな種類がありますが、ここでは ALL(すべての権限)を指定して、このDBに対して自由に操作できるようにしました。権限によっては、テーブルの閲覧のみ、または作成のみといったように、細かい権限設定ができます。
権限の種類と合わせると、「このDBのこのテーブルの閲覧だけできる」などの局所的なユーザを作成することもできます。
今回は tennisuser が tennis データベースのすべてのテーブルを操作できるようにしたいので、「tennis.*」のようにテーブル名に*を使います。*はワイルドカードといい、「すべて」を表す記号です。
データベース
この GRANT クエリを実行するまでは rootユーザしか存在しなかったため、指定したユーザが存在しないので新しく tennisuser というユーザが作られます。
@以降のホスト名は、DB がどこにあるのかを示しています。ここでは PHP とMariaDBが同じサーバ内(同じコンピュータ、つまりlocalhost)にあるため、localhostとしています。
最後に、IDENTIFIED BYでこのユーザが MariaDBにログインするときのパスワードを設定することができます。
ユーザを作成したら、MariaDB内部の一時データを削除して設定を反映するため、以下のクエリを実行してください。ユーザ情報が反映されます。
作成したユーザの確認
では、本当にユーザが作成できたか確認してみましょう。quit コマンドを入力してMariaDBを終了させてから一旦コマンドプロンプトを閉じ、再度コントロールパネルから Shell を起動し、次のように入力します。
mysql -u tennisuser -p
tennisuserとしてログインします。パスワードを入力して[Enter]キーを押すと、ユーザが正しく作成できていれば MariaDBにログインできます。
続けて、次の2つのクエリを順番に実行しましょう。
USE tennis
DESC bbs;
USE で tennis データベースを利用できる状態にし、「DESC テーブル名」と入力することで、作成したテーブルの構造が表のように表示されます。
これで、パスワード付きのユーザが正しく作成できたということがわかりました。
掲示板を作成しよう
書き込みフォーム – bbs.php
まずは、掲示板の書き込みフォームを作成します。 navbar.phpを修正し掲示板へのリンクを追加し、コピーした index.php から bbs.phpというファイルを作成し、tennis フォルダに保存します。
navbar.php
<li class="nav-item"><a class="nav-link" href="album.php"> アルバム</a></li>
<li class="nav-item"><a class="nav-link" href="bbs.php">掲示板</a></li>
bbs.php
<h1>掲示板</h1>
<form action="write.php" method="post" >
<div class="form-group">
<label>タイトル</label>
<input type="text" name="title" class="form-control">
</div>
<div class="form-group">
<label>名前</label>
<input type="text" name="name" class="form-control">
</div>
<div class="form-group">
<textarea name="body" class="form-control" rows="5">
</textarea>
</div>
<div class="form-group">
<label>削除パスワード(数字4桁) </label>
<input type="text" name="pass" class="form-control">
</div>
<input type="submit" class="btn btn-primary" value="#*26">
</form>
実行結果
form タグの通り、データは POST メソッドで write.phpへ送信されます。
書き込みプログラム – write.php
では次に、データを受け取ってDBへ登録する write.phpを作りましょう。
bbs.phpとwrite.phpの関係と流れは、次のようになっています。
bbs.phpで入力されたデータを受信し、必須項目である名前、本文、パスワードがきちんと入力されているか調べます。入力漏れがなければDBに接続し、テーブルにレコードを追加します。
write.php
<?php
// データの受け取り
$name $_POST['name'];
$title = $_POST['title'];
'
$body = $_POST['body'];
'
$pass = $_POST['pass'];
]
// 必須項目チェック(名前か本文が空ではないか?)
if ($name == ""|| $body == ""){
header("Location: bbs.php"); // 空のときbbs.phpへ移動
exit();
}
// 必須項目チェック(パスワードは4桁の数字か?)
if (!preg_match("/^[0-9]{4}$/", $pass)){
header("Location: bbs.php"); // 書式が違うときbbs.phpへ移動
exit();
}
// DBに接続
$dsn = 'mysql:host=localhost;dbname=tennis; charset=utf8';
$user 'tennisuser';
$password 'password'; // tennisuserに設定したパスワード
try{
// PDOインスタンスの作成
$db = new PDO($dsn, $user, $password);
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
// プリペアドステートメントを作成
$stmt = $db->prepare("
$3 $>(
INSERT INTO bbs (name, title, body, date, pass)
VALUES (:name, :title, : body, now(), :pass)"
);
//プリペアドステートメントにパラメータを割り当てる
$stmt->bindParam(':name', $name, PDO::PARAM_STR);
$stmt->bindParam(':title', $title, PDO:: PARAM_STR);
$stmt->bindParam(':body', $body, PDO::PARAM_STR);
$stmt->bindParam(': pass', $pass, PDO:: PARAM_STR);
//クエリの実行
$stmt->execute();
//bbs.phpに戻る
header('Location: bbs.php');
exit();
} catch (PDOException $e) {
exit('13-:' . $e->getMessage());
}
?>
これまでのプログラムとは違い、DBが絡んでくると少し複雑さが増したように見えますね。少しずつ分解して理解していきましょう。
必須項目のチェック
POSTされてきたデータは、すでに学んだようにPOSTメソッドで受け取りました。その後、名前と本文、パスワードについて、大きく2つのまとまりで必須項目をチェックしています。
名前と本文のチェック
ユーザが入力した項目のうち、名前と本文は必須項目のため、それらが空に
なっていないか調べます。いずれかが空の場合、bbs.phpへ戻ります。
if ($name “” || $body == “”){
‘){
header(“Location: bbs.php”); // 空のときbbs.phpへ移動
exit( );
}
header関数は HTTPヘッダを送信するための関数です。
header( ‘Location: 戻り先のURL’);
HTTPヘッダとは、Web上でやりとりをしていく中で必要な情報のことで、リクエストやレスポンスにも含まれています。
例えば、あるWebサイトに表示のリクエストがあるとHTMLがレスポンスとして返ってきますが、HTMLが送信されてくる際に、送信するデータの容量などHTML以外の情報を送ることがあります。そういった、実データ以外の情報をヘッダといいます。
中でも Location ヘッダは、「他のページへのジャンプを指示する情報」です。
URLを指定することで、任意のページにジャンプすることができます。
このプログラムでは、名前と本文が空のときは書き込みできませんので、bbs.phpに戻ります。
削除パスワードの書式チェック
削除パスワードは数字4桁で、必須項目となっています。もし条件に合わなければ、名前・本文のチェックと同様にbbs.phpへ戻ります。
// 必須項目チェック(パスワードは4桁の数字か?)
if (!preg_match("/^[0-9]{4}$/", $pass)){
header("Location: bbs.php"); // 書式が違うとき bbs.phpへ移動
exit();
}
preg_matchは正規表現という方法を用いて、文字列が指定の形式と合っているかをチェックする関数です。第1引数に指定したパターンに、第2引数に指定した文字列が合っていれば TRUEを返します。preg_matchに!がついて否定の意味となるため、「正規表現パターンに合わないとき、bbs.phpに移動する」という処理になっています。この正規表現パターンには、次のような意味があります。
preg_match(“/^[0-9]{4}$/”, $pass)
「1234」→マッチするのでtrue
「aaaa」→マッチしないのでfalse
正規表現
/ 開始終了マーク
^ 先頭
[0-9] 0から9の数字
{4} 4桁
$ 末尾
正規表現パターン
パターンについては複雑なため、ここでは紹介にとどめます。ですが、数字の桁数を調べる以外にも、メールアドレスや電話番号の形式をチェックできるなど、とても便利です。
DBに接続する部分では、DSN とユーザ名、パスワードを設定しています。
// DBに接続
$dsn = ‘mysql:host=localhost;dbname=tennis; charset=utf8’;
$user = ‘tennisuser’;
$password = ‘password’; // tennisuserに設定したパスワード
DSN とは Data Source Nameの略で、どのサーバのどのDBを使うのかを指定した文字列のことで、書式は次の通りです。
mysql:host=ホスト名;dbname=DB名;charset=文字コード
write.phpでは、ホスト名がlocalhost、DB名は tennisです。ここでtennisデータベースを指定しているので、USE文を実行したのと同じ効果があります。文字コードはutf8(UTF-8)にします。
例外処理
いよいよDBに接続します。ここで、try-catchという見慣れない構文が出てきましたね。
try {
$db = new PDO($dsn, $user, $password);
(略)
} catch (PDOException $e) {
exit('エラー:' . $e->getMessage());
))
}
try-catchは例外処理のための構文です。DBに接続するときには、さまざまなエラーが起きる可能性があります。例えば、DBに接続できないなどは致命的なエラーですね。このような、プログラムで発生したエラーのことを例外といいます。
例外が発生した場合にも、なんらかの処理をしたいときがあります。今回でいうと、例外がどういう内容だったのかを表示してからプログラムを終了させたいです。もしtry-catch文で例外の処理をしなかったら、例外発生の段階でプログラムが止まってしまい、期待通りにその後の処理がされません。
そこで、例外が発生しそうな処理を tryのカッコ内に入れて、その中で例外が発生した場合、Exception (例外を表すもの)を catchに向かって投げます
(Throw するともいいます)。catchは例外を受け取ると、カッコ内の処理を行います。
例外が発生する可能性のある関数は、PHPマニュアルに「失敗時には例外が発生……」など説明が載っています。参考にしましょう。
受け取った例外の種類によっては、catchで行う処理を変えたい場合もあります。例えば、「DB関連の例外ならこの処理を、その他の例外なら別の処理を行う」というような場合です。こういったときのため、catchの中では受け取る例外を限定することができます。
} catch (PDOException $e) {
このプログラムでは、PDOExceptionという種類の例外が発生したときのみ、catch内の処理を実行します。PDOException は seという変数に代入されていて、$e->getMessage( ) でエラーメッセージが取得できます。exit に引数としてエラーメッセージを指定すると、それを表示してプログラムを終了することができます。
PDO
では、tryの中の処理を見ていきましょう。tryの中に入っている処理は、DBに接続してクエリを実行する部分です。
まずは、PDOインスタンスを作成します。PDO(PHP Data Object)とは、さまざまなDBMSを簡単に利用できるようにする、PHPの拡張機能です。
例えば、このプログラムで使う MariaDB以外にも、Oracle DatabaseやSQLiteなどさまざまなDBMSがあり、種類によってプログラムの書き方が異なります。
そのため利用する DBを変更すると、PHPプログラムを大幅に修正する必要が出てきます。
このような手間を防ぐため、PHP と DBMSの間に抽象化レイヤを挟んで、各種 DBMSの違いをこのレイヤで吸収し、異なる DBMSに対して同じプログラムで同じ処理をできるようにする、というのがPDOの機能です。
抽象化レイヤには、PDO と PDO が使う各種ドライバ(DBMS と PDOをつなぐ機能)が含まれています。しかし、私たちはドライバを意識する必要はなく、PDOをPHPで使うだけで、各種DBMSを同じように扱うことができます。つきり、DBの細かい扱いは PDOに任せて、私たちはクエリが正しく書けているかどうかや、プログラムが間違っていないかどうかに集中することができるのです。
オブジェクト指向
PDOインスタンスの作成では、newという演算子が出てきました。PHPは関数を使ったプログラミング方法と、オブジェクト指向というプログラミング方法のどちらも利用できる言語です。
オブジェクト指向ではプログラムの再利用がしやすいように、クラスという処理の設計図からインスタンスと呼ばれる実体を作成します。この作成した実体のことをオブジェクトということから、オブジェクト指向と呼ばれています。
$db = new PDO($dsn, $user, $password);
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
プログラムでは、new演算子を使ってPDOクラスのインスタンスを作成します。PDO クラスはインスタンス作成時に、引数に DSN、ユーザ名、パスワードを必要とします。それらの情報から実際に使える「実体」、インスタンスを$dbに代入します。
アロー演算子(->)により、setAttribute という処理が実行されます。
クラスには関数のようないくつかの処理のまとまりがあり、メソッドと呼ばれています。クラスを実体化したインスタンスから、アロー演算子を使ってメソッドを呼び出せるのです。
難しいことはさておき、プログラムでは $db に setAttribute メソッドを使うことで、PDOへ設定を施しています。プリペアドステートメントを使う際に、セキュリティを高めるための設定です。
プリペアドステートメント
プリペアドステートメントを作成します。プリペアドステートメントとは、実行したいクエリのテンプレートのようなるのです。「:名前」の部分に、bindParam メソッドで後から値を埋め込みます。「~」部分のことをプレースホルダと呼びます。
$stmt = $db->prepare("
INSERT INTO bbs (name, title, body, date, pass)
VALUES (:name, :title, body, now(), :pass)"
);
$stmt->bindParam(':name', $name, PDO::PARAM_STR);
prepare メソッドでは、文を実行する準備を行い、文オブジェクトを返します。のINSERT文によるレコード追加では、実際にどのようなクエリを実行しているのでしょうか。
クエリの内容を見ていきましょう。
INSERT文は、テーブルに新しいレコードを追加する構文です。
INSEERT INTO テーブル名(カラム1, カラム2, カラム3….)
VALUES(値1,値2,値3…)
テーブル名は追加したいテーブルを書きます。ここでは bbsテーブルです。
次のカッコ内には、データを入れたいカラムを列挙します。VALUESの後に、各カラムに対応する値を列挙します。
name や titleのように文字列を値としたいときは、通常、クォーテーション(“や’)で文字列を囲む必要がありますが、プリペアドステートメントを使う場合は、自動的にクォーテーションを付けてくれるので不要です。
date カラムは日付時刻を指定するカラムですが、MariaDBが持つ関数「now()」を指定しておくと、レコード追加時に自動で日付時刻に展開してくれます。
クエリを実行しているのはexecuteメソッドです。プリペアドステートメントでクエリを組み立ててexecuteで実行する、というのが基本的な流れです。
$stmt->execute();
クエリ実行が終わったら、header関数を使い bbs.phpへ戻ります。
サンプルプログラムでは説明をわかりやすくするため、セキュリティ対策を簡略化しています。本来、ユーザが入力した値やアップロードしたファイルをプログラム中で扱う場合は、セキュリティ対策を行う必要があります。サンプルプログラムをWebアプリケーションとして外部に公開する場合は、セキュリティ対策をしっかり行ってください。
テーブルデータの読み込みと表示
では次に、bbs テーブルからデータを読み込んで表示するよう、bbs.php を修正しましょう。ページ冒頭と本文部分を修正します。
bbs.phpを修正
<?php
//ページに表示される書き込みの数
$num=10;
// DBに接続
$dsn = 'mysql: host=localhost;dbname=tennis; charset=utf8';
$user = 'tennisuser';
$password = 'password'; ;
// GETメソッドで2ページ目以降が指定されているとき
$page = 1;
if (isset($_GET['page']) && $_GET['page'] > 1){
$page = intval($_GET['page']);
}
try {
// PDOインスタンスの生成
$db = new PDO($dsn, $user, $password);
$db->setAttribute(PDO:: ATTR_EMULATE_PREPARES, false);
// プリペアドステートメントを作成
$stmt = $db->prepare("SELECT * FROM bbs ORDER BY date DESC LIMIT :page, :num");
// パラメータを割り当て
$page = ($page-1) * $num;
$stmt->bindParam(': page', $page, PDO:: PARAM_INT);
$stmt->bindParam(':num', $num, PDO::PARAM_INT);
// クエリの実行
$stmt->execute();
} catch (PDOException $e) {
exit("エラー: " . $e->getMessage());
}
?>
<!DOCTYPE html>
<html lang="ja" >
(略)
<input type="submit" class="btn btn-primary" value="書き込む">
</form>
<hr>
<?php while ($row = $stmt->fetch()): ?>
<div class="card">
<div class="card-header"><?php echo $row['title']?
$row['title']: ' (2) '; ?></div>
# ?>
<div class="card-body">
<p class="card-text"><?php echo nl2br($row['body']) ?></p>
</div>
<div class="card-footer">
<?php echo $row['name'] ?>
(<?php echo $row['date'] ?>)
</div>
</div>
<hr>
<?php endwhile; ?>
<?php
// ページ数の表示
try {
// プリペアドステートメントの作成
$stmt=$db->prepare("SELECT COUNT(*) FROM bbs");
// クエリの実行
$stmt->execute();
} catch (PDOException $e) {
exit("15-:" . $e->getMessage());
}
// 書き込みの件数を取得
$comments = $stmt->fetchColumn();
// ページ数を計算
$max_page = ceil($comments / $num);
// ページングの必要性があれば表示
if ($max_page >= 1) {
echo '<nav><ul class="pagination">';
for ($i =1; $i <= $max_page; $i++) {
echo '<li class="page-item"><a href="bbs.php?page='.$i.'">'.$i.'</a></li>';
}
echo '</ul></nav>';
}
?>
プログラムの前半は DB接続を行い、書き込みデータを新しい順に10件ずつ取得しています。後半は書き込みフォームの下部に、取得したデータを表示しています。まずは前半部分について説明していきます。
DB接続とデータの取得
はじめに1ページに表示される書き込みの件数を決めています。ここでは10件ずつ表示します。
ページ数を指定する処理は、album.phpで利用したページングの考え方と同じです。指定がない場合は1ページ目ですが、GET メソッドで1より大きなページ数が指定されていれば、指定ページのコメントを表示しましょう。
SELECT文によるデータ取得
DB接続前の準備が終わったら、いよいよデータを取得します。まずはtry-catch構文内で PDO インスタンスを生成します。そして、次のようなプリペアドステートメントを作成しています。
SELECT * FROM bbs ORDER BY date DESC LIMIT :page, :num
このクエリには、次のような意味があります。
「bbsテーブルから全部のカラムを取得してください。並び方は「date」の降順(日付の新しい順)で、取得件数は:page 件目から:num件です。」
SELECT文はデータを取得するときの命令です。その次に指定するのは取得するカラムで、*と指定するとすべてのカラムを取得します。name, titleのようにカンマ区切りで指定することもできます。
FROMはどのテーブルから取得するかを示しています。
ORDER BY句は、取得結果を並べ替えるときの基準カラムと並び順を示しています。ASCが昇順、DESCが降順で、指定がなければ昇順となります。日付ですと新しい日時のほうがタイムスタンプとして大きいので、DESCで新しい順を指定できます。LIMIT句は取得件数を制限するもので、何件目から何件取得するかを指定します。
このような取得結果のことを結果セットといいます。プリペアドステートメントである $stmt を execute した後、$stmtに保存されていると考えましょう。
プリペアドステートメントを作成したら、bindParam メソッドで値を割り当てます。以前bindParam メソッドを使ったときは文字列を割り当てるため、第3引数に PDO::PARAM STRと指定していました。今回は数字を割り当てるため、PDO::PARAM_INT という定数を使います。
1ページ目なら0件目から、2ページ目なら10件目から取得したいので、:pageにはGET メソッドで指定されたページ数から1引いた数に、1ページの表示件数を掛けたものを指定します。
// パラメータを割り当て
$page = ($page-1) * $num;
$stmt->bindParam(':page', $page, PDO::PARAM_INT);
$stmt->bindParam(':num', $num, PDO::PARAM_INT);
レコードの取得
では、本文部分のレコードの取得部分を見ていきましょう。$stmtには結果セットがありますので、while構文で1レコードずつ取り出しています。HTMLを多く含むので、while構文の別の書き方 (while~endwhile)を使って見やすくしています。結果セットからレコードが取得できている間はカッコ内の式の評価がTRUEになり、レコードを取得し終えたときにwhile構文を抜けます。
<?php while ($row = $stmt->fetch()); ?>
レコードを取得するには、$stmtのfetchメソッドを利用します。このメソッドを使うと、レコード1件がカラム名をキーにした連想配列として取得できるので、$rowに代入しています。
タイトルがないまま投稿されていた場合は(無題)と表示しますが、ここでは三項演算子を使っています。
<?php echo $row[‘title’]? $row[‘title’]:'(無題)’; ?>
三項演算子の書式は次の通りです。
条件式 ? 式1 : 式2
条件式の結果がTRUEなら式1を、FALSEなら式2を返します。プログラムでは、$row[‘title’]にタイトルがあれば式1としてそのままタイトルを表示、なければ式2として(無題)と表示しています。
三項演算子はif構文を簡単にしたものです。if構文で書くこともできますが、今回のように単純な処理の場合にif構文を使うと、かえってプログラムが長くなってしまい見づらくなるため、三項演算子を使うことがあります。
三項演算子に慣れない場合は、以下のようにif構文で書き直してもかまいません。
if ($row['title']) {
echo $row['title'];
} else {
echo(無題)';
}
ページング処理
書き込みの表示の後は、ページング処理を行います。リンクを何ページ分表示するか決めるには、書き込みの全件を1ページあたりの書き込み数である10件で割り、その結果を端数切り上げしてページ数を算出します。
まず書き込みの件数を取得します。
SELECT COUNT(*)
FROM bbs←取得したいテーブル名
COUNT(カウントしたいカラム名)で、値がNULLでないカラムの数をカウントできる(*)とすると、取得した行数がわかります。
COUNT関数は MariaDBの持つ関数の1つで、カッコ内に指定したカラムのうち、値がNULLでないものの行数を数えます。ここではbbsテーブルにあるすべての書き込みの数を調べたいので、特定のカラムではなく「*」を指定します。idのような主キーのカラムを指定してもよいでしょう。
$comments = $stmt->fetchColumn();
fetchColumn メソッドの引数を指定しなかった場合、最初のカラムの内容を取得します。今回のクエリで取得してくるのは bbsテーブルの行数です。
ページ数を算出するには、書き込みの全件数を表示件数である10件で割り、結果を端数切り上げします。例えば35件のコメントを10件ずつ表示すると、割り算の結果は3.5のため、端数を切り上げて4ページになります。
切り上げは ceil関数で行います。
ceil(数値)
端数切り上げの結果、1ページ以上表示する場合には、1ページ目から$max_pageまでページングリンクを表示します。
コメント削除フォームの追加
最後に、コメントの削除機能を追加しましょう。書き込み内容を表示している部分に削除フォームを追加します。
削除フォームを追加(bbs.php)
<div class="card-footer">
<form action="delete.php" method="post" class="form-inline">
<?php echo $row['name'] ?>
(<?php echo $row['date'] ?>)
<input type="hidden" name="id" value="<?php echo $row['id'] ?>">
<input type="text" name="pass" placeholder=" 削除パスワード" class="form-control">
<input type="submit" value="削除" class="btn btn-secondary">
</form>
</div>
” typeがhiddenのテキストボックスは、画面に現れない非表示の入力欄です。
非表示ですが name や valueなどの属性を持たせることができます。削除パスワードと一緒に書き込みのidを送信して、削除したい書き込みを指定します。
書き込みを削除する – delete.php
では、書き込みid と削除パスワードを受け取って削除を行う delete.php を作ります。
delete.php
<?php
// データの受け取り
$id = intval($_POST['id']);
$pass = $_POST['pass'];
// 必須項目チェック
if ($id == '" || $pass == ''){
header('Location: bbs.php');
exit();
}
// DBに接続
$dsn = 'mysql:host=localhost; dbname=tennis; charset=utf8';
$user = 'tennisuser';
$password = 'password';
try {
$db = new PDO($dsn, $user, $password);
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
// プリペアドステートメントを作成
$stmt = $db->prepare("DELETE FROM bbs WHERE id=: id AND pass=: pass");
// パラメータ割り当て
$stmt->bindParam(': id', $id, PDO:: PARAM_INT);
$stmt->bindParam(': pass', $pass, PDO:: PARAM_STR);
// クエリの実行
$stmt->execute();
} catch (PDOException $e){
exit('エラー:' . $e->getMessage());
}
header('Location: bbs.php');
exit();
?>
DBに接続をし、受け取った書き込みのid とパスワードを元にクエリを実行し、レコードを削除します。
DELETE 文によるレコード削除
削除のクエリにはDELETE文を使います。
DELETE FROM 5 – WHERE 14;
DELETE 文は、FROM で指定したテーブルから、WHERE句の条件に合うレコードを削除します。ここでは主キーであるidと、削除パスワードであるpass がどちらも一致する(AND条件)レコードを削除しています。どちらも一致することが条件のため、ある書き込みを削除したいときにでたらめな数字を入力されても、一致するレコードがないために削除されません。
DELETE文で削除するとき、WHERE句の条件を書き忘れてしまった場合にはすべてのレコードが削除されてしまいます。誤って全件削除するのを防ぐため、DELETE文を書くときには慎重になってください。
例えば、DELETE文で指定する WHERE 条件をSELECT文につけて実行すると、削除するレコードを確認できます。こうすることで、削除前に条件が正しいかどうかや、消したくないレコードが含まれていないかどうか確認することができます。
セッションとクッキー
サイト上のデータを保存する方法として、ブラウザに保存する
小さな情報であるクッキーと、クッキーを元にしたセッションがあります。セッションにより、
買い物カートやログイン機能など、どのページに遷移してもデータを保持できます。
クッキーの仕組み
Webページのログイン画面を開くと、すでにIDやメールアドレスが入力された状態だったという経験はありませんか? これはクッキー(Cookie)と呼ばれる仕組みでデータを保存することで実現しています。
現在作成中のサークルサイトでも、クッキーを使って掲示板の名前欄を保存して入力の手間を省けるようにしたいですね。まずはクッキーについて学びます。
クッキーの仕組み
クッキーとは、サーバ上のデータをブラウザに保存することで、同じ情報を再利用するための仕組みです。
掲示板では、最初にコメントを書き込んだときに名前をPOST メソッドで送信しています。サーバは受け取った名前データを書き込んだクッキーをブラウザに送信します。これにより、サーバが持っているデータをブラウザで保存することができます。次に掲示板にアクセスしたとき、ブラウザは自身に保存されているクッキーの情報をサーバへ送ります。これにより、サーバは同じ情報を再利用することができるのです。クッキーはブラウザの内部に保存されているので、ブラウザの画面を閉じても情報が消えることはありません。
クッキーという名前の由来は、「クッキーのように小さい情報を保存する」
ところや「ブラウザに保存させる様子がクッキーを食べさせているかのよう」というところからきています。ブラウザにクッキーを保存することを「クッキーを食わせる」と表現することもあります。
|クッキーの保存
では、サークルサイトの掲示板で、名前欄を保存するように修正してみましょう。名前データを受け取ってクッキーに保存するのは、DBに書き込みをするwrite.phpで行います。write.phpの必須項目チェックの後に、次の2行を追加します。
write.phpの修正
// 必須項目チェック(パスワードは4桁の数字か?)
if (!preg_match("/^[0-9]{4}$/", $pass)){
header("Location: bbs.php"); // 書式が違うとき bbs.phpへ移動
exit();
}
// 名前をクッキーにセット
setcookie('name', $name, time() + 60*60*24*30);
setcookie関数は、ブラウザに対してクッキーを発行して保存させるため
の関数です。
setcookie(クッキーの名前, 保存する値, 有効期限のタイムスタンプ)
第1引数のクッキーの名前は必須で、第2引数以降は省略可能な引数です。
しばらく利用していなかったときに、古くなりすぎた情報を使わないように
するため、第3引数でクッキーの有効期限を決めることができます。さながら
ブラウザが食べるクッキーの賞味期限を決めているようですね。
有効期限は指定しなければ自動的に0がセットされ、ブラウザを閉じたときに有効期限が切れることになります。指定していればブラウザを閉じても、その日時まで有効になります。
有効期限はタイムスタンプで指定します。先ほどのコードでは、現在のタイムスタンプである time()に、60秒*60分*24時間*30日を足した日時である、「30日後まで有効」と指定しています。
クッキーの読み込み
では次に、保存したクッキーを読み取って利用するためのコードを書きましょう。bbs.phpを修正し、掲示板の書き込みフォームを表示する際に、名前のテキストボックスにクッキーから読み取った値を表示させます。
書き込みフォームの名前のテキストボックスに value属性を追加し、あらかじめ文字列が入力された状態にします。
bbs.phpの修正
<?php
// クッキーを読み込んでフォームの名前を設定する
if (isset($_COOKIE['name'])){
$name = $_COOKIE['name'];
} else {
$name = “” ;
}
(略)
<div class="form-group">
<label>名前</label>
<input type="text" name="name" class="form-control" value=“<?php echo $name ?>">
</div>
クッキーはスーパーグローバル変数である $_COOKIEで扱います。連想配列になっており、キーはクッキーの名前になります。
冒頭部分のisset関数で$_COOKIE[‘name’] が存在するか確認し、あれば $name に代入します。存在しない場合は空欄にします。
クッキーの注意点
クッキーを使うことで、名前を入力する手間を省くなど、ユーザにとって使
いやすいWebサイトを作ることができます。しかし便利な一方で、いくつか注
意しなくてはいけないこともあります。
・容量の大きい情報を入れない
・重要な情報を入れない
・内容が書き換えられると困る情報を入れない
クッキーが保存できる容量は4キロバイト程度です。文字数にすると2000文字程度となり、あまりにも大きなデータは保存できないので注意しましょう。
また、クッキーはブラウザに保存されるという点から、重要な情報を入れないようにしましょう。例えば、パスワードのような重要な情報はDBなどを使いサーバ側のプログラムを通して扱うことにして、クッキーには保存しないことが大切です。
クッキーが保存されている場所はユーザのブラウザなので、なんらかの方法で書き換えることができてしまいます。例えば Webサイトにログインしていないにもかかわらず、ログインしたようにクッキーの内容が書き換えられていたら困ります。
あくまでも、クッキーは入力補助のように「ちょっと便利にする」という目的で使ってください。
セッションの利用
セッションを使おう
作成中のサークルサイトでは、掲示板の書き込みに個人情報が含まれていたり、アップロードされた写真にメンバーの顔が写っていたりすることを考えると、
サークルメンバーだけで使えるようにしたいですね。
SNSなどの会員制サイトでは、メンバーがログインすることでサイト内のコンテンツが見えるようになるという機能があります。このようなログイン機能をサークルサイトでも実現できればよさそうですね!
ログイン機能を作るには、セッションという仕組みを使うと便利です。セッションはログイン機能を実現するだけでなく、ショッピングサイトの買い物カートや、お問い合わせフォームの確認画面などにも使われています。複数の画面をまたがる動作で情報を引き継ぐために利用されています。
セッションの仕組み
Sessionという英単語には、集会や開会という意味があります。また、シンポジウムでの講演のように、時間を区切っての発表もセッションといいます。
コンピュータ用語のセッションにも似たイメージがあり、システムやネットワークに接続を開始してから終了までの一連の操作や通信のことをセッションといいます。
例えば、あるショッピングサイトにアクセスし、商品をカートに入れて支払いを終えるまでが1セッションです。
これまでPOST メソッドやGET メソッドについて学んできましたが、これらはあるページからあるページへ送信するためのものでした。しかし、ショッピングサイトのように、いろいろなページに移動してもカートの中身を保持するためには、セッションの間ずっと情報を保持する機能が必要です。1セッションの開始から終了までの情報保持を目的としているのがセッション機能です。
セッションの流れ
①サーバがセッションIDと呼ばれるユニークな(重複のない) ID を作成します。
②セッションIDをファイル名としたセッションファイルという、データ保存用のファイルを作成します。
③サーバはセッションIDを、ユーザのブラウザに保存させます。これによりサーバからすると、特定のユーザのブラウザと、自分の持つセッションファイルが紐付きました。
④ページが遷移するときは、セッションIDを送信することで、アクセスしてきた人がどのセッションファイルに紐付いているかを確かめられます。
⑤保持したいデータがある場合は、セッションファイルにデータを書き込みます。
⑥データが保存されたセッションファイルと、ユーザの持つセッションIDの関係から、操作中はどのページに移動してもデータを保持することができます。
⑦POST メソッドで各ページに遷移するたびにデータを送受信するよりも、簡単にデータを保持することができます。
サークルサイトにセッション機能を付けよう
ログイン機能の概要
では、サークルサイトにログイン機能を追加しましょう!
ログインしているときは通常通りそのページのコンテンツを表示しますが、ログインしていないときには login.phpというページに遷移することにします。
login.phpはログインフォームを表示させ、ログインが成功すると、トップページであるindex.phpへ遷移することにします。
ログインフォームからユーザ名とパスワードを送信し、それをもとに DBからユーザを検索します。ユーザが存在していればレコードの主キーをユーザIDとしてセッションに保存します。セッションにユーザID があるということは「ログインしている状態である」とします。
ログイン機能の下準備
ログイン機能の下準備としてユーザ情報をDBに登録しましょう。次のよう
なテーブル「users」を作成します。
id
INT
自動採番。主キー。セッションに格納する。ユーザIDになる
name
VARCHAR(255)
ログインフォームから入力するユーザ名
password
VARCHAR(255)
このテーブルは次のクエリで作成します。bbsテーブルを作ったときと同様に、コマンドプロンプトでテーブルを作成しましょう。
CREATE TABLE users(
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL
) DEFAULT CHARACTER SET=utf8;
実行結果
Maria B (tennis, CREATE TABLE users
id INT PRIMARY KEY NOT NULL AUTO INCREMENT,
name VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL
DEFAULT CHARACTER SET=utf8;
Ruery OK,Orows affected (0.218 sec)
MariaDB (tennis]
次に、ユーザ情報として次の3人を保存してみましょう。
ユーザ情報(テストデータ)
ユーザ名とパスワード
yamada yamadapass
tanaka tanakapass
kikuchi kikuchipass
INSERT INTO users (name, password) VALUES
(‘yamada’, SHA2(‘yamadapass’, 256)),
(‘tanaka’, SHA2(‘tanakapass’, 256)),
(‘kikuchi’, SHA2(‘kikuchipass’, 256));
実行結果
MariaDB [tennis! INSERT INTO users (name, password) VALUES
->(yamada’, SHA2(yamadapass, 256),
-> (tanaka’, SHA2 (‘tanakapass’. 256)),
-> (kikuchi, SHA2(‘kikuchipass’, 256));
Query OK, 3 rows affected (0.281 sec)
Records: 3 Duplicates: 0 Warnings: 0
MariaDB [tennis]
INSERT文では、カンマ区切りにすることで複数データを登録できます。
パスワードのハッシュ化
ここで、パスワードが普通の文字列でなく、SHA2という関数を通していることに気づいた人もいると思います。
SHA2の第2引数にハッシュ長を指定しており、今回利用したのは SHA-256というもので、ハッシュ化の手法の1つです。この関数を通して登録したデータは次のようになります。SELECT文でテーブルの中身を見てみましょう。
SELECT * from users;
実行結果
Mariale Items SELECT * FROM sche:
id name
password
taka
e70a6e5as3656912 44 714754996045-5802703678544126-25107fb87da075-816
Graef file-4ed67b37Lede Fucoidadascb759589c6bb1c8fb4b3639 170do
16fbido estaba bichebrahel daa44c\+9bbibas duradaShetataaf
kikarchi
rows in set (0.027 sec)
Mariane Eternist
password カラムには指定した文字列とは違う文字列が登録されています。SHA2はハッシュ関数と呼ばれるもので、文字列をハッシュ化して元の平文(そのままのパスワード文字列)に戻すことができないようになっています。
なぜ平文でなくハッシュ化した文字列をテーブルに入れているかというと、このテーブルの内容が、Webアプリケーションのセキュリティ上のミスなどによって、万が一誰かに知られてしまった場合に、ハッシュ化した複雑な文字列だと安易にログインできなくなるからです。
ハッシュ化の方式は他にもありますが、今回はあらかじめ MariaDB からユーザデータを入れて試作するので、MariaDB と PHP のどちらでも使える SHA-256を利用しました。PHPからユーザデータを登録する場合は password_hash関数を使ってより強力な暗号化方式でハッシュを生成できるので、実際に Webアプリケーションを開発するときにはそちらを採用してください。
SHA-256でパスワードのみをハッシュすると、ディクショナリ攻撃(辞書攻撃)を受ける場合があります。人間が思いつくパスワードは似たものが多いので、攻撃者たちはすでによく使われるパスワードをハッシュ化した辞書を持っていて、その辞書をもとにログインできないか攻撃を試みます。
このため、パスワードに独自の文字列を追加したものをハッシュ化することで、辞書攻撃を難しくすることができます。このように追加した独自の文字列のことを、salt(ソルト)といいます。肉に振りかける塩のように、パスワードに追加する少しの文字列です。
password_hash関数では自動的に salt が追加されます。
ログインフォームの作成 – login.php
ユーザデータの下準備ができたところで、ログインフォームの表示をしましょう。 index.phpと同じ階層にテンプレートをコピーして、login.php を新たに作成します。
login.php
<?php
session_start(); // セッション開始
if (isset($_SESSION['id'])) {
// セッションにユーザIDがある=ログインしている
// ログイン済みならトップページに遷移する
header('Location: index.php');
} else if (isset($_POST['name']) && isset($_POST['password']
)) {
// ログインしていないがユーザ名とパスワードが送信されたとき
// DBに接続
$dsn = 'mysql:host=localhost;dbname=tennis;charset=utf8';
$user = 'tennisuser';
$password = 'password';
try {
$db = new PDO($dsn, $user, $password);
$db->setAttribute(PDO :: ATTR_EMULATE_PREPARES, false);
// プリペアドステートメントを作成
$stmt = $db->prepare("SELECT * FROM users WHERE name=;name AND password=: pass");
// パラメータ割り当て
$stmt->bindParam(': name', $_POST['name'], PDO :: PARAM_STR);
$stmt->bindParam(':pass', hash("sha256", $_POST
['password']), PDO:: PARAM_STR);
// クエリ実行
$stmt->execute();
if ($row = $stmt->fetch()) {
// ユーザが存在していたら、セッションにユーザIDをセット
$_SESSION['id'] = $row['id'];
header('Location: index.php');
exit();
} else {
//1レコードも取得できなかったとき
// ユーザ名・パスワードが間違っている可能性あり
//もう一度ログインフォームを表示
header('Location: login.php');
exit();
}
} catch (PDOException $e) {
exit('エラー:' . $e->getMessage());
}
}
//ログインしていない場合は以降のログインフォームを表示する。
?>
<!DOCTYPE html>
<html lang="ja" >
<head>
<title>サークルサイト</title>
<link rel="stylesheet" href="https://stackpath.
bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">
<style type="text/css">
form {
width: 100%;
max-width: 330px;
padding: 15px;
margin: auto;
text-align: center;
}
#name {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
#password {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
</style>
</head>
<body>
<main role="main" class="container" style="padding: 60px 15px 0">
<div>
<form action="login.php" method="post">
<h1>サークルサイト</h1>
<label class="sr-only">ユーザ名</label>
<input type="text" id="name" name="name" class="form-control" placeholder="ユーザ名">
<label class="sr-only">パスワード</label>
<input type="password" id="password" name="password" a
class="form-control" placeholder="パスワード">
<input type="submit" class="btn btn-primary btn-block" value="ログイン">
</form>
login.phpでは、3つの状態を管理しています。
ログインしている状態
セッションにユーザID があり、ログインしている状態です。この場合はlogin.phpで処理する必要がないため、index.phpへ遷移します。
ログインしておらず、ユーザ名とパスワードが送信された状態
ログインしていない状態で、ログインフォームからユーザ名とパスワードが送信されてきた場合です。DBにユーザ情報が存在するか確認し、存在していればセッションにユーザID をセットしてログイン済みにします。ユーザ名やパスワードが間違っていて目的のユーザが存在しなければ、login.phpへ遷移させて、もう一度ログインフォームを表示させます。
ログインしておらず、login.phpへアクセスしたばかりの状態
ログインフォームを表示させます。
では、プログラムと処理の対応を確認していきましょう。
セッションの開始
セッションを使った処理をする場合には、最初に「これからセッションを使います!」と宣言する必要があります。
session_start(); // セッション開始
session_start関数は、セッションの開始と、クッキーで渡されたセッションIDを元に、すでに開始しているセッションを復帰するのに使います。セッションを利用したプログラムを書く場合には必ず必要な処理です。
この関数は、画面への出力処理などよりも先に実行する必要があります。セッションを使うときには忘れずに、プログラム冒頭に書いてください。
ログインしている状態
セッションの連想配列に id というキーが存在しているか調べ、存在していればログインしている状態とみなしています。セッションは POST、GET、COOKIEと同じように、$_SESSION というグローバル変数で参照できます。
if (isset($_SESSION[‘id’])){
// セッションにユーザIDがある=ログインしている
//ログイン済みならトップページに遷移する
header(‘Location: index.php’);
isset関数により$_SESSION[‘id’]が存在していれば、if構文の式はTRUEと判断されます。ログインしていればログインフォームを表示する必要はないため、header関数で index.phpへ遷移します。
ログインしておらず、ユーザ名とパスワードが送信された状態
はじめのif構文に該当しない(ログインしていない状態)で、POST メソッドでユーザ名とパスワードが送信されてきた場合の処理です。
} else if (isset($_POST[‘name’]) && isset($_POST[‘password’]
)) {
まず、DB接続を行い、ユーザが存在するか確認します。
$stmt = $db->prepare(“SELECT * FROM users WHERE name=: name
AND password=: pass”);
このプリペアドステートメントは、WHERE 条件に名前が:name かつ(AND)パスワードが:pass であるユーザを検索するものです。
bindParam メソッドで:name と :passに値を割り当てます。
$stmt->bindParam(‘:pass’, hash(“sha256”, $_POST[‘password’]), 回
PDO :: PARAM_STR);
:passへの割り当てのためのbindParam メソッドの第2引数は、ユーザが送信したパスワードです。先ほどDBにデータを登録したとき、パスワードをSHA-256でハッシュ化していました。DBに登録されているのはハッシュ化された文字列(ハッシュ値)なので、検索を行うときも、入力したパスワードをハッシュ化したものとマッチしないといけません。
ユーザが存在していれば、結果セットからデータが取得できるはずです。データを取得したら、自動採番された主キーであるid をセッションに保存し、index.php へ遷移します。
if ($row = $stmt->fetch()){ SEE
// ユーザが存在していたら、セッションにユーザIDをセット
$_SESSION[‘id’] = $row[‘id’];
header(‘Location: index.php’);
結果セットからデータが取得できなかった場合は、ユーザ名・パスワードが間違っている可能性や、そもそも該当データが存在しないなどの理由が考えられます。そのため、header関数でlogin.phpへ遷移させてログインフォームを再表示させます。
ログインしておらず、login.phpへアクセスしたばかりの状態
login.phpへ直接アクセスしたり、ログインを試みたものの該当データが存在しないためにログインできず header関数で遷移したりする場合には、ログインフォームを表示します。ログインフォームのページには上部のナビバーが不要ですので、navbar.phpを読み込む必要はありません。また、他のページとデザインが少し違いますので、HTMLのhead タグ内部にスタイルを整えるためのCSSを記載しています。
その他のページのログイン処理
ログインフォームとログイン機能ができたので、その他のページにもログインしているかどうかを判別するためのプログラムを書く必要があります。
しかし、これまで作ったどのプログラムにもその処理を書かなければならないとなると面倒ですね。そこで、ログインのための処理を別プログラムにして、各プログラムから呼び出すことにします。 index.phpと同じ階層に includesというフォルダを作り、そこに login.phpというプログラムを作成してください。
includesフォルダの中に作成するlogin.php (includes-login.php)
<?php
session_start();
if (!isset($_SESSION['id'])) {
header('Location: login.php');
exit();
}
?>
内容は簡単で、ログインしていなければlogin.phpへ遷移させるだけです。どのページからも最初に読み込むことを考え、session_start関数をこのプログラム冒頭で実行させることにします。まずは index.phpで読み込ませてみましょう。
<?php
include 'includes/login.php';
$fp = fopen("info.txt", "r"); // ファイル読み込み
読み込むには include関数を使い、指定したファイルを読み込みます。includeを使った部分にそのファイルがポコっとハマるイメージです。
album.php、bbs.php、delete.php、info.php、upload.php、write.phpでも同様に、include でログイン処理を呼び出しましょう。何度も同じことを書く手間を減らすことができますし、今後ログイン処理の内容を変更することになっても修正箇所が少なくて済みます。
include によるログイン処理呼び出しの追加
<?php
include ‘includes/login.php’;
include以外の読み込み方法
include 以外にもファイルを読み込む方法があります。それぞれの違いについてまとめます。
include
指定ファイルを読み込みます。ファイルが見つからない場合は警告が出ますが、処理は続きます。
include ‘includes/login.php’;
require
指定ファイルを読み込みます。ファイルが見つからない場合はエラーが発生してプログラムが止まります。
require ‘includes/login.php’;
include_once
include と同じ動作をしますが、他のファイルからすでに読み込まれていた場合には、再度読み込みをしません。
include_once ‘includes/login.php’;
require_once
require と同じ動作をしますが、他のファイルからすでに読み込まれていた場合には、再度読み込みをしません。
require_once ‘includes/login.php’;
ログアウト処理
サークルサイトからログアウトしたいときのために、ログアウト処理を作りましょう。この処理は、複数人で同じPCを使っている場合などに、別ユーザとしてログインするためにも必要です。ログアウトのリンクが押されたら、セッションからユーザID を削除してログアウト状態にし、ログインフォームへ遷移させます。
ログアウトメニューの追加 navbar.php
ログアウト処理へのリンク追加(navbar.php)
<li class="nav-item"><a class="nav-link" href="bbs.php">
掲示板</a></li>
<li class="nav-item"><a class="nav-link" href="logout.php">ログアウト</a></li>
</ul>
ログアウト処理
<?php
session_start();
if (isset($_SESSION['id'])) {
unset($_SESSION['id']);
}
header('Location: login.php');
?>
logout.phpでは、セッションにユーザID が保存されていたときに、unset関数を使って変数を削除します。
unsetは、引数に指定された変数そのものを削除する関数です。変数に nullやりを代入した場合、変数自体を削除したわけではないので isset 関数の返り値は TRUEになりますが、unset関数を使った場合は変数自体が削除されるため、FALSEを返します。
セッション変数の削除
たくさんの変数をセッションに保存したとき、ログアウト処理を行うと各変数を削除するのが面倒ですね。そこで、unset 関数を使ってスーパーグローバル変数 $_SESSION自体を削除してしまおうと考えた方もいるでしょう。
しかし、セッションの登録が不可能になってしまうので絶対にダメです!代わりに、全部のセッション変数を削除するには次のように書きます。
$_SESSION = array();
セッションファイルを削除するには、次の関数を使います。
session_destroy();
セキュリティ
Webアプリケーションでは、しばしば重要なデータを扱うことがあります。悪意あるユーザ
から大切なデータを守るため、セキュリティ対策をしましょう。
どのように守るかを学ぶには、攻撃手法を学ぶのが一番の近道です。
昨今、ニュースで個人情報の漏えいなどが騒がれ、セキュリティ意識を高めようという機運が広がっていますね。
「PHPはセキュリティが弱い」という話をよく聞きます。これには、PHP自体が急速に発展してきたため、セキュリティホール(セキュリティの穴:突かれると弱いところ)が多く発見されてきたという背景があります。また、PHPが初心者にとって使いやすいプログラミング言語であることから、セキュリティに対してケアしていないプログラムが量産され、セキュリティが弱いというイ
メージが付いてしまったのです。こういったセキュリティ上の攻撃に対する弱い部分を、脆弱性といいます。
PHPも、誕生した当初の「HTMLに少し動きを付ける」程度の貧弱なプログラミング言語から、きちんとしたWebアプリケーションを構築できる立派な言語に進化したのですから、それを使う私たちもセキュリティに対してよく考えるべきです。
ともあれ、どういった弱点があるのかわからないと対策できません。PHPを書き慣れていない方は、なかなかすぐに実践というわけにはいかないでしょうけれども、1つずつセキュリティが守れているか確認し、安全なWebアプリケーションを作っていきましょう!
SSL通信を使おう
これまで開発したWebアプリケーションの確認をする際、httpから始まるURLにアクセスしていました。httpというプロトコルは、通信経路が暗号化されていません。そのため、悪意あるユーザが通信経路を盗み見する可能性があります。
通常の Webアプリケーションなら問題ないかもしれませんが、クレジットカード番号やパスワードを扱う場合、ブラウザから PHPプログラムが置いてあるサーバへの経路が暗号化されていないと不安ですね。こういう場合に、SSL(SecureSockets Layer)暗号化通信を使って、通信経路を守ることができます。
現在では、ブラウザのアドレスバーに鍵マークのついたサイトが多く存在しています。これらは暗号化された経路だという証明です。
SSL通信の適用方法は、PHPプログラムではなくサーバの設定になりますのでここでは詳しく説明はしません。
セキュリティに対する心構え
「外部入力を疑え」ということがPHPにおける基本的なセキュリティへの心構えです。あなたのWebアプリケーションを使う大半の人々は心優しいかもしれませんが、一部の心無い人によってセキュリティホールが暴かれてしまう事態は避けなければなりません。
このため、PHPでは外部入力に対して「これ、本当に正しいのかな?」と常に疑う必要があります。
外部入力とは次のような内容を指します。
GETの値 (URLのパラメータを変更するなど)
POST の値(フォームを改ざんして想定外のデータを送るなど)
想定外の入力内容(JavaScript のコードが書かれるなど)
クッキーの値
どの不正も、少し知識があれば簡単に実行できてしまいます。外部入力を疑わなければならない理由がわかります。
セキュリティ対策を万全に施すとなると、プログラム全体に多くの修正が必要になるため、フレームワークを利用するのも1つの手です。
フレームワークを使うと安全なWebアプリケーションを作成することができます。とはいえ、どういった攻撃があるのかを知っておくことは重要です。
攻撃してみよう
クロスサイトスクリプティング(XSS:Cross Site Scripting)という攻撃があります。普通に略すとCSSですが、デザインを整える CSS と混同するためXSSと略されている攻撃です。実際にサークルサイトに攻撃してみましょう!
掲示板の本文に、次のように入力して書き込んでみてください。
こんにちは、<script>alert(“XSS!!”)</script>
掲示板を表示すると警告ボックスが表示されてしまいました!
書き込んだ内容は、「XSS!!」と表示するという JavaScriptのプログラムです。JavaScript は、操作するうえで少し便利な動きを実現するため、ブラウザ上で動くプログラムです。
XSSとは
XSSは、あるサイトに記述された JavaScriptのプログラムが、別のサイトで実行されてしまうという脆弱性です。サイト間をまたがるため「クロスサイト」という名前が付いています。サイト間をまたがらずとも、JavaScript で任意のコードが実行されてしまうことがある点も問題です。
XSS
ユーザが入力した文字列を適切に処理していなかったがために、JavaScriptが実行されてしまったのです。
対策
XSSの対策には、出力値のエスケープが有効です。エスケープとは、特別な意味のある文字列を別の無害な文字列に置き換えることです。入力された内容を出力する際に、JavaScriptのコードになるような文字列を、次のような実体参照に置換します。こうすることで、コードやHTMLとして認識されるのでなく、通常の文字列として表示されます。
&→&
<→$lt;
>→>
しかし、1つ1つチェックしていたらキリがないですね。そこで、出力時にhtmlspecialchars 関数でエスケープしましょう。
echo nl2br(htmlspecialchars($body, ENT_QUOTES, ‘UTF-8′));
第2引数で指定した定数で、ダブルクォーテーション(“)だけでなくシングルクォーテーション() もエスケープします。第3引数は文字コードです。エスケープ後の文字列をnl2br関数の引数として、改行コードを改行タグに変換します。
サークルサイトのXSS対策
サークルサイトでユーザが入力した文字列を出力している部分といえば、bbs.phpです。本文出力部分に対し、htmlspecialchars 関数を使います。
修正前のbbs.php
<p class=”card-text”><?php echo nl2br($row[‘body’]) ?></p>
修正後のbbs.php
<p class=”card-text”><?php echo nl2br(htmlspecialchars($row:[‘body’], ENT_QUOTES, ‘UTF-8’)) ?></p>
HTMLタグの属性値にも注意!
出力以外にも気を付けることがあります。HTMLタグの属性値は必ずダブルクォーテーションで囲みましょう。属性値はクォーテーションで囲まなくてもブラウザがうまく解釈してくれますが、ユーザが入力した値がHTMLタグの属性値として使われるような場合、次のような攻撃を受けることがあります。
<input type=text value=<?php echo $param ?>>
↓
<input type=text value=a onMouseover=alert(‘XSS’)>
$paramに半角スペースを含んだ JavaScript が入っているため、属性値としては「a」が認識されます。そして、以降の onMouseOverが新たに属性として認識されるので、テキストボックスにマウスが重なるとアラートが表示されます。
ダブルクォーテーションで囲んでおけば、onMouseOver属性までもがクォーテーションの中に含まれるので、XSSを防ぐことができます。
属性値中にくや>、&を表示させるには、実体参照を使います。
<input type=”text” name=”title” value=”ねこ& いぬ”>
↓
ねこ&いぬ
サークルサイトに具体的な対策はしませんが、クッキーを発行する際にも一工夫するとよりよいです。
setcookie関数は、実は次のように引数をたくさんとる関数です。
setcookie(クッキー名, 値, 有効期限, パス , ドメイン , secure設定,httponly設定)
パスとドメインは、それぞれクッキーがどのディレクトリ・ドメインで有効かを示します。
secure 設定がTRUEになっていると、ブラウザは HTTPS接続時のみクッキーを送信します。
httponly設定がTRUEになっていると、JavaScript などからクッキーにアクセスできなくなります。
クロスサイトリクエストフォージェリ(CSRF)
脆弱性と攻撃の方法
クロスサイトリクエストフォージェリ(CSRF : Cross Site Request Forgeries)とは、意図しないリクエストを強要する攻撃手法です。
例えば、ログインなどの認証を行わずに書き込みができる掲示板があったとします。悪意を持ったユーザは、攻撃用のプログラム(偽のサイト)を作成します。このプログラムは、アクセスがあると攻撃対象の掲示板に勝手にデータを送信します。つまり、偽サイトのリンクをうっかりクリックすると、掲示板にあなたの意図しない書き込みがなされてしまうのです。怖いですね。
このように、自分はリクエストをしていないのに、他のプログラムを経由して強制的にリクエストを送られてしまうのがこの攻撃の特徴です。
本来ならば、掲示板に書き込めるデータは、掲示板の書き込みフォームから送信されないといけません。悪意を持ったユーザが作った他のサイトから、データだけ送信して書き込みができるという状況が問題です。
対策
この対策には、トークンを使って正しい書き込みフォームから送信されたデータであることを確かめるのが効果的です。トークン(token)には「証拠」という意味があり、コンピュータ用語では文字列などの最小単位を指します。こ
こでのトークンとは、特定のプログラムが作った証拠を示す短い文字列のことです。
フォームの内容とともにサーバ側で作成したトークンか送信されれば、そのフォームは正しくサーバ側で用意したものだとわかります。
具体的な対策では、ページにアクセスしてきたときにPHPでトークンを作成し、セッション変数にトークンを保存します。そのトークンを input typeがhiddenのフォームに埋め込みます(画面上に表示されなくなります)。データ送信時にトークンを含めることで、サーバが割り当てたトークンを持っているかどうかを確かめることができます。
1、アクセス時、トークンを作成セッションに保存する
2、トークンをhidden フォームに埋め込む
3、フォームデータ送信時、データとともにトークンを送信
4、セッションのトークンと送信されたトークンを比較
トークンは、次のようにセッションIDをハッシュ化して作成しましょう。
hash(“sha256”, session_id()); // SHA-256方式のハッシュ
session_id関数は、現在のセッションIDを取得する関数です。
サークルサイトのCSRF対策
ここでは一例として、掲示板に対策を行います。まずは掲示板の投稿フォームと削除用のフォームの送信ボタンの下に、トークンを送信するためのhiddenフォームを埋め込みます。
bbs.phpのCSRF対策
<label>削除パスワード(数字4桁)</label>
<input type=”text” name=”pass” class=”form-control”>
</div>
<input type=”submit” class=”btn btn-primary” value=”書き込む”>
<input type=”hidden” name=”token” value=”<?php echo hash
(“sha256″, session_id()) ?>”>
(略)
<input type=”text” name=”pass” placeholder=”削除パスワード” class=a
“form-control”>
<input type=”submit” value=”削除” class=”btn btn-secondary”>
<input type=”hidden” name=”token” value=”<?php echo hash
(“sha256”, session_id()) ?>”>
入力データを受け取るwrite.phpでは、次のようにして送信されたトークンが正しいか確認します。
write.phpのCSRF対策
$pass = $_POST[‘pass’];
$token = $_POST[‘token’]; // CSRF
; 対策
// CSRF対策:トークンが正しいか?
if ($token != hash(“sha256”, session_id())) {
header(‘Location: bbs.php’);
exit();
}
データの削除を行う delete.phpでも同様に、トークンの確認をします。
delete.phpのCSRF対策
$pass = $_POST[‘pass’l;
$token = $_POST[‘token’]; // CSRF対策
// CSRF対策:トークンが正しいか?
if ($token != hash(“sha256”, session_id())) {
header(‘Location: bbs.php’);
exit();
}
このようにして、データ送信側でトークンを埋め込み、受信側でトークンが正しいか確認することで、正しいフォームから送信されていることがわかります。
セッションハイジャック
脆弱性と攻撃の方法
セッションハイジャックとは、セッションが他人によってハイジャック、つまり乗っ取られてしまう脆弱性です。これは、他人のセッションIDを知ることで本人になりすますという攻撃手法です。
通常、セッションID はクッキーによって管理されています。自分のブラウザに保存されているセッションIDと、サーバが保存しているセッションファイルの名前によって同一人物であるとし、ページ間でセッションデータを受け渡していることは前章で学びましたね。
しかし、例えばこのセッション ID が悪意あるユーザに知られブラウザに保存されてしまったら、サーバは悪意を持ったユーザを「本当のユーザ」と信じて処理を続けてしまいます。
セッションID が連番で想像しやすいものだった、文字列長が短かった、通信経路に盗聴器を仕掛けてセッションIDを抜き出した、攻撃者が作成したセッションIDを使わせた……など、セッションハイジャックは、セッションID が他人に知られてしまうことから始まります。
対策
この対策には、ログイン直後にセッションID を再作成するのがよいでしょう。これはログインフォームが表示されたページで、なんらかの方法によりセッションIDが盗まれてしまった場合を想定しています。
そのページでセッション IDが盗まれた場合でも、正しくログインできたらセッションIDを再発行することで、本来のユーザは新しいセッションID で操作を続けることができます。悪意を持ったユーザが知っているのは無効になった古いセッションIDのため、乗っ取りが失敗します。
セッションIDの再作成には、session_regenerate_id 関数を使います。
session_regenerate_id(true);
第1引数にtrueを指定すると、サーバに残った古いセッションファイルを削除します。
サークルサイトのセッションハイジャック対策
サークルサイトにセッションハイジャック対策を施しましょう。ログインした直後にセッションIDの再作成を行います。次のようにして、セッション IDを再発行したのちにユーザIDをセッションへ書き込みます。
セッションハイジャックの対策 (login.php)
//セッションID再作成
session_regenerate_id(true);
//ユーザが存在していたら、セッションにユーザIDをセット
$_SESSION[‘id’] = $row[‘id’];
利用者からするとログイン処理の動きは変わっていませんが、ログイン前後でクッキーのセッションIDの値が変更されています。
SQLインジェクション
SQLインジェクションとは、不正にDBを操作して、意図しない情報を表示したり削除したりする非常に危険な脆弱性です。
例えば、名簿から名前を検索するサイトがあったとします。フォームの入力欄に名前を入力して検索ボタンを押すと、users テーブルのname カラムを入力値で検索するというサイトです。
実行するクエリは、$nameの部分にユーザが入力したデータを埋め込みます。
「鈴木」と入力すれば、name カラムが鈴木であるレコードだけが検索されます。
SELECT * FROM users WHERE name=’$name’
SELECT * FROM users WHERE name=‘鈴木’
しかし、「鈴木’OR ‘1’ = ‘1」のようなデータが送信されたら……?
クォーテーションがどこで閉じているのか注意して見てみてください。
SELECT * FROM users WHERE name=‘$name’
実際に実行されるクエリ
SELECT * FROM users WHERE name= ‘鈴木’OR ‘1’ = ‘1′
OR’1’=’1’の部分は常に真のため、name がなんであろうと全部のレコードが選択されます。
$nameを囲んでいたクォーテーションが一旦閉じられ、「OR ‘1’ = ‘1」の条件が追加されたような状態になります。$nameを囲んでいた後ろのクォーテーションは、「OR ‘1’=’1」の後ろを閉じるクォーテーションになってしまいます。
「’1′ = ‘1″」の結果は真(TRUE)のため、このWHERE句は「nameが鈴木、または真」という意味になります。name の値にかかわらず、OR 以降が真のために、すべてのレコードが選択される結果になってしまいます。
SELECT文ではレコードが検索されるだけなので、害はなさそうかも? と思うかもしれませんが、例えばこれがログイン画面だったらどうでしょうか。パスワードがわからなくても、他人のユーザ情報でログインできてしまうかもしれません。
さらに、これがDELETE文だったらどうでしょうか。該当の1行だけを削除したかったのに、すべてのレコードが削除されてしまうと考えたら、この脆弱性がいかに危険かがわかります。
では、実際にどのようにして SQLインジェクションの攻撃を受けるのか、プログラムを交えて確認してみましょう。
<?php
// SQLインジェクションの攻撃用文字列
$name = “鈴木’ OR ‘1’ = ‘1”;
//DBに接続
$dsn = ‘mysql:host=localhost;dbnam=tennis;charset=utf8’;
$user = ‘tennisuser’;
$password = ‘password’;
try {
$db = new PDO($dsn, $user, $password);
$records = $db->query(“SELECT * FROM users WHERE name=’ $name’ “);
foreach ($records as $row) {
echo ‘<p>ID:’ . $row[‘id’] . ‘</p>’;
echo ‘<p>name:’. $row[‘name’].'</p>’;
echo ‘<p>password:’ . $row[‘password’].'</p>’;
echo ‘<hr>’;
}
} catch (PDOException $e) {
exit(‘エラー:’.$e->getMessage());
}
?>
$nameに代入した文字列は、悪意あるユーザが攻撃用に指定した文字列だと思ってください。本来なら「鈴木」というユーザだけを検索するプログラムですが、攻撃により users テーブルのすべてのレコードが取得されてしまいます。
queryメソッドはSQL文をそのまま実行するメソッドなので、悪意ある文字列がそのまま当てはめられてしまったのですね。
対策
これを対策するには、ユーザが入力したデータをエスケープしなければなりません。エスケープを行うには専用の関数などが存在しますが、一番適切な方法はプリペアドステートメントを使うことです。
これまで作成してきたサークルサイトのプログラムのように、PDOの持つプリペアドステートメント機能を使い、bindParam メソッドで値を割り当てることで、PDOが自動的に適切なエスケープ処理を行ってくれます。