跳到内容

使用 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 中做两件事来启用文件上传

  1. 添加文件上传小部件
  2. 确保当我们上传文件时,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 条目。

这项工作,包括代码示例,已根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本