[Symfony] エンティティに添付ファイル系の項目を持たせる実装例
はじめに
Symfony Advent Calendar 2020 の19日目の記事です!🎄🌙
昨日も僕の記事で、[Symfony] 機能テストでCSRFトークンを送る方法 でした✨
ちなみに、僕はよく TwitterにもSymfonyネタを呟いている ので、よろしければぜひ フォローしてやってください🕊🤲
やりたいこと
- いくつかのエンティティに添付ファイル系の項目がある
- 各項目それぞれに1〜複数のファイルを添付できる
こんな要件の実装を考えてみましょう。
File
エンティティを作る
やり方1:普通に ごく普通に作ると以下のような感じになるかなと思います。
/**
* @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 さんです!お楽しみに!
Discussion