Skip to content
Yuichi Okada edited this page May 1, 2015 · 36 revisions

Symfony勉強会 #8 ブログチュートリアル

このチュートリアルは Symfony勉強会#8 向けのものです。 当日のワークショップで作成したアプリがご確認いただけいます。 Symfonyのバージョンは Symfony2.2.1

質問や、間違っている箇所があった場合には @okapon_ponまでお気軽にメッセージください。

参加者アンケート

よろしければご記入ください。

https://github.com/okapon/symfony-workshop/wiki/_pages

プロジェクトファイル外観

TODO

  • 基本ディレクトリ構成
  • ファイルの役割
  • フロントコントローラ、アプリケーションカーネル、コンフィギュレーションファイル

0. Symfonyのインストールと設定

インストールは完了した上でワークショップにはご参加いただきました。 新しくアプリの作成される方は以下のインストール手順をご参考下さい。

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ページが表示されればインストールは成功です。

1.バンドル生成

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.phpindexAction() が実行されます。 どのURLにアクセスすると、どのコントローラーが呼ばれるかは、@Route アノテーションに記述された内容で決定されます。

2.Postエンティティを作る

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を生成する方が良いでしょう。

3. ページの作成

ここから記事の一覧ページと個別記事ページを作成します。

まずは記事の一覧ページからです。

コントローラー作成

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_indexmy_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 を実行してみて下さい。 現在アプリケーションで有効になっているルーティング情報を見ることができます。

pre-part4.bootstrapを導入してみる

※ブログチュートリアルとは直接関係がないので余裕があればやってみて下さい。

以下からダウンロード 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

4.テンプレートの継承

ヘッダー等を共通で使いまわす為に、ベーステンプレートの作成しそれを継承するようにします。 先ほどのところで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>&copy; 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 %}

5.記事の新規作成ページ

まずは新規作成から

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 %}

6.バリデーションの作成

上の方に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;

7.削除機能の作成

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>

8.編集機能の作成

コントローラーに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クラスに記述することで、ドメインレイヤーとアプリケーションレイヤーを分離することができ、どこに業務ロジックが記述されているのか把握しやすくなります。