ありがとう。また会おう。

ゆるいかんじで。かたのちからぬいて。やってます。

defined()はクラス定数もチェックできる

define()で定義する(グローバルな)定数が、定義されているか否かをチェックするdefined()関数
http://php.net/manual/ja/function.defined.php
ですが、この関数、実はクラス定数に対しても使えます。
(Version 5.4.17にて確認)

<?php
class Hoge
{
    const FUGA = 1;
}

var_dump('Hoge::FUGA'); // 出力bool(true)
var_dump('Hoge::BAR'); // 出力bool(false)

定数の値を返す constant()関数
http://www.php.net/manual/ja/function.constant.php
には、

この関数は クラス定数に対しても動作します。

って明記されてるんですけどね。
なぜがdefined()の方には書いてない。

DateTimeのインスタンス達はmax(), min(), sort()できる

PHPerのみなさん、DateTimeクラス使ってますか?
意外とまだPHP4由来の関数(date()、strtotime()など)使っているケースもよくみかけるのですが
DateTimeクラス使えばいろいろスマートにプログラム書けるし、例外処理もできる*1んで
これからはこっちを使うようにしましょうよ、と思うのです*2

で、これも意外とまだ知らない人多いようなので触れておくと
PHP5.2.2以降、DateTimeクラスのインスタンス同士を、比較演算子で直接比較できるようになりました。
http://www.php.net/manual/ja/datetime.diff.php
の例2のところに記載されています。

<?php
$date1 = new DateTime("now");
$date2 = new DateTime("tomorrow");

var_dump($date1 == $date2);  //false
var_dump($date1 < $date2);     //true
var_dump($date1 > $date2);     //false

日付の比較って、Webアプリケーション書いていると大抵必要になるシチュエーションだと思うので
この書き方ができることを知ってると、読みやすくスマートにコード書けるんじゃないかなと。

で、さらにこの「DateTimeクラスのインスタンス同士は比較演算子で直接比較できる」ことの帰結として
2つのインスタンスを比較できるということは、もっと多数のインスタンスがあれば、それらも比較できる。
つまり、max(), min(), sort() といった関数にDateTimeクラスのインスタンス(の配列)を渡すこともできます。

<?php
$date1 = new DateTime;
$date2 = new DateTime('Yesterday');
$date3 = new DateTime('Tomorrow');
$dateList = array($date1, $date2, $date3);

//max()
var_dump(max($date1, $date2, $date3)); 
var_dump(max($dateList));
//min()
var_dump(min($date1, $date2, $date3));
var_dump(min($dateList));
//sort()
sort($dateList);
var_dump($dateList);

実行結果

object(DateTime)#3 (3) {
  ["date"]=>
  string(19) "2013-10-22 00:00:00"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(10) "Asia/Tokyo"
}
 
object(DateTime)#3 (3) {
  ["date"]=>
  string(19) "2013-10-22 00:00:00"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(10) "Asia/Tokyo"
}

object(DateTime)#2 (3) {
  ["date"]=>
  string(19) "2013-10-20 00:00:00"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(10) "Asia/Tokyo"
}

object(DateTime)#2 (3) {
  ["date"]=>
  string(19) "2013-10-20 00:00:00"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(10) "Asia/Tokyo"
}

array(3) {
  [0]=>
  object(DateTime)#2 (3) {
    ["date"]=>
    string(19) "2013-10-20 00:00:00"
    ["timezone_type"]=>
    int(3)
    ["timezone"]=>
    string(10) "Asia/Tokyo"
  }
  [1]=>
  object(DateTime)#1 (3) {
    ["date"]=>
    string(19) "2013-10-21 01:01:03"
    ["timezone_type"]=>
    int(3)
    ["timezone"]=>
    string(10) "Asia/Tokyo"
  }
  [2]=>
  object(DateTime)#3 (3) {
    ["date"]=>
    string(19) "2013-10-22 00:00:00"
    ["timezone_type"]=>
    int(3)
    ["timezone"]=>
    string(10) "Asia/Tokyo"
  }
}

max()、min()とかは、例えばいくつかの日時候補があって、そのうちもっとも現在日時に近いものを採用したい、というケースで使えます。
ソシャゲでいうと、レイドボスの討伐期限が普通はエンカウントして2時間後なんだけど、イベント終了やメンテナンスがある場合は
その日時と比較して、もっとも現在日時に近いものを討伐期限として採用する、みたいなことができます。

