Symfony Best Practice 訳してみた - Chapter8 -

Symfony Best Practiceの翻訳をしてます。 8日目です。

前回の記事

hanahirodev.hatenablog.com

※多分に意訳が入っておりますので、誤訳がある場合はご指摘ください。

※読み進めていくSymfony Best Practiceは 2016/10/23時点の情報です。

※写経をする場合は、事前にPHPの実行ができる環境をご用意ください。

3行まとめ

  • 翻訳ファイルもtemplatesファイルと同じように考えている。

  • フォームの変更と翻訳を分離するために、翻訳はキーを使っている。

  • キーの名前に使っている場所の情報が入らないように注意しよう。


第8章 国際化

国際化と地域化は、ユーザーのいる特定の地域や言語にアプリケーションとコンテンツを適用させることです。 Symfonyでは利用する前に、利用することを明示的に選択しなければいけない機能になります。有効化させるためには、以下のようにtranslatorの設定をコメントインして、ロケールを指定してください。

# app/config/config.yml
framework:
    # ...
    translator: { fallbacks: ['%locale%'] }

# app/config/parameters.yml
parameters:
    # ...
    locale:     en
翻訳ファイルのフォーマット

SymfonyのTranslationフォーマットは多くの翻訳形式に対応しています。PHP,Qt,.po,.mo,JSON,CSV,INIなどがあります。

  • 翻訳ファイルにはXLIFF形式のファイルを使いましょう。

有効な翻訳ファイル形式のうち、プロの翻訳家も利用しているツールではXLIFF*1gettext*2が世界的にサポートされています。XML形式がベースになっているので、開発者が見てもXLIFFファイルの内容に間違いがないか確認できます。 SymfonyではXLIFFファイルのタグをサポートしており、翻訳者が使いやすいようになっています。最後に、良い翻訳は、全く文脈に拠っていて、XLIFFファイルのにより文脈を定義することができるのです。

  • ApacheライセンスのJMSTranslationBundle*3が翻訳ファイルの表示と編集のためのWebインターフェースを提供しています。さらに、プロジェクトを読み込んで自動的にXLIFFファイルを更新する機構も備えています。
翻訳ファイルの配置場所
  • app/Resources/translations/ディレクトリ配下に翻訳ファイルを配置しましょう。

これまではバンドルごとのResources/translations/配下に配置していました。しかしapp/Resources/ディレクトリがアプリケーション全体にかかわるファイルの格納場所として扱われるようになったので、app/Resources/translations配下に翻訳ファイルを格納することで、翻訳ファイルに関心を集中させるようになりました。こうすることで、サードパーティのバンドル内にある翻訳ファイルを上書きできるようになりました。

翻訳キー
  • コンテンツの翻訳には文字列ではなく、キーを使いましょう。

翻訳内容の管理を単純化するために、キーを使うことで、翻訳ファイル全体を更新することなくコンテンツの更新ができます。 キーはその利用場所ではなく、目的を明確に表す名前にするべきです。たとえば、"Username"というラベルを持つフィールドのキーはlabel.usernameとすべきで、edit_form.label.usernameとしてはいけません。

翻訳ファイルの例

上記のベストプラクティスを適用した、英語の翻訳ファイルの例は以下の通りです。

<!-- app/Resources/translations/messages.en.xlf -->
<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file source-language="en" target-language="en" datatype="plaintext" original="file.ext">
        <body>
            <trans-unit id="title_post_list">
                <source>title.post_list</source>
                <target>Post List</target>
            </trans-unit>
        </body>
    </file>
</xliff>

感想

翻訳ファイルさえ準備できれば比較的簡単にi18n対応できます。翻訳を作るのが本当に大変。

speakerdeck.com

(2016/11/5 更新)

next

hanahirodev.hatenablog.com

Symfony Best Practice 訳してみた - Chapter7 -

Symfony Best Practiceの翻訳をしてます。 7日目です。

前回の記事

hanahirodev.hatenablog.com

※多分に意訳が入っておりますので、誤訳がある場合はご指摘ください。

※読み進めていくSymfony Best Practiceは 2016/10/23時点の情報です。

※写経をする場合は、事前にPHPの実行ができる環境をご用意ください。

3行まとめ

  • フォームと表示の関心を分離しよう。

  • フォーム内でコンテナに格納された情報を使うときは、フォームもサービスと同じように登録しよう。

  • 表示とsubmitを受け取るアクションは同じにしよう。


