PHP – pdoのprepareでLIKEのサンプル

LIKEを使って検索したい。検索するワードは自由にユーザーに入力させたい。結論からいえば以下が答えである。

<?php
$query = '%' . $query . '%';
$stmt = $this->pdo->prepare("SELECT * FROM test WHERE name LIKE :query;");
$stmt->bindParam(':query', $query, PDO::PARAM_STR);

これは違う

"SELECT * FROM test WHERE name LIKE '%:query%';

これも違う

$stmt = $this->pdo->prepare("SELECT * FROM test WHERE name LIKE :query;");
$stmt->bindParam(':query', '%' . $query . '%', PDO::PARAM_STR);

一番最初に見せた例が答えだな

PHP – array_mapで連想配列を扱う

PHPのarray_mapで連想配列を使う

<?php
$array = [
'hoge1' => 'value1',
'hoge2' => 'value2'
];
$result = array_map(function($key, $value) {
return 'key:' . $key . ', value:' . $value;
}, array_keys($array), array_values($array));
print_r($result);

これはPHP Manualの下の方のコメント欄を参考にした。なんでも配列は複数渡せるらしいが、その2つの配列の要素数は同値でなければいけないらしく、少ない方は多き方に対して空要素で拡張されるんだそうだ。

PHP: array_map - Manual

MySQL – auto_incrementをしつつテーブルの中身を全て消す

新しいシステム構築時はDBに対して何度もINSERTしたりとかする。DB側のリセットも頻繁に行うが空にするだけでも十分なときがあるんだけど、面倒くさいからいつもDROP TABLEしてCREATE TABLEしちゃうんだけど、ちゃんと使うべきだという自戒を込めて。

TRUNCATE TABLE tablename;

DELETEでも同様の結果を得られるが

DELETE FROM tablename WHERE 1 = 1;

auto_incrementがリセットされないという違いがあるぞ

PHP – pdoでselectするサンプル

pdoでSELECTしてみた。途中エラーが出たからできないんじゃないかと思ったけど単なる構文ミスだった。

<?php
$pdo = new PDO();
$stmt = $pdo->prepare('SELECT * FROM `post` WHERE `category_id`=:category_id;');
$stmt->bindParam(':category_id', $category_id, PDO::PARAM_INT);
if ($stmt->execute()) {
$result = $stmt->fetchAll();
foreach($result as $post) {
echo $post['title'];
}
}

prepareを使うメリットについてPHP Manualに書いてあった。

クエリのパース (あるいは準備) が必要なのは最初の一回だけで、 同じパラメータ (あるいは別のパラメータ) を指定して何度でも クエリを実行することができます。クエリを実行するには、準備として クエリの解析やコンパイル、そして実行プランの最適化が行われます。 クエリが複雑になると、この処理には時間がかかるようになります。 同じクエリを異なったパラメータで何度も実行すると、アプリケーションの 動作は目に見えて遅くなるでしょう。 プリペアドステートメントを使用すると、この 解析/コンパイル/最適化 の繰り返しを避けることができます。 端的に言うと、プリペアドステートメントは使用するリソースが少なくいため 高速に動作するということです。
プリペアドステートメントに渡すパラメータは、引用符で括る必要は ありません。それはドライバが自動的に行います。 アプリケーションで明示的にプリペアドステートメントを使用するように すれば、SQL インジェクションは決して発生しません (しかし、もし信頼できない入力をもとにクエリの他の部分を構築している のならば、その部分に対するリスクを負うことになります)。

PHP: プリペアドステートメントおよびストアドプロシージャ - Manual

つまり

ということだった。せっかくなのでいろんなパターンでこれからテストしてみよう。あと、SELECT * FROM hoge*で指定しているけど、ワイルドカードは基本使わないで、本番では必要最小限を指定しよう。全てのカラムが必要だったとしても、できれば都度指定するほうが良さ気だと思ってるけど、まあ結果が一緒ならワイルドカードを使うほうがナウいのかもしれない。

PHP – pdoでMySQLへの変更をrollbackしたりするトランザクション

フレームワークに頼りきりだったので試しにPDOでトランザクションを書いてみた。全体的に動作させられるものを書いた方が誰かのためになりそうだったので、最小限のDB操作クラスを丸ごと作ってみたが、どっかおかしいところあれば教えてくだしあ。

