Home > PHP

PHP Archive

書評「PHP逆引きレシピ」

  • 2009-08-10 (月)
  • PHP

お久しぶりです。
もうすっかり夏ですね。僕といえばバリ島で結婚式を挙げてきました。楽しかったです。
これのおかげで割りとハードな毎日を送っているためブログの更新などは余りできませんが、忘れないであげて下さい。

さて、著者の鈴木憲治様よりPHP逆引きレシピをご献本頂きました。ありがとうございます。

以下書評。

「直接的な関数の組み合わせでプログラムを”組み立てる”」という PHP のスタンスにおいて最も重要なのは「目的に応じた答えを知る事」であると言えます。そのような視点から見ると本書はこれ以上ないほどに”実践的”です。
目的に対する方法を引く事ができ、その方法に関連する他の方法に対してもインデクシングされているばかりか、PHP バージョンごとの差異やメジャーなレンタルサーバに依存する問題なども詳解されているため、本書とパソコンと「ウェブアプリを作りたい」という目的意識さえあれば作れてしまう程です。

それだけに留まらずセキュリティに関しての広範な対策方法や、PHP で書かれたオープンソースソフトウェアの基本的な利用法も解説されており、さらには PDF、画像、グラフなどの生成方法といった「PHP をベースとしたシステムの利用」についても言及されているため、これはもう「答えのフルコース」だと言えるでしょう。

しかしながら残念な点もいくつかあります。
@ によるエラー抑制に始まり、参照渡しを「混乱しやすいから」という乱暴な理由で推奨していなかったり === ではなく == での評価がほとんどだったりと、初心者向けだからこそ肌感覚で培って欲しい「PHP のだめなところ」に関する言及がとても少なく、本書だけで学べてしまうが故に、本書だけを参照してコーディングしていると早い段階で「あれ?なんでこうなるの?」と戸惑う場面に出くわすでしょう。
特に、セオリーとされている mb_strlen() や htmlspecialchars() に文字コードを渡す事が書かれていない点は大きな穴です。

しかし本書は数多く出版されている PHP 本の中でも「決定版」と言えるほど完成しており、PHP での開発を生業とする企業ならば新入社員用の教科書として必携の一冊であると言えるでしょう。

と、まとめるとこんな感じです。
折角なので残念なところの補足をいくつか。

@によるエラー抑制

これは大変まずい事です。自己満足的なプログラムならば良いとしても、割と実践的な事が書かれているだけにビジネス用途で作ったようなプログラムでこれを使ってはいけません。
@でエラー抑制すると遅くなるとか、そんな事はどうでもいいんです。エラー抑制する代わりにハンドリングすれば似たような負荷がかかる事が多いですから。それよりも「@でエラー抑制をしているとエラーが表示されなくなる」というところにこそ問題があります。殴られても痛くないみたいな。出さないのがいいものはエラーであってエラーメッセージではないんです。

もし@で抑制している箇所に起因するバグがあった場合、追いかけるのは結構面倒くさいです。
「いくつかあたりをつけて探してみたけどそれっぽいところはない。じゃあちょっと@消してみようか」みたいなのはバッチリ二度手間です。
そんな事するくらいなら最初から@で抑制するんじゃなくて条件式なりなんなりでエラーのハンドリングをするべきです。

=== じゃなくて == を多用している

これもよくありません。基本的に == を使うべきではありません。== で評価するくらいなら型キャストしてしまって if( (int) $a === 5 ) というように書いた方がマシです。これと同じ理由で if( $a ) と言うような評価もよくありません。== を使わないよう癖つけるくらいでちょうどいいと思います。
評価する変数の型によって結果が違う評価式なんてのは気難しい人に様子を伺うようなものです。逆に == でしか評価できないような実装はそもそも設計に問題があります。

mb_strlen() や htmlspecialchars() に文字コードを渡す事

よく「PHP の mbstring の設定はこうした方がいい」なんつって mbstring.internal_encoding = utf-8 みたいな事を書いているような人がいますが、これも合理的じゃありません。そもそもプログラムごとに文字コードが違うし、リクエストに含まれる文字の文字コードだってバラバラです。それなのに PHP の設定としてサーバソフトウェア側で文字コードを制御するのは考慮しなければいけない点を増やすだけで全く持って非効率的です。そもそも文字コードを扱うレイヤーじゃないです。
大体 web アプリを作るときにはアプリで扱う文字コードを決めて、その文字コード以外の文字コードで送られてきたリクエストははじくのが前提ですから、そのアプリで解釈すべき文字コードは(原則的には)1種類です。
htmlspecialchars() のシノニムで h() を作るのも今ではセオリーで本書にも書かれていますが、第二引数だけではなく第三引数まで書かないと意味がありません。

<?php
define( 'SITE_ENCODE', 'utf-8' );
h( $var ){
  return htmlspecialchars( $var, ENT_QUOTES, SITE_ENCODE );
}
?>

みたいな。

大体 utf-8 な文字を mb_strlen() 辺りで図ろうとしても文字コード指定してるか mbstring.internal_encoding で utf-8 が指定されていないと正しい結果を返しません。mbstring.internal_encoding は前述の通り文字コードを制御するようなレイヤーにある設定ではないので、消去法的に考えるとマルチバイト系の文字列関数にはきちんと文字コードも渡してあげるのがモアベターです。

でも結局は全部 PHP の設計に起因する問題なんですよね。
これらのつかみにくい設計に関するノウハウまで求めるのは酷かもしれませんが、web デザイナーが css ハックを必要とするようにこういった情報も必要なんじゃないんでしょうか。