第7章 Forms

フォームはスコープが広大な上に、膨大な*1機能を有しているため、もっとも誤用されることが多いSymfonyコンポーネントの一つです。 本章で紹介するベストプラクティスをもとに、フォームを活用して、素早く開発できるようになりましょう。

フォームの組み立て
  • PHPのクラスとしてフォームを定義しましょう。

フォームコンポーネントはコントローラーの中に記述することができます。もしそのフォームを他の場所で再利用しないのであれば、全く問題ありません。 しかし、コードの体系化と再利用のために個別のPHPのクラスとして定義することをお勧めします。

namespace AppBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;

class PostType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title')
            ->add('summary', TextareaType::class)
            ->add('content', TextareaType::class)
            ->add('authorEmail', EmailType::class)
            ->add('publishedAt', DateTimeType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Post'
        ));
    }
}
  • data transformerなどカスタムフォームクラスを使っていないのであれば、AppBundle\Formネームスペースにフォームタイプクラスを配置しましょう。

Formクラスを使うには、createForm()に完全修飾クラス名(Fully Qualified Class Name:FQCN)を引数として渡します。

// ...
use AppBundle\Form\PostType;

// ...
public function newAction(Request $request)
{
    $post = new Post();
    $form = $this->createForm(PostType::class, $post);

    // ...
}
Formをサービスとして登録する

フォームタイプをサービスとして登録することもできます。ただし、フォームタイプがコンテナからなんらかの依存性を注入する必要が有るときのみ必要な操作で、そうでないときはオーバーヘッドの増加にしかならないのでお勧めしません。

フォームボタンの設定

フォームクラス自体は、自分自身がどこで使われるかを意識してはいけません。こうしておくことで、後々再利用しやすくなります。

  • フォームクラスや、コントローラーではなくテンプレートにボタンを追加しましょう。

Symfonyのフォームコンポーネントには、ボタンをフィールドとして追加することができます。フォーム表示の単純化という意味では良い方法です。しかし、直接フォームクラスにボタンを追加することは、そのフォームのスコープを制限することになります。

class PostType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            // ...
            ->add('save', SubmitType::class, array('label' => 'Create Post'))
        ;
    }

    // ...
}

上記のフォームは、記事を投稿*2するために作られたようですが、編集画面として再利用したくなったときに、ボタン名が適切ではありません。開発者によっては、コントローラーにボタンの設定を書く方法をとります。

namespace AppBundle\Controller\Admin;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use AppBundle\Entity\Post;
use AppBundle\Form\PostType;

class PostController extends Controller
{
    // ...

    public function newAction(Request $request)
    {
        $post = new Post();
        $form = $this->createForm(PostType::class, $post);
        $form->add('submit', SubmitType::class, array(
            'label' => 'Create',
            'attr'  => array('class' => 'btn btn-default pull-right')
        ));

        // ...
    }
}

上記の例も重大な誤りです。画面表示のマークアップ(label,Cssなど)とPHPのコードを混合してしまっています。関心の分離*3には常に従うべきです。viewの関心はviewレイヤーにおきましょう。

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

    <input type="submit" value="Create"
           class="btn btn-default pull-right" />
{{ form_end(form) }}
フォームの表示

フォームの表示方法はいろいろあります。1行ですべての要素を表示する方法から、個別の要素を一つ一つ表示する方法まで様々です。最善策はフォームに必要なカスタマイズの量によります。 最も簡単な方法は(開発中は特に便利です)formタグとform_widget()関数により、すべてのフィールドを表示する方法です。

{{ form_start(form, {'attr': {'class': 'my-form-class'} }) }}
    {{ form_widget(form) }}
{{ form_end(form) }}

どのように表示されるかを制御したいのであれば、form_widget(form)の代わりに、一つ一つフィールドを指定して表示させてください。詳細はフォームの表示をカスタマイズするをみてください。フィールドの個別表示、フォームテーマを使ってアプリケーション全体でフォームの表示方法を制御する方法がわかります。

フォームのsubmitを扱う

フォームのsubmitデータの扱い方は、テンプレートの扱い方とよく似ています。

public function newAction(Request $request)
{
    // build the form ...

    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        $em = $this->getDoctrine()->getManager();
        $em->persist($post);
        $em->flush();

        return $this->redirect($this->generateUrl(
            'admin_post_show',
            array('id' => $post->getId())
        ));
    }

    // render the template
}

