読者です 読者をやめる 読者になる 読者になる

はやいで! つよいで! ワテらのPHP!! #phpkansai

この記事は「 PHPカンファレンス関西2015 」の リレーブログ のエントリーです。

昨日は、@aa7th さん PHPとScalaと私 #phpkansai でした。

このエントリーのタイトル、「はやいで! つよいで! ワテらのPHP!!」というのは、もしかしたら今年のPHPカンファレンス関西のテーマになるかもしれなかかった文言です。これ結局は採用されず、今年は「テーマなし」ということでボツになりました。

が、こいつが出てきたときの空気感がすごく良くて忘れたくなかったので、こうして残しておこうと思いました。関西人の冗談みたいな文言ですが、これ実は今のPHPコミュニティにとって、とても大事なメッセージを含んでいると思うのです。まじで。

  • はやいで!
  • つよいで!
  • ワテらのPHP!!

では、ひとつひとつ解説していきましょう。

はやいで!

速いといえばPHP7ですよ。

ausweb.com.au

これまでも、PHP4から5の間で、また、PHP5.3から5.6の間でも、ものすごく高速化されてきました。HHVMなんて別の処理系もあります。Wikipediaを爆速にしたことで有名ですね。実は今のPHP処理系は、インタプリタとしてはかなり速い部類になります。

とはいえ、HHVMまで行くとさすがにちょっとハードルが高かったんですが、これからは、それ相当の速度がなんと、PHP7という「ふつうのPHP」で得られるんです。

また、仮想マシンが一般的なWebの言語にあって、Phalcon という「フレームワークコードがほぼネイティブ」という速さへのアプローチもあります。ReactPHP という、Node.js のような非同期IOを実現することで、裏方の処理系のパフォーマンスを高めるアイデアもあります。

いかに早く作れるかというのもまた、PHPがこれまでWebで活躍してきた理由のひとつです。近年、エンタープライズ指向での用途にも使えるようになったとはいえ、当初 LAMP という言葉が意味していたこと、その中にPHPがいたことは、とても大事なことだと思います。

今回のカンファレンスでは、基調講演にPHP7のお話があります。Phalconのセッションも設けています。自分個人としては、開発の早さ=RAD (Rapid Application Development) の視点から、フレームワークトラックのうち1セッションを担当させてもらいます。

つよいで!

繰り返し言っていることなんですが、PHP はリクエストごとにクラス定義すら最初から読み直し、終わったら全部捨てるんですよ。それがマトモな速さで動くんです。Webリクエストはぜんぶ別のプロセス空間です。ここ、強さの秘訣ですよね。

はじめから完全分離されているということは、サーバを簡単にスケールアウトできるってことです。global使ってようが、static変数使っていようが、そいつらも分離されてるんですよ。この、ラフでもタフというのは、まぎれもないPHPの強さです。

また、ここ数年で強力なツールがどんどん登場しています。PHPUnit はもちろん、Composer, PhpStorm... ライブラリなら SwiftMailer や Guzzle、フレームワークもさまざまな用途に派生し、どんどん洗練されています。その裏には、PSR という強い実効力のある標準化機構があったりします。個人的には、型をベースに静的解析できるLLだという点で、LLで堅実なプログラムを書くならPHPは最強なんじゃないかと思っています。

セキュリティ面でも、いっときは「たとえばPHPを使わない」と言われたこともありましたが、今では、もっとも叩かれて鍛えられたセキュリティ意識を持つ文化になっているんじゃないかと、個人的に思っています。Webアプリケーションのセキュリティに関して、もっとも見識のある層の方々からの関心を集めているのは、間違いないと思うのです。

カンファレンスのセッション内容としては、フレームワークだけを特集するトラックを設けています。強力なモダンフレームワークの世界を楽しんでいただければと思います。セキュリティについてはなんと2セッションあります。CodeceptionやCI(継続的インテグレーション)の話題も楽しみですね。

ワテらのPHP!!

最先端ではどんどん速く、強くなっているPHPですが、そのいっぽうで、PHPはごく一般的なWebエンジニアから見て、けして遠い存在ではありません。Webの開発に携わる人々にとって、PHPはもっとも親しみのある言語のひとつです。

PHPは主に、Webのサーバサイドの最前面に立つ技術です。フロントエンドの人との関係もとても深いでしょう。Webデザイナーが覚えたいプログラム言語ナンバーワンかナンバーツーは、なんといってもJavaScriptPHPですもんね。

開発とは関係のないWebのユーザーにとってもそうです。FacebookWikipediaも、みんなPHPのおかげさまです。なんだかんだ言いながらも、みんな日々、WordPress で公開される記事を読んでいますね。いまPHPは、Webになくてはならない、みんなのPHPです。

いまだに、まだドジっ子だった頃の癖が出るかもしれません。それもまた、みんなで「あるある〜」と共感できる愛嬌ある部分なのです。ほら、配列と辞書がごちゃまぜとか、文字列を数値計算できちゃうとか、なんかかわいいじゃないですか。(で、それは案外実務で便利だったりすることの方が多い)

カンファレンスのセッションには、PHPに限定した話のほかに、フロントエンド、クラウドアクセス解析、プロジェクト管理といった、他の分野に広がっている話題も満載です。また、PHPカンファレンス関西は参加者全員にとって、「ワテらの」カンファレンスです。上級者向けだけでなく、初心者向けセッションやIDEとGitの体験ハンズオンなども、重要なテーマとして盛り込んでいます。

ともすればそのドジっ子なところを揶揄されがちなPHPですが、そんな声はものともせず、PHPPHP文化の「はやさ」「つよさ」を、誇りを持って「これがワテらのPHPじゃ」とアピールできる場を作りたい。そんなPHPカンファレンスを関西で提供していきたい。テーマ案「はやいで! つよいで! ワテらのPHP!!」はそんな思いから出てきました。

