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 AbstractFileorFileTraitthat 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 Images.
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.