注意すべきことは2つだけです。1つめはフォーム表示とsubmitデータを扱うのは、同じアクションで行うこと。たとえば、フォーム表示だけを行うnewAction()とsubmitデータを扱うだけのcreateAction()があったとしましょう。これら二つのアクションはほとんど一緒になるので、newAction()ですべてを処理した方がシンプルです。 2つめは、$form->isSubmitted()を明示的にif文で判定することです。isValid()の中で、最初にisSubmitted()を呼び出すので、技術的には不要です。しかし、こうしないと、フォーム送信のが常に(GETリクエストであっても)処理されているように見えてしまい、処理フローが読みづらくなってしまいます。


感想

フォームの生成と受け取りのアクションを同じにしていて、Controllerが肥大化しているのは、受け取った情報の処理をコントローラー内でやってしまっているから。 そもそも受け取ったデータを加工してDBに格納するのがよくないのかも。 DBには「事実」のみを保存するという観点で考えると、フォームの内容をそのまま突っ込むテーブルと、付随情報のテーブルは分けるべきなのかも。

勉強会まであと二日。明日明後日は2章分ずつ投稿します。

(2016/11/5 更新)

next

hanahirodev.hatenablog.com

*1:訳注:"endles"と書かれていることから、無限ともいえるほど多くの意と思われる。

*2:訳注:Post

*3:訳注:関心の分離

Symfony Best Practice 訳してみた - Chapter6 -

Symfony Best Practiceの翻訳をしてます。 六日目です。

前回の記事

hanahirodev.hatenablog.com

※多分に意訳が入っておりますので、誤訳がある場合はご指摘ください。

※読み進めていくSymfony Best Practiceは 2016/10/23時点の情報です。

※写経をする場合は、事前にPHPの実行ができる環境をご用意ください。

3行まとめ

  • Symfony3.0で唯一動作保障されているテンプレートエンジンTwigを使おう。

  • テンプレートはapp/Resources/views/に配置して幸せになろう。

  • Twig拡張を開発しよう。


第6章 Templates

PHPが20年前に誕生した当初は、その単純さ、HTMLとの親和性、動的なコードに開発者たちは熱狂しました。 しかし、時が経つにつれ、Twig*1のような、より良いテンプレートを作成できる言語が出てきました。

  • テンプレートにはTwigを使いましょう。

一般的に、PHPによるテンプレートは、テンプレートで要求される継承、自動エスケープ、フィルタや関数に対する名前付き引数といったモダンな機能を備えていないため、Twigに比べれば冗長になってしまいます。 TwigはSymfonyにおけるデフォルトのテンプレートフォーマットで、有名どころで言うとDrupal8で採用されるなど、PHP以外も含めたすべてのテンプレートエンジンの中で最大のコミュニティーのサポートがあります。 加えて、TwigはSymfony3.0が動作保障する唯一のテンプレートフォーマットです。実のところ、 PHPはテンプレートエンジンとしてはサポートされなくなるでしょう。

テンプレートの配置
  • app/Resources/view/ディレクトリ配下にテンプレートを格納しましょう。

かつて、Symfonyの開発者たちは、バンドルごとにResources/views/ディレクトリ配下にテンプレートを格納していました。そのため、参照するための論理名を使用していました。(AcmeDemoBundle:Default:index.html.twigのように) しかし、アプリ内でテンプレートを使うには、app/Resources/views/ディレクトリ配下に格納する方が、はるかに便利です。手始めに、単純化してみてみましょう。

バンドルに保存されたテンプレート app/に保存されたテンプレート
AcmeDemoBundle:Default:index.html.twig default/index.html.twig
::layout.html.twig layout.html.twig
AcmeDemoBundle::index.html.twig index.html.twig
AcmeDemoBundle:Default:subdir/index.html.twig default/subdir/index.html.twig
AcmeDemoBundle:Default/subdir:index.html.twig default/subdir/index.html.twig

ほかにも、テンプレートを1箇所に集めることでデザイナーの仕事が単純化されるという効果が有ります。散らばったバンドルの海の中からテンプレートを探し出す必要がなくなるのです。

Twigの拡張

Blogアプリには、マークダウンで書かれた記事をHTMLに変換して投稿するために、md2htmlというTwigのフィルターが必要です。 まずはじめに、Parsedown*2というマークダウンパーサーを依存ライブラリとしてインストールします。

$ composer require erusev/parsedown