ま、ボツってるんですけどね(^^; いいじゃないですか、気持ちさえあれば、体裁なんて。

※ @shin1x1, @aa7th, @omoon とのミーティングと肉に感謝。

さあリレーブログでどんどんテンション上げていきましょー! 次は @slywalker さんです。 → 忍び歩く男 - SLYWALKER あしたもみてね (夏休みこども劇場)

IntelliJ でブランチ間の差分と作業ブランチ全体のコードレビュー

IntelliJ でブランチ間の差を見たいときは、まず基準ブランチをチェックアウトしてからここ

f:id:tanakahisateru:20150328011746p:plain

f:id:tanakahisateru:20150328011855p:plain

それぞれのコミットの diff を見て、トータルでどれだけ違うかは、Log を Diff に

f:id:tanakahisateru:20150328011859p:plain f:id:tanakahisateru:20150328011919p:plain

これで HEAD 同士の比較です。

でも

f:id:tanakahisateru:20150328012404p:plain

master が他のプルリクエストをいろいろマージしていると、作業ブランチはあまり変えてないのに、master が進んでいくせいで差がどんどんできてしまいます。ここまでのスクリーンショットでもすでに README.md が master 側で進んじゃってますね。

そうなると時間が経つほどに、ブランチ HEAD 同士を比べることと、作業ブランチのコミットを累積してどんな作業をしたのかを見ることは、意味が違ってきます。数人で分担作業したものがリリース直前になると、個別のブランチよりもマージを進めている master の方が多くのファイルを変更しちゃってるなんてことが多々あります。

そこで、作業ブランチのコミットの累積をコードレビューする方法を考えます。まずは作業ブランチの base を探します。たぶんその作業は master からブランチしてますよね。じゃあ、Changes パネルのごちゃごちゃした Logs タブに master と対象作業ブランチだけを表示 (補完ありでブランチ名を2つここに書く) しましょう

f:id:tanakahisateru:20150328012834p:plain

それから、2つが合流する共通のコミットを探します

f:id:tanakahisateru:20150328013037p:plain

で、こいつをチェックアウト。この状態で最初の方法で作業ブランチと Compare すると、 作業ブランチでやった変更だけ を確認できます。

f:id:tanakahisateru:20150328015300p:plain f:id:tanakahisateru:20150328015253p:plain

GitHub とか Bitbucket みたいな、プルリクエストのコミット群のサマリーを見ることができるサービスを使ってればいいんですが、そうでない場合、マージしたいブランチで変更のあった行がどこなのか調べるのはちょっと面倒です。まあ、GitHub の プルリクエストページでも Unified Diff になっちゃうので、ソースコードの全体像を見ながらレビューしたいと思っても難しいですね。

f:id:tanakahisateru:20150328020740p:plain

ようは、IDE のパワーを使える状況で、かつソース全体をレビューできるこれが便利なわけですよこれが。

Yii Framework 2.0 リリース記念勉強会 資料

第19回関西PHP勉強会 Yii Framework 2.0 リリース記念勉強会 の資料はこちらです。

だいぶあおり気味なのは、一般公開を意識せずに書いているためです。その場にいる人向けに、今日はこういう勉強会なので空気読んでね、というのが入っているのをご了承ください。

Yii 2.0 とは

ベータリリース記念 第一回勉強会の資料

https://gist.github.com/tanakahisateru/cf8ef578d7ea24ea429a ※ イベントまわりでちょっとオススメじゃない書き方があります

過去の発表やブログなど

http://www.slideshare.net/tanakahisateru/phpcon-2014yii

http://image.slidesharecdn.com/yii-121012040114-phpapp01/95/yii-31-728.jpg?cb=1350032550

http://tanakahisateru.hatenablog.jp/entry/2014/06/16/040047

http://tanakahisateru.hatenablog.jp/entry/2013/12/22/030817

ユーザーガイド

http://www.yiiframework.com/doc-2.0/guide-README.html

Yii の立ち位置

開発規模の想定

開発に必要な前提知識

Symfony との類似点

  • 適切なオブジェクト指向を前提にしていてIDEフレンドリー
  • 設定の項目がオブジェクトのプロパティと完全に対応する
  • デバッグツールを標準装備
  • アセットをPHPライブラリのパッケージ内で管理、Webにパブリッシュして使う (Assetic, Laravelのpublishも似てる)
  • POSTをフォーム オブジェクト で受け取りバリデーションする設計 (Laravelは5で入るあれ)

Symfony に対する大きな違い

  • SymfonyのようなフルDI設計を指向せず、グローバルに参照できるサービスロケーターを良しとする
  • Requestが独立したインスタンスではなくアプリケーションインスタンスと1:1になっていて、リクエストごとにメモリが全破棄される挙動に最適化されている
  • アノテーション等の高度な記述をさらに高度な高速化技法で上塗りするのではなく、遅くなるならメカニズムを捨て、あくまで「ありのままで速い」ことを指向
  • クラスがPOJO(?フレームワークの知識と独立したドメインモデル)であることにこだわらない
  • ORMのインスタンスがフォームを兼任できる (Struts型なのがRails風になる) = ActiveRecord
  • DIされないと作れない制約感 (エンタープライズでは下請けが下手しないための安全策として有効) がない
  • ちょっとしたカスタマイズならクロージャでさっと済ませられる

かつてSymfonyをやめてYii2-alphaを採用した理由

Symfonyでだいぶ作っていたものを、残り2ヶ月でYii2(当時alpha)に変更してイチから作りなおしたことがありました。

  • Doctrineはパッシブレコード、ただの入れ物で小さなロジックを持てるArray。そこからはSQLを実行できる層が見えない。モデルの設計が別途必要だった。
  • 自力で 自由に 設計する必要があるということは、引き継いだ人のOO設計力を問われる。真似する雛形がフレームワークから与えられない
  • Twigで再利用可能な部品を書くのが大変すぎる
  • サードパーティー製ライブラリがないと変わったことが何もできない。その選定の負担が大きい

要するにちゃんとしようとした場合の開発コストです。

他と異なる特徴

基本

  • すべてのコンポーネントが一貫して、getter/setterやビヘイビアをサポートする基底クラスを継承している
    • 名前によるプロパティ参照を活かして、実装がpublicフィールドなのかgetterなのかDBのカラムなのかを区別しない
    • どこにでもイベントとビヘイビアが差し込める
    • イベントフックを利用していろんなものができている
  • すべてのコードがPHPの文法、Yiiの基底クラスのルールを知ればいいだけ => 学習コストの低さ
  • ブラウザから使えるコード生成ツールが便利 = Gii

ビュー

  • ビューがPHPスクリプトである積極的な理由 = ウィジェット
  • フレームワークのサポート範囲にJavaScriptを含む (jQuery等フロント側で必要なライブラリを初期インストール/パッケージバージョン管理下に)
  • 削除の確認などで積極的にJavaScriptのconfirm()を使うなど
  • アセットバンドルによるCSS/JSの依存順序解決
  • わざわざ artisan xyz:publish しなくても自動パブリッシュ

ページャーフレームワーク

  • DataProvider (未評価のQueryのインスタンスにorderとlimitをかけて範囲内のデータを得るオブジェクト)
  • ListView (DataProviderをビューでイテレーションするウィジェット)
  • 任意のSearchForm (フィルタ条件を預かりDataProviderを生成)

Laravelとの類似点 (yii2)

  • ActiveRecordはEloquentとかなり似ている。クエリのしかた、リレーションのやりかた。後発なのでよりちゃんとした設計になってる。
  • 世界的な人気は同クラス (どっちが強いかというよりは、今のPHPにどういうニーズが高いか)

Laravelとの相違点

まずはこれ

f:id:tanakahisateru:20141119203616p:plain

※ LaravelのEloquentはコンクリートなModelとBuilderの2つでできていて、生SQL用のBuilderはメソッド体系が似た別の実装になっています。つまりこの図に対応させると関係線のないクラスが3つ登場するだけ。

で、結果

$firstAdminId = (new Query())->from('user')
    ->where(['admin' => true])->orderBy('create_time')
    ->select('id')->scalar();

というコードとほとんどの内部フローを共有してこれ

$firstAdmin = User::find()->where(['admin' => true])->orderBy('create_time')->one();

が動く。ActiveRecordでのイベント/ビヘイビアとバリデーションは、ComponentとModel(フォーム)から継承している。

で、MongoDBのActiveRecordもまったく同じ継承構造。なのでこのぐらいのクエリならほぼ名前空間を変えるだけで MongoDB に切り替えられる。それでも 〜Trait とModel継承の部分は実装を共有できてて半分ぐらいは完全に同じ動きをする。

  • というように、全体的に一貫したオブジェクト指向設計がある。
  • 内部設計の追跡しやすさ、カスタマイズしやすさは段違い。
  • ライフサイクルイベントのあるヘビーウェイトなMVC構造に見えて、実はLaravelよりも速く、キャッシュ効率が高い(PHPに素直なことへの恩恵)

CakePHPとの類似点 (yii1,2)

  • 適用するプロジェクト規模がわりと近い印象
  • データベースの編集画面を最速で作れる系

CakePHPとの相違

  • 1.1の頃からすでにActiveRecordがだいぶ先行している
  • 規約ベースで動いてないので自由な設計がしやすい
  • OOP的に素直で全体の一貫性がある(ビヘイビアのサポートはより基底なクラスにあってどこでも使える)
  • イベントのおかげで無理せずファットコントローラを避けられる(経験上)
  • メジャーバージョンを上げるときのポリシー = 下位互換なしでいい、旧バージョンを長くサポートすればいい

特徴的なコンセプト

  • コードをかっこよくするために遅くなるような選択はしない (プログラマーの満足を満たすより、PHPで適切に動くコードはどうか、という意識)
  • いまどきのPHP、真面目にやれば速さは落ちない、かっこよさは結果ついてくるもの
  • フレームワーク固有な記述方法 (Yamlを使ったりアノテーションを使ったり裏ワザみたいなArray記法だったり) がないのでわかりやすく、自分独自のカスタマイズも容易
  • とにかくものすごくアプリケーション開発者目線。個々の部品をどういう使い方しても動くという完全性より、現場目線で「あー自分だったらこれ変えられるようにしておくのになぁ」と思ったところは、だいたい開発者の自己責任で変更できるようにしてある。

Railsと比べて何が良いか ← この視点で見るのが適切

  • もっともRailsと比較されることの多いPHPフレームワーク
  • PHPランタイムで動くということ。これはでかい
  • PhpStormのPHPは型(docコメント)のあるLL、動的すぎない安心感
  • requireとか暗黙で生まれるオブジェクトより、名前空間をuseするクラスローダーのほうが好き
  • ActiveRecordのフィールドはデフォルトでホワイトリスト (デフォルトでMassive Assignmentを防げる)
  • TurboLinkやらCoffeeScriptやらSassやらHamlやら、フレームワークの仕事はそういうことじゃない感
  • どこに何が入っているのかが単純明解で、自分独自なプロジェクト構成も作りやすい
  • 理想 (生き字引きなRubyエキスパートが現場に常駐してくれるという仮定) より現実 (普通の人がそこそこ調べたらどうにか動くものが作れること) を重視

Yii2で採用された新技術など

1 Composer

いまどき当たり前な Composer によるプロジェクト作成。

composer create-project --prefer-dist yiisoft/yii2-app-basic basic

yiisoft/yii2-app-basic は Packagist の登録名。公開されていればどんなテンプレートからでも生成可能。

ってだけならよくある話で、アプリケーション構造はPSR-4に準拠していて名前空間で浅いディレクトリから任意のクラスをオートロード。

2 composer-asset-plugin

https://github.com/francoispluchino/composer-asset-plugin

BowerやNPMからパッケージをインストールできるComposer拡張

インストール

composer global require "fxp/composer-asset-plugin:1.0.0-beta4"

vendorの下にダウンロードされ、Yiiのアセットバンドルの機能で動的にweb公開場所にパブリッシュされます。

3 Twitter Bootstrap

Bootstrap3 拡張が最初から組み込み済み。使うか使わないかは名前空間で使い分け。

yii\widgets\* ← → yii\bootstrap\*

Bootstrapを使いたくなければ名前空間を切り替える。(Bootstrap特有のものを使っていない箇所、ActiveFormなど) これでウィジェットが吐くHTMLにbootstrapなクラスが付いたり付かなかったり。

4 SwiftMailer

SwiftMailerもBootstrapのように使い分け可能。デフォルトは組み込み済み。

Transport実装を切り替えて、SMTPなのをAmazon SESに対応したりsendmailにしたりと、ブリッジがSwiftMailerの便利さを活かすインターフェース設計になっている。

useFileTransport (bool型)という開発用のマスタースイッチが便利。

メールテンプレートのレンダラーがはじめからHTMLメールを想定している。

5 Markdown

https://github.com/cebe/markdown

1.1のときの PHPMarkdownExtra に代わって、これが採用された。すでにコアに入ってる。

  • みんなおなじみのGFM (GitHub方言) モードあり
  • シンタックスごとにモジュールを分割して書かれている
  • Traitとして分けられたモジュールを必要なぶん結合し、自分のカスタムパーサーを作ることができる
  • Traitなのでオブジェクト間のメッセージングがなく速度が出る

ベンチマーク https://github.com/kzykhys/Markbench#readme

  • parsedownに匹敵する速度 (ただしparsedownは拡張性なさすぎてPHPMarkdownかというほど)
  • ciconaに匹敵する拡張性 (ただしciconaは凝集度に対して誠実すぎ、もっと実行速度に最適化していいと思う)

ちなみにYiiには以前から HtmlPurifier が初期インストール済みで、デフォルトで適度に Markdown をサニタイズします。

6 最新のPHP関数とかセキュリティ

  • 日時や通過などの国際化がintlベース(polyfillあり)に
  • password_hash() / password_verify() (5.5〜) 推奨
  • なくても互換実装あり (なんも知らんでもセキュア)
  • ちなみにプロジェクト生成時にクッキー暗号化ソルトを自動生成 (なんも知らんでもセキュア)
  • ちなみに何も書かなくてもフォームにCSRF対策が効いていて、わざわざ解除しないかぎり有効 (なんも知らんでもセキュア)

7 Codeception

http://codeception.com/

BDD風のAcceptanceテスト(Webサーバの外からテスト)とFunctionalテスト(フレームワークのリクエスト全体をテスト)

アプリケーションの実装はだいたいが Yii::$app が指すサービスロケーターに依存するので、だったらいっそリクエストまるごとテストする高級なテスティングフレームワークをサポートしよう。

まあSeleniumにもできるけど、デフォルトのPhpBrowserなら、PHPだけあればHTTPをサーバの外からテストできるよ。

もちろんDbUnitっぽいことが可能な単体テストもあるし、任意にPHPUnitを使ってもOK

8 HATEOASなREST

RESTfulなCRUDに特化したコントローラあり

デフォルトでActiveRecord(正確にはModel)への直接読み書きが実装済みのものあり

で、なんとこれまでSpring(Java)とBEAR.Sunday関連でしか聞いたことのなかったHATEOASをサポート http://en.wikipedia.org/wiki/HATEOAS#Implementations

レスポンスに次のアクションの選択肢リンクや現在のページャー状態などを含んで返すやつ。PHPはREST-APIに特化して、フロントはアプリやピュアなAjaxで作ってもいい。

9 その他今っぽいライブラリ

フレームワークと同時並行開発された豊富な拡張ライブラリ。ほぼ標準機能と言ってもいいです。プロジェクトによっては使わない場合もあるので composer.json に書いていないけど。

アルファ版以前から Elasticsearch の実装があるあたり、最新技術に貪欲でいいですね。

OAuthのフレームワークがあるおかげで設計迷わなくて助かりました。これは時代的にとても現実的。

10 コミュニティのライブラリ

などなど、Packagistでyii2で検索。正式リリース1ヶ月で66ページある。fuelやfuelphpで試すと1桁なので...

新しいサービスとのブリッジやウィジェットなど周辺を補助するものがやたら多い。YiiがCSS/JSを含んでのフレームワークであるというのが効いている。

逆に開発手法を変えるようなものは少ない。時期がまだ熟していないからか、それとも、だいたい yiisoft/* 以下でカバーできてるからか。

(Packagistでヒット数が多すぎるフレームワークは、それはそれで、「え、それサードパーティー製なの?!」ってなることはある)

デモに使ったプロジェクト

(追記)

Dropbox - phpkansai-19-yii2-basic.zip

.git 込みです。vendor が入ってないので、 composer install でどうぞ。

JavaScriptはいかにしてprototypeを捨てクラスベース継承を得るのか

きっかけは、prototypeconstructor__proto__ の関係を再確認していたときでした。JavaScriptはこうだけど、これって、AltJSな言語の継承はどうなってるんだろうと思って試したくなりました。

ちょっと気持ち的に、なんだか宗教に入ったみたいに俺は JavaScript がわかったって声高に言う人、だいたいみんな、プロトタイプチェーンによる移譲がクラスベースの継承に束縛されていた思考のブレイクスルーなんだぜ、みたいに言うんだけど、いったいそれがどれほど素晴らしいものなんだろうと考えてしまい...

もしプロトタイプチェーンがそんなに素晴らしいんなら、npm にあるほとんどのライブラリがチェーンを活かして作られてない理由が説明できない。もしかしたら、仕組みがいくら面白くても、実際のメンタルモデルにフィットせず、実は使い物にならないんじゃないか ---- と疑ってみるところから考えなおしてみました。

TypeScript で継承したらこうなりました。

class A {
    public p:number;
    public q:number;
    constructor(n:number) {
        this.p = n;
        this.q = n * 10;
    }
}

class B extends A {
    constructor(n:number) {
        super(n);
        this.p = n * 2;
        // this.q = n * 20;
    }
}

var b = new B(1);
console.log(b.p, b.q);

var __extends = this.__extends || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    __.prototype = b.prototype;
    d.prototype = new __();
};
var A = (function () {
    function A(n) {
        this.p = n;
        this.q = n * 10;
    }
    return A;
})();

var B = (function (_super) {
    __extends(B, _super);
    function B(n) {
        _super.call(this, n);
        this.p = n * 2;
        // this.q = n * 20;
    }
    return B;
})(A);

var b = new B(1);
console.log(b.p, b.q);

なんだこの __extends って。TypeScript は元のコードとわりと対応する JavaScript を吐くってことになってなかったっけ? なんで元のコードと対応しないこんな関数を作らなきゃいけないんだろう。

CoffeeScript ではどうかな?

class A
  constructor: (n)->
    @p = n
    @q = n * 10

class B extends A
  constructor: (n)->
    super(n)
    @p = n * 2
    # @q = n * 20

b = new B(1);
console.log b.p, b.q

var A, B, b,
  __hasProp = {}.hasOwnProperty,
  __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };

A = (function() {
  function A(n) {
    this.p = n;
    this.q = n * 10;
  }

  return A;

})();

B = (function(_super) {
  __extends(B, _super);

  function B(n) {
    B.__super__.constructor.call(this, n);
    this.p = n * 2;
  }

  return B;

})(A);

b = new B(1);

console.log(b.p, b.q);

やっぱり同じようなのができた。親のクラスを参照する方法が動的か静的か違うだけで、ほとんど同じことやってる。

ちょっとまって、JavaScript のプロトタイプチェーンの教科書でこの、n という初期化パラメータを持つオブジェクトの継承はどういうふうに実装したっけ??

function A(n) {
  this.p = n;
  this.q = n * 10;
}

function B(n) {
  this.p = n * 2;
  // this.q = n * 20;
}
B.prototype = new A(0);  // とりあえずオブジェクト要る、けどこれじゃダメだ

var b = new B(1);
console.log(b.p, b.q);
console.log(b.constructor);

おや? 伝統的な JavaScript の教科書的なプロトタイプチェーンだと、new A(0); のところがどうしてもうまく書けないぞ。あとで (new B(1) のときに) 初期化パラメータの値を決めたいのに、最初に何か仮に決めておかないといけない?? で、それだと b.q の値は new B(1) によって決定されるものと食い違う??

もっと不自然なのは、function A(n) { ... } がコールされるタイミング。B.prototype = ...; の時点、つまり何の実体も作られずただ定義だけしたときもうすでに、関数が呼ばれちゃってる。もし親になる A がタイムスタンプ取得やリソースのロックみたいな責務を持っていたら...

と、ここでプロトタイプ信者になった人がいうのは、「クラスベースとは違うのだよクラスベースとは」ってことなんですが、ちょっと落ち着いて、考え方が間違っているっていう前に、TypeScript と CoffeeScript がやっていることは何なのかを、なるべく平易な JavaScript に書き換えて考えてみますね。

function A(n) {
  this.p = n;
  this.q = n * 10;
}

function B(n) {
  A.call(this, n);  // ここで親の初期化プロセスを呼ぶ
  this.p = n * 2;
  // this.q = n * 20;
}
(function() {
  var tmp = function() {  // プロトタイプはパラメータを持ってはいけない
    this.constructor = B;  // __proto__の1段階目にconstructorプロパティがないJS謎仕様への対策
  };
  // hasOwnProperty はちょい割愛
  tmp.prototype = A.prototype;  // BのプロトタイプtmpはAに似た別のダミーオブジェクト
  B.prototype = new tmp();
})();

var b = new B(1);
console.log(b.p, b.q);
  • B のプロトタイプとして new されるのは A ではない
  • それは A に似た別のダミーオブジェクト
  • プロトタイプのコンストラクタはパラメータを持たない (new A(0); の件)
  • 親の初期化プロセスを初期化関数のように呼ぶ必要がある
  • __proto__ の1段階目にconstructorプロパティがないJS謎仕様への対策 ←ひどい

JavaScript でクラスベース継承で得られる恩恵を享受しようと思ったら、ユーザー側でこれだけのコードを書く必要があるんですかね? クラスベースのOOP言語でプロトタイプチェーンの真似をしたいときは、面倒だけどいちいちターゲットに移譲すれば何とかなるというシンプルさだというのに対して、あまりにフェアじゃないんじゃないですかこれ。

うーん。

そろそろプロトタイプチェーンの正義に準じて考えてみましょうか。プロトタイプチェーンは、例えていえばレストランで、無愛想な店長=プロトタイプとのやりとりしかできなかったところに、愛想のいい店員さんが入ってくれて、代わりにもっと愛嬌のある接客をしたり、お客さんのいろんな要望に答えたりしてくれる、みたいなやつですね。その実体は is-a 関係じゃなくてコンポジションによる拡張。なんでも継承で拡張しない、コンポジションを好むべきというプラクティスに合致している気がします。

気がします...

ところが JavaScript の初期設計には Java の圧力があったことを忘れちゃいけません。new Hoge(); だのコンストラクタだの、そもそもそれらは、本質的に必要なんでしょうか? なにか政治的な理由が、言語としての設計の完全性より優先したって可能性はないですか?

で、プロトタイプチェーンやるならこっちね、って最近 ECMA Script 5 で増えた、ナウなヤングに推奨な感じのやつがこれですよ。

Object.create - JavaScript | MDN

Object.create 概要 指定したプロトタイプオブジェクトおよびプロパティを持つ、新たなオブジェクトを生成します。

function Constructor(){}
o = new Constructor();
// これは以下と同じです:
o = Object.create(Constructor.prototype);
// もちろん、実際にコンストラクタ関数の初期化コードがある場合でも、Object.create はそれを反映できません

Polyfill

if (!Object.create) {
  Object.create = function (o) {
    if (arguments.length > 1) {
      throw new Error('Object.create implementation only accepts the first parameter.');
    }
    function F() {}
    F.prototype = o;
    return new F();
  };
}

この代替コードの中でやってることは、AltJS系がクラスベース継承を再現したものをJSに書き下したコードにずいぶん近いですね。コンストラクタは何でもいい、要するにターゲットオブジェクトに変更安全ラッパーをかぶせた何らかのオブジェクトが作れればいいんだ。

...やっぱりね。ユーザーコードに「コンストラクタ」なんて存在、そもそも必要なかったんじゃん。ECMA 曰く、形を定義してから実体を作っていく必要なんてない、と。いきなり実体を持つオブジェクトが存在して、ユーザーはその実体に皮をかぶせたオブジェクトを、好きなときに自由に作ってそれ(=皮)をデコレーションできるよ、コンストラクタ関数の定義なんて要らないよ、というやりかたでOKだそうですよ。Java に言わされてただけなんですっ newconstructor もなかったことにしてください、と...?

ここ、F.prototype = o;oConstructor.prototype ですね。クラスベース再現のほうでは、tmp.prototype = A.prototype; に対応しますね。オブジェクト直接ではなく、あくまで、型があって、その型にプロトタイプオブジェクトが存在するという考え。

プロトタイプチェーンの信奉者は、リアルな値(実際に構造がどうなってるか予測不能)が先にあって、さてこれをどうしよう、という発想なんだけど、これどうなんでしょ? 制限された型(構造は想定の範囲内)が先にあって、それらの関係という抽象概念に対して値を投入する、という発想のほうが、ソフトウェア設計として健全なアイデアなんじゃないかと、自分なら考えますけどね...

たしかに、チェーンの先の値や関数であっても、自分が持っているプロパティのように this.foo とするだけで読み取りアクセスできるのは楽ですが、さて、読み取りアクセスしたもを書き込みアクセス this.foo *= 2; などすると、別の実体ができてしまいますよね。で、またそれを読むと、こんどはプロトタイプのほうにアクセスしない。えーと、これって本当に移譲って言える?

いっぽう、明示的な移譲には何の問題もなかった。さほど面倒でもないし、意図しない操作はターゲットに届かない。

class Operator
  constructor: (@target)->
  exec: ->
    @target.foo *= 2

data = { foo: 1, bar: 10 }

op = new Operator data
op.exec()

console.log data.foo  # =>2

えーと、もしかして JavaScript のプロトタイプチェーンは、オブジェクト間の移譲もマトモに機能していない?

イエス!

ユーザーレベルで使えるプロトタイプチェーンとは、コピーコストをケチったクローンという用途を除いて、あらかじめ定義されたメソッドのセットを引き継ぐしかできない、単一選択のミックスインでしかないのであーる。

あれ...? んーっと、 _.extend(targetObject, myMethodSet) っと...

// Extend a given object with all the properties in passed-in object(s).
_.extend = function(obj) {
  if (!_.isObject(obj)) return obj;
  _.each(slice.call(arguments, 1), function(source) {
    for (var prop in source) {
      obj[prop] = source[prop];
    }
  });
  return obj;
};

どう考えてもこっちのほうがいいですね。何個でも、何回でもいけるもん。そりゃあ Underscore も lodash も大人気なわけだ。

ユーザーの本音「あんなややこしいもん使うぐらいなら、多段な継承なんかなくてもいいよ。他に使える道具は十分揃ってるからなくても我慢できる。使えたとしても、あんなもん手出したら逆に生産性が落ちる」

かくして、JavaScript はクソの役にも立たないプロトタイプチェーンを封印し、すべて関数と単一階層オブジェクトで語れる世界で予定調和を迎えましたとさ。

いっぽうで、この隠されたプロトタイプの秘術を利用してコンパイラを作り、あろうことか邪教であるクラスベースオブジェクト指向JavaScript 内に再現しようとするのが AltJS、という世界の流れなわけです。

いやー、そう考えるしかないでしょこれどう考えても。npm 見ても、AltJS 言語見ても。客観的に。(自分だってそんな結末は期待してなかったけど)

感想

プロトタイプチェーンという「しくみ」の魅力にハマることなく、現実世界でメンタルモデルにフィットし、かつ、実際に使える技術とは何か、ということを見失ってはいけないと思いました。あと、やっぱり JavaScriptJava の政治圧力を受けてだましだましリリースされた、ブラウザマクロのための中途半端なデザインの言語なのだというのは、いまだに重要な視点なんだなと思い出し直しました。

ウィーン ウィーン マサカリ防壁展開中...

保守性・管理性が劇的に上がるScalaのスマートなコードの書き方(ネタ)

最初に断っておくと、Scalaは初心者だからマサカリなんて怖くないもん。というわけで、ScalaになるとPHPではご法度だったいろいろが、どういうふうに解釈できるか考えてみました。

中カッコを省略する

def fizzbuzz(numbers:Seq[Int]) = {
  numbers.map((n) => {
    if (n % 15 == 0) {
      "FizzBuzz"
    } else if (n % 3 == 0) {
      "Fizz"
    } else if (n % 5 == 0) {
      "Buzz"
    } else {
      Integer.toString(n)
    }
  })
}

println(fizzbuzz(1 to 15))

こうやる

def fizzbuzz(numbers:Seq[Int]) = numbers.map((n) =>
  if (n % 15 == 0)
    "FizzBuzz"
  else if (n % 3 == 0)
    "Fizz"
  else if (n % 5 == 0)
    "Buzz"
  else
    Integer.toString(n)
)

println(fizzbuzz(1 to 15))

中カッコを外すことによって、if-else のネストが1本の式で、その中身も単一の式で、関数が単独の if 式であるということがわかるようになります。

中カッコを外してコンパイルエラーが出るようなコード、つまり何かをやるとき2つ以上のステートメントを必要とするコードは、関数型言語的でない状態を持つ可能性があります。状態がなければテストも簡単で、とても保守性アップにつながりますね。

デフォルト引数を使う

def fn(a:Int, b:Int=0) = a + b
fn(1)

便利ですね。ただまあ、これでもいいですが、せっかく型があるなら、個人的にはオーバーロードのほうが嬉しいです。jQueryの引数みたいに真ん中のを省略できる、とかなっちゃっても、型をちゃんと付けてオーバーロードしてれば平気な気がします。

def fn(a:Int, b:Int) = a + b
def fn(a:Int) = a  // fn(a, 0)
fn(1)

グローバル変数

みんな大好きグローバル変数

object TheEarth {
  var population = 600000000
}

TheEarth.population += 100000000
println(s"地球人口はいまや${TheEarth.population}人に")

Scalaのコンパニオンオブジェクトはシングルトンなので、入れ物の時点で完全な実体を持ったグローバル変数なところがJavastaticより優れてるし、PHPみたいにいちいち関数の中で宣言しなくても import で見えるようになります。...こわ

構造化されてない裸の変数だから悪い? 構造化されてようがされてなかろうが、グローバルなものが見えることが悪い? ちがいますね。状態の変化がテストでフォーカスしているインスタンスに閉じないことが悪いんですよね。見えるのはかまわないんですよ。

つまりけっきょく、 できるだけ var はやめろ とくにグローバル変数、ってことですよ。犯人は var ってこと。スコープの広さより先に、状態が変化することが問題。逆に言えば、global でもそれは状態を書き換えない前提なら、そこまで悪くはない。

これがもし val だったら、「式で表現された定数」ってことで、すごくいいですね。グローバル変数なのに。

文字列内に変数を展開する

あ、先やってもうた...

println(s"地球人口はいまや${TheEarth.population}人に")

展開するかしないか s"..." で明示的に分けられるのは、LL系の言語にあまりないメリットですね。

自分を呼ぶ関数を使う

再帰アツいですね。階乗を計算して 5! 求めてみます。

def factorial(n:Int):Int =
  if (n <= 1) 1 else factorial(n - 1) * n

println(factorial(5))

前までの階乗に今の数をかける。5! = 4! * 5 ですね。 でもこれ末尾再帰してないのでスタック使っちゃいます。なんとかしてみた。

def factorial(n:Int):Int = {
  def f(x: Int, s:Int): Int =
    if (x <= 1) x * s else f(x - 1, x * s)
  f(n, 1)
}

これなら末尾再帰しますよ。IntelliJ IDEA ならぐるぐるマークも違うのが出てわかりやすいですね。

末尾再帰しない

末尾再帰する

すごいねー...

... えーと

... ... えーと

def factorial(n:Int):Int = (1 to n).reduce((s, n)=> s * n)

なんかもんくあっか。必要以上に再帰使わなくても、畳み込の方がいいんじゃないですかね。これ末尾再帰と同じ概念の別表現なわけで、ユーザーが意識するスコープが小さくて済む。このへんのメソッドボキャブラリーを増やすほうが、保守性高いコードになると思いますよ。

ちなみにこれ、IntelliJ 先生がこう書いたほうがいいんじゃないかと警告してくれました。

def factorial(n:Int):Int = (1 to n).product

IntelliJ 先生の添削おそるべし。

結論

中括弧なしはダメ、グローバル変数はダメ、なぜって教科書に書いてあったもん、っていうような認識で他人を批判しちゃうのは、ちょっと踏みとどまったほうがいいかも、と思いました。

で、アルゴリズムを組めることがプログラミング技術なんじゃなくて、その言語の一般的なボキャブラリーをどれだけ適切に使えるかが、保守できるプログラムを書く技術なんじゃなかなと思った次第です。なので、やっぱりその、経験すべき通過点を十分通ってないのに人様に講釈たれるのは、違うなーと...

で、まあ、自分も経験はぜんぜん足りないので、それ補うためにも、保守性の高いScalaを書きたければ IntelliJ IDEA を使うのが一番いいです。

あぁ、それと、保守性の高いPHPを書きたいんなら、同じ理由で PhpStorm をすぐに買えばいいと思いますね。

うまれかわるMVC 〜PHPカンファレンス関西2014にむけて

PHPカンファレンス関西2014リレーブログ11人目です。イレブンです。イレブンといえばワールドカップ観戦で忙しいこの時期ですが、みなさんPHPカンファレンスへの心の準備はいかがですか。サッカー疲れでバテないように、テンション上げていきましょう。

先週は、@tbsmcd さんの『機関区 : カンファレンスで起きる何か』で終わっていました。うまれかわったPHPerのエピソード、涙腺がゆるみますね。今年は、もっと多くのビギナーが最後まで楽しめるように、という構成を意識してみました。まだ勉強会慣れしていない人も、いい意味でショックを受けてもらい、うまれかわり感を持って帰ってもらえたらと思います。

さてタイトルの MVCMVC といえばもちろん Microsoft Visual C++ ですよね。ちがいますね。ごめんなさい。いまどきの PHPer にとっては MVC = Mac, Vagrant, Composer ですね。

Mac:

一時期「勉強会のMac率は異常」みたいな言われようしましたが、もういいかげん馴染んできました。勉強会でMacなのはもう当たり前という空気ができています。Macのメリットってなんだろうと考えると、だいたいこんなところでしょうか。

  • アルミ削り出し筐体が硬い = 持ち運んでも安心
  • 他のOSより修飾キーがひとつ多い
  • キーボードが光る
  • Windowsをインストールできる

...そこじゃないでしょというツッコミが聞こえてきたので追記。

  • いきなり bash が起動
  • はじめから ssh 入り
  • はじめから UTF-8
  • サーバと同じコマンドで操作できる
  • PHPRubyで「Windows特有」の影響を受けなくて済む
  • 電源/外部モニタのコネクタが同じ人が多い
  • 使っている人が多くてノウハウが豊富

というわけで、人と同じのが嫌だというオシャレさんは、Mac以外を選ぶといいですね。あれ? 数年前ってたしかこれ逆でしたよね。よくよく考えると、Macのメリットは単に「サーバと共通だから」「開発者間で共通だから」なんですよね。本質的には、Windowsと立場が逆になっただけで、コミュニケーションというか、インターフェースの問題なだけな気がしています、個人的には。

Vagrant:

Vagrantは、歴史は短いのに、あっという間にWeb開発者の共通語になりました。もっとも有名な、OS仮想化のフロントエンドですね。仮想OSだと、別の人のマシンに自分の持っているものと同じ環境設定を再現できる。もう時代は、開発用共用サーバーにFTPじゃないんです。php.ini がちょっとだけ違うせいで半日トラブルを追いかけていたのが無駄、なんてことはなくなりました。

OSを仮想化してしまえば、NginxだろうとPHP-FPMだろうとNode.jsだろうと未知のRubyGemsだろうと、なんだって入れられます。前の環境に戻せなくなるのが怖くて新しい技術を試せない、なんてことはありませんね。

仮に上のMがMacではなくMicrosoftの人だったとしても、開発ターゲットはみんな同じ仮想OSに揃えられるんです。それも、同じやりかた、vagrant コマンドという共通語で。

Composer:

最近のPHPの世界観にもっとも影響を与えたのがこれです。依存ライブラリのインストール自動化。Composerのおかげで、各クラウドベンダーは、PHPのPaaSを同じルールで提供できるようになりました。もちろんPaaSでなくても、ライブラリのバージョン管理が非常に楽になりました。

楽になりました、というと、「あれ? 昔そんなに苦労していた気がしないけどなぁ」と思うかもしれなませんが、そういうことじゃないんですよ。

昔はzipを解凍するだけオールインワン、あっても追加ライブラリ2〜3個、それでいけるのがPHPの強みでした。が、本当は、世界中の開発者の力を少しづつ集めると巨大企業を凌ぐ信頼性を得られるのが、オープンソースの意義ですよね。オールインワンに依存すると、あらゆる課題を1つのプロジェクトに握られる。1プロジェクトの力はさほど大きくありません。

つまり、楽になった結果として、かつて使われていたよりもはるかに多くの外部ライブラリを使うようになり、以前より設計が多クラス化してくるようになったことがポイントです。ずばり、Composerがなかったら、Symfonyコンポーネントがこれほど受け入れられることもなかったでしょうし、ましてや、名前空間の意義も...。もしいまだに、自分のコードがシステムインストールのPEARと競合して泣いていたなんて考えたら、ぞっとしますね。

あ、そういえば、PHPカンファレンス関西2014の基調講演は、「全てを結ぶ力」だそうです。MVC = (Mac,Vagrant,Composer) は、技術そのものではなく、エンジニアを結ぶ力であることが面白いところでしたね。さてじゃあ「全て」とは何なのか ... 気になりますね。

ご参加お待ちしております。

まじめにやってください

(読者の声: あの、そういう話が聞きたくてこのページ開いたんじゃないんですけど...)

えっ? あ、MVC? モデル・ビュー・コントローラ?

---- あれは ... 夜空の星になりました。えぇ

だいたい、なんで MVC すぐ死んでしまうんでしょうね。これからはMOVEだとか、MVVMだとか、新しいこと言うために毎回やられ役です。生き返ったとは聞いた覚えがないのに、またすぐ死んだって言われちゃう。もうね、いいかげん生きてるとか死んでるとか、そういう次元から解き放ってあげませんか。

MVCは人間とそのメンタルモデルに関するものであり、オブザーバパターンに関するものではない。

DCIアーキテクチャ - Trygve Reenskaug and James O. Coplien - Digital Romanticism

ほら、物理的にどういう形をしているか、とは違う次元の概念って、言い出しっぺの人が言ってるじゃないですか。

モデルの変化がビューに通知されようがされまいが、ナントカControllerに明らかにビジネスロジックを書いていようが、いや、もういっそソースコードを見なくても、画面を持つあらゆるアプリケーションには、

  • アプリケーションの本質を構成する部分
  • 人間に見せるために作られている部分
  • コンピューターシステムにそれらを乗せるがためにどうしても必要なコード

この3つがあるでしょって話ですよ。それらがコード中のどこにどう配置されてようが、存在しないなんてことはありえない。

神話や宗教では、そういうのは、人間や動物の仲間ではなくて、精霊とか神とか言われますね。だからお空のお星様でいいんです。どれだけ人間界が汚れようとも、天界かどっかで永遠に生きてる、現世では実体のない存在。人の無意識にあって正義感の拠り所になっている。MVCはいつも人々の心の中に、それでいいじゃないか、と。

MVC、ぼくたちはきみのことを忘れない、永遠に...

フレームワークごとに、いろいろな形で3つのフォルダに入れられて、いつもどこかでdisられていたね、今となってはなつかしい思い出だよ...

f:id:tanakahisateru:20140622021744p:plain

はいっ、濃い人はこんな感じでテンション上げていきますよ〜。

いよいよ本番は今週末、次は @shin1x1 さん、Shin x blog です。お楽しみに。

Yii2.0-beta v.s. Laravel4.1 ベンチマーク

PHP5.5.13のビルトインサーバーで、Yii2.0-betaのDBアクセスを含めた実装をベンチマークテストしてみました。あ、ベンチマークは意味が無いとかいうのはナシです。

HelloWorldベンチだと、ルーティングとビューのオーバーヘッドを比較するしかできません。簡単にチートできてしまいます。データベース接続などのライブラリをプリロードしている方が不利になってしまいます。Yii1は公式発表のHelloWorldベンチがずば抜けて速かった(曰く、ほとんどのコードは必要になるまでロードされないことを表しているらしい)のですが、そういう部分だけを際立たせて、だから全体が速い/遅いと考えるのはおかしいです。

そこで、postとcommentテーブルを持つ同じデータベースに接続して、postデータを1件とそれに付随するコメントをすべて取得する(実際にはデータが1件だけある)処理を含みました。

比較対象は ... Laravel です。

比較したかった理由:

DBクエリの結果がArrayになる系のものは除外します。またDoctrineは、動的なActiveRecordとは動作特性が違いすぎました。同じ種類のもので比較したいとなったとき、Eloquent ORMがすぐに動くのがLaravelでした。

...という理由はそれほどたいした問題ではないです。

比較したかった真の理由:

Laravelは中身にSymfonyコンポーネントを使っていて、いわば二重の構造になっています。さらに、記述姓のために実行時にメタプログラミング的なトリックをしかけています。

Yii2はパフォーマンスを重視して、コア部分にサードパーティーライブラリの層を持たない方針で設計されています。また、IDEでの生産性も重視しており、その結果、PHPの型システムに素直に従ったコードになっています。

で、まあ、Laravelのほうが遅いのはわかっていて、実はポイントは、この作りの違いがPHPコードキャッシュでどれぐらいの差になって現れるのだろうか、というのを知りたかったというわけです。

テストに用いたデータの構造:

CREATE TABLE `post` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) NOT NULL,
  `body` text NOT NULL,
  `created_at` int(11) NOT NULL,
  `updated_at` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `fk_post_category_id` (`category_id`),
  CONSTRAINT `fk_post_category_id` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `comment` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `post_id` int(11) NOT NULL,
  `body` text NOT NULL,
  `created_at` int(11) NOT NULL,
  `updated_at` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `fk_comment_post_id` (`post_id`),
  CONSTRAINT `fk_comment_post_id` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Yii2

コントローラ

<?php
namespace frontend\controllers;

use common\models\Post;

class PostController extends \yii\web\Controller
{
    public function actionBenchmark($id)
    {
        $model = Post::findOne($id);
        if (!$model) {
            throw new NotFoundHttpException();
        }

        $this->layout = 'benchmark-layout';
        return $this->render('benchmark', [
            'model' => $model,
        ]);
    }
}

ビュー

<?php
use yii\helpers\Html;

/**
 * @var yii\web\View $this
 * @var common\models\Post $model
 */
?>
<h1><?= Html::encode($model->title) ?></h1>
<p><?= nl2br(Html::encode($model->body)) ?></p>

<ul>
<?php foreach ($model->comments as $comment): ?>
    <li><?= nl2br(Html::encode($comment->body)) ?></li>
<?php endforeach; ?>
</ul>

レイアウト

<?php
use yii\helpers\Html;

/**
 * @var \yii\web\View $this
 * @var string $content
 */
?>
<?php $this->beginPage() ?>
<!DOCTYPE html>
<html lang="<?= Yii::$app->language ?>">
<head>
    <meta charset="<?= Yii::$app->charset ?>"/>
    <title><?= Html::encode($this->title) ?></title>
    <?php $this->head() ?>
</head>
<body>
<?php $this->beginBody() ?>
    <div class="container">
        <?= $content ?>
    </div>
<?php $this->endBody() ?>
</body>
</html>
<?php $this->endPage() ?>

ActiveRecord

<?php
namespace common\models;

use Yii;
use yii\db\ActiveRecord;

/**
 * This is the model class for table "post".
 *
 * @property integer $id
 * @property string $title
 * @property string $body
 * @property integer $created_at
 * @property integer $updated_at
 *
 * @property Comment[] $comments
 */
class Post extends ActiveRecord
{
    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return 'post';
    }

    // 途中割愛

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getComments()
    {
        return $this->hasMany(Comment::className(), ['post_id' => 'id'])->orderBy(['created_at' => SORT_ASC]);
    }
}
<?php
namespace common\models;

