-
Notifications
You must be signed in to change notification settings - Fork 0
Home
このチュートリアルは Symfony勉強会#8 向けのものです。 当日のワークショップで作成したアプリがご確認いただけいます。 Symfonyのバージョンは Symfony2.2.1
質問や、間違っている箇所があった場合には @okapon_ponまでお気軽にメッセージください。
よろしければご記入ください。
https://github.com/okapon/symfony-workshop/wiki/_pages
TODO
- 基本ディレクトリ構成
- ファイルの役割
- フロントコントローラ、アプリケーションカーネル、コンフィギュレーションファイル
インストールは完了した上でワークショップにはご参加いただきました。 新しくアプリの作成される方は以下のインストール手順をご参考下さい。
http://docs.symfony.gr.jp/symfony2/book/installation.html
もし、php5.4を利用されているなら、わざわざWEBサーバーのセットアップを行わなくて済むので、built-in serverを利用されることをオススメします。
WEBサーバーを起動します。 php5.4なら、PHP Built-in Web Serverを利用するのが簡単です。
$ php -S localhost:8000 -t /path/to/work/Symfony/web/
http://localhost:8000 にアクセスしてみて wellcomeページが表示されればインストールは成功です。
MyBlogBundleを作成する
Symfony2では、アプリケーションはバンドル単位で作成していきます。実はSymfonyのフレームワーク自体も全てバンドルで構成されているのですが、ひとまずバンドルというものを作成してその中にコードを書いてくのだと理解して進んで下さい。
以下のコマンドを実行
$ php app/console generate:bundle --namespace=My/BlogBundle --format=annotation
コマンドを実行したら、Enterキーを押して進めていって下さい。
途中 yes
に書き換える箇所があるので注意して下さい。
Welcome to the Symfony2 bundle generator
In your code, a bundle is often referenced by its name. It can be the
concatenation of all namespace parts but it's really up to you to come
up with a unique name (a good practice is to start with the vendor name).
Based on the namespace, we suggest MyBlogBundle.
Bundle name [MyBlogBundle]:
The bundle can be generated anywhere. The suggested default directory uses
the standard conventions.
Target directory [/path/to/work/Symfony/src]:
To help you get started faster, the command can generate some
code snippets for you.
Do you want to generate the whole directory structure [no]? yes ← 後でtwitter bootstrapを導入するためyesに
Summary before generation
You are going to generate a "My\BlogBundle\MyBlogBundle" bundle
in "/path/to/work/Symfony/src/" using the "annotation" format.
Do you confirm generation [yes]?
Bundle generation
Generating the bundle code: OK
Checking that the bundle is autoloaded: OK
Confirm automatic update of your Kernel [yes]?
Enabling the bundle inside the Kernel: OK
Confirm automatic update of the Routing [yes]?
Importing the bundle routing resource: OK
You can now start using the generated code!
追加されたファイル
modified: app/AppKernel.php
modified: app/config/routing.yml
new file: src/My/BlogBundle/Controller/DefaultController.php
new file: src/My/BlogBundle/DependencyInjection/Configuration.php
new file: src/My/BlogBundle/DependencyInjection/MyBlogExtension.php
new file: src/My/BlogBundle/MyBlogBundle.php
new file: src/My/BlogBundle/Resources/config/routing.yml
new file: src/My/BlogBundle/Resources/config/services.yml
new file: src/My/BlogBundle/Resources/doc/index.rst
new file: src/My/BlogBundle/Resources/translations/messages.fr.xlf
new file: src/My/BlogBundle/Resources/views/Default/index.html.twig
new file: src/My/BlogBundle/Tests/Controller/DefaultControllerTest.php
ページにアクセスしてみます。 ホスト名は各自の環境に読み替えて下さい。 http://localhost:8000/app_dev.php/hello/Fabien
このURLにアクセスすると src/My/BlogBundle/Controller/BlogController.php
の indexAction()
が実行されます。
どのURLにアクセスすると、どのコントローラーが呼ばれるかは、@Route
アノテーションに記述された内容で決定されます。
Entityを生成するコマンドを実行
$ php app/console generate:doctrine:entity --entity=MyBlogBundle:Post --format=annotation --fields="title:string(255) body:text createdAt:datetime updatedAt:datetime"
Welcome to the Doctrine2 entity generator
This command helps you generate Doctrine2 entities.
First, you need to give the entity name you want to generate.
You must use the shortcut notation like AcmeBlogBundle:Post.
The Entity shortcut name [MyBlogBundle:Post]:
Determine the format to use for the mapping information.
Configuration format (yml, xml, php, or annotation) [annotation]:
Instead of starting with a blank entity, you can add some fields now.
Note that the primary key will be added automatically (named id).
Available types: array, simple_array, json_array, object,
boolean, integer, smallint, bigint, string, text, datetime, datetimetz,
date, time, decimal, float, blob, guid.
New field name (press <return> to stop adding fields):
Do you want to generate an empty repository class [no]? yes
Summary before generation
You are going to generate a "MyBlogBundle:Post" Doctrine2 entity
using the "annotation" format.
Do you confirm generation [yes]? yes
Entity generation
Generating the entity code: OK
You can now start using the generated code!
以下のファイルが生成されます。
new file: src/My/BlogBundle/Entity/Post.php new file: src/My/BlogBundle/Entity/PostRepository.php
MySQLの設定(parameters.yml)の設定・権限がうまくいっていれば、下記コマンドでテーブルを作成することができます。
$ php app/console doctrine:database:create
$ php app/console doctrine:schema:create
ATTENTION: This operation should not be executed in a production environment.
Creating database schema...
Database schema created successfully!
※ 今回は簡易な方法でデータベースやテーブルの作成を行いましたが doctrine:database:create
にはアプリケーションユーザーの権限で、create database
ができてしまう権限の問題がありますので本番環境では実行できないようにしておくのが望ましいでしょう。
また、doctrine:schema:create
についても、Doctrineのマッピングは完璧ではないため意図した通りのSQLが発行できるとは限りません。自分でSQLを実行し、そこからEntityを生成する方が良いでしょう。
ここから記事の一覧ページと個別記事ページを作成します。
まずは記事の一覧ページからです。
annotationを用いてroutingを定義します。
src/My/BlogBundle/Controller/BlogController.php
<?php
namespace My\BlogBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
/**
* @Route("/blog")
* @Template()
*/
class BlogController extends Controller
{
/**
* @Route("/")
*/
public function indexAction()
{
$em = $this->get('doctrine')->getManager();
$posts = $em->getRepository('MyBlogBundle:Post')->findAll();
return array('posts' => $posts);
}
}
src/My/BlogBundle/Resources/views/Blog/index.html.twig
<h1>Blog posts</h1>
<table class="table">
<thead>
<tr>
<td>ID</td>
<td>タイトル</td>
<td>作成日</td>
</tr>
</thead>
<tbody>
{# ここから、posts配列をループして、投稿記事の情報を表示 #}
{% for post in posts %}
<tr>
<td>{{ post.id }}</td>
<td><a href="">{{ post.title }}</a></td>
<td>{{ post.createdAt|date('Y/m/d H:i') }}</td>
</tr>
{% else %}
<tr>
<td colspan="3">No Posts</td>
</tr>
{% endfor %}
</tbody>
</table>
以下のURLにアクセスしてみます。
http://localhost:8000/app_dev.php/blog/
データベースにデータが入っていないので「No Posts」と表示されると思います。 それではデータベースにデータを入れてみます。
mysql> INSERT INTO Post (title, body, createdAt, updatedAt) values ('初めての投稿', '初めての投稿です。', NOW(), NOW());
再度アクセスすると追加した記事が表示されます。
続いて個別記事ページを作ります。
class BlogController extends Controller
{
// ...
/**
* @Route("/{id}/show")
*/
public function showAction($id)
{
$em = $this->get('doctrine')->getManager();
$post = $em->getRepository('MyBlogBundle:Post')->find($id);
if (!$post) {
throw $this->createNotFoundException('The post does not exist');
}
return array('post' => $post);
}
}
<h1>{{ post.title }}</h1>
<p><small>Created: {{ post.createdAt|date('Y/m/d H:i') }}</small></p>
<p>{{ post.body|nl2br }}</p>
以下のURLにアクセスしてみます。
http://localhost:8000/app_dev.php/blog/1/show
記事の詳細が表示されます。
記事一覧ページと記事詳細ページをリンクでつなぎます。
コントローラーの @Route
アノテーションにnameをつけて、テンプレートに記述します。
class BlogController extends Controller
{
/**
* @Route("/", name="blog_index")
*/
public function indexAction()
// ...
/**
* @Route("/{id}/show", name="blog_show")
*/
public function showAction($id)
// ...
@Route に name=""
をつけることで、そのルートに対して名前をつけることができます。つけたルート名を利用してテンプレート内でリンクを張る事ができます。
nameをつけていない場合、[ベンダープレフィックス][バンドル名][コントローラー名]_[アクション名]になります。
上記コントローラーのアクションの場合 my_blog_blog_index
や my_blog_blog_showになります
。
// ...
{% for post in posts %}
<tr>
<td>{{ post.id }}</td>
<td><a href="{{ path('blog_show', {'id': post.id}) }}">{{ post.title }}</a></td>
<td>{{ post.createdAt|date('Y/m/d H:i') }}</td>
</tr>
// ...
src/My/BlogBundle/Resources/views/Blog/show.html.twig の末尾に追記
<h1>{{ post.title }}</h1>
<p><small>Created: {{ post.createdAt|date('Y/m/d H:i') }}</small></p>
<p>{{ post.body|nl2br }}</p>
<p><a href="{{ path('blog_index') }}">一覧に戻る</a></p>
余談:コマンド
以下のコマンドで、symfonyで利用可能なコマンドの一覧が表示されます。
app/console
試しに app/console router:debug
を実行してみて下さい。
現在アプリケーションで有効になっているルーティング情報を見ることができます。
※ブログチュートリアルとは直接関係がないので余裕があればやってみて下さい。
以下からダウンロード http://twitter.github.io/bootstrap/getting-started.html
以下のディレクトリに展開
src/My/BlogBundle/Resources/public/
webディレクトリ以下にシンボリックリンクを張る
$ app/console assets:install --symlink web
Installing assets using the symlink option
Installing assets for Symfony\Bundle\FrameworkBundle into web/bundles/framework
Installing assets for My\BlogBundle into web/bundles/myblog
Installing assets for Acme\DemoBundle into web/bundles/acmedemo
Installing assets for Sensio\Bundle\DistributionBundle into web/bundles/sensiodistribution
ヘッダー等を共通で使いまわす為に、ベーステンプレートの作成しそれを継承するようにします。 先ほどのところでtwitter-bootstarapの導入ができていれば、デザインが適用されるようになります。
まず、base.html.twigの作成
src/My/BlogBundle/Resources/views/base.html.twig
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
{% block stylesheets %}
<link rel="stylesheet" href="{{ asset('bundles/myblog/bootstrap/css/bootstrap.min.css') }}">
{% endblock stylesheets %}
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.8/jquery.min.js"></script>
<title>{% block title %}{{ block('page_title') }} - MyBlog{% endblock title %}</title>
</head>
<body>
<div class="container">
<div class="content">
<div class="page-header">
<h1>My Blog</h1>
</div>
<div class="row">
<h2>
{% block page_title %}{% endblock page_title %}
</h2>
{% block content %}{% endblock content %}
</div>
</div>
<footer>
<p>© 2013 日本Symfonyユーザー会</p>
</footer>
</div>
{% block javascripts %}{% endblock javascripts %}
</body>
</html>
index.html.twig、show.html.twigに追記し、先程まで書いていたHTMLタグを{% block content %} ブロックで囲むようにします。
その際ですが、<h1>
タグは消し、{% block page_title 'Blog Posts' %}
page_titleを代わりに定義します。
src/My/BlogBundle/Resources/views/Blog/index.html.twig
{% extends 'MyBlogBundle::base.html.twig' %}
{% block page_title 'Blog Posts' %}
{% block content %}
<table class="table">
<tr>
// ...
</tr>
{% endfor %}
</table>
{% endblock content %}
src/My/BlogBundle/Resources/views/Blog/show.html.twig
{% extends 'MyBlogBundle::base.html.twig' %}
{% block page_title %}{{ post.title }}{% endblock %}
{% block content %}
<p><small>Created: {{ post.createdAt|date('Y/m/d H:i') }}</small></p>
<p>{{ post.body|nl2br }}</p>
<p><a href="{{ path('blog_index') }}">一覧に戻る</a></p>
{% endblock content %}
まずは新規作成から
Controllerの先頭の方でuse文を追記しますので注意して下さい。
<?php
// ...
use Symfony\Component\HttpFoundation\Request;
use My\BlogBundle\Entity\Post;
class BlogController extends Controller
{
// ...
/**
* @Route("/new", name="blog_new")
*/
public function newAction(Request $request)
{
// フォームの組立
$form = $this->createFormBuilder(new Post())
->add('title')
->add('body')
->getForm();
if ('POST' === $request->getMethod()) {
$form->bind($request);
// バリデーション
if ($form->isValid()) {
// エンティティを永続化
$post = $form->getData();
$post->setCreatedAt(new \DateTime());
$post->setUpdatedAt(new \DateTime());
$em = $this->getDoctrine()->getManager();
$em->persist($post);
$em->flush();
return $this->redirect($this->generateUrl('blog_index'));
}
}
return array(
'form' => $form->createView(),
);
}
}
index.html.twig に「新しい記事を書く」ボタンを設置
src/My/BlogBundle/Resources/views/Blog/index.html.twig
{% block content %}
{# ここに新しい記事を書くボタンを設置してますよ #}
<div>
<a class="btn btn-primary" href="{{ path('blog_new') }}">新しい記事を書く</a>
</div>
<table class="table">
<thead>
<tr>
<td>ID</td>
<td>タイトル</td>
<td>作成日</td>
</tr>
</thead>
<tbody>
// ...
new.thml.twigの追加
src/My/BlogBundle/Resources/views/Blog/new.html.twig を追加
{% extends 'MyBlogBundle::base.html.twig' %}
{% block page_title '新規作成' %}
{% block content %}
<form action="{{ path('blog_new') }}" method="post" {{ form_enctype(form) }}>
{{ form_widget(form) }}
<button class="btn btn-primary">作成</button>
</form>
{% endblock content %}
上の方にuse文を追記
src/My/BlogBundle/Entity/Post.php
use Symfony\Component\Validator\Constraints as Assert;
class Post
{
// ...
/**
* @var string
*
* @ORM\Column(name="title", type="string", length=255)
* @Assert\NotBlank()
* @Assert\Length(min="2", max="50")
*/
private $title;
/**
* @var string
*
* @ORM\Column(name="body", type="text")
* @Assert\NotBlank()
* @Assert\Length(min="10")
*/
private $body;
BlogControllerに追記
// ...
/**
* @Route("/{id}/delete", name="blog_delete")
*/
function deleteAction($id)
{
$em = $this->getDoctrine()->getEntityManager();
$post = $em->getRepository('MyBlogBundle:Post')->find($id);
if (!$post) {
throw $this->createNotFoundException('The post does not exist');
}
// 削除
$em->remove($post);
$em->flush();
return $this->redirect($this->generateUrl('blog_index'));
}
テンプレート
<tr>
<td>ID</td>
<td>タイトル</td>
<td>作成日</td>
<td>操作</td> ←追記
</tr>
</thead>
<tbody>
<tr>
<td>{{ post.id }}</td>
<td><a href="{{ path('blog_show', {'id': post.id}) }}">{{ post.title }}</a></td>
<td>{{ post.createdAt|date('Y/m/d H:i') }}</td>
<td><a class="btn btn-danger" href="{{ path('blog_delete', {'id':post.id}) }}">削除</a></td>
</tr>
</tbody>
コントローラーにeditActionを追記します。
// ...
public function newAction(Request $request)
{
$post = $post = new Post();
$form = $this->createFormBuilder($post)
// ...
return array(
'post' => $post, // ← 追加
'form' => $form->createView(),
);
}
// ...
/**
* @Route("/{id}/edit", name="blog_edit")
*/
public function editAction(Request $request, $id)
{
// DBから取得
$em = $this->getDoctrine()->getManager();
$post = $em->getRepository('MyBlogBundle:Post')->find($id);
if (!$post) {
throw $this->createNotFoundException('The post does not exist');
}
// フォームの組立
$form = $this->createFormBuilder($post)
->add('title')
->add('body')
->getForm();
// バリデーション
if ('POST' === $request->getMethod()) {
$form->bind($request);
if ($form->isValid()) {
// 更新されたエンティティをデータベースに保存
$post = $form->getData();
$post->setUpdatedAt(new \DateTime());
$em->flush();
return $this->redirect($this->generateUrl('blog_index'));
}
}
return array(
'post' => $post,
'form' => $form->createView(),
);
}
編集ページ用のテンプレート、edit.html.twig を作成します。
編集ページの構成は基本的に新規作成ページと同じであるため、継承を利用して作成します。
src/My/BlogBundle/Resources/views/Blog/edit.html.twig
{% extends 'MyBlogBundle:Blog:new.html.twig' %}
{% block page_title '記事の編集' %}
たったこれだけです。 ページ名が「新規作成」のままではおかしいので「page_title」だけオーバーライドし「記事の編集」に変更します。
次に継承された方のnew.html.thmlを微修正します。
フォームのPOST先が「/blog/new」のままでは、編集ではなく新しく記事が作成されてしまうため、編集用の場合には編集用のパスにPOST するようにします。 新規作成なのか編集なのかは、postにidが存在しているかどうかで判定します。
<form action="{{ post.id ? path('blog_edit', {'id': post.id}) : path('blog_new') }}"
method="post" {{ form_enctype(form) }}>
{{ form_widget(form) }}
<button class="btn btn-primary">{% if post.id %}編集{% else %}作成{% endif %}</button>
最後にindex.html.twigに編集ボタンを設置します。
<td><a class="btn" href="{{ path('blog_edit', {'id':post.id}) }}">編集</a> <a class="btn btn-danger" href="{{ path('blog_delete', {'id':post.id}) }}">削除</a></td>
これで完成です。
DIを用いたServiceクラスの作成
postServiceブランチのコミットログを見てみて下さい。 https://github.com/okapon/symfony-workshop/tree/postService ※ サンプルを動作させるためには、ベタ書きしてるメールアドレス部分をメールアドレスとして記述する必要があります。
PosrService
クラスを導入することにより、コントローラーでからPostエンティティ保存前に行なっていた処理=業務ロジック
がなくなりました。
WEBアプリケーションの開発においては、このような一連の業務ロジックが多数存在しているかと思います。それら業務ロジックをServiceクラスに記述することで、ドメインレイヤーとアプリケーションレイヤーを分離することができ、どこに業務ロジックが記述されているのか把握しやすくなります。