🎻

[Symfony] nelmio/aliceのフィクスチャでエンティティのidなどprivateプロパティも指定したい!

2022/10/20に公開約3,700字

やりたいこと

Symfonyプロジェクトでは liip/test-fixtures-bundle などのバンドルを使って nelmio/alice でテストフィクスチャを作ることが多いと思います。

nelmio/aliceがオブジェクトのプロパティにアクセスする手段として、デフォルトでは Nelmio\Alice\PropertyAccess\StdPropertyAccessor というクラスが使われます。

このクラスはオブジェクトのprivateプロパティにはアクセスできないため、エンティティの $id などの(通常は)セッターを提供しないprivateプロパティについては、フィクスチャで値を指定することはできません。

App\Entity\Foo:
  foo1:
    id: 1
    bar: baz

このフィクスチャをロードしようとすると、

Nelmio\Alice\Throwable\Exception\Generator\DebugUnexpectedValueException: An error occurred while generating the fixture "foo1" (App\Entity\Foo): Could not hydrate the property "id" of the object "foo1" (class: App\Entity\Foo).

このようなエラーになります。

しかし、場合によっては $id などprivateプロパティの値を指定してフィクスチャを作成したいこともあるでしょう。

例えば

例えば、Single Table Inheritance(STI) を使った以下のようなエンティティがあるとします。

#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\DiscriminatorColumn('type')]
#[ORM\DiscriminatorMap([
    'shop' => Shop::class,
    'customer' => Customer::class,
    'admin' => Admin::class,
])]
abstract class User
#[ORM\Entity(repositoryClass: ShopRepository::class)]
class Shop extends User
#[ORM\Entity(repositoryClass: CustomerRepository::class)]
class Customer extends User
#[ORM\Entity(repositoryClass: AdminRepository::class)]
class Admin extends User

これらのクラスのインスタンスがテスト時にそれぞれ必要で、以下のようなフィクスチャを書いたとしましょう。

App\Entity\Shop:
  shop{1..2}:
    # ...

App\Entity\Customer:
  customer{1..2}:
    # ...

App\Entity\Admin:
  admin1:
    # ...

この場合、振られるidはYAMLに定義した順番で

フィクスチャ id
shop1 1
shop2 2
customer1 3
customer2 4
admin1 5

となってほしいところですが、実はそうなりません😓(場合によっては)

詳細は調べられていませんが、DBレイヤーで同じテーブルを共有するエンティティの場合には、YAMLの書き順とINSERT文の発行順が必ずしも一致しないようです。(詳しい方いたら詳細コメントいただけると嬉しいです🙏)

このような場合、実際に一度実行してみて、結果的にどのフィクスチャがidいくつになるのかを調べて、それに合わせてテストコードを書く、ということをすればとりあえずは問題なくテストできるのですが、そのidの採番順が常に同一であることがどこかで保証されているのかどうなのか分からないのでとても不安です。

そこで、こんなふうにidを指定したくなります。

App\Entity\Shop:
  shop{1..2}:
    id: <current()>
    # ...

App\Entity\Customer:
  customer{1..2}:
    id: <identity($current+2)>
    # ...

App\Entity\Admin:
  admin1:
    id: 5
    # ...

やり方

前置きが長くなりましたが、解決策はとても簡単で、デフォルトの Nelmio\Alice\PropertyAccess\StdPropertyAccessor の代わりに Nelmio\Alice\PropertyAccess\ReflectionPropertyAccessor というサービスを使えばよいだけです。

具体的には、config/services_test.yaml に以下の設定を追記するだけでOKです。

services:
  nelmio_alice.property_accessor:
    class: Nelmio\Alice\PropertyAccess\ReflectionPropertyAccessor

これで、

App\Entity\Foo:
  foo1:
    id: 1
    bar: baz

このようなフィクスチャがエラーなくロードできるようになります。

注意点

$id についてこの方法を使う場合、注意点があります。

Doctrine+PostgreSQLでは、#[ORM\GeneratedValue]デフォルトだとpersistしただけでidが進む

この記事で紹介したような方法でPostgreSQLのidカラムのデフォルト値に nextval() が設定されている場合、$id に値を指定してINSERTすると、nextval() が実行されない ため、フィクスチャをロードしたあと、テストコードから普通にエンティティを新規作成しようとすると id=1をINSERTしようとしてしまい、DBレイヤーでエラーになります。

なので、テストコード内で新規作成する必要のあるエンティティの $id は値を指定することを諦めるしかありません。(要出典)

GitHubで編集を提案

Discussion

ログインするとコメントできます