ここらへんの情報が無い以外は本当に素晴らしい書籍です。
僕みたいに受託で開発やってるような人間はクライアントから「社内でエンジニアを育てたい」的なお話をよく聞きますが、本書はそこらへんのニーズをバッチシ抑えてます。
逆引きリファレンスですが関数依存の構成ではなくユースケースに依存しているため、明確な目的意識が無くてもノウハウの蓄積に役立つとも思います。

以上。

PHP 勉強会にいってきました

僕は昨年起業し、基本的に今までお世話になったクライアントからのご紹介を経て新たなクライアントと出会っているわけなのですが、いかんせん組織に属してウェブに関する仕事をこなしてきた訳ではないのでバックボーンが薄っぺらいのが弱点だと自分では理解しています。つまり、IT ビジネスにおけるスタンダードな論理的思考が培われてこなかった点にコンプレックスを抱えているわけです。
そんなコンプレックスを”客観視”を通じてクリアにしていこうと言う気持ちと、ビジネス的な意味でコネクションを張れたらいいなと言う浅ましい気持ちを薄っぺらなかばんに詰めて昨日は PHP 勉強会に行ってきました。

会場に着くと自己紹介が始まったばかりで、もう大分席も埋まっていました。会社名だしてやってる事を話してしまうとそれだけでライトニングトークになってしまいそうだったのでここのブログのドメインと個人で受託開発してますと軽く挨拶をしました。懇親会ではどうかわかりませんが勉強会内で参加者同士の対話が無いのに自己紹介する合理的な理由が見つかりませんでした。他の参加者の方々の自己紹介はどちらかと言うと「所属企業+業務内容+興味のある事柄+個人的に出力している事柄」と言ったような内容。若干セミナーっぽい雰囲気ですね。

僕の心構えが所謂”勉強会”対するものであったがためにこの辺りから違和感を感じてしまっていたのですが、PHP 勉強会は即ち”小さなセミナー”であると同時に、勉強会そのもののベクトルも聴衆ではなく発表者に対してのものであると結論付けるとこの違和感は消えました。
「40人を超える参加者が一つにテーマに対して同じレイヤーで議論するなんてめちゃくちゃだな」と参加前には期待ageだったわけですが、良くあるカンファレンスと規模だけの差だと理解したわけです。

さて、そんな違和感を覚えたとは言え、発表する方は発表する方なりの論理的思考に基づく判断と、その判断に対して得られる結果と支払う対価が存在しており、プログラムと言うものはすべからく「コストとメリット」のトレードオフだと僕は常々感じています。
プログラムを組んだ事がある人と、組んだ事がない人において一線を画すのもこの辺りの論理的判断力に由来していると考えています。
それだけにまるっきり一人で全部やってきた僕としては、そろそろ自分の論理的判断の客観的な評価が欲しくなるわけです。汚い話ですが他の人の思考プロセスとの対比で自分を見つめなおしたいと言う浅ましい気持ちなわけです。

gusagi さんの発表

そんな風に軽くがっついた状態で始まった勉強会、トップバッターは司会進行も勤められた gusagi さん。
真面目で真摯そうな方です。技術系の集まりであるはずですが、そこそこ大きなカンファレンスの司会進行よりもずっとスムーズでした。

テーマは「XOOPS CubeにCakePHPアプリを組み込んでみた」
gusagi さんはしきりに「グダグダですいません」と申し訳なさそうにされていましたが、これはご自身のプレッシャーを聴衆に対するバイアスに変換しているだけなので余り感心しませんでした。
映画本編を見る前に「余り面白くないかもしれませんが」と前置きされると観客への映り方も違うはずです。

発表の内容は XOOPS Cube という CMS のプラグインとして CakePHP で作ったプログラムを動作させる、と言う目的に対するアプローチ解説だったわけですが・・・
そもそもからして XOOPS Cube 上で CakePHP を動作させるための合理的理由と言う前提が説明されていない状況ですので「Windows で Mac のソフトを動かすためには」と言うような解説を受けているのと感覚的に大差ありませんでした。結局のところ「CakePHP のブートストラップを XOOPS Cube に依存させるんだよ」と言うのが話の大筋でしたが、フレームワークにしろ CMS にしろブートストラップ型のプラットフォームを混在させるには当たり前の話です。
逆に「なぜ XOOPS Cube と CakePHP を混在させる上で発生する管理コストの増大を加味しても混在させるというアプローチを取るにいたったか」と言う思考プロセスから紐解いていったほうが「なぜフレームワークを選ぶのか」を合理的に理解できる糸口になって、発表者と参加者の思考を議論させる良いカンフル剤になったのでは、と思いました。

結局のところプラグインであったりモジュールであったりするものは、先駆者が作ったものを後発者が利用するのが現実です。結局のところ「車輪の再発明の仕方」なだけであって、そこにユニークな設計思想があるならば別ですが、10人いれば10人が同じアプローチを取るであろうケースにおいて「こうやります」と言うのは若干ナンセンスに感じました。

kunit さんの発表