まず以下のDatabaseクラスには

の3つの簡易的なものがくっついてる。

<?php
class Database
{
public $pdo;
public function __construct()
{
$with = function($s) { return $s; };
try {
$this->pdo = new PDO("mysql:host={$with(DB_HOST)};dbname={$with(DB_NAME)};charset={$with(DB_CHAR)}", DB_USER, DB_PASS, [PDO::ATTR_EMULATE_PREPARES => false]);
} catch (PDOException $e) {
exit('Database Error');
}
}
function transaction($callback) {
$this->pdo->beginTransaction();
try {
$callback();
$this->pdo->commit();
} catch (OriginalException $e) {
$this->pdo->rollBack();
throw $e;
}
}
function create_test($data)
{
extract($data, EXTR_SKIP);
$stmt = $this->pdo->prepare('INSERT INTO `table_test` (`name`, `age`) VALUES (:name, :age);');
$stmt->bindParam(':name', $name, PDO::PARAM_STR);
$stmt->bindParam(':age', $age, PDO::PARAM_INT);
if ($stmt->execute()) {
return array_merge($data, ['id' => $this->pdo->lastInsertId()]);
} else {
throw new OriginalException();
}
}
}
# 本当はファイル分けたいけど今回は検証なのでここに置く!
class OriginalException extends Exception {}

このDatabaseクラスを使うと以下のようなコードでMySQLへのINSERTができる

<?php
$db = new Database();
$db->create_test(['name' => 'hoge1', 'age' => 10]);

そしてさっそくDatabaseクラスに作ったトランザクションを使ってみた例がこれ

<?php
try {
$db = new Database();
$db->transaction(function() use ($db) {
$db->create_test(['name' => 'hoge1', 'age' => 10]);
$db->create_test(['name' => 'hoge2', 'age' => 20]);
$db->create_test(['name' => 'hoge3', 'age' => 30]);
});
} catch (OriginalException $e) {
echo "失敗だったみたいなのでロールバックしてとりあえず停止";
exit;
}

3つの書き込みを行っているが、どれか一つで書き込みに失敗すれば、全体の書き込みはcommitされずroolbackされる。実際にrollbackされるのか確かめたければ特定のカラムの文字列を欠落させたINSERTを一つ実行してみれば確認できる。

便利なところはトランザクションでラップしている感じにしているところで、それぞれのcreate_test()が失敗すればExceptionを返すようになっているので、transaction()はExceptionを監視して問題なければcommit、Exceptionが一つでも発生すれば処理を中断してrallbackしてくれるという仕組みになっている。これによって一つずつの実行結果を受け取って都度rollback処理を書いたりする必要がない。

ちなみにPDO::beginTransactionなんだが元々がオートコミットモードなんだけど、これを使えばオフにできるんだそうだ。

オートコミットモードをオフにします。オートコミットモードがオフの間、 PDO オブジェクトを通じてデータベースに加えた変更は PDO::commit() をコールするまでコミットされません。 PDO::rollback() をコールすると、 データベースへの全ての変更をロールバックし、 オートコミットモードに設定された接続を返します。

PHP: PDO::beginTransaction - Manual

Macの標準PHPにimageftbboxが無い

Macには標準でPHPとかRubyとか入ってるんだけどimagettftext()が入っていなくて使えない

PHP Fatal error: Call to undefined function imagettftext()

結局たどり着いたのがPHP for OS X as binary packageを使ってPHPを入れること。どうせPHP5.6で開発したかったからこれで入れて対応した。liip.chので入るPHPにはいろいろ入っているので楽。

curl -s http://php-osx.liip.ch/install.sh | bash -s 5.6

私の環境ではこれでインストールしても新しいPHPは使えなかったので以下でパスを通した。

export PATH=/usr/local/php5/bin:$PATH

ここまできたらそろそろ開発用のdockerイメージでも作ればいいんだけど。。面倒くさいけど今度やるかー。。でもそれやっちゃうと絶対dockerコンテナでアプリ公開したくなっちゃって、インフラに費用掛かっちゃうだろうなw むしろそっち側に時間使っちゃってアプリケーションの構築できなくなりそw

