3
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

PHP+SQLiteから始めるWebアプリケーションの作り方(その2)

Last updated at Posted at 2020-04-05

PHP

PHPは、HTML文書に埋め込み可能なサーバ側(サーバサイド)で実行されるスクリプト言語です。(有料の)レンタルサーバではPHPとCGIが実行できる環境が多いです(最安で年額1000〜2000円前後か、VPSなどに比べて共有サーバだとかなり安い)。CGIというのは、対象のファイルにブラウザから呼び出しがあったとき、そのファイル(プログラム)を都度実行し、出力を応答する仕組みです。印象ですが、CGIのプログラムファイルには(Shebangが1行目に記述された)スクリプトファイルを使うことが多く、CGIの言語にはRubyやPerlが主に使われ(Pythonも使えますが)、一昔前の掲示板やチャット、一部のゲーム(箱庭諸島やブラジャータウンなど)はこのような形式のCGIで動いていました(cgi-binというディレクトリや拡張子cgiが特徴的か)。今だったらDjangoとかWebSocketとかFirebaseとか使うんでしょうか。

PHPには、WebサーバのSAPI経由で動作する方式(主にApache HTTP Serverで動かす場合、モジュールモード)と、CGIとして動作する方式(CGIモード)があります。PHPとApache HTTP Serverはしばしばセットで扱われる気がしますが、nginxでも動きます(Apache HTTP Server、nginxというのはTCP80番、443番への通信=HTTPリクエストを受け付ける代表的なWebサーバです)。今の流行りはnginxであるようです(ドメインベースでもNginxが1位に - 3月Webサーバシェア | マイナビニュース, 2020年3月)。CGIモードでPHPを動かす際には、FastCGIという常駐することでCGIの性能を改善する手法を実装したphp-fpmというプログラムがセットで使われます。

PHPには開発用サーバが組み込まれていて、簡単なコマンドで開発用ディレクトリをDocument root(Webサーバが公開する一番上のディレクトリ)とした、PHPの動作する簡易Webサーバを起動することができます。php -S localhost:8000のようなコマンドです。localhostの部分(ホスト)をPCに割り当てられたIPアドレスや0.0.0.0にするとTCPソケットを外部に公開できますが、あくまで簡易サーバなので、実装されていないHTTPの機能や処理性能の低さ、脆弱性リスクがあることを踏まえるとやめておいた方がいいです。本番環境に投入するときは素直に実績のあるWebサーバを使いましょう。ある程度PHPの文法に慣れてきて、本番環境と同じ環境で開発したくなったときは、Dockerを使うといいのではないかと思います(サードパーティのライブラリを使うときはcomposerも使うとか)。

Webサーバのことはひとまずおいておいて、PHPスクリプトを実行するプログラム、すなわちPHPを導入してみましょう。Windows用のバイナリはこちらから、他の方はパッケージマネージャ等からインストールしてください(最新版ではないかもですが)。必要な場合、binディレクトリにパスを通すなどしておいてください。(特にWindowsの方、環境変数PATHの設定)。

<!DOCTYPE html>
<meta charset="utf-8">
<title>山田太郎のウェブサイト</title>

<h2>Welcome to My Website!</h2>
<p>こんにちは! わたしは山田太郎です

さて、HTMLの節のサンプルをそのまま持ってきました。PHPはHTMLに埋め込み可能なので、これ自体もPHPスクリプトといえなくもないかもしれませんが、ともあれPHPスクリプトを書き足してみます。

<?php
  $name = '池田花子';
?>
<!DOCTYPE html>
<meta charset="utf-8">
<title><?= $name ?>のウェブサイト</title>

<h2>Welcome to My Website!</h2>
<p>こんにちは! わたしは<?= $name ?>です。

