[PHP]投稿数や出現回数に応じて文字サイズが変わるタグクラウド

ブログなどで記事数の多さによってタグリンクの文字サイズが変わるタグクラウドを設置する事はよくあります。
作り方はいくつもあると思いますが、2種類の方法で作ってみました。

tagcloud

タグ名・出現数・リンク先を指定する方法

<?php
// フォントサイズの指定
$max_size = 28;
$min_size = 8;

// name, counts, link
$tags = array(
	array('Technology',    3, '1.html'),
	array('Entertainment', 3, '2.html'),
	array('Movie',         3, '3.html'),
	array('Music',         5, '4.html'),
	array('Culture',       2, '5.html'),
	array('News',          3, '6.html'),
	array('Web',           5, '7.html'),
	array('Radio',         2, '8.html'),
	array('Business',      1, '9.html'),
	array('Photo',         2, '10.html'),
	array('Sports',        1, '11.html'),
	array('Art',           2, '12.html'),
	array('History',       1, '13.html'),
	array('Blog',          1, '14.html'),
	array('Hobby',         1, '15.html'),
	array('Food',          1, '16.html'),
	array('Communication', 1, '17.html')
);

// 多い順に並び替える場合
//usort($tags, function($a, $b){ return $b[1] - $a[1];});

function generate_tagcloud($tags, $max_size, $min_size){
	$counts = array();
	foreach($tags as $value){ $counts[] = $value[1]; }
	$sum = array_sum($counts);

	// 最小値と最大値
	$max = max($counts);
	$min = min($counts);

	foreach($tags as $tag){
		$perc = ($max == $min) ? 1.0 : ($tag[1] - $min) / ($max - $min);
		$size = round( ($max_size - $min_size) * $perc + $min_size );
		
		if(isset($tag[2])){
			printf(
				'<span style="font-size:%dpx;"><a href="%s">%s</a></span>' . "\n",
				 $size, h($tag[2]), h($tag[0])
			);
		} else {
			printf('<span style="font-size:%dpx">%s</span>' . "\n", $size, h($tag[0]) );
		}
	}
}