次は kunit さんによる「Propel をあきらめるまえに」という発表。僕と同じでノウハウがぎっしり詰まったビジュアルをされている方です。
こういう人が上司にいると部下は安心して仕事が出来そう、そんな雰囲気です。
発表の内容は「symfony で採用されている OR マッパである Propel を使う事で想定される問題に対する解決アプローチ」だったわけなのですが、微妙にマッチングの難しいテーマであると感じました。こういった問題にぶちあたって”あきらめてしまう”というレイヤーに属している方にとって、Propel という OR マッパはユースケースとしては理解しにくいレベルだったのではないでしょうか。
むしろ PEAR::MDB2 (の Extended モジュール)くらいが妥当な気がしました。まあそれでは「フレームワーク」というテーマにかすりもしなくなってしまうので仕方がないといえばそうなのかもしれませんが・・・
逆に Propel の実装を理解できている人にとってはむしろ”当たり前”の思考プロセスであり「問題にぶち当たったけどまだ解決の途中」と言うような人にのみ有益な発表であったように思えます。
実務的な発表だったので自分の作業を見つめなおす良い機会にはなりましたが、この発表内容はむしろこれが出発点であり、この発表で扱われたアプローチをどれだけプログラム自体に反映させるか、つまり設計的にどう解釈すべきであるかこそが僕にとって興味のあるテーマでした。
「SQL インジェクションに自分で対応しなければならないのか」と言うテーマに対しては僕が漠然とフレームワークに感じている不安が同調しました。
つまり OR マッパに依存しないで自分でクエリを書いた場合のセキュリティかかるコストはどうする、と言うような話です。そもそも OR マッパから入っていなければ自明な論理なのですが、フレームワークを使っての開発が出発地点のような人にとってはこのような事が検討事案になってしまうんだと言う事実は、紛れも無く「フレームワークを使う弊害」だと感じました。
まあ答えとしては「データベースが提供するエスケープ機能を使う」であり平たく言うなら「プリペアドステートメントを使う」と言うことなのですが。
世の中が便利になればなるほど人間は肉体的に退化していくという進化論的な話と同じく、プログラムの原理的な部分に対する理解が薄まるほど設計も退化してしまうのではないかと考えるとおそろしくすらなります。
また、僕の被害妄想かもしれませんが「Eclipse を使ってる非 Java 開発者はスイーツ」みたいな風潮が蔓延している(ように感じられる)昨今において、Eclipse の有用性を説いて頂けた事は非常に素晴らしく感じました。

wozozo の発表

日ごろ #team-one でくだらない事や性的な事しか話しておらず、たまにちょこっと技術的な会話が、本当にたまにある程度だったのでどのような感性を持った人間なのか判断しかねてはいましたが・・・
テーマは「rhaco2 について」
最新情報的なニュアンスの発表でしたが、PHP 勉強会近辺のクラスタに属していないと全く理解できない所謂”身内ネタ”だったので割愛します。
ただ、rhaco2 の実装が面白そうに感じたのでちょっとソースを読もうという気にはなりました。
プレゼンに大きく顔文字だけ表示して「これが」「はーい」「こうで」「はーい」という発表スタイルはエンタの神様を見ているような不思議な感覚でした。

BoBpp さんの発表

ある意味一番論理的な発表だったと思います。
Perl の携帯端末向けサービス開発に特化したフレームワーク「MobaSiF」についてです。
MobaSiF の設計思想に始まり、規模の大きな企業やサービス内での実際の開発・運用スタイルについての言及。
唯一とも言える「高速」と言う設計思想は極めて実務的であり合理的です。その高速という思想をサービスの運用だけではなく、開発スピードにまで持っていく辺りに”成功したウェブサービスを維持し続ける事の大変さ”が滲んでいるとも感じました。
現実問題フレームワークを基準とすることに限界を感じるであろうタイミングは「開発スピードに起因する」と僕自身漠然と感じていました。開発スピードに起因するとは、ウェブサービスが違うという事は文化が違う事であり、文化が違うという事はその空間の制御速度が違う、と言う点に帰結します。日本とインドでは「時間が違う」と感じる事に近い話です。
その制御速度を以ってして開発に当たるわけですから、それぞれのウェブサービスに対して同じフレームワークを採用しているのにフレームワークの実装そのものがウェブサービスごとに固有の物となっていくのは、自然言語における方言に近いものなのかもしれません。
そう考えると「方言を使う事のメリットやデメリット」という視点そのものがナンセンスであるとも考えられます。
ウェブサービスの性質や文化を反映した結果、そのウェブサービスを形作るフレームワークが変化していくと言う思想は、技術者にとっては客観的に不自然な事かも知れませんが、その現場に生きる主観的な立場からすると「そうなる事が自然」である事の反映なのでしょうから「そのウェブサービスに特化し続けた結果」であるとも言えるはずです。
いつかは飯食って糞するだけで金が入ってくるような生活を送ってみたいと考えるに僕にとってかなり勉強になる発表でした。

sotarok さんの発表

GREE 発の国産フレームワーク Ethna の特色や開発スタイルについての発表です。
「Ethna のここが凄い」と言うアプローチでいくつかの特色が解説されましたが、若干分かりにくかったです。
僕の理解力に起因する事なので決して sotarok さんの発表が分かりにくかったのではない、とは思いますが、いかんせん思考プロセスに準じていないフラットな解説だったので、状況を頭に浮かべながら解説の通りに状況をトレースしていきましたがイマイチ強さを伝えきれていないように感じました。
アクションをいくつかのレイヤーに分けることで再利用性を高めるというアプローチは凄く良く思えましたが、そこをフレームワークが担当する合理的理由が明示されなかったのが残念でした。
解説自体もフレームワークを利用するプログラムの設計でフォロー出来そうなケースを用いてだったので、実感として有用性がそこまで感じられなかったのは僕が天邪鬼だったからという点に大きく依存しますが、やはり kunit さんの発表のように「問題ありき」で有用性を実証していない、即ち「この機能以外では解決できないと言う問題ではなかった」と言う点に起因すると感じました。
開発スタイルはとても参考になりました。歴史のある配布物ならではの苦労や”だからこその恩恵”、若干自嘲的にフレームワークごとの利用統計を発表して頂いたのもとても有益でした。

flyfront さんの発表