*1:例えば、strtotime('hoge')のように日付書式として正しくない引数が与えられた場合、strtotime()はfalseを返します。DateTimeのコンストラクタはExceptionをthrowします。

*2:PHP5.5からは、インスタンス生成時以外値を変更できない(代わりに新しいいインスタンスを生成して返す)DateTimeImmutableも使えます。個人的にはこちらの方がお勧めです。

xdebug の各種設定がどのコンテキストで変更できるかを調べる

NetBeans + xdebug + Chromexdebug拡張という環境がなかなか良い感じです。
この環境さえ構築できれば、もうvar_dump()要らず\(^o^)/

で、この環境を作る上でひとつハマったことがあったのでメモ。

最初、xdebugでリモートデバッグできるようにini_set()でスクリプトから設定変更*1してみたものの、うんともすんとも言わず。

で、そういえば、xdebugの設定って、それぞれどのコンテキストで指定できるんだっけ?と思い当たり。
公式サイトのドキュメントを当たってみるも、それらしき記述は無し。。。
結構詳しく書いてあるんですけどね、xdebugのドキュメント。英語だけど。

で、結局行き着いたのが、ソースコード

おおむね、PHP_INI_ALL が多いんですが、いくつかの設定値は異なるものがあります。

PHP_INI_SYSTEM|PHP_INI_PERDIR のもの

PHP_INI_SYSTEM のもの

リモートデバッグで僕がはまったのはxdebug.remote_enable でした。
まあ、実現する機能のことを考えたらそりゃそうか、という結論だったわけですが。

*1:正確には、Zend_Applicationを使ってるので、設定自体はZendのiniファイルに書いてますが、実質ini_set()で定義してるのと同じ

ArrayAccessを実装したインスタンスとarray_key_exists()

自分メモ。
ArrayAccessを実装したクラスのインスタンス

<?php
//$objは instanceof ArrayAccess
array_key_exists('key', $obj);

は使えない。

<?php
isset($obj['key']);

を使うこと。


おそらく、配列のキーと、オブジェクトのプロパティの違いで動かないんだろうね。
ちょっと不便な気もするけど。
まぁarray_key_exists()自体、isset()で代用効くのであまり使わないからいいか。
array_key_exists()だけじゃなく、他の「キーを引数に指定する」ような配列関数もだめかもね。試してないけど。


ちなみになんでこれに気づいたかというと、DoctrineのDoctrine_Recordクラスのインスタンスを扱っていて。
Doctrine_Recordのインスタンスのまま使用するときと、連想配列にhydrateする時を
透過的に扱おうとして、array_key_exists()の結果が違ってびびったのでありました。

SOAP関数を使う時の注意点

ハマったので備忘録。


PHP標準SOAPライブラリを使う場合、
接続先や利用できるメソッドなどを記述してあるWSDLファイルをキャッシュする機能がある。
http://jp.php.net/manual/ja/soap.configuration.php


この「soap.wsdl_cache_enabled」ディレクティブの値が、デフォルトOnなので、開発時には注意が必要。
WSDLファイルを切り替える時にキャッシュが効いていて、古いWSDLファイルを読み込んだままになっている可能性がある。
ちなみにこのキャッシュファイルの格納先は
「soap.wsdl_cache_dir」(デフォルトは /tmp)
キャッシュ有効期限は「soap.wsdl_cache_ttl」(デフォルトは86400秒=1日)


詳しくは大人の事情で書きませんが、auの携帯公式サイト案件で
「まとめてなんちゃら」のところで
WSDLがキャッシュ読んでるのを気づかずに、動かねぇ、動かねぇ、って頭を悩ませておりました。。。


本番稼働入ったら、キャッシュOnの方がもちろんいいんでしょうけどね。
開発中とか、移行期間中とかはOffの方が変なことに悩まされずに済みますよ、というお話。

SELECT文で rowCount() が使えるか?

PDOではなく、各DB固有の関数だと、「SELECT結果の件数」を返す関数として、

があるんですが、PDOの場合、それっぽいメソッドが rowCount()くらいしかないのですが。PHPマニュアルによると。

