使用 DoctrineORM 和 SonataAdmin 上传和保存文档(包括图像)
这是一个使用 SonataAdmin 和 DoctrineORM 持久层的文件上传管理方法的完整工作示例。
先决条件
- 您已经安装并运行了 SonataAdmin 和 DoctrineORM
- 您已经有一个实体类,您希望能够将上传的文档连接到该类,在本例中,该类将被称为
Image
。 - 您已经设置了一个 Admin,在本例中,它被称为
ImageAdmin
- 您了解您的 Web 服务器上的文件权限,并且可以管理允许您的 Web 服务器在相关文件夹中上传和更新文件所需的权限
步骤
首先,我们将介绍您的实体需要包含哪些基本内容,才能使用 Doctrine 启用文档管理。Symfony 网站上有一篇关于 使用 Doctrine 和 Symfony 上传文件 的优秀 Cookbook 条目,因此我将在此处展示代码示例,而不会深入探讨细节。强烈建议您首先阅读该 Cookbook。
为了使文件上传与 SonataAdmin 一起工作,我们需要
- 向我们的 ImageAdmin 添加文件上传字段
- 当上传新文件时“触摸”实体,以便触发其生命周期事件
基本配置 - 实体
按照 Symfony Cookbook 中的指南,我们有一个实体定义,如下面的 YAML 所示(您也可以使用 XML 或基于注解的定义来实现类似的效果)。在本例中,我们使用 updated
字段通过基于上传时间戳设置它来触发生命周期回调。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
# src/Resources/config/Doctrine/Image.orm.yaml
App\Entity\Image:
type: entity
repositoryClass: App\Entity\Repositories\ImageRepository
table: images
id:
id:
type: integer
generator: { strategy: AUTO }
fields:
filename:
type: string
length: 100
# changed when files are uploaded, to force preUpdate and postUpdate to fire
updated:
type: datetime
nullable: true
# ...
lifecycleCallbacks:
prePersist: ['lifecycleFileUpload']
preUpdate: ['lifecycleFileUpload']
然后,我们在我们的 Image
类中包含以下方法来管理文件上传
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
// src/Entity/Image.php
final class Image
{
const SERVER_PATH_TO_IMAGE_FOLDER = '/server/path/to/images';
/**
* Unmapped property to handle file uploads
*/
private ?UploadedFile $file = null;
public function setFile(?UploadedFile $file = null): void
{
$this->file = $file;
}
public function getFile(): ?UploadedFile
{
return $this->file;
}
/**
* Manages the copying of the file to the relevant place on the server
*/
public function upload(): void
{
// the file property can be empty if the field is not required
if (null === $this->getFile()) {
return;
}
// we use the original file name here but you should
// sanitize it at least to avoid any security issues
// move takes the target directory and target filename as params
$this->getFile()->move(
self::SERVER_PATH_TO_IMAGE_FOLDER,
$this->getFile()->getClientOriginalName()
);
// set the path property to the filename where you've saved the file
$this->filename = $this->getFile()->getClientOriginalName();
// clean up the file property as you won't need it anymore
$this->setFile(null);
}
/**
* Lifecycle callback to upload the file to the server.
*/
public function lifecycleFileUpload(): void
{
$this->upload();
}
/**
* Updates the hash value to force the preUpdate and postUpdate events to fire.
*/
public function refreshUpdated(): void
{
$this->setUpdated(new \DateTime());
}
// ... the rest of your class lives under here, including the generated fields
// such as filename and updated
}
当我们上传文件到我们的 Image 时,文件本身是瞬态的,不会持久化到我们的数据库(它不是我们映射的一部分)。但是,生命周期回调触发对 Image::upload()
的调用,该调用管理将上传的文件实际复制到文件系统,并更新我们的 Image 的 filename
属性,此 filename 字段会持久化到数据库中。
以上大部分内容来自 使用 Doctrine 和 Symfony 上传文件 Cookbook 条目。强烈建议阅读它!
基本配置 - Admin 类
我们需要在 Sonata 中做两件事来启用文件上传
- 添加文件上传小部件
- 确保当我们上传文件时,Image 类的生命周期事件被触发
当您知道该怎么做时,这两者都很简单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
// src/Admin/ImageAdmin.php
use Symfony\Component\Form\Extension\Core\Type\FileType;
final class ImageAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $form): void
{
$form
->add('file', FileType::class, [
'required' => false
])
;
}
public function prePersist(object $image): void
{
$this->manageFileUpload($image);
}
public function preUpdate(object $image): void
{
$this->manageFileUpload($image);
}
private function manageFileUpload(object $image): void
{
if ($image->getFile()) {
$image->refreshUpdated();
}
}
// ...
}
我们将 file
字段标记为非必需,因为我们不需要用户每次更新 Image 时都上传新图像。当上传文件时(并且表单上的其他内容没有更改),Doctrine 需要持久化的数据没有更改,因此不会触发 preUpdate
事件。为了处理这个问题,我们挂钩到 SonataAdmin 的 preUpdate
事件(每次提交编辑表单时都会触发),并使用它来更新持久化的 Image 字段。这然后确保 Doctrine 的生命周期事件被触发,并且我们的 Image 按照预期管理文件上传。
这就是全部内容!
但是,当 ImageAdmin
使用 sonata_type_admin
字段类型嵌入到其他 Admin 中时,此方法不起作用。为此,我们需要更多...
高级示例 - 适用于嵌入式 Admin
当一个 Admin 嵌入到另一个 Admin 中时,当提交父 Admin 时,子 Admin 的 preUpdate()
方法不会被触发。为了处理这个问题,我们需要使用父 Admin 的生命周期事件在需要时触发文件管理。
在本例中,我们有一个 Page 类,它定义了三个一对一的 Image 关系,linkedImage1 到 linkedImage3。PostAdmin 类的表单字段配置如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// src/Admin/PostAdmin.php
use Sonata\AdminBundle\Form\Type\AdminType;
final class PostAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $form): void
{
$form
->add('linkedImage1', AdminType::class, [
'delete' => false,
])
->add('linkedImage2', AdminType::class, [
'delete' => false,
])
->add('linkedImage3', AdminType::class, [
'delete' => false,
])
;
}
}
这就足够了 - 我们嵌入了三个字段,然后将使用我们的 ImageAdmin
类来确定要显示的字段。
在我们的 PostAdmin
中,我们然后有以下代码来管理关系的生命周期
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
// src/Admin/PostAdmin.php
final class PostAdmin extends AbstractAdmin
{
public function prePersist(object $page): void
{
$this->manageEmbeddedImageAdmins($page);
}
public function preUpdate(object $page): void
{
$this->manageEmbeddedImageAdmins($page);
}
private function manageEmbeddedImageAdmins(object $page): void
{
// Cycle through each field
foreach ($this->getFormFieldDescriptions() as $fieldName => $fieldDescription) {
// detect embedded Admins that manage Images
if ($fieldDescription->getType() === 'sonata_type_admin' &&
($associationMapping = $fieldDescription->getAssociationMapping()) &&
$associationMapping['targetEntity'] === 'App\Entity\Image'
) {
$getter = 'get'.$fieldName;
$setter = 'set'.$fieldName;
/** @var Image $image */
$image = $page->$getter();
if ($image) {
if ($image->getFile()) {
// update the Image to trigger file management
$image->refreshUpdated();
} elseif (!$image->getFile() && !$image->getFilename()) {
// prevent Sf/Sonata trying to create and persist an empty Image
$page->$setter(null);
}
}
}
}
}
}
在这里,我们循环遍历 PageAdmin 的字段,并查找那些是 sonata_type_admin
字段的字段,这些字段嵌入了管理 Image 的 Admin。
一旦我们有了这些字段,我们就使用 $fieldName
来构建引用我们的访问器和修改器方法的字符串。例如,我们可能会在 $getter
中得到 getlinkedImage1
。使用此访问器,我们可以从 PageAdmin 管理的 Page 对象中获取实际的 Image 对象。检查此对象会显示它是否具有待处理的文件上传 - 如果有,我们触发与之前相同的 refreshUpdated()
方法。
最后的检查是防止 Symfony 尝试在表单中没有输入任何内容时创建空白图像的故障。我们检测到这种情况并将关系置为 null 以阻止这种情况发生。
注意
如果您正在寻找更丰富媒体管理功能,则有一个完整的 SonataMediaBundle
可以满足此需求。它已在线记录,并且由与 SonataAdmin 相同的团队创建和维护。
要了解如何向您的 ImageAdmin
添加图像预览,请查看相关的 Cookbook 条目。