「TCPDFでお手軽PDF生成」というテーマでした。要するに配布されているライブラリの使い方ですね。
大変申し訳ないのですが普段”聞くだけ”と言うスタイルに慣れていないせいか、この辺りから猛烈な眠気に襲われて余り覚えていません。
OSS で尚且つ日本語が扱える PDF 生成ライブラリは数少なく、あったとしても動かすのは大変なんですよ、という話だったと記憶しています。
ユースケースではないので漠然と操作方法的な話が続いていたのが印象的でした。たぶん自分で使ってみればトレースできる内容だったと思います。
PDF という極めて実務的な表現方法に対して漠然とした操作説明だけを発表されるのは逆に難しいんじゃないかとも思いましたが、どちらにしても主体性のない内容だったので flyfront さん自身の思想が反映されているとは感じられず、それがひいては「内容のなさ」に感じられてしまいました。

k-kishida さんの発表

前日行われた CakePHP 開発合宿の模様を発表されていました。
アラフォーだとか湯けむり事件簿だとか、わりとバズワードが好きな方なんだなと感じました。
僕は解釈のショートカットが余り好きではないのでこの辺りの表現には食傷気味でしたが、内容はわりと面白そうでした。
「面白そうでした」となってしまっているのは発表のほとんどが「合宿楽しかったよ!みんなも行こうよ!」と言う感想で占められてしまっていたため、合宿に行くことでこれだけの成果が得られたと言う客観的な事実に基づいていなかったためです。僕の想像における話ですが、CandyCane が凄い興味深かったので早くソースを読んでみたいと思いました。

すずきさんの発表

最後は勉強会の会場を用意して下さったすずきさんです。
k-kishida さんと同じく先日の合宿に関する報告でしたが、が、その模様を撮影した写真をスライドショーで流しつつ、それぞれの写真にあわせて一言話されていただけでした。
要するに人力 flickr ですね。
「ブログで感想を書くまでが勉強会だ」と仰られていたのが印象的でした。

総評

総評とか書くと上から目線に見えますね。良くも悪くもとても勉強になりました。
やはり勉強会はおでこがくっつくくらいの少人数が限界なんだろうなと強く感じました。人数が多ければ多いほど一つの思想はマイノリティになっていくわけで、距離感が遠くなればなるほど議論の旨みも出しにくいものであり、僕が勉強会に期待するのはその旨みなわけです。
それぞれの方の発表を聞く前には「どんな事を話してくれるんだろう、どんな事を質問しよう」とかなり期待していましたが、操作方法などでは質問も糸瓜もなく「今回の発表にいたった経緯を教えて下さい」とか「今回のアプローチを選んだ論理的根拠はなんですか」なんて事を聞ける空気ではない(と感じた)ため大人しくしていました。
今回参加させていただいた PHP 勉強会はむしろ”小さいカンファレンス”であり、”小さなセミナー”であると感じました。
僕はあくまでも勉強会は対流が基本だと感じている人間なので、今回のように「発表者から聴衆へ」と言う一方向の流れに対して論理的根拠に乏しい発表が含まれていると「大味だな」と感じてしまいます。それが正しいのかどうかはさておき。

4月18日に参加させてもらった CMS 勉強会は、IT に関する知識で言えば明らかに今回よりも劣っていると言わざるを得ませんでしたが、参加者がそれぞれに抱える問題や、その問題に対する解決アプローチを議論しあう事で素晴らしい意見や知識の共有が果たせたと感じられるとても意義のある勉強会でした。
勉強会のフィールドが言語に向けられているため、発表者ごとに大きな差が生まれるのは仕方がないと思いましたが、発表する内容がないのであればテーマに対する解決アプローチを議論する方がよっぽど効果的なのではないでしょうか。

「発表する側と聞く側」の物理的なコスト差があるのは事実ですが、僕みたいにワイフから「月に100万は献上」などとシビアなハードルが課せられている人間からすると「死ぬ気で作った空き時間に知らない人が笑ってる写真集見せてもらった」と言うところに価値を見出す事は難しかったです。
これは明らかに僕の認識不足だったのでむしろ僕が申し訳ないと謝るべき事ですね。すいません。

批判的な内容ばかりですが、発表して下さった、またこのような勉強会に参加させて頂いたという事に対する僕なりのお礼です。
僕は基本的に S ですが褒められて得られる性的な興奮に比べ、批判される事で得られる新たな知見の方が万倍有益であると考えたための感想です。

どうもありがとうございました。

Wordpress におけるメタボックスの構造解説

Wordpress 2.5 辺りから整備されてきたメタボックス。2.7 にもなるとかなりシステマチックな構造となってきました。
このメタボックスの構造があるおかげで 2.7 からはユーザーごとに「表示オプション」という形でそれぞれのページでのメタボックスをコントロールできています。

さて、このメタボックス。プラグインでも存分にその機能を利用できます。設定項目が多岐に渡るようなプラグインの場合、設定画面の UI が煩雑になりがちです。
そんなときにこのメタボックスを利用する事で、すっきりとシステマチックな UI を設定画面に提供する事が出来ます。

使い方はいたって簡単。

  1. メタボックスの登録
  2. メタボックスの出力
  3. メタボックス状態の保存と復元

これだけです。

メタボックスの登録

<?php
add_meta_box( 'メタボックスの識別名', 'メタボックスのラベル', 'コールバック関数', 'ページ名', 'コンテキスト', '優先度');
?>

これがメタボックスを登録する関数です。
まず、第一引数にメタボックスの識別名を指定します。これは最終的にメタボックスを HTML として出力する際、一番外側の div 要素の id 属性として利用されますので、制限的には id 属性のそれに準じます。PHP 側ではハッシュのキーとして使われます。
同一のページ内で使うメタボックス同士で重複してはいけませんし、そもそも id 属性として利用されるので HTML 内で重複してもいけません。ユニーク性を強調するならば ‘プラグイン名_メタボックス識別名’ という様な構造がお手軽です。

