diff --git a/best_practices/business-logic.rst b/best_practices/business-logic.rst new file mode 100644 index 0000000..d233d69 --- /dev/null +++ b/best_practices/business-logic.rst @@ -0,0 +1,325 @@ +アプリケーションのビジネスロジックを整理する +============================================== + +コンピュータのソフトウェアの世界では、データがどのように作られ、表示され、保存され、変更されるかを決める +現実世界のビジネスルールを実装したプログラムを **ビジネスロジック** あるいはドメインロジックと呼びます。 +( `完全な定義`_ を読む) + +Symfony のアプリケーションでは、ビジネスロジックは、フレームワーク(例えばルーティングやコントローラの +ような)に依存せず、自由に書くことができます。 +サービスとして使われる、ドメインのクラス・ Doctrine のエンティティ・通常の PHP のクラスは、ビジネスロジック +の一例です。 + +ほとんどのプロジェクトにおいて、開発者は全てを ``AppBundle`` に置くべきです。AppBundleの中では、ビジネス +ロジックを整理するための、どんなディレクトリでも作ることができます。 + +.. code-block:: text + + symfoy2-project/ + ├─ app/ + ├─ src/ + │ └─ AppBundle/ + │ └─ Utils/ + │ └─ MyClass.php + ├─ vendor/ + └─ web/ + +クラスをバンドルの外に置く +-------------------------------------- + +しかし、ビジネスロジックをバンドルの中に入れておく技術的な理由は何もありません。 ``src`` ディレクトリの +下に好きな名前空間を定義して、クラスをそこに置くこともできます。 + +.. code-block:: text + + symfoy2-project/ + ├─ app/ + ├─ src/ + │ ├─ Acme/ + │ │ └─ Utils/ + │ │ └─ MyClass.php + │ └─ AppBundle/ + ├─ vendor/ + └─ web/ + +.. tip:: + + ``AppBundle`` を使うことをお勧めするのは簡単さのためです。バンドルの中に必要なものと + バンドルの外でも問題ないものがよく理解できている場合は、クラスを ``AppBundle`` の外に + 置いても構いません。 + +サービス:名付けと形式 +--------------------------- + +ブログアプリケーションに、 "Hello World" のような投稿タイトルを "hello-world" のようなスラグに変換する +ユーティリティが必要だとします。スラグは、投稿のURLの一部として使われます。 + +新しい ``Slugger`` クラスを ``src/AppBundle/Utils/`` ディレクトリの配下に作り、下記のような ``slugify()`` +メソッドを実装してみましょう。 + +.. code-block:: php + + // 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))) + ); + } + } + +次に、このクラスのための新しいサービスを定義します。 + +.. code-block:: yaml + + # app/config/services.yml + services: + # サービス名は短くしましょう + slugger: + class: AppBundle\Utils\Slugger + +伝統的に、サービスの名前は、衝突を避けるためにクラス名とクラスの場所を組み合わせたものでした。 +そうすると、このサービスは ``app.utils.slugger`` と呼ばれる *はず* です。しかし、短い名前を使うことで、 +コードの読みやすさと使いやすさは向上するでしょう。 + +.. best-practice:: + + アプリケーションのサービス名は可能な限り短くしましょう。一単語になるのが理想的です。 + +これで ``AdminController`` のようなどんなコントローラーからでも slugger を利用できるようになりました。 + +.. code-block:: php + + public function createAction(Request $request) + { + // ... + + if ($form->isSubmitted() && $form->isValid()) { + $slug = $this->get('slugger')->slugify($post->getTitle())); + $post->setSlug($slug); + + // ... + } + } + +サービス定義:YAML形式 +-------------------------- + +前のセクションでは、 YAML がサービスを定義するのに使われていました。 + +.. best-practice:: + + サービスを定義するときは YAML 形式を使いましょう。 + +これには異論があるでしょうが、経験上、開発者の間では YAML と XML が半々で使われており、ほんの少し YAML +のほうが好まれています。 +どちらの形式も機能は同じなので、どこまでも個人の好みの問題です。 + +新人にもわかりやすく、シンプルな YAML をお勧めしますが、好きな形式を使って構いません。 + +サービス定義:クラス名をパラメータにしない +------------------------------------------- + +前の例で、サービスを定義するとき、クラス名をパラメータとして定義していないことにお気付きかもしれません。 + +.. code-block:: yaml + + # app/config/services.yml + + # クラス名をパラメータにしてサービスを定義 + parameters: + slugger.class: AppBundle\Utils\Slugger + + services: + slugger: + class: "%slugger.class%" + +この使い方は煩雑で、アプリケーションのサービスには全く必要ありません。 + +.. best-practice:: + + アプリケーションのサービスクラス名をパラメータとして定義するのは止めましょう。 + +この使い方はサードパーティのバンドルから誤って取り入れられたものです。 Symfony がサービスコンテナ機能を +実装したとき、開発者の中にはこのテクニックによってサービスを上書きできるようにした人もいました。 +しかし、クラス名を変更しただけでサービスを上書きするのは非常に稀なユースケースです。というのも、大抵の場合、 +新しいサービスには、上書きされるサービスとは違うコンストラクタ引数があるからです。 + +永続化レイヤーを利用する +------------------------- + +Symfony は、 各 HTTP リクエストに対する HTTP レスポンスを作ることだけを担当する HTTP のフレームワークです。 +そのため、 Symfony は永続化レイヤー(データベースや外部API)にアクセスする方法を提供していません。開発者は +好きなライブラリやデータ保存方法を選ぶことができます。 + +実際には、 Symfony アプリケーションの多くは `Doctrine`_ に依存しており、エンティティやレポジトリを +使ってモデルを定義しています。 +ビジネスロジックと同じく、 Doctrine のエンティティも ``AppBundle`` に配置すると良いでしょう。 + +一例として、サンプルのブログアプリケーションで定義した3つのエンティティがあります。 + +.. code-block:: text + + symfony2-project/ + ├─ ... + └─ src/ + └─ AppBundle/ + └─ Entity/ + ├─ Comment.php + ├─ Post.php + └─ User.php + +.. tip:: + + もちろん、独自の名前空間で ``src/`` の直下にエンティティを置くこともできます。 + +Doctrine のマッピング +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Doctrine のエンティティは、データベースに保存することができるプレーンな PHP オブジェクトです。 +Doctrine は、クラスに対して定義されたマッピングメタデータによってエンティティを扱います。マッピングメタデータ +を定義するには YAML, XML, PHP, アノテーション形式が利用できます。 + +.. best-practice:: + + Doctrine エンティティのマッピングにはアノテーションを使いましょう。 + +アノテーションは、マッピングを定義したり探したりするのに、現在のところ最も便利で素早く使える形式です。 + +.. code-block:: 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 にはデフォルトではデータフィクスチャー機能は存在しないため、フィクスチャーを扱うためには下記のコマンドを +実行して DoctrineFixturesBundle をインストールする必要があります。 + +.. code-block:: bash + + $ composer require "doctrine/doctrine-fixtures-bundle" + +バンドルを ``AppKernel.php`` で有効化します。その際、 ``dev`` 環境と ``test`` 環境だけにしてください。 + +.. code-block:: php + + 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つ* だけ作ることをおすすめします。クラスが大きくなりすぎる +場合はもっとたくさんのフィクスチャークラスを作っても構いません。 + +少なくとも1つのフィクスチャークラスがあり、データベースへのログイン情報が正しく設定されているのを確認したら、 +下記のコマンドを実行するとフィクスチャーを読み込ませることができます。 + +.. code-block:: bash + + $ php app/console doctrine:fixtures:load + + Careful, database will be purged. Do you want to continue Y/N ? Y + > purging database + > loading AppBundle\DataFixtures\ORM\LoadFixtures + + +コーディング規約 +------------------ + +Symfonh のソースコードは、 PHP コミュニティで定められた `PSR-1`_ と `PRS-2`_ のコーディング規約に +従っています。詳しくは `Symfonyのコーディング規約`_ を読んでください。 +または、`PHP-CS-Fixer`_ コマンドを使うこともできます。PHP-CS-Fixerはコードベース全体のコーディング規約を +ほんのの数秒で修正することができるコマンドラインツールです。 + +.. _`完全な定義`: http://en.wikipedia.org/wiki/Business_logic +.. _`Doctrine`: http://www.doctrine-project.org/ +.. _`フィクスチャークラス`: http://symfony.com/doc/master/bundles/DoctrineFixturesBundle/index.html#writing-simple-fixtures +.. _`PSR-1`: http://www.php-fig.org/psr/psr-1/ +.. _`PSR-2`: http://www.php-fig.org/psr/psr-2/ +.. _`Symfonyのコーディング規約`: http://symfony.com/doc/current/contributing/code/standards.html +.. _`PHP-CS-Fixer`: https://github.com/fabpot/PHP-CS-Fixer diff --git a/best_practices/configuration.rst b/best_practices/configuration.rst new file mode 100644 index 0000000..969fcdd --- /dev/null +++ b/best_practices/configuration.rst @@ -0,0 +1,164 @@ +設定 +============= + +設定は、大抵、アプリケーション内の別々の部分(例えば、インフラとユーザー権限のような)と別々の環境 +(開発環境、プロダクション環境)に関連しています。 +そこで、 Symfony はアプリケーションの設定を3つの部分に分けて考えるようにしています。 + +インフラに関係する設定 +------------------------------------ + +.. best-practice:: + + インフラに関係する設定オプションは ``app/config/parameters.yml`` ファイルに定義しましょう。 + +デフォルトの ``parameters.yml`` ファイルはそうなっており、データベースとメールサーバーの設定を定義しています。 + +.. code-block:: yaml + + # app/config/parameters.yml + parameters: + database_driver: pdo_mysql + database_host: 127.0.0.1 + database_port: ~ + database_name: symfony + database_user: root + database_password: ~ + + mailer_transport: smtp + mailer_host: 127.0.0.1 + mailer_user: ~ + mailer_password: ~ + + # ... + + +このオプションは ``app/config/config.yml`` では定義されていません。というのも、アプリケーションの振る舞いに +全く関係がないからです。 +つまり、アプリケーションは、オプションが正しく設定されている限りでは、データベースの場所やユーザー名や +パスワードに関心を持たないのです。 + +デフォルトのパラメータ +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. best-practice:: + + アプリケーションの全てのパラメータを ``app/config/parameters.yml.dist`` に定義しましょう。 + +バージョン2.3以降、 Symfony には ``parameters.yml.dist`` という設定ファイルが含まれており、アプリケーションの +設定で使われるデフォルトのパラメーターリストが保存されています。 + +新しい設定パラメータを定義したなら、このファイルにもそのパラメータを追加して、バージョン管理に反映させましょう。 +開発者がプロジェクトをアップデートした時やサーバーにデプロイした時に、 デフォルトの ``parameters.yml.dist`` と +ローカルの ``parameters.yml`` の差分の有無を Symfony が自動的に調べます。 +もし差分があれば、 Symfony は新しいパラメータの値を尋ね、その値を ``parameters.yml`` に追記します。 + +アプリケーションに関する設定 +--------------------------------- + +.. best-practice:: + + アプリケーションの振る舞いに関する設定は ``app/config/config.yml`` に定義しましょう。 + + +``config.yml`` ファイルには、メール通知の送信元や `feature toggles`_ のような、アプリケーションの振る舞いを +変える設定が含まれます。 +このようなオプション値を ``parameters.yml`` ファイルに定義してしまうと、サーバーごとに変える必要はない設定まで +サーバーごとに設定しなかればならなくなります。 + +``config.yml`` に定義されている設定オプションは、大抵、 `execution environment`_ によって変わります。 +そこで、 Symfony には ``app/config/config_dev.yml`` と ``app/config/config_prod.yml`` があり、環境ごとに +別の値を設定できるようになっているのです。 + +定数か、設定オプションか +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +アプリケーションの設定をするときに最もありがちなミスは、ページ分割の1ページあたりの件数のような、滅多に +変更しない値をオプションとして定義することです。 + +.. best-practice:: + + 滅多に変更しないオプションは定数として定義しましょう。 + +設定オプションを定義する伝統的な方法の結果として、多くの Symfony アプリケーションで下のようなオプションが +定義されています。ブログホームページに表示する投稿の数を制御するオプション値です。 + +.. code-block:: yaml + + # app/config/config.yml + parameters: + homepage.num_items: 10 + +この類いのオプションを最後に変更したのはいつだったか自問してみると、おそらく *一度も変更したことがない* という +答えが出るでしょう。変更しない設定を設定オプションにするのは無駄です。このような値は定数として定義するのが良い +でしょう。 +例えば、 ``Post`` エンティティの ``NUM_ITEMS`` 定数として定義するのです。 + +.. code-block:: php + + // src/AppBundle/Entity/Post.php + namespace AppBundle\Entity; + + class Post + { + const NUM_ITEMS = 10; + + // ... + } + +定数として定義する方式の主なメリットは、値をアプリケーション内のどこでも利用できることです。パラメータとして +定義してしまうと、 Symfony のDIコンテナにアクセスできる場所でしか値を利用できませんでした。 + +例えば、定数は ``constant()`` ヘルパーを利用して Twig テンプレートの中でも使うことができます、 + +.. code-block:: html+jinja + +
+ 最新の投稿から {{ constant('NUM_ITEMS', post) }} 件表示しています。 +
+ +コンテナのパラメータを取得できない Doctrine のエンティティやレポジトリからも値を利用できます。 + +.. code-block:: php + + namespace AppBundle\Repository; + + use Doctrine\ORM\EntityRepository; + use AppBundle\Entity\Post; + + class PostRepository extends EntityRepository + { + public function findLatest($limit = Post::NUM_ITEMS) + { + // ... + } + } + +定数で設定を定義することの唯一の弱点として、テストの時に簡単に値を上書きすることができないので、注意してください。 + +セマンティックな設定(利用してはいけません) +-------------------------------------------- + +.. best-practice:: + + バンドルの設定をセマンティックなDI設定として定義しないようにしましょう。 + +`How to Expose a semantic Configuration for a Bundle`_ で説明されているように、 Symfony のバンドルには設定の方法が +2つあります。 ``services.yml`` を使った通常の設定と、特別な ``*Extension`` クラスを使ったセマンティックな設定です。 + +セマンティックな設定は強力で、設定項目のバリデーションのような役立つ機能もありますが、セマンティック設定を定義する +のにかかる作業が多すぎて、サードパーティのバンドルとして再利用されないバンドルを作る場合には釣り合いが取れないのです。 + +センシティブなオプションを完全に Symfony の外に追い出す +-------------------------------------------------------- + +データベースの接続情報のようなセンシティブなオプションを設定するときには、 Symfony のプロジェクトの外部に保存し、 +環境変数を利用して読み込む方式も推奨します。 +この方式をどのようにして実現するのかについては、 `How to Set external Parameters in the Service Container`_ を +参照してください。 + +.. _`feature toggles`: http://en.wikipedia.org/wiki/Feature_toggle +.. _`execution environment`: http://symfony.com/doc/current/cookbook/configuration/environments.html +.. _`constant() function`: http://twig.sensiolabs.org/doc/functions/constant.html +.. _`How to Expose a semantic Configuration for a Bundle`: http://symfony.com/doc/current/cookbook/bundles/extension.html +.. _`How to Set external Parameters in the Service Container`: http://symfony.com/doc/current/cookbook/configuration/external_parameters.html diff --git a/best_practices/forms.rst b/best_practices/forms.rst new file mode 100644 index 0000000..59d2fb7 --- /dev/null +++ b/best_practices/forms.rst @@ -0,0 +1,206 @@ +フォーム +========== + +フォームは、取り扱う範囲が広く、膨大な機能があるために、誤って使用されることが最も多いコンポーネントです。 +この章では、フォームを活用して素早く実装できるようにすることができるベストプラクティスを説明します。 + +フォームを構築する +------------------- + +.. best-practice:: + + フォームをPHPクラスで定義しましょう。 + +Formコンポーネントでは、コントローラーのコードの中でもフォームを構築できるようになっています。はっきり言って、フォームをどこか他の場所で使う予定がないのなら、それでも全く構いません。 +しかし、コードの整理と再利用のために、あらゆるフォームをPHPクラスとして定義することをおすすめします。 + +.. code-block:: php + + namespace AppBundle\Form; + + use Symfony\Component\Form\AbstractType; + use Symfony\Component\Form\FormBuilderInterface; + use Symfony\Component\OptionsResolver\OptionsResolverInterface; + + class PostType extends AbstractType + { + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('title') + ->add('summary', 'textarea') + ->add('content', 'textarea') + ->add('authorEmail', 'email') + ->add('publishedAt', 'datetime') + ; + } + + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'data_class' => 'AppBundle\Entity\Post' + )); + } + + public function getName() + { + return 'post'; + } + } + +このフォームクラスを利用するためには、 ``createForm`` を使い、この新しいクラスのインスタンスを渡してください。 + +.. code-block:: php + + use AppBundle\Form\PostType; + // ... + + public function newAction(Request $request) + { + $post = new Post(); + $form = $this->createForm(new PostType(), $post); + + // ... + } + +フォームをサービスとして登録する +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`フォームタイプをサービスとして定義する`_ こともできます。 +しかし、多くの場所でフォームを再利用する予定があったり、他のフォームに直接あるいは `collection`_ として埋め込む予定がない限り、この方法は推奨 *しません* 。 + +ほとんどのフォームは、何かを編集したり作成したりするのに使われるだけなので、フォームをサービスとして登録するのはやりすぎです。コントローラーの中で使われているフォームクラスがどれなのかわかりにくくなってしまいます。 + +フォームのボタン定義 +------------------------- + +フォームクラスは、フォームがアプリケーション内の *どこで* 使われるのかなるべく無関心であるべきです。後から再利用しやすくなります。 + +.. best-practice:: + + ボタンはテンプレート上で追加し、フォームクラスやコントローラーに書かないようにしましょう。 + +Symfony 2.5以降では、フォームにボタンの定義を追加することができます。フォームをレンダリングするテンプレートを簡略にするのに良い方法ですが、フォームクラスの中に直接ボタンを定義してしまうと、フォームの扱う範囲を極端に狭めてしまうことになるのです。 + +.. code-block:: php + + class PostType extends AbstractType + { + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + // ... + ->add('save', 'submit', array('label' => 'Create Post')) + ; + } + + // ... + } + +このフォームは投稿を新規作成する用途で作られたの *かも* しれませんが、投稿を編集する場面で再利用しようとすると、ボタンのラベルが間違っていることになります。 +その代わりに、コントローラーでフォームボタンを定義する開発者もいるでしょう。 + +.. code-block:: php + + namespace AppBundle\Controller\Admin; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Bundle\FrameworkBundle\Controller\Controller; + use AppBundle\Entity\Post; + use AppBundle\Form\PostType; + + class PostController extends Controller + { + // ... + + public function newAction(Request $request) + { + $post = new Post(); + $form = $this->createForm(new PostType(), $post); + $form->add('submit', 'submit', array( + 'label' => 'Create', + 'attr' => array('class' => 'btn btn-default pull-right') + )); + + // ... + } + } + + +これも重大な誤りです。というのも、プレゼンテーション用のマークアップ(ラベル、CSSクラスなど)を純粋なPHPコードの中に混在させてしまっているからです。 +関心の分離は常に意識すべきプラクティスであり、全ての見た目に関係する物事はviewレイヤーに置くべきでしょう。 + +.. code-block:: html+jinja + + + +フォームをレンダリングする +--------------------------- + +フォームをレンダリングする方法は、フォーム全体を一行でレンダリングするのから、フィールドの各パーツを個別にレンダリングするのまで、多岐に渡ります。 +一番良い方法は、アプリケーションでどこまでのカスタマイズが必要かによって異なります。 + +一番シンプルな方法(特に開発中に便利な方法)はHTMLのフォームタグを書いてから、 ``form_widget()`` を使って全てのフィールドを一度にレンダリングする方法です。 + +.. code-block:: html+jinja + + + +.. best-practice:: + + フォームの開始タグや終了タグに ``form()`` や ``form_start()`` を使わないようにしましょう。 + +Symfony に慣れた開発者なら、 ``