オープンソースこねこね

Webプログラミングなどについてあれこれ。

SymfonyとLaravelを比べてみての考察

SymfonyとLaravelはPHPのWebアプリケーションフレームワークで 2014年現在、モダンなPHPフレームワークとして人気があるようです。 両方ともComposerによるパッケージ管理、MVCアーキテクチャ、 開発時のPHP組み込みサーバのサポート、 バンドル(Laravelではパッケージ)などでの機能拡張性を持っています。

ここ最近、個人的な興味でこのふたつのフレームワークを触ってみていたので、 感想と比較を述べてみます。

なお、実際に触ったバージョンはSymfony2.4とLaravel4.1です。 また実際のプロダクトの開発に使用したわけではないのと、 パフォーマンス比較などは行っていないことを断っておきます。

ざっくりとした比較イメージ

細かい感想と比較を書く前に、ざっくりとイメージを述べておきます。

  • Symfonyは構造がしっかりしていて重厚
  • Laravelは構造がフラットで軽量

ここでの重厚とか軽量とかは、プログラミングをしているときの感触であって、 アプリケーションの動作のことではありません。 また、開発するアプリケーションの規模にそれを合わせて、 サービスが小規模だからSymfonyは適していないとか、 大規模だからLaravelはよくないという話でもありません。

基本的にアプリケーションの規模が小規模でも大規模でもどっちのフレームワークを使ってもよいと思いました。

アプリケーションの規模にフレームワークの向き不向きが全くないとは思いませんが、 それよりも開発するプログラマのやり方、好みにマッチしているかどうか が一番のポイントなんじゃないかと思っています。以下、個別にピックアップします。

ネームスペース

PHPでは5.3からネームスペースによるコードの構造化がサポートされました。

Symfonyで開発するコードはこのネームスペースで厳格に構造化する必要があります。 たとえば、Pakagistのトップページ のコントローラはPackagist\WebBundle\Controller\WebController というネームスペースに置かれています。

Laravelではコントローラをはじめ多くのクラスがグローバルなネームスペースを使用します。Laravelの典型的なコントローラは以下のようにnamespaceなしで定義されます。

<?php
class HomeController extends BaseController {

    public function index()
    {
        return View::make('home.index');
    }
}

ネームスペースによる構造化はクラス名の衝突を避けられ、構造の見通しをよくします。 一方でそれに合わせてディレクトリ構造が深く複雑になりがちです。

Symfonyは構造が健全であることに重きをおいて設計され、Laravelは記述のシンプルさに重きをおいて設計されている感じです。

デフォルトのテンプレートエンジン

SymfonyはTwig、LaravelはBladeというテンプレートエンジンを標準で使います。

ところで、一世代まえのPHPフレームワーク(CakePHP,Symfony1,ZF1)だと、素のPHPをテンプレートエンジンに使用するのがスタンダードでした。 でも最近はそうでもなく、Symfonyの開発者であるFabien Potencierの ブログなどで言及 されているように、素のPHPでは「最適」なテンプレートの記述が行えないということから、 専用のテンプレートエンジンを利用するようになってきています。

素のPHPがスマートに対応できない領域は、テンプレート継承や、エスケープ処理、イテレータ処理などです。 もちろん、全くできないわけではないが、インラインのphpタグでテンプレートがごちゃごちゃしてしまうのが避けられない。 このへんの問題は、前述のFabienさんのブログでも参照してください。

で、SymfonyとLaravelのテンプレートエンジンのはなし。

SymfonyのTiwgは前述のPHPテンプレートのダメな部分を完全に取り除いて 理想的なテンプレートエンジンを目指して設計されている感じです。 テンプレート継承もできるし、エスケープも自動。またfor文は以下のように書ける

{% for user in users %}
    * {{ user.name }}
{% else %}
    No users have been found.
{% endfor %}

このテンプレートはPHPとは基本的に別ものであり、テンプレートにPHPを書くことはできません。

LaravelのBladeも同様にテンプレート継承やエスケープ機能を提供してくれますが、 設計としてPHPと別ものではなく素のPHPテンプレートに正規表現による薄い変換処理をかぶせて、ちょっと文法を拡張した感じのものです。 なので、bladeのテンプレートにはPHPのコードも書けるのですが、Twigのfor文みたいなPHPの書式から大きく異るような機能は用意されていません。

テンプレートに素のPHPを記述できることを、害悪ととるか柔軟性ととるかは、プログラマの好みによる話だと思います。 私はPHPを記述できたほうがいいと感じるタイプです。

Symfonyはその領域において最適なコンポーネントを提供するのに対して、 Laravelは最適ではないが、PHPプログラマが慣れ親しみやすいコンポーネントを提供する。 そういう設計になっています。

設定ファイル

設定ファイルの記述に関しても設計思想の違いが感じられます。 たとえば、URLとアクションをひもづけるルーティングの定義をSymfonyyamlアノテーションを使います。 (PHPでも記述できるが、かなり冗長なのでこれを使うひとは多分いない)