次に第二引数。これはそのままメタボックスの上部に表示されるラベルです。それ以外に利用される事はありません。
また、この引数はそのまま HTML に出力されるので、タグを利用してメタボックスの表現に変化をもたせることが可能です。しかし、ラベルは span 要素で括られているためボックス要素を利用する事は出来ません。せいぜい <span class=”extra”>ラベル</span> とする程度にとどめた方がよいでしょう。

第三引数はコールバック関数です。後述する do_meta_boxes() でメタボックスの HTML が出力される際に呼び出されます。
つまりメタボックス内部の HTML を出力するための関数やメソッドを指定します。
例えば、コールバック関数に display_meta_box() を指定した場合 display_meta_box() では以下のようなコードを書きます。

<?php
function display_meta_box(){
?>
<label>今朝食べたもの<input type="text" name="breakfast" value="スパンコール丼" /></label>
<?php
}

call_user_func() で呼び出されますので、メソッドの場合は配列で指定します。

第四引数は ‘ページ名’ となっていますが、これはプラグインの URI には依存していません。自由です。
ページ名というよりはページの識別名と考えた方がよいかもしれません。後述する JavaScript 側でのコントロールにおいて重要な役割を持ちます。
プラグイン名を利用するのがベターでしょう。

第五引数のコンテキストは、要するに同一ページ内に複数列のレイアウトを組むような場合の識別名として利用します。
例えば以下のような HTML があったとします。

<div class="sidebar"></div>
<div class="body"></div>

sidebar と body にはそれぞれ別のメタボックスを出力したい、という様な場合にこのコンテキストで分類し、後述する do_meta_boxes() で区別して呼び出します。
指定しない場合は ‘advanced’ として扱われます。

最後の第六引数は優先度です。優先度は高い順に ‘high’, ’sorted’, ‘core’, ‘default’, ‘low’ となっており、これ以外の優先度を指定してしまうとメタボックスが出力されなくなってしまいますので気をつけてください。優先度が適用されるのは「メタボックスが呼び出される順番」です。プラグインの構造上出力したい順番にメタボックスの登録が出来ない場合などに力を発揮します。
指定しない場合は ‘default’ として扱われ、優先度が重複するメタボックス同士は登録された順に出力されていきます。

以上がメタボックスの登録関数について。

メタボックスの出力

次に登録したメタボックスを出力する関数である do_meta_boxes() についてです。

<?php
do_meta_boxes( 'ページ名', 'コンテキスト', 'コールバック関数に渡したい変数');
?>

第一引数と第二引数はそれぞれ add_meta_box() で指定したものと相関関係にあります。
呼び出した居場所で適時呼び出して下さい。

<div class="sidebar"><?php do_meta_boxes( 'pluginName', 'sidebar' )?></div>
<div class="body"><?php do_meta_boxes( 'pluginName', 'body' )?></div>

という様な具合です。
第三引数にはコールバック関数に渡したい変数を指定することが出来ます。
コールバック関数には二つの引数が渡されます。

function callbackFn( ‘do_meta_boxesから渡された変数’, ‘このコールバック関数を呼び出しているメタボックスの設定’ ) となっておりますので、例えば1種類のコールバック関数で複数のメタボックスを出力したい場合などには、第二引数で呼び出し元のメタボックスを評価し第一引数にある変数を使ってメタボックスを構成する、という様な利用方法が挙げられます。

メタボックス状態の保存と復元

ここまでは PHP のお話でしたが、このメタボックスは JavaScript によって折りたためたり、ドラッグして移動する事ができます。
これらの直感的な操作は wp-admin/js/postbox.js にそのスクリプトが書かれていますので、とりあえずこれを呼び出します。
postbox.js は規定のスクリプトとして登録されているので、呼び出しは wp_enqueue_script( ‘postbox’ ); で OK です。

呼び出そうと思っている本体のスクリプトの呼び出し名を知りたい場合は以下の URI を参照して下さい。
Function Reference/wp enqueue script « WordPress Codex

次におまじないを2行、JavaScript として追加します。

postboxes.add_postbox_toggles('ページ名');
jQuery('.if-js-closed').removeClass('if-js-closed').addClass('closed');

1行目はメタボックスのイベント登録をします。
また、メタボックスの状態を保存する際にどのページで出力されたメタボックスの情報であるかを識別するページ名を引数に渡します。
このページ名は前述の add_meta_box() で指定したページ名と同一のものを利用します。
これによって PHP 側で生成したメタボックスと、JavaScript 側で制御している表現の状態を結び付けます。
2行目はメタボックスが閉じられたものであると指定された場合に、そのメタボックスを予め閉じるための指示です。
PHP で生成されたメタボックスが閉じられる予定のものであった場合、class 属性に if-js-closed が指定されていますので、そのクラス名を closed に変えています。
これにより closed のスタイルが適用される事になります。

JavaScript によってメタボックスを閉じたり、移動した情報はその度にサーバにリクエストされます。
その際 nonce_field が無いと状態を保存してくれません。
ページ内のどこでもよいので nonce_field を出力しておきましょう。

<?php
wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false );
wp_nonce_field( 'meta-box-order', 'meta-box-order-nonce', false );
?>

これで OK です。
nonce_field の説明は長くなってしまうので機会があれば別エントリで言及しますが、これは簡単に言えばユーザーの権限評価も含めたワンタイムチケットです。

メタボックスが表示され、状態が保存されていれば全てはうまくいったという事です。
さほど複雑な構造ではないので是非利用してみて下さい。

参考