use Yii;
use yii\db\ActiveRecord;

/**
 * This is the model class for table "comment".
 *
 * @property integer $id
 * @property integer $post_id
 * @property string $body
 * @property integer $created_at
 * @property integer $updated_at
 *
 * @property Post $post
 */
class Comment extends ActiveRecord
{
    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return 'comment';
    }

    // 途中割愛

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getPost()
    {
        return $this->hasOne(Post::className(), ['id' => 'post_id']);
    }
}

Laravel4

<?php

use Illuminate\Database\Eloquent\ModelNotFoundException;

class PostController extends Controller
{
    public function benchmark($id)
    {
        try {
            $post = Post::findOrFail($id);
        } catch(ModelNotFoundException $ex) {
            App::abort(404);
        }

        return View::make('post.benchmark', array(
            'post' => $post,
        ));
    }
}

ビュー

@extends('layouts.benchmark-layout')

<?php
/* @var $post Post 手で追加 */
/* @var $comment Comment 手で追加 */
?>

@section('main')

<h1>{{{ $post->title }}}</h1>
<p><?php echo nl2br(htmlspecialchars($post->body, ENT_NOQUOTES, 'UTF-8')) ?></p>
<ul>
    @foreach ($post->comments as $comment)
    <li><?php echo nl2br(htmlspecialchars($comment->body, ENT_NOQUOTES, 'UTF-8')) ?></li>
    @endforeach
