[PHP]ページごとの訪問者数を調べるアクセスカウンター

ページごとに訪問者数をカウントするためには、概ね次のような流れを取ります。

・訪問者の IP アドレスを取得する
・同じ日に同一の IP アドレスが記録されていないか調べる
・その日に初めてアクセスしていた場合はカウントを増やす
・訪問者の IP アドレスを記録する

一日のログのうち、直近100件に同じ人がアクセスしていなければ1増えるという条件で、カウンターをクラス化してみました。
今回はデータベース代わりにテキストファイルを利用した簡素なものなので MySQL 等の RDBMS は不要です。

[view_counter.class.php]

<?php
class ViewCounter
{
	const SALT = 'hello_world.12345';
	private $log_dir;
	private $db_dir;
	
	function __construct($log_dir=null, $db_dir=null){
		//ログディレクトリ設定
		$this->log_dir	 = is_null($log_dir) ? dirname(__FILE__) . '/log/' : $log_dir;
		$this->db_dir	 = is_null($db_dir)	 ? dirname(__FILE__) . '/db/' : $db_dir;
	}

	//IPをチェックしてカウントを増やす
	function log($id){
		$ip = date("Ymd_") . md5($this::SALT . $_SERVER['REMOTE_ADDR']);

		$file = $this->log_dir . $id . '_' . md5($this::SALT . $id) . '.log';
		
		$data = array();
		$flag = true;
		
		$fp = fopen($file, 'a+b');
		flock($fp, LOCK_EX);
		
		//直近100件までを読み込む
		for($i=0;$i<100;$i++){
			if(feof($fp)) break;
			$line = fgets($fp);
			if($ip === rtrim($line)){
				$flag = false;
				break;
			} else {
				$data[] = $line;
			}
		}
	
		if($flag){
			array_unshift($data, $ip . "\n");
			ftruncate ($fp, 0);
			rewind($fp);
			foreach($data as $value){
				fwrite($fp, $value);
			}
		}
		
		fflush($fp);
		flock($fp, LOCK_UN);
		fclose($fp);
		
		if($flag){
			$count = $this->count_up($id);
		} else {
			$count = $this->get_count($id);
		}

		return $count;
	}
	
	//データベースのカウントを増やす
	function count_up($id, $num=1){
		$file = $this->db_dir . $id . '_' .md5($this::SALT . $id) . '.log';
		
		if(file_exists($file)){
			$count = (int)file_get_contents($file);
		} else {
			$count = 0;
		}
		
		if($num > 0){
			$count = $count + $num;
			file_put_contents( $file, $count, LOCK_EX );
		}
		return $count;
	}
	
	//データベースのカウントを得る
	function get_count($id){
		$count = $this->count_up($id, 0);
		return $count;
	}
}

【使い方】

<?php
include_once 'view_counter.class.php';
$counter = new ViewCounter();

// ページ固有のID
$id = 1234;
$count = $counter->log( $id );

echo $count;

使用する前に書き込み可能な2つのディレクトリを作成します。
ひとつは訪問者の IP を記録するディレクトリ($log_dir)、 もう一つはページごとの総アクセス数を記録するディレクトリ($db_dir)です。
必要に応じてディレクトリ内に「.htaccess」ファイルを設置し「deny from all」を記述して直接アクセスされないようにしたほうが良いでしょう。
指定方法は直接 __construct の初期設定を書き換えるか、new でインスタンス化する際に引数として与えます。

$log_dir = dirname(__FILE__) . '/log/';
$db_dir  = dirname(__FILE__) . '/db/';
$counter = new ViewCounter($log_dir, $db_dir);

「const SALT」の内容は適切な値に書き換えて下さい。
暗号化に使用するだけなので推測されにくい文字列であれば構いません。

$id の値はページ番号やファイル名など、ページ固有の値を指定します。
この ID ごとにアクセスが記録されます。
複数のページで同じ ID を用いればグループ単位でのアクセスカウンターにも出来ます。

「for($i=0;$i<100;$i++)」となっているのは、ログファイルから同一 IP の訪問者を検索する際、直近 100 行を調べるように制限しているからです。
これはログファイルの肥大化を防ぐためのものですが、厳密に一人一日1回までとしたい場合はこの記述を削除するか、大きな数字にしておくのがいいと思います。

一日一回ということを判断するために、記録する IP アドレスの先頭に年月日をつけています。
IP アドレスはプライバシー保護の都合上暗号化していますが、不正なアクセスをするユーザーを調べたいのであれば md5() を使わずに生の IP で記録するのも手ですが、他人の目に触れないように配慮が必要です。

実際にアクセスをカウントするには「log( $id )」を使います。
実行すると IP アドレスが記録され、現在のアクセス数が返ります。
カウントを増やさずにアクセス数だけ取得したい場合は「get_count($id)」で取得できます。

[PHP]ディレクトリ内の一定時間が経過した古いファイルだけを削除する

フォルダ内の古くなったファイルだけを消すには、
filemtime() で最終更新日を取得し、unlink() で削除します。