Wordpress プラグインに依存関係を組み込んでみる

「このプラグインを動作させるにはあのプラグインがないと駄目なんです><」と言う場合がときたまあります。
しかし Wordpress にはプラグイン同士の依存関係を管理する機能が備わっていないため、せいぜいプラグインの readme.txt に「こっちのプラグインを導入してから使ってね☆」と書くくらいしか方法がありませんでした。
が、しかし。readme.txt を読まずに「プラグインが動きません><」と言ってくるかわいそうな子は少なからずいるわけで、そんなパーリピーポーのためにプラグインに依存関係をチェックする機能を実装してみましょう。

ソースを見てみよう

以下に挙げるクラスは依存関係だけを組み込んだクラスです。
このクラスをプラグインのベースに流用すれば依存関係が組み込まれたプラグインを作れるという寸法です。
ちなみに被依存プラグインは「Hello Dolly」です。あの素晴らしい標準プラグインを削除した人はいませんよね。

<?php
class pluginClass {
    private static
        /**
         * プラグイン名を格納
         */
        $pluginName = 'My plugin',
        /**
         * プラグインの場所(wp-content/plugins/以下)を格納
         */
        $pluginPlace,
        /**
         * クラス名を格納
         */
        $class,
        /**
         * キーに被依存プラグイン名(Wordpress に登録されるプラグイン名)
         * 値には依存関係が解消されていない場合のエラーメッセージ
         */
        $dependencePlugins = array( 'hello.php' => 'Hello dolly プラグインが有効である必要があります。' ),
        /**
         * このプラグインを停止する前に被依存プラグインを停止しようとした場合に表示されるメッセージ
         */
        $dependenceMessage = 'My plugin プラグインを先に停止する必要があります。';
 
    /**
     * 初期処理
     */
    public function init(){
        self::$class = get_class();
        self::$pluginPlace = preg_replace( "/".preg_quote( WP_PLUGIN_DIR."/", "/" )."/", null, __FILE__ );
        /**
         * このクラスが定義されているスクリプトファイル(プラグイン)の使用にフック
         */
        add_action( 'activate_'.self::$pluginPlace, array( self::$class, 'checkDependence') );
        /**
         * プラグインの有効・無効を切り替えるリンクのフィルタ
         */
        add_filter( 'plugin_action_links', array( self::$class, 'dependenceDeactivateFilter' ), 10, 4 );
        /**
         * 被依存プラグインの停止にフック
         */
        foreach( self::$dependencePlugins as $dependencePlugin => $message )
        add_action( 'deactivate_'.$dependencePlugin, array( self::$class, 'dependenceDeactivateHook' ) );
        self::dependenceError();
    }
 
    /**
     * 依存関係の状態を評価するメソッド
     * @return void
     */
    public static function checkDependence(){
        $plugins = get_option( 'active_plugins' );
        foreach( self::$dependencePlugins as $dependencePlugin => $message )
            if( !in_array( $dependencePlugin, $plugins ) ){
                foreach( $plugins as $plugin )
                    if( $plugin !== self::$pluginPlace )
                        if( !in_array( $plugin, $current ) ) $current[] = $plugin;
                ob_end_clean();
                echo 'error';
                ob_start();
                $messages[] = $message;
            }
        update_option( 'active_plugins', ( isset( $current )? $current: $plugins ) );
        if( isset( $messages ) && count( $messages ) )
            update_option( self::$pluginName.'_dependenceError', $messages );
    }
 
    /**
     * プラグインの依存関係に問題があった場合に実行されるメソッド
     * @return void
     */
    private static function dependenceError(){
        require_once( ABSPATH.'wp-includes/pluggable.php' );
        $adminurl = strtolower( admin_url() );
        $referer = strtolower( wp_get_referer() );
        if ( strpos( $referer, $adminurl ) === false || !current_user_can( 'activate_plugins' ) )
            return;
 
        if( $_GET['action'] === 'error_scrape' ){
            $messages = get_option( self::$pluginName.'_dependenceError' );
            if( count( $messages ) )
                update_option( self::$pluginName.'_dependenceError', array() );
            else{
                $plugins = get_option( 'active_plugins' );
                foreach( $plugins as $plugin )
                if( array_key_exists( $plugin, self::$dependencePlugins ) )
                $messages = array( self::$dependenceMessage );
            }
 
            if( count( $message ) )
                die(
                    '<p style="color:#333;font-size:90%;margin-bottom:0.5em;">依存関係を解決して下さい。</p>'.
                    '<ul style="color:#333;font-size:90%;margin:0;"><li>'.implode( "</li><li>", $messages ).
                    '</li></ul>'
                );
        }
    }
 
    /**
     * プラグインを使用・停止するリンクのフィルタ
     * @return string
     * @param $link string リンク部分の HTML
     * @param $plugin string プラグインの場所
     * @param $args array プラグインの情報
     * @param $status string プラグインの状態
     */
    public static function dependenceDeactivateFilter( $link, $plugin, $args, $status ){
        return array_key_exists( $plugin, self::$dependencePlugins ) 
            && preg_match( "/action=deactivate/iu", $link[0] )?
                array( self::$pluginName.'が依存関係にあるため停止出来ません' ): $link;
    }
 