PDOStatement::rowCount() は 相当する PDOStatement オブジェクトによって実行された 直近の DELETE, INSERT, UPDATE 文によって作用した行数を返します。 関連する PDOStatement によって実行された直近の SQL ステートメントが SELECT 文の場合、いくつかのデータベースは文によって返された 行数を返すかも知れません。しかしながら、 この振る舞いは全てのデータベースで保証されていません。 さまざまな場所で使用するアプリケーションでは、 これに頼ってはいけません。

つまり、このメソッドは、**_num_rows()のPDO版ではなく、**_affected_rows()のPDO版って位置づけなんですね。。。

で、その代替策として

ほとんどのデータベースでは、PDOStatement::rowCount() は SELECT 文によって作用した行数を返しません。代わりに、 PDO::query() を使って 意図する SELECT 文として同様の述部を持つ SELECT COUNT(*) 文を発行し、PDOStatement::fetchColumn() を使って返される行数を取得することができます。

と記述があるんですが・・・
いやね、それはわかってるの。
でも、今まで1回で問い合わせ済んでたのが、PDOにするから2回クエリ投げろってのはあんまりでないかい?


ってことで、最初の引用にある「SELECT 文の場合、いくつかのデータベースは文によって返された 行数を返すかも知れません。」てことなので、具体的によく使うMySQLPostgreSQLはどうなのよ?ってとこを調べてみました。


ちなみに実験に使用したコードはこんな感じ。

<?php
// DB_CONNECT_STR 定数には、それぞれのDBで必要な接続情報が定義されているとして読んで下さい。
try {
    $db = new PDO(DB_CONNECT_STR);    
    $stmt = $db->query('SELECT * FROM hoge_table ');
    var_dump($stmt->rowCount());
 } catch (PDOException $e){
    die($e->getMessage());
 }

hoge_tableには、テストデータを3件ほど突っ込みました。

PostgreSQLの場合

実行結果:

int(3)

おお!返ってきましたよ!
ちなみに使用したバージョンは、PHPが5.1.6。PostgreSQLが8.1。

MySQLの場合:その1

実行結果:

int(0)

う〜ん・・・どうもダメっぽい・・・。
使用したバージョンは、PHP5.1.6、MySQL5.0。


ところが・・・別の環境で試したところ・・・

MySQLの場合:その2

実行結果:

int(3)

あれ?今度は返ってくるぞ?
プログラムは(DB接続文字列以外は)変えてないのに?
使用したバージョンは、PHP5.2.6、MySQL5.0。


さて、この差異はなんなのか?

google先生にいろんな角度(単語)からあ〜でもない、こ〜でもないと聞きまくり、どうやらそれっぽい情報を発見。
PHP5.2.1のChangeLog

  • PDO_MySQL Extension Improvements (Ilia)
    • Enabled buffered queries by default.

そういえば、(PDOでない)MySQL関数の mysql_num_rows()関数のヘルプにも

注意: mysql_unbuffered_query() を使用した場合、 結果セットのすべての行を取得するまで mysql_num_rows() は正しい値を返しません。

ってある。
どうやら、「バッファードクエリ」がポイントのようだ。

MySQLの場合:その3

で、よくよく調べてみると、PDOのMySQL用のオプション設定で、このバッファードクエリを使用するか否かを設定できるらしい。
そこで、先の実験プログラムをちょっとだけ修正。

<?php
// DB_CONNECT_STR 定数には、それぞれのDBで必要な接続情報が定義されているとして読んで下さい。
try {
    $db = new PDO(DB_CONNECT_STR);
    $db->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true); //←この行追加
    $stmt = $db->query('SELECT * FROM hoge_table ');
    var_dump($stmt->rowCount());
 } catch (PDOException $e){
    die($e->getMessage());
 }

追加した行にある、「PDO::MYSQL_ATTR_USE_BUFFERED_QUERY」は詳しくはこちらを:
http://www.php.net/manual/ja/ref.pdo-mysql.php

で、先ほどのPHPバージョン違いの2つの環境(PHP5.1.6/PHP5.2.6)で試したところ・・・

int(3)

よし!両方返ってきた!

結論

