This guide explains how to attach a single image to any entity using ImageType.
The image widget provides:
ImageType operates in two modes:
AttachedImage. The parent entity holds a OneToOne relation to this image entity.This guide focuses on entity mode, which is the standard approach for all Aropixel bundles.
The relationship:
YourEntity (OneToOne, owning)
ββ YourEntityImage extends AttachedImage (stores image ref, alt, title, positionβ¦)
ββ YourEntityImageCrop extends Crop (optional, one row per crop filter)
The bundle provides a command that generates the image entity (and optionally the crop entity) and can automatically update the parent entity:
bin/console aropixel:make:image
The command asks interactively for:
App\Entity\Artistimage (produces ArtistImage)ArtistImageCrop)Non-interactive usage with options:
bin/console aropixel:make:image \
--parent-class="App\Entity\Artist" \
--property=image \
--croppable \
--auto-update
| File | Description |
|---|---|
src/Entity/ArtistImage.php |
Image entity extending AttachedImage |
src/Entity/ArtistImageCrop.php |
(if croppable) Crop entity extending Crop |
The parent entity is updated (or the code is displayed for manual insertion) with the OneToOne property and the getImage() / setImage() methods.
After running the command, two manual steps remain:
The image entity extends AttachedImage, which provides: image (FK to the shared image library), position, title, link, description, attrTitle, attrAlt, attrClass, and timestamps.
// src/Entity/ArtistImage.php
namespace App\Entity;
use Aropixel\AdminBundle\Entity\AttachedImage;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'app_artist_image')]
class ArtistImage extends AttachedImage
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\OneToOne(targetEntity: Artist::class, inversedBy: 'image')]
#[ORM\JoinColumn(name: 'artist_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
private ?Artist $artist = null;
public function getId(): ?int { return $this->id; }
public function getArtist(): ?Artist { return $this->artist; }
public function setArtist(?Artist $artist): self
{
$this->artist = $artist;
return $this;
}
}
Note:
AttachedImage::setImage()saves the previous image reference internally to detect changes β never callsetOldImage()manually.
Add the OneToOne relation (inverse side) and the getImage() / setImage() methods to the parent entity.
// src/Entity/Artist.php
// --- add in the class body ---
#[ORM\OneToOne(targetEntity: ArtistImage::class, mappedBy: 'artist', cascade: ['persist', 'remove'], orphanRemoval: true)]
private ?ArtistImage $image = null;
public function getImage(): ?ArtistImage
{
return $this->image;
}
public function setImage(?ArtistImage $image): self
{
if ($image === null || $image->getImage() === null) {
$this->image = null;
} else {
$this->image = $image;
$this->image->setArtist($this);
}
return $this;
}
Note:
orphanRemoval: trueensures the image entity is deleted when the image is removed from the form.
// src/Form/ArtistType.php
use Aropixel\AdminBundle\Form\Type\Image\Single\ImageType;
use App\Entity\ArtistImage;
$builder->add('image', ImageType::class, [
'label' => 'Photo',
'data_class' => ArtistImage::class,
'required' => false,
]);
CREATE TABLE app_artist_image (
id INT AUTO_INCREMENT NOT NULL,
title VARCHAR(255) DEFAULT NULL,
link VARCHAR(255) DEFAULT NULL,
description LONGTEXT DEFAULT NULL,
attr_title VARCHAR(255) DEFAULT NULL,
attr_alt VARCHAR(255) DEFAULT NULL,
attr_class VARCHAR(255) DEFAULT NULL,
position INT NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME DEFAULT NULL,
image_id INT DEFAULT NULL,
artist_id INT DEFAULT NULL,
INDEX IDX_app_artist_image_image (image_id),
UNIQUE INDEX UNIQ_app_artist_image_artist (artist_id),
PRIMARY KEY (id)
) DEFAULT CHARACTER SET utf8mb4;
ALTER TABLE app_artist_image
ADD CONSTRAINT FK_app_artist_image_image
FOREIGN KEY (image_id) REFERENCES aropixel_image (id),
ADD CONSTRAINT FK_app_artist_image_artist
FOREIGN KEY (artist_id) REFERENCES app_artist (id) ON DELETE CASCADE;
Tip: Let Doctrine generate the migration instead of writing it by hand:
bin/console doctrine:migrations:diff
Crops allow the editor to define a specific region of the image for each Liip Imagine filter (e.g., a 16/9 banner crop, a square thumbnail crop).
// src/Entity/ArtistImageCrop.php
namespace App\Entity;
use Aropixel\AdminBundle\Entity\AttachedImageInterface;
use Aropixel\AdminBundle\Entity\Crop;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'app_artist_image_crop')]
class ArtistImageCrop extends Crop
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: ArtistImage::class, inversedBy: 'crops')]
private ?ArtistImage $image = null;
public function getId(): ?int { return $this->id; }
public function getImage(): AttachedImageInterface { return $this->image; }
public function setImage(?ArtistImage $image): self
{
$this->image = $image;
return $this;
}
}
Add CroppableInterface, CroppableTrait, and the $crops collection:
// src/Entity/ArtistImage.php
use Aropixel\AdminBundle\Entity\AttachedImage;
use Aropixel\AdminBundle\Entity\CroppableInterface;
use Aropixel\AdminBundle\Entity\CroppableTrait;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
#[ORM\Entity]
#[ORM\Table(name: 'app_artist_image')]
class ArtistImage extends AttachedImage implements CroppableInterface
{
use CroppableTrait;
// ... id and artist fields as before ...
/** [@var](https://github.com/var) Collection<int, ArtistImageCrop>|null */
#[ORM\OneToMany(targetEntity: ArtistImageCrop::class, mappedBy: 'image', cascade: ['persist', 'remove'])]
protected ?Collection $crops = null;
public function addCrop(ArtistImageCrop $crop): self
{
if (!$this->getCrops()->contains($crop)) {
$this->crops[] = $crop;
$crop->setImage($this);
}
return $this;
}
public function removeCrop(ArtistImageCrop $crop): self
{
if ($this->getCrops()->removeElement($crop) && $crop->getImage() === $this) {
$crop->setImage(null);
}
return $this;
}
}
CroppableTrait provides getCrops(), getImageUid(), and getCropsInfos().
$builder->add('image', ImageType::class, [
'data_class' => ArtistImage::class,
'crop_class' => ArtistImageCrop::class,
'crops' => [
'artist_portrait' => 'Portrait (3/4)',
'artist_thumbnail' => 'Thumbnail (1/1)',
],
]);
The keys in crops must match Liip Imagine filter names defined in config/packages/liip_imagine.yaml. Filters without a thumbnail transformation are silently ignored by the crop tool.
CREATE TABLE app_artist_image_crop (
id INT AUTO_INCREMENT NOT NULL,
filter VARCHAR(255) NOT NULL,
crop VARCHAR(255) DEFAULT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME DEFAULT NULL,
image_id INT DEFAULT NULL,
INDEX IDX_app_artist_image_crop_image (image_id),
PRIMARY KEY (id)
) DEFAULT CHARACTER SET utf8mb4;
ALTER TABLE app_artist_image_crop
ADD CONSTRAINT FK_app_artist_image_crop_image
FOREIGN KEY (image_id) REFERENCES app_artist_image (id) ON DELETE CASCADE;
In filename mode, no dedicated image entity is needed. The image filename is stored directly as a string field on the parent entity.
// src/Entity/Article.php
#[ORM\Column(nullable: true)]
private ?string $coverFilename = null;
// src/Form/ArticleType.php
$builder->add('cover', ImageType::class, [
'data_value' => 'coverFilename', // property that stores the filename
'crops_value' => 'coverCrops', // property that stores crops as a JSON array (optional)
'crops' => [
'article_cover' => 'Cover (16/9)',
],
]);
Trade-offs:
| Entity mode | Filename mode | |
|---|---|---|
| Separate DB table | Yes | No |
| Shared media library | Yes | Yes |
| Per-image alt text | Yes | No |
| Crop support | Full (dedicated entity) | Partial (stored as JSON in parent) |
| Recommended for | Most cases | Simple cases, rapid prototyping |
| Option | Type | Default | Description |
|---|---|---|---|
data_class |
string |
null |
FQCN of the image entity (extends AttachedImage). Required in entity mode. |
data_value |
string |
null |
Property name that stores the filename on the parent entity. Required in filename mode. |
crop_class |
string |
null |
FQCN of the crop entity (extends Crop). Used in entity mode with crops. |
crops |
array |
null |
Map of Liip filter slug β human label. Enables the crop tool. |
crops_value |
string |
'crops' |
(Filename mode only) Property that stores crop coordinates as JSON on the parent entity. |
library |
string |
null |
FQCN used to filter the media library. Defaults to the image entity class. |
required |
bool |
false |
Whether the image field is required. |
description |
string |
null |
Helper text displayed below the widget. |
card_footer |
bool |
true |
Whether to display the widget footer (upload/library buttons). |
accept |
string |
null |
MIME types accepted by the upload input (e.g., 'image/png,image/jpeg'). |
max_size |
int |
null |
Maximum upload size in bytes. |
grid |
string |
null |
Bootstrap grid class applied to the widget container. |
How can I help you explore Laravel packages today?