🎻

[Symfony] エンティティに添付ファイル系の項目を持たせる実装例

2020/12/18に公開約6,900字

はじめに

Symfony Advent Calendar 2020 の19日目の記事です!🎄🌙

昨日も僕の記事で、[Symfony] 機能テストでCSRFトークンを送る方法 でした✨

ちなみに、僕はよく TwitterにもSymfonyネタを呟いている ので、よろしければぜひ フォローしてやってください🕊🤲

やりたいこと

  • いくつかのエンティティに添付ファイル系の項目がある
  • 各項目それぞれに1〜複数のファイルを添付できる

こんな要件の実装を考えてみましょう。

やり方1:普通に File エンティティを作る

ごく普通に作ると以下のような感じになるかなと思います。

/**
 * @ORM\Entity(repositoryClass=FooRepository::class)
 */
class Foo
{
    /**
     * @var Collection|File[]
     *
     * @ORM\OneToMany(targetEntity=File::class, mappedBy="foo", cascade={"persist", "remove"})
     */
    public Collection $files;
}
/**
 * @ORM\Entity(repositoryClass=BarRepository::class)
 */
class Bar
{
    /**
     * @var Collection|File[]
     *
     * @ORM\OneToMany(targetEntity=File::class, mappedBy="bar", cascade={"persist", "remove"})
     */
    public Collection $files;
}
/**
 * @ORM\Entity(repositoryClass=FileRepository::class)
 */
class File
{
    /**
     * @ORM\Column(type="string", length=255)
     */
    public ?string $url = null;

    /**
     * @ORM\ManyToOne(targetEntity=Foo::class, inversedBy="files")
     */
    public ?Foo $foo = null;

    /**
     * @ORM\ManyToOne(targetEntity=Bar::class, inversedBy="files")
     */
    public ?Bar $bar = null;
}

File クラスやアップロード処理の実装の詳細についてはこの記事では解説しません🙏

他のエンティティにも $files 項目が登場したら、その都度 File クラスにリレーションを追加していく感じですね。

  /**
   * @ORM\Entity(repositoryClass=FileRepository::class)
   */
  class File
  {
      /**
       * @ORM\Column(type="string", length=255)
       */
      public ?string $url = null;
  
      /**
       * @ORM\ManyToOne(targetEntity=Foo::class, inversedBy="files")
       */
      public ?Foo $foo = null;
  
      /**
       * @ORM\ManyToOne(targetEntity=Bar::class, inversedBy="files")
       */
      public ?Bar $bar = null;
+ 
+     /**
+      * @ORM\ManyToOne(targetEntity=Baz::class, inversedBy="files")
+      */
+     public ?Baz $baz = null;
  }

これで特に問題なく対応できますが、この実装だと 1つのエンティティに複数の添付ファイル項目が必要になると、ちょっとエンティティが散らかってきます。

1つのエンティティに複数の添付ファイル項目があると

/**
 * @ORM\Entity(repositoryClass=BazRepository::class)
 */
class Baz
{
    /**
     * @var Collection|File[]
     *
     * @ORM\OneToMany(targetEntity=File::class, mappedBy="bazAsXxxOwner", cascade={"persist", "remove"})
     */
    public Collection $xxxFiles;

    /**
     * @var Collection|File[]
     *
     * @ORM\OneToMany(targetEntity=File::class, mappedBy="bazAsYyyOwner", cascade={"persist", "remove"})
     */
    public Collection $yyyFiles;
}
/**
 * @ORM\Entity(repositoryClass=FileRepository::class)
 */
class File
{
    /**
     * @ORM\Column(type="string", length=255)
     */
    public ?string $url = null;

    /**
     * @ORM\ManyToOne(targetEntity=Baz::class, inversedBy="xxxFiles")
     */
    public ?Baz $bazAsXxxOwner = null;

    /**
     * @ORM\ManyToOne(targetEntity=Baz::class, inversedBy="yyyFiles")
     */
    public ?Baz $bazAsYyyOwner = null;
}

こんな感じで、 File から見ると親である Baz を2つ別々に認識する必要があるため、プロパティの変数名も、どうしてもちょっと面倒臭い感じの名前になってしまいます。

やり方2:Single Table Inheritanceを使って少し整理する

Doctrineの Single Table Inheritance を使うと、上記のちょっと気持ち悪い実装を少しきれいに整理することができます。

Single Table Inheritanceの詳細については以下の過去記事をご参照ください。