    /**
     * プラグインを停止する際の依存関係監視メソッド
     * このプラグインが有効な状態で被依存プラグインを停止すると実行
     * @return void
     */
    public static function dependenceDeactivateHook(){
        $bt = debug_backtrace();
        $plugins = $bt[3]['args'][0];
        $result = activate_plugins( $plugins );
        if ( is_wp_error( $result ) )
            wp_die( $result->get_error_message() );
        $recent = (array) get_option('recently_activated');
 
        $action = 'plugin-activation-error_'.self::$pluginPlace;
        $user = wp_get_current_user();
        $uid = (int) $user->id;
        $i = wp_nonce_tick();
        $nonce = substr(wp_hash($i . $action . $uid), -12, 10);
 
        wp_redirect( 'plugins.php?error=true&plugin='.rawurlencode( self::$pluginPlace ).'&_error_nonce='.$nonce );
        exit;
    }
}
pluginClass::init();
?>

基本的な動作原理

依存関係のチェックは大きく分けて二つ、このプラグインを有効にするときと、被依存プラグインが無効になるときです。
依存関係によってこのプラグインが有効になったとしても、被依存プラグインが無効になったときには同時にこのプラグインも無効にならなければ依存関係が適切に守られてるとは言えません。
上記のスクリプトでは、依存しているプラグインが使用されている状態で被依存プラグインを停止した場合、”プラグインを有効にした際にエラーが発生した状況”を人為的に引き起こしています。
Wordpress に依存関係を管理する機能がないため、プラグインを停止した際のエラーは制御されていません。プラグインを停止した際に出力されるエラー内容を「プラグインを停止したために起きたエラーです」と出力するには本体に手を入れなければならないため、次善の策ですが「プラグインを有効にしたために起きたエラーです」と言う出力を流用しています。

プラグインエラーの評価基準

Wordpress でプラグインを使用する際、エラーがあると「重大なエラーを引き起こしたのでプラグインの有効化はできませんでした。」と表示されますが、これは出力がバッファリングされている状態でプラグインファイルを読み込むと、PHP 側でエラーの出力がない限り何も表示されない仕様を生かした評価方法によるものです。
逆を返せば add_action( ‘activate_plugin_プラグイン名’, ‘func’ ) と言う「プラグインが有効になるタイミング」で出力のバッファリングを停止して、何らかの出力を行えば人為的にエラーを引き起こせる事になります。
今回作った依存関係の実装はこの人為的なエラーによって制御されています。原則としてリファラを元に整合性を評価していますが、Wordpress では hogehoge_nonce などといった、所謂ワンタイムチケットによってユーザーとアクセスの一意性を保っているため、仮にリファラを偽装したとしても悪用できません。

プラグインの簡単な解説

ざっくりと作ったものなので野暮ったいところや間違っているところがないとは言い切れません。
「こういう事も出来るね!Wordpress かっこいい!」と言うひとつの目安程度になればいいかなと思っています。

以下に簡単なメソッドの説明を挙げておきます。

pluginClass::checkDependence()
依存関係を監視するメソッドです。このプラグインの使用開始にフックしています。
プラグインを使用するには被依存プラグインが有効でなければなりません。
もし被依存プラグインが停止した状態であれば出力のバッファリングを一時的に停止して適当な文字列を出力、再度バッファリングを開始して人為的にプラグインエラーを引き起こします。
また、どの被依存プラグインが原因なのかを明確にするため、停止している被依存プラグインのエラーメッセージ(pluginClass::$dependencePluginsの値)をデータベースに保存します。
pluginClass::dependenceError()
リファラが管理画面のプラグインページで尚且つ “activate_plugins” 権限を持ったユーザーのリクエストであり、リクエストパラメタ(action)が ‘error_scrape’ だった場合にエラーだと判断します。
と言うのも、Wordpress の管理画面のプラグインページでは、プラグインエラーがあった場合に iframe でプラグインを読み込んだ内容を出力しているためです。エラーだと判断し、尚且つ依存関係に関するエラーがあればその旨を出力します。
依存関係に関するエラーがない場合のプラグインエラーであればもちろん依存関係に関するエラーであるとは表示しません。
pluginClass::dependenceDeactivateFilter( $link, $plugin, $args, $status )
依存しているプラグインと被依存プラグインが使用中であった場合、被依存プラグインの停止リンクが「”プラグイン名”が依存関係にあるため停止出来ません」と言う文字列に置き換わるフィルタです。
本質的な対策ではありませんが、抑止力と言う視点で有効なアプローチなため実装しています。
pluginClass::dependenceDeactivateHook()
このプラグインが使用中である限り被依存プラグインを停止できないようにするため、被依存プラグインを停止しようとすると人為的にプラグインエラーを引き起こすようにしています。
このプラグインが使用中であればどんな状況であれ被依存プラグインが停止してはならないため、評価も糞もなく停止を阻止します。

まとめ

「あるプラグインを使うとさらに機能を強化できる」、「自分で複数のプラグインを開発しているけど、二つ以上のプラグインを同時に使用していないと駄目」などというケースは割りとよくあります。
この依存関係の構造をうまくいじれば「プラグインがアップロードされていなければプラグインがダウンロード出来るページへのリンクを出力する」なども割りと簡単に実現できます。
もっと言えば複数のプラグインを同時に使用・停止する事も可能です。
まさに夢がひろがりんぐですね。

Wordpress プラグインの作り方(4)

Wordpress の翻訳の仕組みを扱った前回から約半月経ってしまいましたが第4回はプラグインの設定画面を管理画面に追加してみたいと思います。

設定画面にリンクを追加してみよう

今回はWordpress プラグインの作り方(2)で作ったプラグイン用の抽象クラスを使います。使いますと言うかすでに実装されています。
中身は同じですが、便宜上以下にプラグイン用の抽象クラスを貼り付けます。

<?php
class abstractClass {
 
    private static
        /**
         * プラグイン名
         */
        $pluginName = 'This plugin name.',
        /**
         * クラス名
         */
        $className,
        /**
         * オプションの規定値
         */
        $defaultOptions = array(
            'options1' => 1,
            'options2' => 2,
            'options3' => 3
        );
 