最終更新日は Unix 時間で取得されるので削除期限の指定には strtotime() を使いました。
「24 hours ago」であれば 24 時間前より古いファイルが削除されます。
単純に 「time() – (60 * 60 * 24)」としたり、mktime() を使ったりするのも良いでしょう。

【注意】
このスクリプトを実行するとファイルが削除されます。
実行前に必ず echo などで削除対象のファイルが適切に指定されているか確認して下さい。

<?php
date_default_timezone_set('Asia/Tokyo');

//削除期限
$expire = strtotime("24 hours ago");

//ディレクトリ
$dir = dirname(__FILE__) . '/dir/';

$list = scandir($dir);
foreach($list as $value){
	$file = $dir . $value;
	if(!is_file($file)) continue;
	$mod = filemtime( $file );
	if($mod < $expire){
		//chmod($file, 0666);
		unlink($file);
	}
}

削除にパーミッションの変更が必要な場合は chmod() で適切な権限に変更して下さい。
それでも削除できない場合はディレクトリのパーミッションや所有者を確認して下さい。

ディレクトリが多階層になっていて、指定したディレクトリ以下の全てのファイルを再帰的に探索して削除するには次のようにします。

<?php
date_default_timezone_set('Asia/Tokyo');

//削除期限
$expire = strtotime("24 hours ago");

//ディレクトリ
$dir = dirname(__FILE__) . '/dir/';

remove_old_files($dir, $expire);
 
function remove_old_files($dir, $timestamp){
	$iterator = new RecursiveIteratorIterator(
		new RecursiveDirectoryIterator(
			$dir,
			 FilesystemIterator::CURRENT_AS_FILEINFO
			|FilesystemIterator::SKIP_DOTS
			|FilesystemIterator::KEY_AS_PATHNAME
		), RecursiveIteratorIterator::LEAVES_ONLY
	);

	foreach($iterator as $pathname => $info){
		if($info->getMTime() < $timestamp){
			//chmod($pathname, 0666);
			unlink($pathname);
		}
	}
}

RecursiveCallbackFilterIterator を利用すると除外項目をより詳細に設定できます。
下の例では 指定した時間より古く、指定した項目名でないもの を条件に処理をしています。

<?php
date_default_timezone_set('Asia/Tokyo');

//削除期限
$expire = strtotime("24 hours ago");

//ディレクトリ
$dir = dirname(__FILE__) . '/dir/';

// 除外項目
$ignores = [
    $dir . 'example'
];

remove_old_files($dir, $expire, $ignores);

function remove_old_files($dir, $timestamp, $ignores = [])
{
	// フィルター処理
    $filter = function ($file, $key, $iterator) use ($timestamp, $ignores) {
        if ($iterator->hasChildren()) {
            return true;
        }

		// 除外対象を正規表現で選別
        foreach ($ignores as $ignore) {
            $pattern = '/^' . preg_quote($ignore, '/') . '/';
            if (preg_match($pattern, $file)) {
                return false;
            }
        };

		// 古くない項目は除外
        if ($file->getMTime() >= $timestamp)  return false;

        return true;
    };

    $dirIterator = new RecursiveDirectoryIterator(
        $dir,
         FilesystemIterator::CURRENT_AS_FILEINFO
        |FilesystemIterator::SKIP_DOTS
        |FilesystemIterator::KEY_AS_PATHNAME
    );

    $iterator = new RecursiveIteratorIterator(
       new RecursiveCallbackFilterIterator($dirIterator, $filter),
        RecursiveIteratorIterator::LEAVES_ONLY
    );

    foreach ($iterator as $pathname => $info) {
        //chmod($pathname, 0666);
        unlink($pathname);
    }
}

[PHP]ユーザー定義関数の出力結果を任意の変数に格納する

通常ユーザー定義関数の出力結果は return で戻り値(返り値)として返すのが一般的ですが、
preg_match() の $match のように結果を指定された変数に保存したい場合や、 エラー発生時に任意の変数にエラーコードを格納したい場合などがあると思います。
そういった場合は引数に & を付けて参照渡しにします。(参照渡しについて

下の例では2つの数値を足した結果を任意の変数に格納しています。

<?php
sample(3, 4, $result);
echo $result;

function sample($a, $b, &$x){
	$x = $a + $b;
}

【出力結果】

7

変数 $x を参照渡しにすることで、$x に与えられた変更が $result に影響するようにしています。

次の例では関数 sample() に渡された文字列が「ok」でない場合エラーコード(例では「Error 123」)を 変数 $code に格納します。

<?php
$value = 'test';

$result = sample($value, $code);
var_dump($result);
echo $code;

function sample($value, &$error=null){
	if($value == 'ok'){
		return true;
	} else {
		$error = "Error 123";
		return false;
	}
}

【出力結果】

boolean false
Error 123

エラーコードをどの変数に格納するかは関数 sample() の第 2 引数 $error に & をつけて指定しています。
引数 $error には初期値を設定してあるのでエラーコードを受け取る必要がない場合は引数を省略できます。
例では格納先に変数 $code を指定していますが、「$err」を指定すれば「$err」に格納されます。