[Symfony][Doctrine] Single Table Inheritanceを使ってエンティティに基底クラスを持たせる

// src/Entity/File.php

namespace App\Entity;

use App\Entity\Baz\XxxFile as BazXxxFile;
use App\Entity\Baz\YyyFile as BazYyyFile;

/**
 * @ORM\Entity(repositoryClass=FileRepository::class)
 * @ORM\InheritanceType("SINGLE_TABLE")
 * @ORM\DiscriminatorColumn(name="type", type="string")
 * @ORM\DiscriminatorMap({
 *     "baz_xxx_file" = BazXxxFile::class,
 *     "baz_yyy_file" = BazYyyFile::class,
 * })
 */
abstract class File
{
    /**
     * @ORM\Column(type="string", length=255)
     */
    public ?string $url = null;
}
// src/Entity/Baz/XxxFile.php

namespace App\Entity\Baz;

use src/Entity/File as BaseFile;

/**
 * @ORM\Entity(repositoryClass=FileRepository::class)
 */
class XxxFile extends BaseFile
{
    /**
     * @ORM\ManyToOne(targetEntity=Baz::class, inversedBy="xxxfiles")
     */
    public ?Baz $baz = null;
}
// src/Entity/Baz/YyyFile.php

namespace App\Entity\Bar;

use src/Entity/File as BaseFile;

/**
 * @ORM\Entity(repositoryClass=FileRepository::class)
 */
class YyyFile extends BaseFile
{
    /**
     * @ORM\ManyToOne(targetEntity=Baz::class, inversedBy="yyyfiles")
     */
    public ?Baz $baz = null;
}
use App\Entity\Baz\XxxFile as BazXxxFile;

/**
 * @ORM\Entity(repositoryClass=BazRepository::class)
 */
class Baz
{
    /**
     * @var Collection|BazXxxFile[]
     *
     * @ORM\OneToMany(targetEntity=BazXxxFile::class, mappedBy="baz", cascade={"persist", "remove"})
     */
    public Collection $xxxFiles;

    /**
     * @var Collection|BazYyyFile[]
     *
     * @ORM\OneToMany(targetEntity=BazYyyFile::class, mappedBy="baz", cascade={"persist", "remove"})
     */
    public Collection $yyyFiles;
}

こんな感じです。

各添付ファイル項目( $xxxFiles $yyyFiles )それぞれに別々のエンティティ( BazXxxFile BazYyyFile )が対応するので、変数名もきれいになりますし、他にも

  • 誤って複数の親に紐づくような File を作ってしまうことがない
  • どの項目の File なのかを instanceof で正確に知ることができる

などのメリットがあります。

添付ファイル項目が増えたときは、対応する File 派生クラスを作って、 File 基底クラスにSingle Table Inheritanceの定義を書き足せばOKです。

use App\Entity\Baz\XxxFile as BazXxxFile;

/**
 * @ORM\Entity(repositoryClass=FooRepository::class)
 */
class Foo
{
    /**
     * @var Collection|FooFile[]
     *
     * @ORM\OneToMany(targetEntity=FooFile::class, mappedBy="foo", cascade={"persist", "remove"})
     */
    public Collection $files;
}
  // src/Entity/File.php
  
  namespace App\Entity;
  
  use App\Entity\Baz\XxxFile as BazXxxFile;
  use App\Entity\Baz\YyyFile as BazYyyFile;
+ use App\Entity\Foo\File as FooFile;
  
  /**
   * @ORM\Entity(repositoryClass=FileRepository::class)
   * @ORM\InheritanceType("SINGLE_TABLE")
   * @ORM\DiscriminatorColumn(name="type", type="string")
   * @ORM\DiscriminatorMap({
+  *     "foo_file" = FooFile::class,
   *     "baz_xxx_file" = BazXxxFile::class,
   *     "baz_yyy_file" = BazYyyFile::class,
   * })
   */
  abstract class File
  {
      /**
       * @ORM\Column(type="string", length=255)
       */
      public ?string $url = null;
  }

おわりに

Symfonyでエンティティに添付ファイル系の項目を持たせる実装例を2パターン紹介してみました。

添付ファイルの他にも、例えば 色々なエンティティに対してコメントを書ける機能 とかでも同様の実装が使えるかなと思います。

Symfony Advent Calendar 2020、明日は @kaino5454 さんです!お楽しみに!

GitHubで編集を提案

Discussion

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