もうすぐPHPとSymfonyを使うかもしれないので、雰囲気をつかむために触ってみました。PHPからして初めてなので、軽くそこも勉強しつつ。
Symfonyは、フルスタックでMVCなWebアプリケーションフレームワークなんですって。
こんなん作る
最低限のMVCを組み込める以下のようなWebアプリを、RailsとSymfonyで作り比べてみることにしました。
ちなみに私、Railsユーザって言ってもそんなにRails使い込んでるわけではないですが、まあいい比較対象かなと。
コメントを保存するボタン、保存したコメントのリスト表示、全コメントを削除できるリンクのあるアプリです。画面は1つだけ。
Railsで
とりあえずRailsで作ってみました。
環境
Cloud9の初期環境のままです。
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も生成&マイグレーション。
$ rails new commenter-r
$ rails generate controller comments
$ rails generate model Comment content:string
$ bundle exec rake db:migrate
Controllerとルーティングのコードはこんな感じ。
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
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はこれだけ。
<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では以下のようなコマンドで起動させます。
$ rails s -p $PORT -b $IP
とりあえずこれで大作アプリ(笑)完成しました。
Symfonyで
さあSymfonyでも作るぞ。
PHP…?
そもそもPHPが初めてだったので軽く勉強しました。WikipediaとかQiitaとかの入門記事色々読みました。意外とWikipediaの記事よかったです。
Symfonyのドキュメントも含め、ネット上の記事は古いバージョンのPHP向けの記述も多く、注意が必要です。(現場で使えるPHPのバージョンはまだわからないけど…)Qiitaでは@tsdsanのモダンPHPアンチパターン - Qiitaが、過去と現在の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インストール&アプリ新規作成
公式に従い、Symfonyをインストールします。
$ sudo curl -LsS https://symfony.com/installer -o /usr/local/bin/symfony
$ sudo chmod a+x /usr/local/bin/symfony
そしてcommenter-s
プロジェクトを新規作成。
$ symfony new commenter-s
作ったプロジェクトを起動するrails s
に相当するコマンドはphp bin/console server:run
なんですが、やはりCloud9で動作させるにはちょっとコツが必要。Cloud9の公式にSymfonyプロジェクトの起動方法が載ってます。
とりあえず動きました。
バンドル作る
Symfonyで特徴的なのはバンドルという概念です。
Railsでは生成したアプリケーションの雛型にガリガリコードを書いてく感じですが、Symfonyではアプリケーション上にバンドルという再利用可能なモジュールを作り、そこにModel/View/Controllerなどのコードを書きます。Symfonyの本体やプラグインもすべてバンドルで出来ていて、フラクタルな感じがいいですね。
バンドルの命名規約のベストプラクティスも公式にあって、{ベンダー名}/{機能名}Bundle
がおすすめらしいです。今回はOreore/CommenterBundle
という名前にしました。
$ php bin/console generate:bundle --namespace=Oreore/CommenterBundle
このコマンドでバンドルを生成してくれます。対話的に何やら聞かれますが、今回は全部デフォルトで作りました。
ルーティング
ルーティングは、Railsと同じようにルーティング用の定義ファイルに記述することもできて、日本Symfonyユーザー会のドキュメントではそのようになっています。
でも今イケてるのは、ルーティング先となるControllerのメソッドに、ルーティング元のパスとメソッドをアノテーションで書くスタイルらしいです。
こんな感じ。
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() {}
}
ルートとルーティング先メソッドを並べて見れるため、いい感じだと思います。
ルーティングの一覧は、以下のコマンドで確認できます。
$ php bin/console debug:rou
Model
RailsでActiveRecordに当たるのが、SymfonyではDoctrineというモジュールです。
初めにEntityクラスを作り、これを元にしてDBテーブルを生成できます。
Entityは手書きでもつくれますが、以下のコマンドで対話的にEntityを作成できます。
text型のcontent
フィールドを持つ、Comment
クラスを作ってみます。
$ 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テーブルを生成してくれます。
$ php bin/console doctrine:schema:update --force
Controller
コントローラのindex用メソッドindexAction
はこんな感じになりました。
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
です。
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式の表示、{% ... %}
でロジックを書けます。
今回はこんな風になりました。
<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版とちょっと見た目が違っちゃいましたが、機能的にはほとんど同じです。
感想とか!
- バンドルという仕組みが面白い。
- Railsほど、シンプルな記法にこだわってない。
- アノテーションで出来ることが多い。IDEの恩恵高そう。
参考資料
- Symfony, High Performance PHP Framework for Web Development : 公式
- Symfony2 ドキュメントポータル : 一部情報は古いですが、一読すれば雰囲気つかめました。実際にコーディングする前に、公式で対応する原典確認したほうがいいかも。
- Cloud9でSymfony2.8を動かす | kanonjiのブログ
- PHP: Hypertext Preprocessor - Wikipedia
- モダンPHPアンチパターン - Qiita