WebサーバのDirectoryIndexの設定次第なのですが、index.htmlに相当する(URLにパスを指定しなかった場合に開く)ファイルは通常index.phpとなります。開発用サーバは自動でindex.phpを見に行くので、とりあえずこの名前で保存してください。開発用サーバを起動し、ブラウザで当該URL(http://localhost:8000)を開くとPHPで処理された結果が表示されるはずです。ページのソース(view-source)を見るなどして、サーバ側のスクリプトがブラウザに送信されておらず、サーバ側で処理が行われているらしいことを確認してみてください。なお、PHPの文字列結合は.演算子です。

SQLite / SQL

SQLiteは、軽量な単一ファイルデータベースシステムです。データベースの種類としては、MySQLやPostgresSQLと同じリレーショナルデータベースです。現在はSQLite3というメジャーバージョンであるようです。

印象ですが、SQLiteは単一のファイルとしてDBを扱うことができ、分かりにくいTCPを通じたアクセスやDBシステム上のユーザ管理が不要、シンプルにDBの構造を定義できるという点で、SQLで操作可能で、ある程度実用的な、もっとも手軽なDBシステムであると思っています。DB Browser for SQLiteのようなマルチプラットフォームのGUIツール(表計算ソフトみたいにDBの中身と構造をいじれる)もあります(他のDBシステムにもあります)。

ただ気をつけてほしいのは、SQLiteはいわゆる同時編集に弱かったり、速度の面で他のMySQLやPostgresSQLといったシステムに劣っていたり(印象)、システム自体にユーザや権限といった機能がないという点です。一部のローカルDBとしてSQLiteが使われているのは見ますが、サーバサイドで動作し、複数のユーザが利用する(特に書き込む)ならば、そのプロジェクトでは別のDBシステムに乗り換えてから運用することを考えておいたほうがいいのではないかと思います。またDockerを利用すると他のDBシステムも比較的手軽に扱える、という印象も持っています。

さて、SQLiteのCLIを触ってみます。Windowsの方は上の公式サイトからバイナリを、他の方はパッケージマネージャ等からインストールしてください(最新版ではないかもですが)。sqlite3コマンドになるかと思います。sqlite3 db.sqlite3のようなコマンドを適当なディレクトリで実行してみてください(引数はファイル名です)。

CREATE TABLE entries(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, body TEXT);

SQLite3のCLIに入ったら、上のコマンドを実行してみてください。これでentriesという名前の、3つのフィールドを持ったテーブルが生成されます。フィールドは表計算ソフトでいう列のことです、カラムともいいます。テーブルは表計算ソフトでいうシートです。

テーブルの一覧は.tablesと打って実行すると確認できます。Ctrl+Dで一度CLIを抜けて、もう一度sqlite3 db.sqlite3を開いてもこのテーブル(もちろんすべてのデータ、トランザクションしてない限り)は維持されています。最初からやり直したいときは、db.sqlite3ファイルを普通にファイルとして削除すればいいです(くれぐれも本番環境でやらかさないように気をつけてください。全データが消滅します。バックアップと入念な確認が必要です。特にワイルドカード/ディレクトリ削除などに巻き込まないように)。

SELECT * FROM entries;

次に上のコマンドを実行してみてください。何も表示されないはずです。

INSERT INTO entries VALUES(NULL, "はじめまして", "こんにちは!");

これを実行したら、もう一度SELECTしてみましょう。今追加したレコードが表示されるはずです。レコードは表計算ソフトでいう行です。

SELECT * FROM entries;

表示が見にくいと感じたら、.headers onを実行してみてください。出力の1行目にカラム名が表示されます。他にも表示を見やすくするコマンドがあるので調べてみてください。

PRIMARY KEY制約やAUTOINCREMENT制約、フィールドの型やSQL文法についての詳しい説明は専門書/公式ドキュメントに譲ります。ただ、何度かCREATEINSERT文を実行したり、数値型(INTEGER)のフィールドを追加したりすればだいたい分かるかと思います。INTEGERは数値型、TEXTは文字列型です。NULLを使っているのはSQLite側に自動でIDを決めさせるためです。JSONや時間などの細かいデータ型はシステムとしてはサポートされていません(時間は組み込み関数で扱うことができるみたいですが、ホスト側=DBを呼び出す側の言語で処理してもいいと思います)。また、型名/型がかなり柔軟です。INTEGERというと整数のイメージですが、浮動小数点数を格納できたり、型名をFLOATとして定義できたりします(別名扱い)。

既存のレコード(の中身)を更新するUPDATE文、REPLACE文について調べて試してみましょう。テーブルの削除はDROP TABLEです。

レコードの検索、例えばSELECT * FROM entries WHERE id=1;SELECT * FROM entries WHERE title LIKE "%hoge%";を試してみましょう(適当なデータを追加して)。

PHP + SQLite(その1)

DBをプログラミング言語から扱うときは、通常それぞれの言語のライブラリを使用します。

プログラム中のオブジェクトとDB中のオブジェクトとの変換を隠蔽したり、異なるDBシステムの違いを吸収したり、マイグレーション(後からのDBの構造変更)を助けたりするORMという種類のライブラリもありますが、入門ですのでここでは使用しません。

それでは、PHPからSQLiteのデータベースを扱ってみましょう。と、その前に少し準備が必要な場合があります。PHPからSQLiteを使用する際には、PHPのSQLite3に関する拡張機能(Extension)を導入する必要があります。Windowsの方は拡張機能自体は標準で含まれていたかと思いますが、php.iniファイルを自分で編集して必要な拡張機能を有効化する必要があったように思います。調べてやってみてください。それ以外の方は、パッケージマネージャ等からインストールできると思います(Ubuntuの場合、php-sqlite3)。日本語のようなマルチバイト文字を扱う組み込み関数mb_*を利用するときなども拡張機能の導入・有効化が必要な場合があります。レンタルサーバの場合はリストが提供されていたり、またphpinfo関数を呼び出すPHPスクリプトをブラウザから開くことで調べられます(環境情報が流出するおそれがあるのでおすすめはしませんが)。準備ができましたら、次に進みましょう。

<?php
  $title = 'はじめまして';
  $body = 'こんにちは!';

  $db = new SQLite3('db.sqlite3');
  $db->exec('CREATE TABLE IF NOT EXISTS entries(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, body TEXT)');

  $stmt = $db->prepare('INSERT INTO entries VALUES(NULL, :title, :body)');
  $stmt->bindValue(':title', $title, SQLITE3_TEXT);
  $stmt->bindValue(':body', $body, SQLITE3_TEXT);
  $stmt->execute();

上のようなPHPスクリプトを(拡張子をphpにして)保存し、ブラウザから開いてください。DBが生成され、ページを開くごとにレコードが追加されていきます(CLIやSELECT文で確認してみてください)。下にPHPからSELECT文で(とりあえず)中身を確認するスクリプトを示します。

<?php
  header('Content-Type: text/plain; charset=UTF-8');

  $db = new SQLite3('db.sqlite3');

  $result = $db->query('SELECT * FROM entries');
  while ($row = $result->fetchArray()) {
    print_r($row);
  }

header関数はHTTPヘッダを出力する関数で、ここではボディのMIMEタイプを出力しています。

exec関数とquery関数はシンプルにSQL文を実行する関数で、この2つの違いはSQL文を実行した結果を返すかどうかです。

では、prepare関数を使っている部分は何をしているのでしょう? これはPreparedStatementというPHP-SQLite3の機能を使っています。前節のSQL文の例を思い出してみると、VALUESの中に変数の値を直接文字列結合によって入れ込んでしまえばいいのではないか、と思うかもしれませんが、特にユーザからの入力をSQL文に入れ込もうとする場合、それは実はよくない実装です(変数の値が固定なのだからそのまま書けばいい、というのは置いておいてください..)。そのような実装をした場合、入れ込んだ文字列によってSQL文が意図しない挙動をするおそれがあるだけでなく、悪意のあるSQL文が実行されるように変数の値を細工されることで、見せてはならない情報をDBから抜き取られたり、DBを破壊されたり、事前の検証をスルーして本来入ってはいけない値がDBに入ってしまうなどといったSQLインジェクションという攻撃にさらされる危険があります。実際にはPreparedStatementはほぼ同一のSQL文を連続で実行する場合のパフォーマンス最適化を主目的としているようですが、入力値を自動でエスケープしてくれるため、SQL的に信頼できない値を安全に入れ込むことができます。もちろん、XSSなどの脆弱性には対処できないので、きちんと入れる値、出す値の検証/処理をするようにしましょう(PHPにはそのような機能が豊富です)。

<?php
  error_reporting(E_ALL);
  ini_set('display_errors', '1');

プログラミング中にエラーが発生したとき、開発中は上のような記述をプログラムの先頭に追記することですべてのエラー詳細をWebページ上で確認することができます。ただし、本番に投入する前に消し忘れないように注意しましょう(内部のディレクトリ構造やアルゴリズム、またAPIキーのような秘密にしなければならない変数値などが流出する危険があります)。

HTTP

ブラウザとWebサーバの間の通信に使われるプロトコルは基本的にHTTPです(FTPなどをサポートしている場合もありますが)。HTTPにもバージョンがあり、現在はHTTP/3の標準化が議論されている段階であったように思いますが、現在主流なのはHTTP/1.1です。HTTP/1.1では通常、サーバはTCP80番(HTTPSの場合は443番)でブラウザ/クライアントからの通信を待ち受け、基本的に一定のルールに沿ったテキスト形式でデータをやりとりします(ボディがgzipなどで圧縮される場合もありますが、ヘッダは常にテキスト形式です)。

HTTP通信でやりとりされるテキストデータのフォーマットのイメージは次のようなものです。空行をはさんでヘッダとボディと呼ばれる2部分に分かれています。

ブラウザ:適当なポートA→HTTPサーバ:80(HTTPリクエスト/要求)

Request Header
(空行)
Request Body

HTTPサーバ:80→ブラウザ:適当なポートA(HTTPレスポンス/応答)

Response Header
(空行)
Response Body

やりとりされるデータはだいたいこんな感じです。

ブラウザ→サーバ(ボディはなし)

GET / HTTP/1.1
Host: example.com

サーバ→ブラウザ

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 50

<!DOCTYPE html>
<meta charset="utf-8">
Hello world

サーバ側の動作は、イメージ的にはこんな感じです。

request_data = tcp_socket.recv_http() # Requestを受け取る

response_data = process_request(request_data) # Requestを処理する

tcp_socket.send_http(response_data) # Responseを返す

PythonのHTTP通信ライブラリrequestsでローカルサーバにHTTPリクエストを送ったとき、こんな感じのテキストデータが送信されました。

GET / HTTP/1.1
Host: localhost:8000
User-Agent: python-requests/2.23.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive

PHPの開発サーバから返ってきた応答はこんな感じでした。

HTTP/1.1 200 OK
Host: localhost:8000
Date: Thu, 02 Apr 2020 12:00:00 GMT
Connection: close
X-Powered-By: PHP/7.2.24-0ubuntu0.18.04.3
Content-type: text/html; charset=UTF-8

<!DOCTYPE html>
<meta charset="utf-8">
Hello world

プログラム

import requests
requests.get('http://localhost:8000')
<!DOCTYPE html>
<meta charset="utf-8">
Hello world

まず、リクエストを見てみましょう。リクエストの1行目に書かれているGETというのは、リクエストメソッド(Request Method)の1種です。GETの他にも、POSTやHEAD、PUTなどがあり、WebサーバはGETとHEADのサポートが必須(RFC 7231 #4. Request Methods)とされています(HEADはヘッダのみを返すもの)。GETHTTP/1.1の間に挟まっている/はパス(Request Target、Reqeust URI)です(RFC7230 #5.3. Request Target)。

GET / HTTP/1.1
Host: example.com

通常、Webページにアクセスするときに意識するのはGETとPOSTの2種類かと思います。Webページをふつうに開くときに使われるのがGET、アンケートやログインといったフォームを送信するときによく使われるのがPOSTといった感じです。WebのAPI作法の一種であるREST APIなどでは、PUT(追加、アップロード)やDELETE(削除)なども利用してCRUDを実現しますが、HTMLのフォームでは使用できず、通常のWebサーバは(動作する機能として)実装していないと思います(POSTにPUTやDELETEに相当する機能を持たせる)。

上にフォームを送信するときにPOSTがよく使われると書きましたが、GETでもフォームを送信することがあります。例えば、Google検索のフォームはGETリクエストを送信します。フォームというのはHTTPリクエストにデータを持たせるための部品ですが、GETリクエストではHTTPヘッダの1行目に含まれるRequest Targetの一部としてPATH/?key=value&hoge=fugaのように送信されます。ブラウザでは、アドレスバーにURLの一部として同様に表示されます。GETリクエストにこのような形で含まれるデータをクエリパラメータといいます。

では、POSTの場合はどこにフォームのデータが含まれるのでしょうか? それはリクエストボディです。ボディにデータを含むPOSTリクエストは、ヘッダにボディのmimetypeを表すContent-Typeフィールドを伴って送信されます。通常、HTMLフォームを使って送信されるデータのmimetypeはapplication/x-www-form-urlencodedまたはmultipart/form-dataです。前者はテキストデータのみを送信するようなフォームに用いられ、後者はファイルのアップロードなどの際に用いられます。(REST APIのような)HTMLフォームを前提としないAPIサーバなどでは、application/jsonなどをContent-Typeとして要求することがあります。

次に、レスポンスを見てみましょう。レスポンスの1行目に200 OKという文字列があります。これはHTTPステータスコードといい、HTTPリクエストがサーバにおいてどのように処理されたかを示す記号です。よく見かけるものは、404 Not Found403 Forbidden503 Service Unavailableのようなものでしょうか。リダイレクトを表すステータスコードもあります。

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 50

<!DOCTYPE html>
<meta charset="utf-8">
Hello world

ブラウザ上でHTTP通信を使ってやりとりされているデータはブラウザの開発者ツールから確認することができます(ヘッダやボディも直接確認できます)。普段使っているサイトで見てみましょう。


3
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?