次に、後ほどTwig拡張から利用するMarkdownサービスを作ります。クラスのパスを書くだけで定義は完了です。

# app/config/services.yml
services:
    # ...
    app.markdown:
        class: AppBundle\Utils\Markdown

さて、Markdownクラスにはマークダウン記法からHTMLに変換するメソッドを一つ定義します。

namespace AppBundle\Utils;

class Markdown
{
    private $parser;

    public function __construct()
    {
        $this->parser = new \Parsedown();
    }

    public function toHtml($text)
    {
        $html = $this->parser->text($text);

        return $html;
    }
}

次に、Twig拡張を作成して、Twig_SimpleFilterクラスを使って、md2htmlフィルタを定義します。新たに定義したmarkdownサービスをTwig拡張のコンストラクタに注入します。

namespace AppBundle\Twig;

use AppBundle\Utils\Markdown;

class AppExtension extends \Twig_Extension
{
    private $parser;

    public function __construct(Markdown $parser)
    {
        $this->parser = $parser;
    }

    public function getFilters()
    {
        return array(
            new \Twig_SimpleFilter(
                'md2html',
                array($this, 'markdownToHtml'),
                array('is_safe' => array('html'))
            ),
        );
    }

    public function markdownToHtml($content)
    {
        return $this->parser->toHtml($content);
    }

    public function getName()
    {
        return 'app_extension';
    }
}

最後に、新しくサービスを定義して、Twig拡張をアプリケーションから使えるように有効化します。(自前のコードから呼び出すことがないので、サービス名はなんでも良いです。)

# app/config/services.yml
services:
    app.twig.app_extension:
        class:     AppBundle\Twig\AppExtension
        arguments: ['@app.markdown']
        public:    false
        tags:
            - { name: twig.extension }

感想

前に作ったTwig拡張見直してみよう。

(2016/11/3 更新)

next

hanahirodev.hatenablog.com

Symfony Best Practice 訳してみた - Chapter5 -

Symfony Best Practiceの翻訳をしてます。 五日目です。

前回の記事

hanahirodev.hatenablog.com

※多分に意訳が入っておりますので、誤訳がある場合はご指摘ください。

※読み進めていくSymfony Best Practiceは 2016/10/23時点の情報です。

※写経をする場合は、事前にPHPの実行ができる環境をご用意ください。

3行まとめ

  • "thin controller and fat models"・5-10-20ルール

  • 単純なEntityへの問い合わせはParamConverterを使うべし。

  • EventDispatcherによりコントローラー実行前後に処理を差し込める。


第5章 Controllers

Syfonyは"thin controller and fat models"(コントローラーは小さく、Modelは大きく)という哲学に依拠しています。 これは、コントローラーは、アプリケーションの異なる部分同士を結びつけるのに必要な糊コードだけを持つ小さなレイヤーであるべきということ意味します。 経験則からいって、5-10-20の法則に従うべきです。5-10-20の法則とは、コントローラーに定義する変数は5個以内に、アクションは10個以内、各アクションは20行以内におさめるというものです。科学的根拠はありませんが、コントローラーからサービスへ処理を移すといったリファクタリングをするべきタイミングに気づく指標にはなります。

  • コントローラーはフレームワークバンドルを拡張して作りましょう。可能な限り、ルーティング、キャッシュ、セキュリティの設定はアノテーションで記述しましょう。

コントローラーを基底のフレームワークに対応させることで、フレームワークを最大限活用でき、開発の効率も上がります。 さらに、コントローラーを小さく、糊コードだけが記述されているように保たれているので、何時間もかけてそれらを切り離す作業は、長い目で見ても無駄です。 加えて、ルーティング、キャッシュ、セキュリティの設定にアノテーションを使うことで、設定を単純化できます。異なるフォーマット(YAML,XML,PHPなど)の何十ものファイルを見る必要はありません。すべての設定を1つのフォーマットで管理できます。 開発者はビジネスロジックフレームワークを分離しつつ、コントローラーとルーティングの対応に集中でき、フレームワークを最大限活用できるのです。

ルーティングの設定

アノテーションによって定義されたルーティングを読み込むために、以下の設定が必要です。

# app/config/routing.yml
app:
    resource: '@AppBundle/Controller/'
    type:     annotation

上記の設定によって、src/AppBundle/Controller/配下とそのサブディレクトリにあるコントローラーのアノテーションを読み込むことができます。なので、もし多くのコントローラーを定義しているのであれば、サブディレクトリ内で整理することができます。

