RailsユーザーだけどPHP/Symfony触ってみた


もうすぐPHPとSymfonyを使うかもしれないので、雰囲気をつかむために触ってみました。PHPからして初めてなので、軽くそこも勉強しつつ。

[Symfony](http://symfony.com/)は、フルスタックでMVCなWebアプリケーションフレームワークなんですって。

### こんなん作る

最低限のMVCを組み込める以下のようなWebアプリを、RailsとSymfonyで作り比べてみることにしました。

ちなみに私、Railsユーザって言ってもそんなにRails使い込んでるわけではないですが、まあいい比較対象かなと。

コメントを保存するボタン、保存したコメントのリスト表示、全コメントを削除できるリンクのあるアプリです。画面は1つだけ。

![rails2.png](https://qiita-image-store.s3.amazonaws.com/0/64812/f8d80245-5b92-c866-39b9-10f78928071a.png)

### Railsで

とりあえずRailsで作ってみました。


#### 環境

[Cloud9](https://c9.io/)の初期環境のままです。
Cloud9は、クラウド上で動作するIDEです。ワークスペース1個作ると開発環境が一通り揃ったUbuntu1つもらえるので、とりあえず何か動かしたいときは便利。

 * Ubuntu 14.04.3 LTS
 * ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-linux]
 * Rails 4.2.5

#### 作って動かす

アプリ`commenter-r`を新規作成、`comments`という名前でControllerを生成し、`Comment`という名前でModelも生成＆マイグレーション。

```shell
$ rails new commenter-r
$ rails generate controller comments
$ rails generate model Comment content:string
$ bundle exec rake db:migrate
```

Controllerとルーティングのコードはこんな感じ。

```ruby:comments_controller.rb
class CommentsController < ApplicationController
  # 全Commentをリスト表示
  def index
    @comment = Comment.new
    @comments = Comment.all
  end

  # コメントをPOST内容から新規作成してindexにリダイレクト
  def create
    Comment.create(content: comment_params[:content])
    redirect_to comments_url
  end

  # コメントを全削除してindexにリダイレクト
  def destroy
    Comment.destroy_all
    redirect_to comments_url
  end

  # Strong Parameters用コード
  …
end
```

```ruby:routes.rb
Rails.application.routes.draw do
  get 'comments', to: 'comments#index'
  post 'comments', to: 'comments#create'
  delete 'comments', to: 'comments#destroy'

  root 'comments#index'
end
```

画面は1個なので、Viewはこれだけ。

```erb:index.html.erb
<h1>コメントリスト</h1>

<%= form_for(@comment) do |f| %>
  <%= f.text_field :content %>
  <%= f.submit %>
<% end %>

<ui>
  <% @comments.each do |comment| %>
  <li><%= comment.content %></li>
  <% end %>
</ui>

<%= link_to '全て削除', comments_path, method: :delete %>
```

動作確認。普通は`rails s`だけど、クラウド上にあるマシンのlocalhostで動かしてもしょうがないので、Cloud9では以下のようなコマンドで起動させます。

```shell
$ rails s -p $PORT -b $IP
```

とりあえずこれで大作アプリ(笑)完成しました。

![rails.png](https://qiita-image-store.s3.amazonaws.com/0/64812/383c422b-19a8-bf48-2b14-2695fe68b102.png)

### Symfonyで

さあSymfonyでも作るぞ。

#### PHP…？

そもそもPHPが初めてだったので軽く勉強しました。[Wikipedia](https://ja.wikipedia.org/wiki/PHP:_Hypertext_Preprocessor)とかQiitaとかの入門記事色々読みました。意外とWikipediaの記事よかったです。

Symfonyのドキュメントも含め、ネット上の記事は古いバージョンのPHP向けの記述も多く、注意が必要です。（現場で使えるPHPのバージョンはまだわからないけど…）Qiitaでは@tsdsanの[モダンPHPアンチパターン - Qiita](http://qiita.com/tadsan/items/157969b338fd8b782b21)が、過去と現在のPHPの雰囲気をつかめてありがたかったです。

PHPのオレオレ理解。
 * javaっぽい見た目
 * `.`の代わりに`->`
 * 変数の頭に`$`
 * ファイルの先頭に`<?php`と書くのは鉄の掟。なぜか閉じなくてよい。
 * `array()`は古語。読むのはともかく自分のコードでは`[]`と書きたい。
 * RubyでいうBundler、PHPではComposer

ちょっとSymfony触るだけなので、言語に関してはサラッとすます感じですが、そのうちちゃんと勉強します…

#### 環境

Rails版と同じく、Cloud9で作ることにします。

 * Ubuntu 14.04.3 LTS
 * PHP 5.5.9-1ubuntu4.14 (cli) (built: Oct 28 2015 01:34:46)
 * Symfony version 3.0.3

#### Symfonyインストール＆アプリ新規作成

[公式](http://symfony.com/doc/current/book/installation.html)に従い、Symfonyをインストールします。

```shell
$ sudo curl -LsS https://symfony.com/installer -o /usr/local/bin/symfony
$ sudo chmod a+x /usr/local/bin/symfony
```

そして`commenter-s`プロジェクトを新規作成。

```shell
$ symfony new commenter-s
```

作ったプロジェクトを起動する`rails s`に相当するコマンドは`php bin/console server:run`なんですが、やはりCloud9で動作させるにはちょっとコツが必要。Cloud9の公式に[Symfonyプロジェクトの起動方法](https://docs.c9.io/docs/symfony2)が載ってます。

![raw.png](https://qiita-image-store.s3.amazonaws.com/0/64812/2874cce3-62e2-305c-70e9-e237f2ed9c52.png)

とりあえず動きました。

#### バンドル作る

Symfonyで特徴的なのはバンドルという概念です。

Railsでは生成したアプリケーションの雛型にガリガリコードを書いてく感じですが、Symfonyではアプリケーション上にバンドルという再利用可能なモジュールを作り、そこにModel/View/Controllerなどのコードを書きます。Symfonyの本体やプラグインもすべてバンドルで出来ていて、フラクタルな感じがいいですね。

バンドルの命名規約のベストプラクティスも[公式](http://symfony.com/doc/current/cookbook/bundles/best_practices.html#bundles-naming-conventions)にあって、`{ベンダー名}/{機能名}Bundle`がおすすめらしいです。今回は`Oreore/CommenterBundle`という名前にしました。

```shell
$ php bin/console generate:bundle --namespace=Oreore/CommenterBundle
```

このコマンドでバンドルを生成してくれます。対話的に何やら聞かれますが、今回は全部デフォルトで作りました。

#### ルーティング

ルーティングは、Railsと同じようにルーティング用の定義ファイルに記述することもできて、[日本Symfonyユーザー会のドキュメント](http://docs.symfony.gr.jp/symfony2/book/routing.html)ではそのようになっています。

でも今イケてるのは、ルーティング先となるControllerのメソッドに、ルーティング元のパスとメソッドをアノテーションで書くスタイルらしいです。

こんな感じ。

```php:src/Oreore/CommenterBundle/Controller/DefaultController.ph
class DefaultController extends Controller
{
    /**
     * @Route("/", name="index")
     * @Method("GET")
     */
    public function indexAction() {}

    /**
     * @Route("/")
     * @Method("POST")
     */
    public function createAction(Request $request) {}

    /**
     * @Route("/destroy", name="destroy")
     */
    public function destroyAction() {}
}
```

ルートとルーティング先メソッドを並べて見れるため、いい感じだと思います。
ルーティングの一覧は、以下のコマンドで確認できます。

```shell
$ php bin/console debug:rou
```

#### Model

RailsでActiveRecordに当たるのが、SymfonyではDoctrineというモジュールです。

初めにEntityクラスを作り、これを元にしてDBテーブルを生成できます。

Entityは手書きでもつくれますが、以下のコマンドで対話的にEntityを作成できます。
text型の`content`フィールドを持つ、`Comment`クラスを作ってみます。

```shell
$ php bin/console doctrine:generate:entity
...
# Entity名
The Entity shortcut name: OreoreCommenterBundle:Comment
# カラム名
New field name (press <return> to stop adding fields): content
# カラム型
Field type [string]: text
...
```

これで`content`フィールドとそのgetter/setterをもつ`Comment`クラスがEntityフォルダに生成されました。各クラスメンバには、ORM用のアノテーションが付加されています。

次のコマンドで、作成済みのEntityクラスからデフォルトのDBエンジン（MySQL）のDBテーブルを生成してくれます。

```shell
$ php bin/console doctrine:schema:update --force
```

#### Controller

コントローラのindex用メソッド`indexAction`はこんな感じになりました。

```php:src/Oreore/CommenterBundle/Controller/DefaultController.ph
class DefaultController extends Controller
{
  /**
   * @Route("/", name="index")
   * @Method("GET")
   * @Template()
   */
  public function indexAction()
  {
    // Viewで使うformをセットアップ
    $comment = new Comment();
    $form = $this->createFormBuilder($comment)
      ->add('content', TextType::class)
      ->add('save', SubmitType::class, ['label' => 'Create Comment'])
      ->getForm();

    // DBからコメント一覧取得
    $repository = $this->getDoctrine()
      ->getRepository('OreoreCommenterBundle:Comment');
    $comments = $repository->findAll();

    // Viewに作ったインスタンスを渡す
    return [
      'form' => $form->createView(),
      'comments' => $comments
    ];
  }
```

基本的にRails版と同じことをやらせてますが、比べると大分コード量増えてますね… ここはまあ設計思想の違いかなと思います。

とにかく短くかけるけど最早Rails語、っていう感じのActiveRecordと違い、Doctrineの場合、Contollerのインスタンスから`Doctrine`、エンティティをフェッチするための`Repository`をとってきて、クエリを投げるというインスタンス間の責務が明確です。

それとViewで参照するための変数をreturnしてるのが特徴的かなと思います。テストしやすそうだし、インスタンス変数経由でViewに値を渡すRailsよりも副作用がない感じ。

ちなみにこのようにreturnできるのはアノテーションで`@Template()`と書いているからで、それがない場合はreturnでレンダリングするHTML文字列を含むResponceインスタンスを返す形になります。それはそれで、HTTPリクエストに対して文字列のレスポンスを返す、というWebアプリケーションの本分が一つのメソッドに集約される形になっていて、割と好きです。

Controllerの残りは、コメント投稿用の`createAction`とコメント全削除用の`destroyAction`です。

```php:src/Oreore/CommenterBundle/Controller/DefaultController.ph
class DefaultController extends Controller
{
  /**
   * @Route("/")
   * @Method("POST")
   */
  public function createAction(Request $request)
  {
    // フォームから投稿内容取得
    $data = $request->request->all();
    $content = $data['form']['content'];

    // エンティティインスタンスを作成
    $comment = new Comment();
    $comment->setContent($content);

    // エンティティインスタンスの内容をDBに保存
    $em = $this->getDoctrine()->getManager();
    $em->persist($comment);
    $em->flush();

    // indexにリダイレクト
    return $this->redirectToRoute('index');
  }

  /**
   * @Route("/destroy", name="destroy")
   */
  public function destroyAction()
  {
    // DBから全件取得
    $repository = $this->getDoctrine()
      ->getRepository('OreoreCommenterBundle:Comment');
    $comments = $repository->findAll();

    // 取得内容をDBから全件削除
    $em = $this->getDoctrine()->getManager();
    foreach ($comments as $key => $comment) {
      $em->remove($comment);
    }
    $em->flush();

    // indexにリダイレクト
    return $this->redirectToRoute('index');
  }
}
```

#### View ＆ 動作確認

SymfonyのデフォルトのテンプレートエンジンはTwigです。

ERBとかと大体同じで、`{{ ... }}`で囲うとPHP式の表示、`{% ... %}`でロジックを書けます。

今回はこんな風になりました。

```twig
<h1>コメントリスト</h1>

{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}

<ul>
  {% for comment in comments %}
    <li>{{ comment.content }}</li>
  {% endfor %}
</ul>

<a href="{{ path('destroy') }}">全て削除</a>
```

`form_xxx`のところは、Controllerで構築したformをデフォルトの見た目やパラメータでレンダリングしてくれる手続きです。

これで動作確認するとこんな感じになりました。
Rails版とちょっと見た目が違っちゃいましたが、機能的にはほとんど同じです。

![symfony.png](https://qiita-image-store.s3.amazonaws.com/0/64812/f5fb03b8-f9d8-040c-1cc0-f07ba40f4b7e.png)

### 感想とか!

 * バンドルという仕組みが面白い。
 * Railsほど、シンプルな記法にこだわってない。
 * アノテーションで出来ることが多い。IDEの恩恵高そう。

### 参考資料
 * [Symfony, High Performance PHP Framework for Web Development](http://symfony.com/) : 公式
 * [Symfony2 ドキュメントポータル](http://docs.symfony.gr.jp/) : 一部情報は古いですが、一読すれば雰囲気つかめました。実際にコーディングする前に、公式で対応する原典確認したほうがいいかも。
 * [Cloud9でSymfony2.8を動かす | kanonjiのブログ](http://kanonji.info/blog/2016/01/27/run-symfony2-8-on-cloud9/)
 * [PHP: Hypertext Preprocessor - Wikipedia](https://ja.wikipedia.org/wiki/PHP:_Hypertext_Preprocessor)
 * [モダンPHPアンチパターン - Qiita](http://qiita.com/tadsan/items/157969b338fd8b782b21)


### 成果物
 * [nakahashi/study-commenter](https://github.com/nakahashi/study-commenter)