EasyAdminBundle
EasyAdminBundle copied to clipboard
ImageField file name pattern with subfolders: admin interface loses the folders and edit ability
Describe the bug
The context is a simple media entity with a file
field and an accompanying ImageField
in its admin interface. Consider this entity:
<?php // src/Entity/Media.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use App\Repository\MediaRepository;
#[ORM\Entity(repositoryClass: MediaRepository::class)]
class Media
{
const UPLOAD_PATH = 'uploads/media/landingpages';
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 1024)]
private ?string $file = null;
public function getId(): ?int
{
return $this->id;
}
public function getFile(): ?string
{
return $this->file;
}
public function setFile(string $file): self
{
$this->file = $file;
return $this;
}
}
and this controller:
<?php // src/Controller/Admin/MediaCrudController.php
namespace App\Controller\Admin;
use App\Entity\Media;
use Doctrine\ORM\EntityManagerInterface;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use Symfony\Component\HttpKernel\KernelInterface;
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
class MediaCrudController extends AbstractCrudController
{
private $projectDir;
public function __construct(KernelInterface $kernel)
{
$this->projectDir = $kernel->getProjectDir();
}
public static function getEntityFqcn(): string
{
return Media::class;
}
public function configureCrud(Crud $crud): Crud
{
return $crud
->showEntityActionsInlined();
}
public function configureActions(Actions $actions): Actions
{
return $actions->add(Crud::PAGE_EDIT, Action::INDEX)->add(Crud::PAGE_NEW, Action::INDEX);
}
public function configureFields(string $pageName): iterable
{
yield IdField::new('id')->hideOnForm();
yield ImageField::new('file')
->setLabel('Mediendatei')
->setBasePath(Media::UPLOAD_PATH)
->setUploadDir('public/'.Media::UPLOAD_PATH)
->setFormTypeOption('upload_new', function (UploadedFile $file, string $uploadDir, string $fileName) {
if (($extraDirs = dirname($fileName)) !== '.') {
$uploadDir .= trim($extraDirs);
if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0750, true);
}
}
return $file->move($uploadDir, $fileName);
})
->setUploadedFileNamePattern('[year]/[month]/[day]/[slug]-[timestamp]-[randomhash].[extension]');
}
}
and for a complete example this repo:
<?php // src/Repository/MediaRepository.php
namespace App\Repository;
use App\Entity\Media;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Media>
*/
class MediaRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Media::class);
}
public function save(Media $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(Media $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
}
The code is more or less directly copied from the Symfony Cast at https://symfonycasts.com/screencast/easyadminbundle/upload .
Then create a Media instance and upload a file. This works like a charm and on the overview page the preview is shown with the correct path.
The problem arises when trying to edit this media object again. (Think editing a hypothetical “title” field or so.) The form features a file upload field that is marked as required. This means one is forced to re-upload the file. Removing the required
attribute leads to an error on saving because of the NOT NULL
constraint on the file
column. Trying to work around the problem by introducing a hidden field on client side with JS falls short, because the field loses its sub-directory information when being rendered and shows only the basename:
Note that the placeholder has no [year]/[month]/[day]/
prefix set.
Unfortunately, this makes setUploadedFileNamePattern()
with subfolders unusable as far as we can tell.
It seems that this issue might be related to #4822 and #4004.
To Reproduce
$ symfony new fileissue
$ cd fileissue
$ composer require easycorp/easyadmin symfony/mime
$ symfony console make:admin:dashboard
$ # copy over the files from above
$ # start DB
$ symfony console doctrine:schema:create
$ symfony server:start
EasyAdmin v4.7.0 is used.
for real no answer on this?
Hey, we encountered the exact same problem, here's our workaround :
We used the getEntityChangeSet of UnitOfWork in the updateEntity function to know what values changed, and forced the previous value of our "driversLicenseUri" field (the equivalent of your "file" field) under certain conditions
// In the CrudController
private function startsWithDatePattern($string)
{
// For a [year]/[month]/[day] subdirectory pattern
$pattern = '/^\d{4}\/\d{2}\/\d{2}/';
if (preg_match($pattern, $string)) {
return true;
}
return false;
}
public function updateEntity(EntityManagerInterface $entityManager, $entityInstance): void
{
$uok = $entityManager->getUnitOfWork();
$uok->computeChangeSet(
$entityManager->getClassMetadata(MembershipRequest::class),
$entityInstance
);
$changeSet = $uok->getEntityChangeSet($entityInstance);
parent::updateEntity($entityManager, $entityInstance);
if (array_key_exists('driversLicenseUri', $changeSet)) {
$before = $changeSet['driversLicenseUri'][0];
$after = $changeSet['driversLicenseUri'][1];
if (
!is_null($before)
&& is_string($before)
&& $this->startsWithDatePattern($before)
&& !is_null($after)
&& is_string($after)
&& !$this->startsWithDatePattern($after)) {
$entityInstance->setDriversLicenseUri($before);
} elseif (is_null($after)) {
$entityInstance->setDriversLicenseUri(null);
}
}
parent::updateEntity($entityManager, $entityInstance);
}
It seems crazy to me that this pretty big bug still remains today ... Hope this helps