一方、Laravelは素のPHPを使います。ただしSymfonyPHP設定ファイルのような冗長さはなく、 グローバルネームスペースとスタティッククラスでRubyDSLっぽい簡潔な記述を提供します。

テンプレートエンジンの話と同様なのですが、 Symfonyは設定ファイルの記述においても最適な方法(PHPは最適ではないのでyamlなどの別の手段)を提供するのに対して、 LaravelはあくまでPHPを使い(PHPプログラマのためのフレームワークだからか)、その上で記述しやすい施策をとっている印象です。

まとめ

最後に抽象的にまとめると

といったところでしょうか。

LaravelのRouteクラスがグローバル空間で呼び出せる仕組み

最近Laravelを触っています。

で、LaravelだとURLと実行するアクションを関連付けるルーティング部分を

Route::get('/', function()
{
    return 'Hello World';
});

こんなふうに、RubySinatraっぽくかけるんですよね。ところがこのRouteというクラス、事前にuseで使う名前空間の指定もしていないし、実際にどのPHPクラスが使われているかよくわからない。そこでソースを追ってみました。

エイリアス

フロントコントローラのpubic/index.phpを起点に読み始めていくと、

pubic/index.php->bootstrap/start.php->vendor/laravel/framework/src/Illuminate/Foundation/start.phpとファイルが読み込まれて、この中で、

$aliases = $config['aliases'];

AliasLoader::getInstance($aliases)->register();

ということをやっている。でregisterメソッドで$aliasesのデータつかってspl_autoload_register関数を呼び出してオートローディングを設定している。$aliasesの内容はメインのコンフィグファイルconfig/app.phpにかかれていて、デフォルトは以下のようになっている。

'aliases' => array(

                'App'             => 'Illuminate\Support\Facades\App',
                'Artisan'         => 'Illuminate\Support\Facades\Artisan',
                'Auth'            => 'Illuminate\Support\Facades\Auth',
                'Blade'           => 'Illuminate\Support\Facades\Blade',
                'Cache'           => 'Illuminate\Support\Facades\Cache',
                'ClassLoader'     => 'Illuminate\Support\ClassLoader',
                'Config'          => 'Illuminate\Support\Facades\Config',
                'Controller'      => 'Illuminate\Routing\Controller',
                'Cookie'          => 'Illuminate\Support\Facades\Cookie',
                'Crypt'           => 'Illuminate\Support\Facades\Crypt',
                'DB'              => 'Illuminate\Support\Facades\DB',
                'Eloquent'        => 'Illuminate\Database\Eloquent\Model',
                'Event'           => 'Illuminate\Support\Facades\Event',
                'File'            => 'Illuminate\Support\Facades\File',
                'Form'            => 'Illuminate\Support\Facades\Form',
                'Hash'            => 'Illuminate\Support\Facades\Hash',
                'HTML'            => 'Illuminate\Support\Facades\HTML',
                'Input'           => 'Illuminate\Support\Facades\Input',
                'Lang'            => 'Illuminate\Support\Facades\Lang',
                'Log'             => 'Illuminate\Support\Facades\Log',
                'Mail'            => 'Illuminate\Support\Facades\Mail',
                'Paginator'       => 'Illuminate\Support\Facades\Paginator',
                'Password'        => 'Illuminate\Support\Facades\Password',
                'Queue'           => 'Illuminate\Support\Facades\Queue',
                'Redirect'        => 'Illuminate\Support\Facades\Redirect',
                'Redis'           => 'Illuminate\Support\Facades\Redis',
                'Request'         => 'Illuminate\Support\Facades\Request',
                'Response'        => 'Illuminate\Support\Facades\Response',
                'Route'           => 'Illuminate\Support\Facades\Route',
                'Schema'          => 'Illuminate\Support\Facades\Schema',
                'Seeder'          => 'Illuminate\Database\Seeder',
                'Session'         => 'Illuminate\Support\Facades\Session',
                'SSH'             => 'Illuminate\Support\Facades\SSH',
                'Str'             => 'Illuminate\Support\Str',
                'URL'             => 'Illuminate\Support\Facades\URL',
                'Validator'       => 'Illuminate\Support\Facades\Validator',
                'View'            => 'Illuminate\Support\Facades\View',

        ),

つまりRouteクラスの実体はIlluminate\Support\Facades\Routeということがわかる。

Laravelは自前で設定ファイルからオートローディングの登録を行うロジックをもっていて、それをつかってグローバル空間にクラスのエイリアスを作るという機能を実装していたわけだった。

※じつはIlluminate\Support\Facades\Routeにはルーティングに使用していた上記のgetメソッドは直接定義されておらず、ここからさらにFacadeという仕組みをつかって、別のクラスに実装を移しているのだけど割愛。

PHPDSLっぽいこと

調べてみると、どうってことはない作りでした。でもこの仕組みは、クラスの実装はネームスペースで構造化された空間に定義でき、インターフェースのみグローバル空間に公開するというのが、うまく出来てると思う。これに無名関数を組み合わせることで、LaravelのRoute定義のような、Rubyの内部DSL的な記述をPHP上に表現しているのだな、とちょっと感心したのでした。

おしまい。