Implementing a Collection of Files
This chapter describes how to implement a collection of files, or one-to-many relation between a Doctrine entity and several files.
Summary
This is what we do to implement a one-to-many relation between an entity and several files:
-
Create a new entity that will represent a file. For convenience, we provide
AbstractFile
orFileTrait
that your entity can extend or use. -
Create a one-to-many relation from an entity to the entity in #1.
You need to install the package rekalogika/file-association-entity
to use this
feature:
composer require rekalogika/file-association-entity
The many-to-one
Side
In the following example, we will be creating an entity Product
that will
have multiple Image
s.
You will need your Product
entity to extend AbstractFile
. Alternatively, if
your entity needs to extend another entity, you can use the trait FileTrait
instead.
- By Extending AbstractFile
- By Using FileTrait
Create the Image
entity by extending AbstractFile
. The following are the
relevant parts.
use Doctrine\ORM\Mapping as ORM;
use Rekalogika\Domain\File\Association\Entity\AbstractFile;
#[ORM\Entity]
class Image extends AbstractFile
{
// ...
#[ORM\ManyToOne(inversedBy: 'images')]
#[ORM\JoinColumn(nullable: false)]
private ?Product $product = null;
public function getProduct(): ?Product
{
return $this->product;
}
public function setProduct(?Product $product): static
{
$this->product = $product;
return $this;
}
// ...
}
Create the Image
entity by using the FileTrait
. The following are the
relevant parts.
use Doctrine\ORM\Mapping as ORM;
use Rekalogika\Domain\File\Association\Entity\AbstractFile;
use Rekalogika\Contracts\File\FileInterface;
use Rekalogika\File\Association\Attribute\WithFileAssociation;
#[ORM\Entity]
#[WithFileAssociation]
class Image implements FileInterface
{
use FileTrait;
#[ORM\ManyToOne(inversedBy: 'images')]
#[ORM\JoinColumn(nullable: false)]
private ?Product $product = null;
public function getProduct(): ?Product
{
return $this->product;
}
public function setProduct(?Product $product): static
{
$this->product = $product;
return $this;
}
}
If you don't use attributes to configure your Doctrine mappings, you will need to add the following configuration to the Doctrine's mapping configuration.
- XML
<doctrine-mapping>
<!-- ... -->
<entity name="Image">
<!-- ... -->
<embedded
name="metadata"
class="Rekalogika\Domain\File\Association\Entity\EmbeddedMetadata" />
</entity>
<!-- ... -->
</doctrine-mapping>
The one-to-many
Side
The relevant parts:
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Product
{
// ...
#[ORM\OneToMany(mappedBy: 'product', targetEntity: Image::class, orphanRemoval: true)]
private Collection $images;
public function __construct()
{
$this->images = new ArrayCollection();
}
/**
* @return Collection<int, Image>
*/
public function getImages(): Collection
{
return $this->images;
}
public function addImage(Image $image): static
{
if (!$this->images->contains($image)) {
$this->images->add($image);
$image->setProduct($this);
}
return $this;
}
public function removeImage(Image $image): static
{
if ($this->images->removeElement($image)) {
// set the owning side to null (unless already changed)
if ($image->getProduct() === $this) {
$image->setProduct(null);
}
}
return $this;
}
// ...
}
(Optional) Accepting FileInterface
in the Adder
For convenience, you might also want to modify the adder addImage
above so
that it also accepts an instance of FileInterface
:
use Rekalogika\Contracts\File\FileInterface;
class Product
{
// ...
public function addImage(Image|FileInterface $image): static
{
if (!$image instanceof Image) {
$image = new Image($image);
}
if (!$this->images->contains($image)) {
$this->images->add($image);
$image->setProduct($this);
}
return $this;
}
// ...
}
(Optional) Decorate the Collection Using FileCollection
In the getter, you can also return a FileCollection
wrapping the original
collection, and change the type hint. Then, the caller will be able to know that
the Collection contains files and also an instance of DirectoryInterface
.
use Rekalogika\Domain\File\Association\Entity\FileCollection;
class Product
{
// ...
/**
* @return FileCollection<int,Image>
*/
public function getImages(): FileCollection
{
return new FileCollection(
$this->images,
sprintf('product %s images', $this->getName())
);
}
// ...
}
The second argument of FileCollection
is the name of the file collection,
and will be used for the directory name, ZIP file name, etc.
Read the chapter Stream a ZIP File if you need to download an entire collection as a ZIP file.
There is also ReadableFileCollection
, which is the read-only flavor of
FileCollection
.
Using The Relation
By following the guide above, your Image
entity is a FileInterface
.
Therefore, with the example above, you can treat the Image
entity as a file.
use Rekalogika\File\File;
$product = new Product();
$image1 = new File('product_image_1.jpg');
$image2 = new File('product_image_2.jpg');
$image3 = new File('product_image_3.jpg');
$product
->addImage($image1)
->addImage($image2)
->addImage($image3);
foreach ($product->getImages() as $image) {
$name = $image->getName(); // product_image_1.jpg, etc.
$description = $image->getType()->getDescription(); // "JPEG image", etc.
}
Indexing and Querying by File Properties
AbstractFile
uses EmbeddedMetadata
under the hood. Read more about it in the
Replicating Metadata in Entities section.