自分の部下には「パーフェクトPHP」を読んでおいてもらいたい理由5つ

自分の部下には、「これ読んどけ!」と必ず渡しておきたい1冊です。

  • PHPの開発現場で実際に必要な知識を幅広くカバー
    • さすがにこれで「全部」というわけにはいきませんが、書かれている内容はどれも「必要」
  • MVCフレームワークの用語や知識
  • オブジェクト指向PHPを書く場合の必須知識を網羅
    • 「5章 クラスとオブジェクト」が50ページ以上、「11章 実践オブジェクト指向」が40ページ
  • PHPでWebアプリケーションを作る際に必須のセキュリティの知識も網羅
    • must readとしか言いようがない
  • PHPの内部構造の知識も適度にカバー
    • 掘り下げ具合のさじ加減がちょうどいい


パーフェクトPHP (PERFECT SERIES 3)

パーフェクトPHP (PERFECT SERIES 3)


そんな私には、今は部下は1人もいませんけどね!
それに、なにより私自身のPHP知識の「弱点」を、パーフェクトPHPを読むことによって浮き彫りにされた気がします。


あと、この本の「はじめに」のページで、「謝辞」に私の名前が並んでいてビックリです。id:Fivestar が書いているフレームワークの章で、(Symfonyつながりということで)ちょっとだけ査読的なことをしただけなんです。

(将来私の部下ができたら)この本を必ず読ませます!

KernelとHttpKernel、その辺の起動順序

Symfony2で名前的に混乱しやすそうなのがKernelとHttpKernelで、

  • Kernelはアプリケーション全体のベースとなる
  • HttpKernelは1つのHTTPリクエストのベースとなる

という感じで、Kernelの方がベースよりです。これは説明するまでもないかもしれませんが、フロントコントローラーで一番最初に作られるAppKernelがKernelの方を継承しています。

で、ポイントはHttpKernelの初期化のあたりですが、これはDIコンテナ経由で行われます。
(※DIコンテナ自身はKernelが作ります)
Kernel::handle()内部で、

<?php
return $this->container->getHttpKernelService()->handle($request, $type, $raw);

とコンテナ経由でHttpKernelサービスにアクセスしているので、この時点でHttpKernelが初期化されます。

DIコンテナの面白い(すごい)のはここからなんですが、このHttpKernelを初期化する際に、同時にいくつかの他のサービスも初期化されます。なぜかというと、DIの定義で、HttpKernelのコンストラクタにいくつかのパラメーターが渡されていて、このパラメーターはDIが渡すのですが、そのパラメーターがまだ初期化されていなければ、それもまたその時点でインスタンス化されるからです。
具体的には、HttpKernelのコンストラクタには次の3つの引数があります。

  • ContainerInterface $container
  • EventDispatcher $dispatcher
  • ControllerResolverInterface $resolver

対応するDIの定義は、FrameworkBundleのservices.xmlにあります。

<service id="http_kernel" class="%http_kernel.class%">
    <argument type="service" id="service_container" />
    <argument type="service" id="event_dispatcher" />
    <argument type="service" id="controller_resolver" />
</service>

なので、HttpKernelがインスタンス化されると同時に、DIコンテナによってEventDispatcherがインスタンス化されます。


symfony 1的に「new EventDispatcher」でgrepしたりしても見つかりませんw
このあたりに慣れるまでは、ちょっとソースを追うのが大変かもしれません。

Symfony2のconfigファイルにおけるimportsの挙動(DIのconfig)

Symfony2のconfigファイルでは、別のconfigファイルをインポートすることができます。
たとえばconfig_dev.ymlでは、共通設定であるconfig.ymlを以下のようにインポートしています。

imports:
    - { resource: config.yml }


また、config_dev.ymlをよく見ると、ルーティングの設定についても似たような記述があることが分かります。

app.config:
    router:   { resource: "%kernel.root_dir%/config/routing_dev.yml" }


前者のimportsで記述したresourceは、このconfigファイルの処理時点で同時にインポートされ、マージされて1つのconfigとして扱われます。
このimportsキーは、YAMLのルート階層にのみ記述できます。
そして、YAMLファイルがパースされるとき、まず最初にimportsの処理が行われ、インポートしているファイルから先に読み込まれて行きます。
この処理は、DependencyInjectionのYamlFileLoaderにおける共通処理となっています。

後者のrouterキーでのresourceは、FrameworkExtensionでの処理になっています。FrameworkExtensionのconfigLoadで、単にresourceのパスなどがコンテナの設定値として保存されます。(コンテナ・エクステンションの読み込み段階ではこれ以上の処理は行われない)