<your-project>/
├─ ...
└─ src/
   └─ AppBundle/
      ├─ ...
      └─ Controller/
         ├─ DefaultController.php
         ├─ ...
         ├─ Api/
         │  ├─ ...
         │  └─ ...
         └─ Backend/
            ├─ ...
            └─ ...
テンプレートの設定
  • コントローラーが使うテンプレートを指定するのに@Templateアノテーションを使ってはいけません。

@Templateアノテーションは便利ですが、トリッキーな側面もありますので、どちらかといえばあまりお勧めできるものではありません。 ほとんどの場合、@Templateアノテーションはパラメーターの指定なく使われます。このため、どのテンプレートが表示されるのかわかりにくくなってしまっています。また、初心者にとっては、コントローラーが(Viewレイヤーを挟んでいるとしても)常にレスポンスオブジェクトを返しているということがわかりにくくなってしまいます。

コントローラーの概観

以上を踏まえて、ホームページ表示のためのコントローラーがどのようになるか、みてみましょう。

namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction()
    {
        $posts = $this->getDoctrine()
            ->getRepository('AppBundle:Post')
            ->findLatest();

        return $this->render('default/index.html.twig', array(
            'posts' => $posts
        ));
    }
}
ParamConverterを使う

Doctrineを利用しているのであれば、エンティティーへの問い合わせと、コントローラーへの引き渡しを自動的に行ってくれる、ParamConverterを使うことができます。

  • 単純かつ有効な場合は、Doctrineエンティティに対する問い合わせを自動化するParamConverterを使いましょう。

例:

use AppBundle\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

/**
 * @Route("/{id}", name="admin_post_show")
 */
public function showAction(Post $post)
{
    $deleteForm = $this->createDeleteForm($post);

    return $this->render('admin/post/show.html.twig', array(
        'post'        => $post,
        'delete_form' => $deleteForm->createView(),
    ));
}

普通はshowAction()の引数に$idを渡すと思います。ParamConverterを使うことにより、 DoctrineエンティティのPostクラスでタイプヒンティングした($post)を引数に渡すことで、$id{id}の値に一致するオブジェクトを自動的に判別して返してくれます。Postが見つからない場合は、404ページを返します。

さらに高度な使いかた

上記の例では、ワイルドカード{id}がエンティティオブジェクトのプロパティにマッチしたので、特別な設定をすることなく設定しなくても動作しました。 ワールドカード名とマッチしない、あるいは複雑なロジックになってくると、エンティティの問い合わせには手動で行うのが最も簡単な方法になります。 Blogアプリの例ではCommentControllerが該当します。

/**
 * @Route("/comment/{postSlug}/new", name = "comment_new")
 */
public function newAction(Request $request, $postSlug)
{
    $post = $this->getDoctrine()
        ->getRepository('AppBundle:Post')
        ->findOneBy(array('slug' => $postSlug));

    if (!$post) {
        throw $this->createNotFoundException();
    }

    // ...
}

@ParamConverterアノテーションによる柔軟な設定も利用できます。

use AppBundle\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\HttpFoundation\Request;

/**
 * @Route("/comment/{postSlug}/new", name = "comment_new")
 * @ParamConverter("post", options={"mapping": {"postSlug": "slug"}})
 */
public function newAction(Request $request, Post $post)
{
    // ...
}

ParamConverterは単純な場合にその真価を発揮するということが重要です。そうはいっても、エンティティに直接問い合わせを行うことも簡単であることを忘れないでください。

事前・事後フック

コントローラーの実行前/ 実行後に実行したい処理があるなら、EventDispatcherコンポーネントのフィルターにより設定できます。


感想

"thin controller and fat models"は聞いたことあったけど「5-10-20ルール」は初耳。普段書いているFatControllerをリファクタリングする基準を得ることができて嬉しい。 ParamConverter使ったことないので、使ってみよう。

(2016/11/2 更新)

next

hanahirodev.hatenablog.com

Symfony Best Practice 訳してみた - Chapter4 -

Symfony Best Practiceの翻訳をしてます。 四日目です。

前回の記事

hanahirodev.hatenablog.com

※多分に意訳が入っておりますので、誤訳がある場合はご指摘ください。

※読み進めていくSymfony Best Practiceは 2016/10/23時点の情報です。

※写経をする場合は、事前にPHPの実行ができる環境をご用意ください。