    /**
     * 初期処理
     * @return void
     */
    public function init(){
        self::$className = get_class();
        self::translation();
        add_action( 'admin_menu', array( self::$className, 'addOptionsPage' ) );
    }
 
    /**
     * 翻訳データ読み込み
     * @return void
     */
    private function translation(){
        load_textdomain( self::$className, dirname(__FILE__).DIRECTORY_SEPARATOR.get_locale().'.mo');
    }
 
    /**
     * オプションの保存と読み込み
     * @return array オプションの値
     */
    private function postAndGetOptions(){
        $options = get_option( self::$className );
        if( !current_user_can( 'manage_options' ) ) return $options;
        foreach( self::$defaultOptions as $name => $default ){
            if( isset( $_POST[$name] ) ) $options[$name] = $_POST[$name];
            if( !isset( $options[$name] ) ) $options[$name] = $default;
        }
        update_option( self::$className, $options );
        return $options;
    }
 
    /**
     * 設定ページのリンクを設定タブに出力
     * @return void
     */
    public function addOptionsPage(){
        if ( !function_exists( 'add_options_page' ) ) return;
        add_options_page(
            self::$pluginName,
            __( self::$pluginName, self::$className ),
            8, __FILE__,
            array( self::$className, 'displayOptionsPage' )
        );
    }
 
    /**
     * 設定ページの出力
     * @return void
     */
    public function displayOptionsPage(){
        $options = self::postAndGetOptions();
        $labels = array(
            'header'   => __( 'This plugin name.', self::$className ),
            'options1' => __( 'options1', self::$className ),
            'options2' => __( 'options2', self::$className ),
            'options3' => __( 'options3', self::$className ),
            'SUBMIT'   => __( 'Save as changes', self::$className )
        );
        echo <<<_HTML_
        <div class="wrap">
            <h2>{$labels['header']}</h2>
            <form action="{$_SERVER[" method="post">
                <table class="form-table" border="0">
                    <tbody>
                        <tr>
                            <th>{$labels['options1']}</th>
                            <td><input name="options1" type="text" value="{$options['options1']}" /></td>
                        </tr>
                        <tr>
                            <th>{$labels['options2']}</th>
                            <td><input name="options2" type="text" value="{$options['options2']}" /></td>
                        </tr>
                        <tr>
                            <th>{$labels['options3']}</th>
                            <td><input name="options3" type="text" value="{$options['options3']}" /></td>
                        </tr>
                    </tbody>
                </table>
                <p class="submit"><input name="SUBMIT" type="submit" value="{$labels['SUBMIT']}" /></p>
            </form>
        </div>
_HTML_;
    }
}
abstractClass::init();
?>

abstractClass::addOptionsPage() が設定画面にプラグイン用の設定画面へのリンクを貼り付けるメソッドになるわけですが、メソッドの中を見ると add_options_page() という関数を呼び出しています。これが「設定(オプション)ページにサブメニュー後述1を追加する関数後述2」になります。

さて、abstractClass::addOptionsPage() でサブメニューが追加できる事はわかりましたので、abstractClass::addOptionsPage() を呼び出してサブメニューに追加してみましょう。abstractClass::addOptionsPage() を呼び出すタイミングは “admin_menu” が実行されるタイミングです。
abstractClass::init() をご覧下さい。3分間クッキングよろしく add_action( ‘admin_menu’, array( self::$className, ‘addOptionsPage’ ) ); という具合にトリガにフック後述3されています。

これでプラグインが有効であれば “admin_menu” が実行されるタイミングで abstractClass::addOptionsPage() が実行されるようになります。
設定ページを開き、サブメニューに abstractClass::$pluginName で指定したプラグイン名が表示されていれば成功です。

設定画面用の HTML を作ってみよう

さて、次はプラグイン用の設定画面を作ってみましょう。管理画面の HTML を参考にすると割と簡単に作れてしまいます。
abstractClass::addOptionsPage() で呼び出されている add_options_page() で設定画面が呼び出されたときに実行するメソッドとして abstractClass::displayOptionsPage() を指定しています後述4が、このメソッドで HTML を出力すれば、ちょうどよく設定画面に HTML が出力されるようになります。
HTML の内容は abstractClass::displayOptionsPage() を見て下さい。class 属性の値なんかを保っていれば特にレイアウトを気にする必要はなくなります。
ちょっと尻すぼみになってしまいましたが、この HTML で送信された設定内容の保存については次回お送りします。

参考

後述1

Wordpress 管理画面 Wordpress の管理画面では上下2段のメニューが表示されます。
Wordpress では上段を「メニュー」下段を「サブメニュー」と定義しています。

後述2

サブメニューを追加できるのは設定ページだけではありません。
管理ページに追加するなら add_management_page()、テーマページならば add_theme_page() と言った具合に、それぞれのページ(メニュー)に対してサブメニューを追加する関数が用意されています。

後述3

Wordpress が実行されている任意のタイミングで任意の関数やメソッドを実行させたい場合は、Wordpress の随所にちりばめられているトリガにフックさせます。
フックの詳しい説明はWordpress プラグインの作り方(1)をご覧下さい。

後述4

abstractClass::addOptionsPage() で呼び出されている add_options_page() の引数の内容は以下のとおりです。
add_options_page(設定画面名, 設定画面へのリンクが貼られるアンカーテキスト, 権限 or キャパビリティ, 呼び出すスクリプトファイル名, 呼び出す関数 or メソッド);
call_user_func() がラップされているので、メソッドを呼び出す場合は array(クラス名, メソッド名) で呼び出します。

ホーム > PHP

Search
Feeds
Meta

Return to page top