はじめに
PHPの勉強でアクセスカウンタを作ってみました。主に参考にさせていただいたのは次のサイトです。
サーバーはさくらのレンタルサーバのライトプランです。なのでsqlite3を使っています。
仕様説明
アクセスするたびにその時刻がタイムスタンプとして記録されていきます。IDも同時に追加されます。直近5回目までのアクセスが遅い順に表示される仕組みとなっています。こんな感じ:
そういうわけなので上記に上げたサイトの方法ではうまくいかず、いろんなサイトのお世話になりました。この場を借りて感謝申し上げます。
コード全文
フォルダ構成
php-cgi
└ counter
│ .htaccess ◀ Apache設定ファイル
│ index.php ◀ アクセスカウンタ表示PHPファイル
│ CCounter.php ◀ カウンタ値データベース操作PHPファイル
│ counter.db ◀ カウンタ値データベースファイル (初回実行時に自動作成)
│ style.css ◀ 整形用スタイルシート
└
Apache設定ファイル(そのまま使用)
これを使うとデータベースにアクセスできないようにできるようです。正規表現難しい...
# ファイルをブラウザからアクセスできないように設定する
<FilesMatch "^\.htaccess|.*\.db|CCounter\.php">
deny from all
</FilesMatch>
phpファイル(メインページ用)
参考にしたサイトではbody内でphp文を展開してたんですが、なんか違和感を感じたので外に移動させました。
<?php
// PHPファイルを1回だけ読み込みます
require_once("CCounter.php");
// データベースを使う準備をします
$db = new CCounter();
// データベースに接続します
// データが存在する場合のみ追加されていく形。
$exist = $db->connectDb();
// データの個数
$dataCount = $db->getDataCount();
$maxCount = $db->getMaxCount();
$minCount = $db->getMinCount();
if($exist and $dataCount < 5){
//echo "データを追加できます";
$db->insertData($maxCount);
}else if($dataCount == 5){
// データを消してから追加する
// cntの一番小さいデータを消去する
// $minCountとWHEREを組み合わせればいける
$db->deleteDataByCnt($minCount);
$db->insertData($maxCount);
}
// 小さい方から順にカウントと訪問時刻を代入する
$cntCol = $db->getCnt();
$updatedCol = $db->getUpdatedAt();
for($i=0; $i<5; $i++){
$tmp_count = $cntCol->fetchColumn();
if($tmp_count != false){
$count[$i] = $tmp_count;
}else{
$count[$i] = 0;
}
}
for($i=0; $i<5; $i++){
$tmp_updated = $updatedCol->fetchColumn();
if($tmp_updated != false){
$updated[$i] = $tmp_updated;
}else{
$updated[$i] = "";
}
}
$totalAccessCount = $exist ? $maxCount + 2 : 1; // 初回のみ1で計算
$lastAccessTime = $exist ? $updated[min(4, $dataCount)] : $updated[0]; // 初回のみ0で計算
// データベースの後始末します。ただほとんどの場合不要らしいです。
$db->closeDb();
?>
<!doctype html>
<html lang = "ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<meta name="description" content="PHPで作ったアクセスカウンタ">
<title>アクセスカウンタ</title>
<link href="style.css" rel="stylesheet" />
</head>
<body>
<main>
<h1>PHPアクセスカウンタ</h1>
<a href="https://hn-carter.sakura.ne.jp/posts/php-access-counter/">参考にしたサイト</a>
<p>
<?php echo "累計アクセス数: ".$totalAccessCount; ?><br>
<?php echo "最終アクセス: ".$lastAccessTime; ?>
</p>
<p>
みつけてくれてありがとう!!<br>これからもよろしくね!
</p>
<ul>
<li> <?php echo "カウンタ: ".$count[0].", 訪問時刻: ".$updated[0] ?> </li>
<li> <?php echo "カウンタ: ".$count[1].", 訪問時刻: ".$updated[1] ?> </li>
<li> <?php echo "カウンタ: ".$count[2].", 訪問時刻: ".$updated[2] ?> </li>
<li> <?php echo "カウンタ: ".$count[3].", 訪問時刻: ".$updated[3] ?> </li>
<li> <?php echo "カウンタ: ".$count[4].", 訪問時刻: ".$updated[4] ?> </li>
</ul>
<a href="https://www.fisce.net">ホームに戻る</a>
</main>
</body>
</html>
phpファイル(データベース操作用)
insertなども使うことになったので大幅に書き換えています。
<?php
/**
* SQLite3カウンターデータベース
*/
class CCounter {
/**
* データベースのインスタンス
*
* @var PDO
*/
private $db;
/**
* データベースファイル
*
* @var string
*/
private $db_file;
/**
* コンストラクタ
*
* @param string $dbfile SQLite3データベースファイル
*/
public function __construct($dbfile = 'counter.db') {
$this->db = null;
$this->db_file = $dbfile;
}
/**
* データベースに接続する
*/
public function connectDb() {
// データベースファイルの存在確認
$exist = file_exists($this->db_file);
// PDOで接続する
$this->db = new PDO('sqlite:'.$this->db_file, null, null,
// 参照:https://qiita.com/mpyw/items/b00b72c5c95aac573b71
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
// データベースを新規作成した場合初期設定する
if ($exist === false) {
$this->createTable();
}
// existを返すことで、初回訪問時のみデータが追加されないようにできる。
return $exist;
}
/**
* データベースから切断する
*/
public function closeDb() {
$this->db = null;
}
public function getCnt(){
$cnt_get = "SELECT cnt FROM cntTable;";
if ($this->db != null) {
try {
$cntCol = $this->db->query($cnt_get);
} catch (Exception $e) {
echo '['.__LINE__.'] Exception : '.$e->getMessage();
print_r($this->db->errorInfo());
}
}
return $cntCol;
}
public function getUpdatedAt(){
$updated_get = "SELECT updated_at FROM cntTable;";
if ($this->db != null) {
try {
$updatedCol = $this->db->query($updated_get);
} catch (Exception $e) {
echo '['.__LINE__.'] Exception : '.$e->getMessage();
print_r($this->db->errorInfo());
}
}
return $updatedCol;
}
public function getDataCount(){
$dataCount = $this->db->prepare("SELECT COUNT (*) FROM cntTable;");
$dataCount->execute();
$result = $dataCount->fetchColumn();
return $result;
}
public function getMaxCount(){
$maxCount = $this->db->prepare("SELECT MAX(cnt) AS maxCount FROM cntTable;");
$maxCount->execute();
$result = $maxCount->fetch(PDO::FETCH_ASSOC);
return $result['maxCount'];
}
public function getMinCount(){
$minCount = $this->db->prepare("SELECT MIN(cnt) AS minCount FROM cntTable;");
$minCount->execute();
$result = $minCount->fetch(PDO::FETCH_ASSOC);
return $result['minCount'];
}
public function insertData($maxCount){
$insert = $this->db->prepare("INSERT INTO cntTable (cnt, updated_at) VALUES (:newCount, DATETIME('now', 'localtime'));");
$insert->bindValue(':newCount', $maxCount + 1, PDO::PARAM_INT);
try{
$insert->execute();
} catch (Exception $e) {
echo '['.__LINE__.'] Exception : '.$e->getMessage();
print_r($this->db->errorInfo());
}
}
public function deleteDataByCnt($cnt){
$delete = $this->db->prepare("DELETE FROM cntTable WHERE cnt = :targetCnt;");
$delete->bindValue(':targetCnt', $cnt, PDO::PARAM_INT);
try{
$delete->execute();
} catch (Exception $e) {
echo '['.__LINE__.'] Exception : '.$e->getMessage();
print_r($this->db->errorInfo());
}
}
/**
* 新規データベースの初期設定
*/
private function createTable() {
// カウンタテーブル作成SQL
$counter_table = "CREATE TABLE cntTable (cnt INTEGER, updated_at TEXT NOT NULL);";
// カウンタテーブル初期化SQL
$counter_init = "INSERT INTO cntTable (cnt, updated_at) VALUES (0, DATETIME('now', 'localtime'));";
try {
// テーブルを作成しカウンタの初期値を設定
$this->db->exec($counter_table);
$this->db->exec($counter_init);
} catch (Exception $e) {
echo '['.__LINE__.'] Exception : '.$e->getMessage();
print_r($this->db->errorInfo());
}
}
}
整形用スタイルシート
html{
font-size:62.5%;
}
body{
margin:0;
width:100%;
background-color:black;
}
main{
width:60%;
min-width:400px;
margin:0 auto 80px;
display:flex;
justify-content:center;
align-items:center;
flex-direction:column;
color:white;
}
h1{
font-size:3rem;
}
a{
color:aquamarine;
font-size:2rem;
}
p, ul{
font-size:1.8rem;
}
ハマった箇所
fetchColumn()の使い方でハマりました。最初これは引数に整数を入れて、その番号の要素を取得できるものだと思っていたんですが、いくらやってもエラーが出るのでおかしいと思っていたらそれが原因だったようです...
これはまずSELECTでカラムを取得した後で、fetchColumn()を引数無しで順番に使うことでひとつずつ小さい方からデータが取得できる仕組みなんですね。すっきりしました。
これを使って、配列に小さい方から順にカウントと訪問時刻を代入しています。最大5つまでです。
PHP文を外に出す
参考にしたサイトではPHP文をbody内で展開していました。自分はPHPについてはど素人なのでそういうものかと思っていたんですが、違うんですね...データベース関連の変数への格納などは外で実行できると知って目からうろこでした。ロジックとしてはその方がすっきりするので早速書き換えてしまいました。変数が使えるHTMLといったところでしょうか。とても面白い仕組みです。
古いデータを消去する
いわゆるランキング的な感じのものを作りたかったので、一番小さいカウントのデータを探し当てて消去する仕組みにしました。そのためにWHEREとDELETEを使いました。prepareとexecuteで書くのが大事なようです。エラー処理についてはほとんどそのままコピーして書いています。
時刻について
なんか調べたらsqlite3には時刻を型で扱う方法が無いっぽい...?
適切に扱うことは可能なようです。元のコードでは何も修正せず単純にUTCで取得していたので、色々調べて最終的に'localtime'を付けて日本の時刻で登録されるようにしました。海外からアクセスされた場合に不具合が出そうなのであれですが、今回はこれで良しとします。
アクセスしたところに応じて時差を考慮して表示できるようにすればいいんじゃないかと思います。もしくはタイムゾーンを記録できるとなおいいかもしれないです(できるのかどうかは知らない)。
おわりに
まだまだ勉強することがたくさんありそうです...今はAJAXでページの一部を更新することに興味があります。次のサイトが参考になりそうです。
調べないと。
ここまでお読みいただいてありがとうございました。
参考にしたサイト