このエントリーをはてなブックマークに追加

Symfony Advent Calendar 2016 11日目の記事です。 昨日は @kalibora さんの Symfony Console のコマンド名を自動的に Monolog のログに出そう でした。

Symfony\Component\ExpressionLanguageとは

式言語と訳されますが、一言で言うと条件式を評価することができるライブラリです。 Symfony ExpressionLanguage公式ドキュメント

通常、条件式の内容を書く人として想定されているのはプログラマではなくユーザーです。ユーザー(エンドユーザーなり管理者なり)が直接条件式を編集できることでいちいちプログラマの手を煩わせる必要がないという利便性があり、生のPHPコードを入力させてevalするのに比べて、 できることを制限しているため に安全に実行することができます。

例えば下記のようなコードを予め書いておきます(実際は $_POST を直接使ってはいけません :sweat_smile:

<?php
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

$condition = $_POST['condition'];

$lang = new ExpressionLanguage();
$isSatisfied = $lang->evaluate($condition, ['stock' => $stock]); 

if ($isSatisfied) {
    // 条件が満たされた場合の処理
} else {
    // 条件が満たされなかった場合の処理
}

ユーザーは、$_POST[‘condition’]に

  • stock == 0 を指定すれば、stockが0の時に条件が満たされた場合の処理を実行させることができます
  • stock < 5 を指定すれば、stockが5より小さい時に条件が満たされた場合の処理を実行させることができます

つまり、ユーザーはプログラマを急かせることなく自分の手で、stockの値に対して必要に応じて様々な条件を指定して、後続の処理を制御できるのです。

上の例では変数を使ってみましたが、基本的な四則計算やand/or条件指定、正規表現、オブジェクトのメソッド呼び出し等も実装されています。 詳しい使い方は Expression syntax を参照してください。

ExpressionLanguageでPHP関数を使う

前述のように、ExpressionLanguageでは、できることを制限してあるため、通常のPHP関数を使うことはできません。ユーザーが条件式として unlink('/') なんて入力してきたら大変ですからね :smirk:

初期状態で使えるPHP関数

初期状態で使えるようになっているのは、 constant() 関数だけです。

PHPの関数をユーザー関数として登録する

PHPの関数を使いたい場合は、ユーザー定義関数として登録してから利用します。 式言語の趣旨を考えると、状態を変える関数は使わず、単に値を返す関数だけにしたほうが良いでしょう。

一例として、 ucfirst() 関数を登録してみます。

<?php
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

$lang = new ExpressionLanguage();

// 登録してない関数を使う
//$lang->evaluate('ucfirst("quartet")'); // Symfony\Component\ExpressionLanguage\SyntaxError がスローされる

// 関数を登録
$lang->register('ucfirst', function($value){
    // コンパイルするときの動作を定義
    return sprintf('ucfirst(%s)', $value);
}, function($arguments, $value){
    // 評価するときの動作を定義
    return ucfirst($value);
});

// 関数を利用できるようになる
$lang->evaluate('ucfirst("quartet")'); // Quartet

// コンパイルするとPHPコードが文字列で書き出される
$lang->compile('ucfirst("quartet")'); // ucfirst("quartet")

ExpressionLanguage::register() の第一引数は、関数名です。 式言語の中で利用するための関数名なので、PHPの関数名をそのまま使う必要はありません。自由に決めることができます。 PHPの省略されすぎてわかりにくい標準関数名をわかりやすく変えて登録するのもOKです。 ただし、後から同名の違う関数を登録すると最後に登録したもので上書きされてしまうので注意してください。

第二引数と第三引数には callable を指定します(クロージャーなど)。

第二引数には、式言語をコンパイルするときの動作を定義します。 上の例では、 ucfirst("quartet") をコンパイルしたときに ucfirst("quartet") が返ってくるクロージャーを書いてあります。

第三引数には、式言語を評価するときの動作を定義します。 上の例では、 ucfirst("quartet") を評価したときに "Quartet" が返ってくるクロージャーを書いておけば良いですね。

まとめ

ExpressionLanguageを使うと、できることをシステムに危険がない範囲に制限した上で、細かい仕様のコントロールをユーザーに任せることも可能になります。 うまく利用して、開発スピードとユーザー満足度をどちらも高められるようにしたいですね。

明日は @eretica さんです。


このエントリーをはてなブックマークに追加

はじめに

前回の記事では、Symfony 3.2で継承関係のあるエンティティをDoctrineのSingle Table Inheritanceを使って実装し、テスト時にYAMLのフィクスチャとしてエンティティのテストデータを用意するサンプルを紹介しました。

前回の記事の時点でのエンティティクラスは、下図のようになっています。

前回時点のエンティティクラス図

今回は、配送時期種別を種別(即納 Instant と見積 Estimation)それぞれ別のクラスにします。アプリケーションコード側ではこれらのクラスのインスタンスで扱い、DBにはこれまで通り文字列で保存を行います。この変換にDoctrineのEntityListenerを使います。クラスは以下のように変わります。

今回の実装後のエンティティクラス図

実装を行ったバージョンのソースコードは、以下から参照してください。

利用バージョン

記事執筆時点では、以下のバージョンになっています。

  • Symfony 3.2.0
  • Doctrine ORM 2.5.5

Embeddableではダメなのか?

DoctrineにはValueObjectを実現するためのEmbeddableの機能があり、前回のサンプル実装でも ItemPrice にこの機能を利用していました。商品の価格を Item エンティティから切り出して、価格の値を中心に、それに強く関係のあるメソッドを1箇所にまとめるには、Embeddableを利用したValueObjectが上手く当てはまります。

今回実装しようとしている例では、配送時期種別の値(instantestimation)は単なるラベルであり、この値自身が重要な役割を持っているわけではありません。より重きがあるのは、種別によって異なる計算ロジックの方です。このような種別のバリエーションを実装する場合は、クラス図に示したように1つのインターフェイスを共有する複数のクラスによって表現することがあります(※1)。

今回示した例のように種別が2つくらいだと、ItemPrice と同じように1つのValueObjectクラスにしておいて、その中で分岐させる方が労力が少なそうに思えるかもしれません。しかし、種別の数が多い場合は手に負えないコードになってしまいますね。また、そのような煩雑さと別としても、種別を個々にクラスにしておくことで、今回取り扱っている配送時期というポイントとは異なる振舞のインターフェイスをミックスするといった表現への道も開けます(※2)。

Docrineではインターフェイスを介した複数のクラスをEmbeddableとして扱うことができません。幸いDoctrineにはEntityListenerがあり、これを使うことで、今回のような高度なニーズにも対応できます。

(※1) PHPには表現方法の選択肢が多くないためこうなりますが、別の言語では事情は異なります。

(※2) 書籍『ジェネレーティブプログラミング』ではジェネレーティブドメインモデルとして、ベースとなるフィーチャの組み合わせを定義してドメインモデルを生成するアプローチが紹介されています。

実装例

EntityListenerを使うと決まれば、実装はさほど難しくはありません。方針としては次のようになります。

  • DBには(前回の記事での実装と同様に)文字列で配送時期種別を保存する
  • プログラムコード内でエンティティを取り出したら、DeliveryTimeTypeInterfaceを実装したクラスのインスタンスに自動的に変換されているようにする

Item エンティティのEntityListenerから呼ばれて変換を行う ItemDeliveryTimeTypeTransformer を最初に実装しておきます。このクラス内で種別ごとのValueObjectのインスタンスを作ってしまっている点はやや手抜きになっています。

<?php
namespace AppBundle\Transformer;

use AppBundle\DeliveryTime\DeliveryTimeTypeInterface;
use AppBundle\DeliveryTime\Type\Estimation;
use AppBundle\DeliveryTime\Type\Instant;
use AppBundle\DeliveryTime\Type\Invalid;

class ItemDeliveryTimeTypeTransformer
{
    /**
     * @var array
     */
    private $map;

    public function __construct()
    {
        $this->map = [
            'instant' =>    new Instant(),
            'estimation' => new Estimation(),
        ];
    }

    /**
     * @param DeliveryTimeTypeInterface $deliveryTimeType
     * @return string
     */
    public function toDB(DeliveryTimeTypeInterface $deliveryTimeType)
    {
        return $deliveryTimeType->getType();
    }

    /**
     * @param string $type
     * @return DeliveryTimeTypeInterface
     */
    public function toModel($type)
    {
        if (!array_key_exists($type, $this->map)) {
            return new Invalid();
        }

        return $this->map[$type];
    }
}

変換処理では、正常な種別とそうではない場合(Invalid)とを区別して扱っています。正常ではない場合にすぐさま例外をスローする方針もありえます。しかし、この変換処理をSymfonyのFormなどと連携させる場合には、変換処理として一旦正しく完了した後に中身を判定してエラーにするという流れが必要になります。ここで安易に例外をスローすると、フレームワークの処理と馴染まなくなってしまいます。

このTransformerを利用する ItemListener は次のように、ほぼ定型のコードのみとなります。

<?php
namespace AppBundle\Entity\Listener;

use AppBundle\Transformer\ItemDeliveryTimeTypeTransformer;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use Doctrine\ORM\Mapping as ORM;
use AppBundle\Entity\Item;

class ItemListener
{
    /**
     * @var ItemDeliveryTimeTypeTransformer
     */
    private $transformer;

    public function __construct(ItemDeliveryTimeTypeTransformer $transformer)
    {
        $this->transformer = $transformer;
    }

    /**
     * @ORM\PostLoad()
     * @param Item $item
     * @param LifecycleEventArgs $event
     */
    public function postLoad(Item $item, LifecycleEventArgs $event)
    {
        $deliveryTimeType = $this->transformer->toModel($item->getDeliveryTimeType());
        $item->setDeliveryTimeType($deliveryTimeType);
    }

    /**
     * @ORM\PrePersist()
     * @param Item $item
     * @param LifecycleEventArgs $event
     */
    public function prePersistHandler(Item $item, LifecycleEventArgs $event)
    {
        $deliveryTimeType = $this->transformer->toDB($item->getDeliveryTimeType());
        $item->setDeliveryTimeType($deliveryTimeType);
    }

    /**
     * @ORM\PreUpdate()
     * @param Item $item
     * @param LifecycleEventArgs $event
     */
    public function preUpdateHandler(Item $item, LifecycleEventArgs $event)
    {
        $deliveryTimeType = $this->transformer->toDB($item->getDeliveryTimeType());
        $item->setDeliveryTimeType($deliveryTimeType);
    }
}

これらをサービスとして定義して、利用できるようにします(バンドル配下でYAMLのサービス定義を読み込むように、お決まりのエクステンションクラスなども追加します)。

services:
  app.entity.item_listener:
    class: AppBundle\Entity\Listener\ItemListener
    arguments:
      - '@app.transformer.item_delivery_time_type_transfomer'
    tags:
      - { name: doctrine.orm.entity_listener }

  app.transformer.item_delivery_time_type_transfomer:
    class: AppBundle\Transformer\ItemDeliveryTimeTypeTransformer

最後に Item エンティティクラスで ItemListener を有効にします。

 /**
  * Item
  *
  * @ORM\Table(name="item")
  * @ORM\Entity(repositoryClass="AppBundle\Repository\ItemRepository")
  * @ORM\DiscriminatorColumn(name="item_type", type="string")
  * @ORM\DiscriminatorMap({
  *   "item_abstract": "AppBundle\Entity\Item",
  *   "normal":        "AppBundle\Entity\Item\NormalItem",
  *   "custom_order":  "AppBundle\Entity\Item\CustomOrderItem",
  * })
  * @ORM\InheritanceType(value="SINGLE_TABLE")
+ * @ORM\EntityListeners({"AppBundle\Entity\Listener\ItemListener"})
  */
 abstract class Item
 {

あとは、DeliveryTimeクラス群を用意したら実装は完了です。

テストの修正

テストの側では、まずYAMLフィクスチャを修正する必要があります。配送時期種別のValueObjectをそれぞれ (local) で定義し、Itemのデータ定義ではそれらを参照するように書き換えます。


+AppBundle\DeliveryTime\Type\Estimation(local):
+  estimation1: ~
+AppBundle\DeliveryTime\Type\Instant(local):
+  instant1: ~

 AppBundle\Entity\Item\NormalItem:
   normal_item1:
     name: 商品1
     price: "@price1"
-    deliveryTimeType: instant
+    deliveryTimeType: "@instant1"
   normal_item2:
     name: 商品2
     price: "@price2"
-    deliveryTimeType: instant
+    deliveryTimeType: "@instant1"
   normal_item3:
     name: 商品3
     price: "@price3"
-    deliveryTimeType: estimation
+    deliveryTimeType: "@estimation1"

 AppBundle\Entity\Item\CustomOrderItem:
   custom_order_item1:
     name: カスタム商品1
     price: "@price4"
-    deliveryTimeType: estimation
+    deliveryTimeType: "@estimation1"
   custom_order_item2:
     name: カスタム商品2
     price: "@price5"
-    deliveryTimeType: estimation
+    deliveryTimeType: "@estimation1"

テストコードには、配送時期種別のクラスを表示するコードのみ追加しておきましょう。 (なお、フィクスチャを読み込んだエンティティマネージャを一旦強制的にクリアしているのは、エンティティ読み込み時の変換処理を明示的に動作させるためです)

<?php
namespace AppBundle\Functional\Entity;

use Doctrine\ORM\EntityManagerInterface;
use Liip\FunctionalTestBundle\Test\WebTestCase;
use AppBundle\Entity\Item;

class ItemTest extends WebTestCase
{
    public function test()
    {
        $this->loadFixtureFiles([
            __DIR__.'/../../Resources/fixtures/items.yml',
        ]);

        /** @var EntityManagerInterface $em */
        $em = static::getContainer()->get('doctrine')->getManager();
        $em->clear();

        $items = $em->getRepository(Item::class)->findAll();
        foreach ($items as $item) {
            echo get_class($item) . PHP_EOL;
            echo get_class($item->getDeliveryTimeType()) . PHP_EOL;
            echo sprintf('%s %s %s',
                    $item->getName(),
                    $item->getPrice(),
                    $item->getDeliveryTimeType()) . PHP_EOL;
        }
    }
}

プロジェクト全体のテストを実行すると、以下のようにテキストが出力されます。 テストコードでは、YAMLフィクスチャでDBに投入したデータを読み込んだものを表示しているだけですが、この段階で配送時期種別が文字列データではなく、各配送時期種別クラスのインタスンスになっていることが分かります。

$ vendor/bin/phpunit
PHPUnit 4.8.29 by Sebastian Bergmann and contributors.

.AppBundle\Entity\Item\CustomOrderItem
AppBundle\DeliveryTime\Type\Estimation
カスタム商品1 400 estimation
AppBundle\Entity\Item\CustomOrderItem
AppBundle\DeliveryTime\Type\Estimation
カスタム商品2 500 estimation
AppBundle\Entity\Item\NormalItem
AppBundle\DeliveryTime\Type\Instant
商品1 100 instant
AppBundle\Entity\Item\NormalItem
AppBundle\DeliveryTime\Type\Instant
商品2 200 instant
AppBundle\Entity\Item\NormalItem
AppBundle\DeliveryTime\Type\Estimation
商品3 300 estimation


Time: 446 ms, Memory: 12.00MB

おわりに

この記事では、単一のクラスではなく、複数のクラスを使って表現されたValueObject群をEntityListenerを使ってエンティティのフィールドにマッピングする方法を紹介しました。

次回は今回実装したValueObject群を持つエンティティに対して、WebAPIの入出力を行えるようにするためのSymfonyのFormの作り方を紹介します。


このエントリーをはてなブックマークに追加

はじめに

Symfonyでアプリケーションを開発する際、テストしたい状況ごとにデータベースのデータをYAMLのフィクスチャファイルとして用意しておくと開発がはかどります。Symfonyではバージョン2の頃から、nelmio/aliceおよびaliceを手軽に使うためのバンドルがよく使われています。先日Symfony 3.2がリリースされましたので、この記事では、Symfony 3.2のプロジェクトとしてaliceを使うプロジェクトの雛形を紹介します。

このプロジェクトは、Doctrineの単一テーブル継承(Single Table Inheritance)、および埋め込みエンティティ(ValueObjectまたはEmbedded)を使う例にもなっています。

利用バージョン

記事執筆時点では、以下のバージョンになっています。

インストール

Symfonyプロジェクトは、Symfony Installerでインストールすると楽ですが、記事執筆時点ではSymfony InstallerがSymfony 3.2に対応していなかったため、composerからインストールしました。

$ composer create-project symfony/framework-standard-edition sf32-app-sample201612
$ cd sf32-app-sample201612

これでSymfony Standard Editionのプロジェクトファイル群が作られます。続いて次のコマンドで、ユニットテストおよびYAMLフィクスチャを利用するのに必要なバンドル/ライブラリをインストールします。

$ composer require --dev phpunit/phpunit liip/functional-test-bundle hautelook/alice-bundle

バンドルの有効化

インストールしたバンドルを、app/AppKernel.php にて有効化します。

<?php

use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = [
            new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
            ...
        ];

        if (in_array($this->getEnvironment(), ['dev', 'test'], true)) {
            ...
+           $bundles[] = new Liip\FunctionalTestBundle\LiipFunctionalTestBundle();
+           $bundles[] = new Hautelook\AliceBundle\HautelookAliceBundle();
        }

テスト用コンフィギュレーションの追記

テストでsqliteを使うため app/config/config_test.yml に、以下を追記します。

+doctrine:
+    dbal:
+        default_connection: default
+        connections:
+            default:
+               driver:   pdo_sqlite
+               path:     "%kernel.cache_dir%/test.db"
+
+liip_functional_test:
+    cache_sqlite_db: true

エンティティの実装

以下の簡単な要件のエンティティを実装します。

  • 商品情報エンティティ
  • 商品名と価格、配送種別を持つ
  • 商品には通常の商品と特注品とがあり、それぞれに特殊なロジックが必要なので、別々のエンティティクラスに分けておきたい

また、今のところ大きな意味はありませんが、価格はValueObjectにしておきます。配送種別は、現段階では単に文字列で種別を格納しておきます。

Doctrineの単一テーブル継承を使って、Item および NormalItemCustomOrderItem、ValueObjectの ItemPrice は、それぞれ以下のようになります。

TIPS エンティティを実装する際、最初の段階から各フィールドの制約(Assert)まで書くようにしておくのは、開発時のドキュメンテーションとしての効果もあってオススメです。

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * Item
 *
 * @ORM\Table(name="item")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\ItemRepository")
 * @ORM\DiscriminatorColumn(name="item_type", type="string")
 * @ORM\DiscriminatorMap({
 *   "item_abstract": "AppBundle\Entity\Item",
 *   "normal":        "AppBundle\Entity\Item\NormalItem",
 *   "custom_order":  "AppBundle\Entity\Item\CustomOrderItem",
 * })
 * @ORM\InheritanceType(value="SINGLE_TABLE")
 */
abstract class Item
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=255)
     * @Assert\NotBlank
     */
    private $name;

    /**
     * @var ItemPrice
     * @ORM\Embedded(class="AppBundle\Entity\ItemPrice", columnPrefix="price_")
     * @Assert\Valid
     */
    private $price;

    /**
     * @var string
     * @ORM\Column(name="delivery_time_type", type="string", length=255)
     * @Assert\NotBlank
     * @Assert\Choice(choices={"instant", "estimation"})
     */
    private $deliveryTimeType;

    /**
     * Get id
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    : 以下getter/setter
}
<?php
namespace AppBundle\Entity\Item;

use Doctrine\ORM\Mapping as ORM;
use AppBundle\Entity\Item;

/**
 * @ORM\Entity()
 */
class NormalItem extends Item
{

}
<?php
namespace AppBundle\Entity\Item;

use Doctrine\ORM\Mapping as ORM;
use AppBundle\Entity\Item;

/**
 * @ORM\Entity()
 */
class CustomOrderItem extends Item
{

}
<?php
namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Embeddable
 */
class ItemPrice
{
    /**
     * @var int
     *
     * @ORM\Column(name="price", type="integer")
     * @Assert\NotBlank
     * @Assert\GreaterThanOrEqual(0)
     */
    private $price;

    /**
     * @return int
     */
    public function getPrice()
    {
        return $this->price;
    }

    /**
     * @param int $price
     */
    public function setPrice($price)
    {
        $this->price = $price;
    }

    public function __toString()
    {
        return (string)$this->price;
    }
}

DBのテーブルを生成

ここで一旦、エンティティの実装および定義が有効か確認するために、データベースのスキーマを生成しておきましょう。

$ php bin/console doctrine:database:create
$ php bin/console doctrine:schema:create

テストディレクトリの準備

Symfony3では、アプリケーション/バンドルの標準ディレクトリ構成が変更になり、これまでは各バンドルの中に配置されていたテスト関係のファイルを、プロジェクトルートディレクトリの tests 配下へまとめるようになりました。 YAMLのフィクスチャファイルもテスト時に利用するものなので、tests 配下の方へ配置します。このサンプルでは、以下のような配置にしました。

tests
└── AppBundle
    ├── Functional
    │   └── Entity
    │       └── ItemTest.php
    └── Resources
        └── fixtures
            └── items.yml

YAMLフィクスチャファイル

商品情報を登録するYAMLフィクスチャファイルは、以下のようになります。ValueObjectを使っているItemPriceの部分がやや手間ですが、スッキリと定義できますね。aliceの細かな機能については、aliceのドキュメントを参照してください。

AppBundle\Entity\ItemPrice(local):
  price1:
    price: 100
  price2:
    price: 200
  price3:
    price: 300
  price4:
    price: 400
  price5:
    price: 500

AppBundle\Entity\Item\NormalItem:
  normal_item1:
    name: 商品1
    price: "@price1"
    deliveryTimeType: instant
  normal_item2:
    name: 商品2
    price: "@price2"
    deliveryTimeType: instant
  normal_item3:
    name: 商品3
    price: "@price3"
    deliveryTimeType: estimation

AppBundle\Entity\Item\CustomOrderItem:
  custom_order_item1:
    name: カスタム商品1
    price: "@price4"
    deliveryTimeType: estimation
  custom_order_item2:
    name: カスタム商品2
    price: "@price5"
    deliveryTimeType: estimation

フィクスチャを読み込むテスト

次は、YAMLのフィクスチャをテストコードから読み込んでみましょう。ItemTest.phpを以下のように作成します。

<?php
namespace AppBundle\Functional\Entity;

use Liip\FunctionalTestBundle\Test\WebTestCase;
use AppBundle\Entity\Item;

class ItemTest extends WebTestCase
{
    public function test()
    {
        $this->loadFixtureFiles([
            __DIR__.'/../../Resources/fixtures/items.yml',
        ]);

        $em = static::getContainer()->get('doctrine')->getManager();

        $items = $em->getRepository(Item::class)->findAll();
        foreach ($items as $item) {
            echo get_class($item) . PHP_EOL;
            echo sprintf('%s %s %s',
                $item->getName(),
                $item->getPrice(),
                $item->getDeliveryTimeType()) . PHP_EOL;
        }
    }
}

liip/functional-test-bundleでは、WebTestCaseクラスに loadFixtureFiles() メソッドがあり、バージョン1.5.0以降ではYAMLフィクスチャファイルのパスを引数にして loadFixtureFiles() メソッドを呼び出すだけで、フィクスチャのロードが完了します。

拙著『基本からしっかり学ぶ Symfony2入門』では、利用しているliip/functional-test-bundleのバージョンが古く、この時はわざわざDoctrineFixturesBundle形式のローダークラスを用意しないといけませんでした(そのためにDoctrineFixturesBundleのインストールも必要でした)。これらは今は不要です。便利になりましたね。

プロジェクト全体のテストを実行すると、以下のようにテキストが出力されます。

$ vendor/bin/phpunit
PHPUnit 4.8.29 by Sebastian Bergmann and contributors.

.AppBundle\Entity\Item\CustomOrderItem
カスタム商品1 400 estimation
AppBundle\Entity\Item\CustomOrderItem
カスタム商品2 500 estimation
AppBundle\Entity\Item\NormalItem
商品1 100 instant
AppBundle\Entity\Item\NormalItem
商品2 200 instant
AppBundle\Entity\Item\NormalItem
商品3 300 estimation


Time: 416 ms, Memory: 12.00MB

おわりに

この記事ではSymfony 3.2でYAMLフィクスチャを使う方法と、Doctrineの単一テーブル継承、ValueObjectを使う例をざっと紹介しました。この記事の応用編として、次回はInterfaceとそれを実装する複数の具象クラスから成るValueObjectを利用する方法を紹介する予定です。