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