実際にrouterキーのresource(つまりrouting.yml)が読み込まれるのは、内部の処理が先に進み、RouterからRouteCollectionのloadが呼び出される時点です。ここでは、Routingコンポーネントが持つローダー(YamlFileLoader)で処理が行われるので、DIコンテナのconfigとは若干ルールが異なることに注意が必要です。

Symfony2のデバッグモードのエラー画面で、ファイルのパスを物理ファイルにリンクする

app/config/config_dev.ymlに次のように記述すると、リンクになります。

app.config:
  ide: macvim

「macvim」だと「mvim://open〜」というURLでリンクされ、「textmate」だと「txmt://open〜」というURLになります。他の値を指定した場合、その値がファイルリンク用のパターンになります。

<?php
if (isset($config['ide'])) {
    switch ($config['ide']) {
        case 'textmate':
            $pattern = 'txmt://open?url=file://%%f&line=%%l';
            break;

        case 'macvim':
            $pattern = 'mvim://open?url=file://%%f&line=%%l';
            break;

        default:
            // should be the link pattern then
            $pattern = $config['ide'];
    }
}

config_dev.ymlではなくて、config.ymlにideの設定を記述するとうまく動かないのは、いまいち理由が分かっていませんが・・・。

PearのServices_TwitterでOAuth認証している場合のfriendships/show

OAuth認証してAPIリクエストを送信している場合は、friendships/showでsource_idまたはsource_screen_nameのいずれも指定しなくてもいけるはずなんだけど、PearのServices_Twitterだとパラメーターの数が足りないと言われエラーになります。

このエラー、twitter側で出しているんじゃなくて、Services_Twitter側で出してるんですね。
で、こういった引数を定義しているのがdata/api.xmlファイルで、そこに引数の最小値が「2」と書かれていたのが原因でした。

303	        <endpoint name="show" method="GET" auth_required="false" min_args="2">
304	            <formats>xml,json</formats>
305	            <param name="source_id" type="id_or_screenname" required="false"/>
306	            <param name="source_screen_name" type="id_or_screenname" required="false"/>
307	            <param name="target_id" type="id_or_screenname" required="false"/>
308	            <param name="target_screen_name" type="id_or_screenname" required="false"/>
309	        </endpoint>

OAuth認証している場合とそうでない場合とで必須かどうかが変わるので、これをバグと言うべきかどうかは判断できませんが・・・。
とりあえず手元で直接1に変更して対処。

Doctrine2を使った開発のワークフローで悩み中

symfony 1では、ORM Designerを使ってスキーマ設計→schema.yml書き出し→モデルやDBを生成(必要に応じてマイグレーション)というワークフローでした。

こういったワークフローをDoctrine2を使った開発でもやりたいと思い、Doctrine2のコマンド等とあれこれ格闘していますが、今のところまだ「これだ」と思える方法が見つかっていません。


現状、ぶつかった問題点

  • ORM DesignerはDoctrine2をサポートしているが完全ではなく、リレーションの情報など、一部書き出されたYAMLが不完全
  • YAMLマッピング設定を書き、doctrine:generate:entitiesでエンティティを生成すると、getter/setterなども一挙に生成できて楽、しかし問題も
    • YAMLにフィールドを追加した場合の追加分の書き出しがうまくいかなかった(2重になったりした)
    • 書き出されたメソッドの名前を変えたりすると、(当然ですが)再度YAMLからの追加書き出しなどに対応できない
      • エンティティ側の情報をリフレクションで取得して処理しているので、こういうのは仕方ない
    • 名前空間固定
    • 生成したエンティティのメンバー変数はprivate
      • これは、(おそらく)エンティティ用のクラスの場合、マッピングするメンバー変数はprivate推奨っぽいです。(要調査)
  • エンティティにアノテーションマッピングを記述するのが現状もっともスムーズに思える
    • getter/setterはIDEなどの力を借りて生成するのが吉
    • アノテーションの補完がIDEでできない
    • 名前空間固定っぽい
    • ORM Designerから直接書き出したいができない
  • マッピング情報はYAML、エンティティにはマッピング情報を記述しないというやり方
    • 2重管理的になるので避けたい(エンティティ側にもメンバー変数などの宣言が必要なため)
  • リレーション(Doctrine2ではアソシエーションと呼んでいる)関連
    • アノテーションの記述方法に慣れるまでややこしい
    • cascading persistに関連する部分は、カスタム実装が必要な場合も


現時点での結論としては、

というやり方がもっとも無難っぽいです。(あくまで個人的な結論)