2つのDBでしか試してませんが、

  • PostgreSQLならrowCount()メソッドでSELECT文の結果件数が取得できる
  • MySQLは、PHP5.2.1以降なら*1、rowCount()メソッドでSELECT文の結果件数が取得できる
    • それ以前の場合は、PDO::MYSQL_ATTR_USE_BUFFERED_QUERY を true にセットすれば取得可能

ということに。
他のDBは手元に環境がないので、わかりません。

あとは、「DBによっては動かないかもしれない」という、このメソッドを使うかどうかは、そのプロジェクト毎の判断でいいのではないでしょうか。
マニュアルには「どのDBでも動作しなければならない製品の場合は、このメソッドではなく、SELECT count(*)を使え」ってあるんですが、使用するDBが限定できるのであれば、使うという選択もありな気がします。プロジェクト責任者の判断で。
rowCount()使った方が、確実にクエリ投げる回数減りますし、ソースコード的にも読みやすいし。

新たな疑問

PDO::MYSQL_ATTR_USE_BUFFERED_QUERYについて、PHPマニュアルには

PDOStatement でこの属性を TRUE に設定すると、 MySQL ドライバはバッファ版の MySQL API を使用します。 移植性の高いコードを書くには、代わりに PDOStatement::fetchAll() を使用すべきです。

とあるのだが、これって実際のところどうなんだろう?
「移植性の高いコードを書く」ことと「PDOStatement::fetchAll() を使用する」ことが自分の中で繋がらないのだが・・・?

職場の同僚がこの記述を見て、
「PDOはPDOStatement::fetch()は非推奨で、PDOStatement::fetchAll()を使わなければいけない」
と思ってたようなんだけど・・・そうではない気がするんだけど・・・

もし万一そうだったら、PDOStatement::fetch()のページに「この関数は非推奨です」って書いてあるべきだよなぁ。。。


この辺がまだもやもやしています。。。

*1:厳密にはこのバージョンの境界で実験してないんだけど、ChangeLogの記述と、今回の実験で変更している属性の意味を考えると、ほぼこれと断定していいのではないかと。間違ってたらツッコミ入れて下さい。

updated_at「だけ」を更新したい場合

地味だけどハマったのでメモ。


symfonyで、というより正確にはPropelで、
updated_atカラムを含むモデルを扱う場合。
この名称のカラムを作っておけば、他に特になにも設定しなくても、レコードを更新かけた時に、更新日時を自動的に保存してくれるので、便利なのですが。


今回ハマったのが、「更新日時【だけ】を更新したい」というケース。
もうちょっと正確には、別のカラムのデータも更新かけるんだけど、
それがたまたま更新前と同じデータになる時があって、
その時でも、更新日時は最新にしたいと。


具体的には、こんな感じ:

<?php
    $foo = mt_rand(1,4);
    $hoge = HogePeer::retrieveByPk(1);
    $hoge->setFoo($foo);
    $hoge->save();

1行目で、1〜4の間で乱数を取得。
そうすると、当然以前保存してた値と同じになることがあるんだけど
その場合でも更新日時だけはアップデートしたいんですわ。


ところがこの場合、Propelは、まぁ賢いっちゃあ賢いんだけど
「オブジェクトのプロパティが何も変わってないんで、UPDATE発行必要なし」と判断しちゃうようで。
更新日時が変わらない・・・というかUPDATE文自体発行しない。。。


で、どうしようかなぁ、DELETEしてINSERTしてもいいけど、なんか格好悪いしなぁ・・・と思い、Propelが生成するBaseHogeクラスのソースを眺め。
こうするのがとりあえずよさそうだ。

<?php
    $foo = mt_rand(1,4);
    $hoge = HogePeer::retrieveByPk(1);
    $hoge->setFoo($foo);
    $hoge->setUpdatedAt('now'); //明示的にupdated_atカラムを更新
    $hoge->save();

updated_atカラムを明示的にsetしたらうまくいった。
setUpdatedAt()メソッドの引数は、strtotime()関数が解釈できる文字列ならOK。


ほんとは、プロパティが変わってなくても、強制的にUPDATE文を発行させる方法がほかにあればいいんだけど、
BaseObjectクラスまで遡ってみたけど、使えそうなメソッドが見あたらなかったので、これがいいのかな。。。うん。