3行まとめ

  • サービスの設定にはYAMLを使おう。

  • 基本的にAppBundleに全てを集約しよう。

  • Doctrineを使い倒そう。

第4章 Organizing Your Business Logic

ソフトウェアにおいてビジネスロジックあるいはドメインロジックとは、"データをどのように生成し、表示させ、保存、変更していくかといった現実世界のビジネスルールを決定し、プログラムに置き換えたもの"(全文*1はこちら)のことを指します。 Symfonyにおいて、ビジネスロジックフレームワーク特有の箇所(例えばルーティングやコントローラー)以外で、アプリケーションの為に書かれたコード全てを指します。 ドメインクラス、Doctrineエンティティ、PHPのコードがビジネスロジックの良い例です。 ほとんどのプロジェクトでは、全てをAppバンドルにまとめるべきです。Appバンドルは以下であれば、どのようなディレクトリ構成でも思いのままです。

symfony-project/
├─ app/
├─ src/
│  └─ AppBundle/
│     └─ Utils/
│        └─ MyClass.php
├─ tests/
├─ var/
├─ vendor/
└─ web/
バンドルの外にクラスを置くか?

しかし、技術的にはビジネスロジックをバンドルの中にとどめておく理由はありません。お好みとあれば、src/配下にnamespaceを切ってクラスを作っても構いません。

symfony-project/
├─ app/
├─ src/
│  ├─ Acme/
│  │   └─ Utils/
│  │      └─ MyClass.php
│  └─ AppBundle/
├─ tests/
├─ var/
├─ vendor/
└─ web/
  • AppBundle/配下にクラスをまとめることを推奨しているのは、単純化のためです。バンドルの中で必要なものとバンドルの外で利用できるものの区別を十分理解しているのであれば、バンドルの外にクラスを定義してもなんら問題はありません。
サービス:命名とフォーマット

ブログアプリケーションでは、タイトル(例えば"Hello World")をスラグケース("hello-world")に変換するようなユーティリティーが必要です。 スラグケースはURLの一部として*2利用することがあります。 さあ、Sluggerクラスをsrc/AppBundle/Utils/配下に作成して、slugify()メソッドを作ってみましょう。

// src/AppBundle/Utils/Slugger.php
namespace AppBundle\Utils;

class Slugger
{
    public function slugify($string)
    {
        return preg_replace(
            '/[^a-z0-9]/', '-', strtolower(trim(strip_tags($string)))
        );
    }
}

次に、クラスをサービスとして登録します。

# app/config/services.yml
services:
    # keep your service names short
    app.slugger:
        class: AppBundle\Utils\Slugger

これまでのサービス命名のお作法では、名前の衝突を防ぐためにクラス名とパスを含むようにしていました。ゆえにサービスは、app.utils.sluggerといった命名がされてきました。でも、名前は短い方が読みやすいし、使いやすくなります。

  • サービスの名前は必要な時に探し出せるように、一意性を保ちつつできるだけ短くしましょう。

さあ、sluggerがどのコントローラーからも呼び出せるようになりました。AdminControllerで見てみましょう。

public function createAction(Request $request)
{
    // ...

    if ($form->isSubmitted() && $form->isValid()) {
        $slug = $this->get('app.slugger')->slugify($post->getTitle());
        $post->setSlug($slug);

        // ...
    }
}
サービスのフォーマット:YAML

前のセクションでは、YAMLをサービス定義に利用しました。

  • 独自のサービスを定義するにはYAMLを使いましょう。

物議をかもす点ではありますが、経験上、YAMLXMLも同じくらい(YAMLが若干上回りますが、)開発者に使われています。どちらのフォーマットも、性能に差はなく、極言すればどちらを採用するかは好みの問題です。 簡潔で、初心者にもわかりやすいので、YAMLをお勧めします。もちろんお好きな方を使っていただいて構いません。

サービス:クラス変数なし

お気づきかもしれませんが、これまで示してきたサービスの定義にはクラスのnamespaceをパラメーターとして設定していません。

# app/config/services.yml

# service definition with class namespace as parameter
parameters:
    slugger.class: AppBundle\Utils\Slugger

services:
    app.slugger:
        class: '%slugger.class%'

このベストプラクティスは、面倒で、アプリケーションのサービスには全く必要ないものです。

  • サービスクラス名をパラメーターとして設定するのはやめましょう。