PHP – session_regenerate_id()でSessionが切れる

フレームワーク使用禁止でな、生PHPでSessionを扱ってsession_regenerate_id()でsessionIdをリクエスト毎に変更していたところSessionが切れる事があった。

確かに同時にsession_regenerate_id()が実行され、それがまさかのデータ引き継ぎ中だと、最終的に採用されるSessionの中身は空になってしまうわけだな。なのでsession_regenerate_id()を必ずしも同時に実行させないスクリプトがこんな感じになる。10分の1の確率で動作するので同時に2度リクエストされても、よっぽど同時実行されない。あんまり確率下げるとそもそもsession_regenerate_id()が実行されないなんてことにもなるからこの辺りは調整が必要だな。

<?php
session_start();
if(mt_rand(1, 10) === 1) {
session_regenerate_id(true);
}

このコードは以下を参考

scrap.php.xdomain.jp

ファイルでSessionを扱う場合はsess_から始まるファイルが生成されたりとかして、そのコストを削減したいって時は有効期限をつけるといい。参考にさせてもらったサイトのいくつかで有効期限チェックをやっていたので参考にしてみた。

<?php
session_start();
if (!array_key_exists('expires', $_SESSION)) {
$_SESSION['expires'] = time();
}
if(mt_rand(1, 10) === 1) {
if($_SESSION['expires'] < time() - 300) {
session_regenerate_id(true);
}
}

このコードは以下を参考

xirasaya.com

確かにリクエスト毎にファイル生成してるときついかな。でもセキュリティレベルを下げる事になるから、負荷が増えたらサーバーをスケールアップするか、Redisとか使ってSessionをメモリに乗せるとかしてスケールアウトしてもいいのかもなあ。。こういうのベンチマークとってみたいね。実は全体の負荷の大部分を占めてたりしてw

PHPの某フレームワークのコード読んでみたらユーザー認証時に一度だけSessionId変えているだけで常に変更していなかったりしてたけど、毎度変えるオプションが付いてるらしいので、どこで変えてるんだか。自前の処理でも作っているんだろうかね。

話それるけどフォームで画像認証をするときに、表示された画像内の文字列がSessionに入っている状態で別ページでsession_regenerate_id()が実行されると画像内文字列消えちゃうんじゃないかと一瞬思ったが、画像内文字列もちゃんと引き継がれるし、次のPOSTリクエスト時には新しいSessionIdでリクエストするんだから何も問題なかったな。少し混乱したw

Java – 配列で重複した値を削除する

Rubyにはuniqがある、他言語にはないけどjQueryとかにもある。Javaではどうするのかと調べたら特にないみたいなんだけど、その前にそもそも重複が入らないようにできるSet型というのがあるみたいだ。

Set set = new HashSet();
set.add("a");
set.add("b");
set.add("c");
set.add("c");
System.out.println(set);

結果は

[a, b, c]

となるのでこれを使おう

IntelliJ – プロジェクトのJavaのバージョンを切り替える

Javaのバージョンの切り替えについて調べてみたら大きくわけてこんな感じだった

IntelliJのデフォルト

まずIntelliJのデフォルトのJDKのやつはここで切り替えられるらしい

stackoverflow.com

現在のInnteliJだとPreferences -> Build, Execution, Deployment -> Compiler -> Java Compiler -> Project bytecode version -> 1.7で変更できるようだ。Project bytecode versionってのはIntelliJ全体のデフォルトの設定なのかな?

コンパイル時のバージョン指定

またよくわかってないんだけど、javacではsourcetargetっていうのでコンパイルする時にバージョンを指定できるみたいなんだ。で、コンパイル時のその設定については、例えばGradleを使っているならbuild.gradleに以下のように書いて設定できるようだ。

apply plugin: 'java'
sourceCompatibility = '1.6' // -source
targetCompatibility = '1.6' // -target

qiita.com

デバッグ実行時のバージョン指定

現在のプロジェクトのデバッグ時のRunner?みたいな奴のバージョンはFile -> Project Structure -> Project SDKから指定したもので設定される。よくわかんないことが多いんだけど、とりあえずデフォルト値みたいな奴以外は統一しておけばいいかな。ちょっと違ったら誰か教えてください。。