</ul>

@stop

レイアウト

<!doctype html>
<html>
    <head>
       <meta charset="utf-8">
       <title></title>
   </head>
    <body>
        <div class="container">
            @yield('main')
        </div>
    </body>
</html>

Eloquent ORM

<?php

/**
 * @property string $title 手で追加
 * @property string $body 手で追加
 */
class Post extends Eloquent
{
    protected $table = 'post';

    public function comments()
    {
        return $this->hasMany('Comment', 'post_id')->orderBy('created_at', 'asc');
    }
}
<?php

/**
 * @property string $body 手で追加
 */
class Comment extends Eloquent
{
    protected $table = 'comment';

    public function comments()
    {
        return $this->belongsTo('Post', 'post_id');
    }
}

コード比較

Yiiのほうがコードが長いですが、大部分は自動的に生成されたコードです。最初はアセットマネージャー、CSRF対策、Flashメッセージなどがあったのですが、それらがベンチマーク結果に影響しないほうがいいと考え、ビューから取り除きました。それでもまだ、ビューにいくつか謎のメソッドコールがありますが、それはフレームワークの機構との関係が強いため、通常外すことがないものです。そこは最低必要なオーバーヘッドだと考えて残しました。

LaravelはIDE用のdocコメントを追加する手間がありました。ide-helper を使ったのですが、hasMany() の先に orderBy() メソッドがないという警告と、App::abort(404); が例外で脱出するのを理解せずに $post 未初期化の可能性の警告が消せませんでした。それを除けば、コード量は十分少なく、やっていることはダイレクトです。