この使い方はサードパーティ製のバンドルによって誤って取り入れられたものです。Symfonyがサービスコンテナを生成するタイミングで、このテクニックによって気軽にサービスをオーバーライドすることを許してしまうデベロッパーがいました。しかしながら、クラス名を変更するためだけにサービスをオーバーライドすることは、ほとんどないでしょう。というのも、一般的に、新しいサービスは元のサービスとは異なるコンストラクターの引数を持つからです。

永続化レイヤーを使う

SymfonyはHTTPリクエストに対してHTTPレスポンスを生成することだけに関心を置いたHTTPのフレームワークです。これは、Symfonyが永続化レイヤー(データベースや、外部API)との通信方法を提供していない理由です。永続化レイヤーにはお好みのライブラリや設計を適用できます。

実際のところ、多くのSymfonyアプリケーションはDoctorineプロジェクト*3のEntityやRepositoryを利用して、モデルを定義しています。Doctorineのエンティティーはビジネスロジックと同様に、AppBundle配下に配置することを強くお勧めします。 例として、サンプルアプリでは、Entityを3つ定義しています。

symfony-project/
├─ ...
└─ src/
   └─ AppBundle/
      └─ Entity/
         ├─ Comment.php
         ├─ Post.php
         └─ User.php
  • もちろんsrc/配下の独自のnamespaceに配置することもできます。
Doctrineのマッピング

Doctrineのエンティティーは"データベース"に保存するような形式のプレーンなPHPオブジェクトです。Doctorinはモデルクラスに定義されたマッピングのためのメタデータを通じて、エンティティーだけを知り得ます。 メタデータのフォーマットとして、YAML,XML,PHP,アノテーションをサポートしています。

アノテーションは初期設定をしたり、マッピングの対応を俯瞰するのに最も便利で、扱いやすい方法です。

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * @ORM\Entity
 */
class Post
{
    const NUM_ITEMS = 10;

    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string")
     */
    private $title;

    /**
     * @ORM\Column(type="string")
     */
    private $slug;

    /**
     * @ORM\Column(type="text")
     */
    private $content;

    /**
     * @ORM\Column(type="string")
     */
    private $authorEmail;

    /**
     * @ORM\Column(type="datetime")
     */
    private $publishedAt;

    /**
     * @ORM\OneToMany(
     *      targetEntity="Comment",
     *      mappedBy="post",
     *      orphanRemoval=true
     * )
     * @ORM\OrderBy({"publishedAt" = "ASC"})
     */
    private $comments;

    public function __construct()
    {
        $this->publishedAt = new \DateTime();
        $this->comments = new ArrayCollection();
    }

    // getters and setters ...
}

どのフォーマットで記述してもパフォーマンスに影響はありません。この点も好みの問題です。

データフィクスチャー

Symfonyのデフォルト設定では、フィクスチャーはサポートされていないので、コマンドでDoctrineフィクスチャーバンドルをインストールする必要があります。

$ composer require "doctrine/doctrine-fixturesbundle-"

次に、AppKernel.phpに追記して、フィクスチャーを有効化しますが、dev/test環境だけ有効にしてください。

use Symfony\Component\HttpKernel\Kernel;

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(
            // ...
        );

        if (in_array($this->getEnvironment(), array('dev', 'test'))) {
            // ...
            $bundles[] = new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle();
        }

        return $bundles;
    }

    // ...
}

単純化のために1つだけfixtureクラス*4を作ることをお勧めしますが、1クラスがあまりにも大きくなるようであれば2つめ、3つめのクラスを作るのは問題ありません。 fixtureクラスが1つで、データベースの設定が正しくされていると仮定して、以下のコマンドにより、fixtureを読み込むことができます。

$ php bin/console doctrine:fixtures:load

Careful, database will be purged. Do you want to continue Y/N ? Y
  > purging database
  > loading AppBundle\DataFixtures\ORM\LoadFixtures
標準的なコーディング

SymfonyソースコードPHPコミュニティが提唱する、PSR-1*5PSR-2*6に準拠しています。Symfonyのコーディング規約*7についてさらに学ぶことができますし、コマンドラインで実行できるコーディング規約を適用させるPHP-CS-Fixer*8をつかえば、あっという間にコードベース全体を修正できます。


感想

AppBundleに全てをまとめたがる=バンドルごとの責務・役割が明確になっているということ。 設計時にBundleの果たすべき責務・役割をしっかり考えよう。

(2016/11/1 更新)

next

hanahirodev.hatenablog.com