function h($str){
	return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
?>
<html>
<head><title>Sample</title>
<style type="text/css">
.tagcloud {width: 300px;padding: 0;"}
.tagcloud li { display: inline; }
</style>
</head>
<body>
  <div class="tagcloud">
  <?php generate_tagcloud($tags, $max_size, $min_size);?>
  </div>
</body>
</html>

タグがの出現数がわからない場合

<?php
// フォントサイズの指定
$max_size = 28;
$min_size = 8;

// タグ
$tags = array(
'Technology', 'Entertainment', 'Movie',
'Music', 'Culture', 'News', 'Music',
'Web', 'Technology', 'Radio', 'News',
'Movie', 'Music', 'Entertainment', 'Web',
'Business', 'Culture', 'Photo', 'Sports',
'Music', 'Web', 'Technology', 'Entertainment',
'Movie', 'Radio', 'Photo', 'Web', 'News',
'Art', 'History', 'Blog', 'Hobby', 'Art',
'Food', 'Web', 'Communication', 'Music'
);

// 対応するリンク
$links = array(
	'Web'           => 'web.html',
	'Music'         => 'music.html',
	'Technology'    => 'technology.html',
	'Entertainment' => 'entertainment.html'
	// 以下略
);

function generate_tagcloud($tags, $max_size, $min_size, $links=array()){
	// 出現回数を配列化
	$counts = array_count_values($tags);

	// 多い順に並び替える場合
	//arsort($counts);

	// 合計を取得
	$sum = array_sum($counts);

	// 最小値と最大値
	$max = max($counts);
	$min = min($counts);

	foreach($counts as $key => $value){
		$perc = ($max == $min) ? 1.0 : ($value - $min) / ($max - $min);
		$size = round( ($max_size - $min_size) * $perc + $min_size );
		
		if(isset($links[$key])){
			printf(
				'<span style="font-size:%dpx;"><a href="%s">%s</a></span>' . "\n",
				 $size, h($links[$key]), h($key)
			);
		} else {
			printf('<span style="font-size:%dpx">%s</span>' . "\n", $size, h($key) );
		}
	}
}

function h($str){
	return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
?>
<html>
<head>
  <title>Sample</title>
</head>
<body>
  <div class="tag cloud" style="width: 300px;padding: 0;">
  <?php generate_tagcloud($tags, $max_size, $min_size, $links);?>
  </div>
</body>
</html>

文字サイズの上限と下限を設定し、タグに関する配列を generate_tagcloud() に渡すとタグクラウドが生成されます。
$perc には 0.00〜1.00 の形でパーセンテージが格納されます。これを使って文字の濃淡を付けてみるのも良いと思います。

[PHP]PDOを使ったMySQLデータベースへの接続と基本操作

PHP 5.5 以降では mysql_query 関数が非推奨になり、代替として MySQLi や PDO の使用が推奨されています。
にもかかわらず古い書籍を参考にしたプログラムでは依然として mysql_query を使用していたり、
PDO を使いつつもプリペアドステートメントを使用していないなどのケースが見られるため、
PDO と MySQL を使ったデータベースへの接続方法と INSERT, SELECT などの基本的なクエリについて考えてみたいと思います。

下記サンプルは現状 PHP 5.3.6 以降を想定しています。
仕様変更などで最新の方法でなくなった場合はこのページを修正もしくは削除する予定です。
情報の誤りや古くなっている内容があった場合はコメントやメールフォームでご連絡下さい。


・接続

<?php
define('DB_NAME', 'sampledb');
define('DB_HOST', 'localhost');
define('DB_USER', 'user');
define('DB_PASS', 'password');

$dsn = 'mysql:dbname=' . DB_NAME . ';host=' . DB_HOST . ';charset=utf8';
$option = array(
	PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
	PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
	PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
	PDO::ATTR_EMULATE_PREPARES => false,
	PDO::ATTR_STRINGIFY_FETCHES => false
);

try {
	$db = new PDO($dsn, DB_USER, DB_PASS, $option);
} catch (PDOException $e){
	echo $e->getMessage();
}

接続に必要なデータベース名などの情報は定数にしました。そのほうがクラス内で使う際にも都合が良いと思います。データベース名、ホスト名、文字コードは DSN の形で指定します。
ここでは文字コードとして「utf8」を指定しています。「utf-8」のようなハイフンを含めたものにしないで下さい。絵文字を扱う可能性がある場合は「utf8mb4」を指定して下さい。
PHP 5.3.6 以上で上記の文字コード指定をしていれば「SET NAMES」や「SET CHARACTER SET」は使用しません。

使用しているオプションは次のとおりです。目的によっては不要なものも有ります。

「PDO::ATTR_ERRMODE」 デフォルトのエラー発生時の処理方法を指定します。サンプルでは Exception の形で例外を投げます。

「PDO::ATTR_DEFAULT_FETCH_MODE」 SELECT 等でデータを取得する際の型を指定します。「PDO::FETCH_ASSOC」のとき配列変数の形で取得します。

「PDO::MYSQL_ATTR_USE_BUFFERED_QUERY」 クエリにバッファを使用するかどうかです。移植性に影響するので使わないケースも有りますが、SELECT した行数を取得する関数 rowCount() が使えるようになるというメリットが有ります。

「PDO::ATTR_EMULATE_PREPARES」 MySQLネイティブのプリペアドステートメント機能の代わりにエミュレートしたものを使う設定です。

「PDO::ATTR_STRINGIFY_FETCHES」 取得時した内容を文字列型に変換するかのオプションです。上記のエミュレートが有効なときは int 型のフィールドであってもこの指定にかかわらず文字列型に変換されます。

参考: PDO::setAttribute
http://php.net/manual/ja/pdo.setattribute.php


クエリ

値を代入する必要のない固定の SQL 文を実行する場合は query() を使います。

$sql = "INSERT INTO `users` (user_name) VALUES('John Doe')";

$db->query($sql);
query() は一つのクエリ文のみ実行できます。複数実行するには exec() を使います。
$sql = <<<SQL
INSERT INTO `users` (user_name) VALUES('John Doe');
INSERT INTO `users` (user_name) VALUES('Jane Doe');
SQL;

$db->exec($sql);

ただし SELECT などの実行結果を取得する場合には exec() は使用できません。


INSERT 文

PDO でクエリを組み立てる際、従来の mysql_real_escape_string のようなエスケープ方法ではなく、プリペアードステートメントを用います。
prepare() にプレースホルダ(挿入位置を決めるための文字列)を用いた SQL を渡し、実際の値は bindValue() や bindParam() などで指定します。
プレースホルダにバインド(代入)する際に内容は適切にエスケープされます。

データベースにユーザー名(VARCHAR)、会員番号(BIGINT)、メールアドレス(VARCHAR, NULL可)からなるレコードを挿入してみます。
SQL のプレースホルダーにはそれぞれ「:name」「:number」「:email」という名前をつけました。
パラメータ名はわかりやすいもので構いませんが、「:」から始まる必要があります。

$sql = <<<SQL
INSERT INTO `users` (`user_name`, `register_num`, `user_email`)
 VALUES(:name, :number, :email)
SQL;

$user_name = 'John';
$number = 1234;
$email  = null;

$stmt = $db->prepare($sql);
$stmt->bindValue(':name', $user_name, PDO::PARAM_STR);
$stmt->bindValue(':number', $number, PDO::PARAM_INT);

$pdo_type = is_null($email) ? PDO::PARAM_NULL : PDO::PARAM_STR;
$stmt->bindValue(':email', $email, $pdo_type);

$stmt->execute();

bindValue() はプレースホルダーのパラメータ名、値、型を指定します。
型は必須ではないのですが省略時は文字列型と判断されるので面倒でも指定します。
NULL を許可するフィールドの場合は型も切り替えておきます。

PDO::PARAM_STR は文字型、PDO::PARAM_INT は数値型で、FLOAT や DOUBLE などは有りません。
他にも PDO::PARAM_BOOL や PDO::PARAM_LOB などがあります。

参考: PDO 定義済み変数
http://www.php.net/manual/ja/pdo.constants.php

プレースホルダーには疑問符「?」を使うことも出来ます。

$sql = <<<SQL
INSERT INTO `users` (`user_name`, `register_num`, `user_email`)
 VALUES(?, ?, ?)
SQL;

この場合 bindValue を行う際は、1 から始まる番号を順番に指定します。

$stmt->bindValue(1, $user_name, PDO::PARAM_STR);
$stmt->bindValue(2, $number,    PDO::PARAM_INT);
$stmt->bindValue(3, $email,     $pdo_type);

SELECT 文

登録されている内容から次の条件でレコードを抽出します

・名前が「John」から始まる ・会員番号が 1,000 以上 ・メールアドレスは NULL

SQL の組み立ては INSERT の時と同様です。
LIKE を使った部分一致をする場合、「%」は「:name%」とせずに、
bindValue の段階で「John%」という値を代入します。

$sql = <<<SQL
SELECT `user_name`, `register_num` FROM `users`
 WHERE `user_name` LIKE :name ESCAPE '#'
 AND `user_email` <=> :email
 AND `register_num` >= :number
SQL;

$stmt = $db->prepare($sql);

$user_name = 'John';
$user_name = preg_replace('/([_%#])/', '#$1', $user_name) . '%';

$number    = 1000;
$email     = null;

$stmt = $db->prepare($sql);
$stmt->bindValue(':name', $user_name, PDO::PARAM_STR);
$stmt->bindValue(':number', $number, PDO::PARAM_INT);

$pdo_type = is_null($email) ? PDO::PARAM_NULL : PDO::PARAM_STR;
$stmt->bindValue(':email', $email, $pdo_type);

$stmt->execute();

$result = $stmt->fetchAll();

var_dump($result);

NULL であるものを探す場合「`user_email` IS NULL」のようにするのが一般的ですが、プレースホルダーに NULL 以外の値を代入する可能性がある場合「<=>」を使って比較します。

MySQLの部分一致は「%」を使いますが、$user_name 自体に「%」や「_」というメタ文字が含まれている可能性を考慮してエスケープしておく必要があります。
「John_1」のような値が渡された場合「John#_1」のような形になるように置換します。この場合の「#_」はメタ文字ではない「_」であることを意味します。
メタ文字の前に何の文字を付けてエスケープするかは「LIKE :name ESCAPE ‘#’」とあるように ESCAPE をつかって決めることが出来ます。
サンプルではシャープ記号を使いましたが他の記号でも構いません。

[PHP]同じ名前のファイルが存在するときは番号(連番)を付ける

ファイルを保存する際、すでに同名のファイルがある場合、上書きを防ぐために「test_1.txt」のように、
番号を付加して保存するケースはよくあると思います。
この時、すでに「test_1.txt」が存在する可能性も考えられるので、その場合は「test_2.txt」として
数字を増やしておく必要があります。
このようなケースではファイル名の重複を再帰的に調べることになるので、下記サンプルのような再帰関数を利用します。

<?php
$filepath = unique_filename(dirname(__FILE__). '/' . 'test.txt');
file_put_contents($filepath, "sample");
echo $filepath;


function unique_filename($org_path, $num=0){
	
	if( $num > 0){
		$info = pathinfo($org_path);
		$path = $info['dirname'] . "/" . $info['filename'] . "_" . $num;
		if(isset($info['extension'])) $path .= "." . $info['extension'];
	} else {
		$path = $org_path;
	}
	
	if(file_exists($path)){
		$num++;
		return unique_filename($org_path, $num);
	} else {
		return $path;
	}
}

例ではテキストファイル「test.txt」を保存しようとしています。
同名のファイルがなければそのままのファイル名で保存し、 存在している場合は「test_1.txt」「test_2.txt」のように番号を加えたファイルパスを返します。