パフォーマンス比較

ともに、PHPビルトインサーバーで動かします。Webサーバが絡む現実のシステム負荷ではなく、純粋にPHPだけの負荷を際立たせたかったので。

OPCache なし/あり で確認。YiiはDB関連のキャッシュを2段階設けました。LaravelのDBログにテーブル定義を見ている様子がなかったので、Yiiはスキーマキャッシュがあるものが標準だと考えるのがよさそうです。接続は4コアCPUなので4本並列で。

siege -c 4 -t 10S -b -q http://localhost:8082/post/benchmark/2

f:id:tanakahisateru:20140616022654p:plain f:id:tanakahisateru:20140616022721p:plain

OPCacheなし

  • Laravel = 10.16 trans/sec
  • Yii チューニングなし = 14.36 trans/sec
  • Yii DBスキーマキャッシュあり = 18.00 trans/sec
  • Yii さらにクエリキャッシュあり = 18.31 trans/sec

OPCacheあり

  • Laravel = 27.63 trans/sec
  • Yii チューニングなし = 47.91 trans/sec
  • Yii DBスキーマキャッシュあり = 69.53 trans/sec
  • Yii さらにクエリキャッシュあり = 72.86 trans/sec

OPCacheの効果

  • Laravel = 2.7倍
  • Yii チューニングなし = 3.3倍
  • Yii DBスキーマキャッシュあり = 3.9倍
  • Yii さらにクエリキャッシュあり = 4.0倍

OPCacheなしで見ると、やっぱり、2層あるLaravelは、1層のYiiに対して、倍は行かないまでも、それなりの遅さになるなぁという差が出ました。これは、Laravelが、コアの堅牢さをサードパーティに委ねることで、上のレイヤーに開発リソースを割けるのがいいという方針で、自覚して進めた結果ですし、それほど驚くところじゃないですね。

それよりも、注目なのはOPCacheの効果です。OPCacheを入れると何倍速くなるか。Laravelが得るものに対して、PHPの型に素直なコードであるYiiのほうが、より多くを得られることがわかります。コードが予測できれば、ランタイム最適化に有利なのかもしれません。

もし将来、PHPのコードキャッシュがJVMやV8のようなネイティブコードレベルの最適化を推し進めたら、Javaでリフレクションがボトルネックになる的な問題にならないかな...?