You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Symfony 7中VichUploaderBundle返回体不显示ContentUrl问题

问题:ApiPlatform + VichUploaderBundle 无法返回 contentUrl 字段

严格按照ApiPlatform官方文档配置VichUploaderBundle、MediaObject实体类及MediaObjectNormalizer序列化器后,在Swagger中执行文件上传(Post)和查询(Get)操作时,返回体始终无法显示ContentUrl字段。

相关配置与代码

1. VichUploader配置文件(config/packages/vich_uploader.yaml)

vich_uploader:
    db_driver: orm
    metadata:
        type: attribute
    mappings:
        media_object:
            uri_prefix: /media
            upload_destination: '%kernel.project_dir%/public/media'
            namer: Vich\UploaderBundle\Naming\OrignameNamer

2. MediaObject实体类(src/Entity/MediaObject.php)

<?php

declare(strict_types=1);

namespace App\Entity;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\OpenApi\Model;
use App\Controller\CreateMediaObjectAction;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Vich\UploaderBundle\Mapping\Annotation as Vich;

#[Vich\Uploadable]
#[ORM\Entity]
#[ApiResource(
    types: ['https://schema.org/MediaObject'],
    operations: [
        new Get(),
        new GetCollection(),
        new Post(
            controller: CreateMediaObjectAction::class,
            openapi: new Model\Operation(
                requestBody: new Model\RequestBody(
                    content: new \ArrayObject([
                        'multipart/form-data' => [
                            'schema' => [
                                'type' => 'object',
                                'properties' => [
                                    'file' => [
                                        'type' => 'string',
                                        'format' => 'binary'
                                    ]
                                ]
                            ]
                        ]
                    ])
                )
            ),
            validationContext: ['groups' => ['Default', 'media_object_create']],
            deserialize: false
        )
    ],
    normalizationContext: ['groups' => ['media_object:read']]
)]
class MediaObject
{
    #[ORM\Id, ORM\Column, ORM\GeneratedValue]
    private ?int $id = null;

    #[ApiProperty(types: ['https://schema.org/contentUrl'])]
    #[Groups(['media_object:read'])]
    public ?string $contentUrl = null;

    #[Vich\UploadableField(mapping: "media_object", fileNameProperty: "filePath")]
    #[Assert\NotNull(groups: ['media_object_create'])]
    public ?File $file = null;

    #[ORM\Column(nullable: true)]
    public ?string $filePath = null;

    public function getId(): ?int
    {
        return $this->id;
    }
}

3. MediaObjectNormalizer序列化器(src/Serializer/MediaObjectNormalizer.php)

<?php

declare(strict_types=1);

namespace App\Serializer;

use App\Entity\MediaObject;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Vich\UploaderBundle\Storage\StorageInterface;

final class MediaObjectNormalizer implements NormalizerAwareInterface
{
    use NormalizerAwareTrait;

    private const ALREADY_CALLED = 'MEDIA_OBJECT_NORMALIZER_ALREADY_CALLED';

    public function __construct(private StorageInterface $storage)
    {
    }

    public function normalize($object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
    {
        $context[self::ALREADY_CALLED] = true;

        $object->contentUrl = $this->storage->resolveUri($object, 'file');

        return $this->normalizer->normalize($object, $format, $context);
    }

    public function supportsNormalization($data, ?string $format = null, array $context = []): bool
    {
        if (isset($context[self::ALREADY_CALLED])) {
            return false;
        }

        return $data instanceof MediaObject;
    }
}

排查解决步骤

1. 确认序列化器服务已注册

检查config/services.yaml,确保src/Serializer目录未被排除在自动注册规则之外:

services:
    App\:
        resource: '../src/'
        exclude:
            - '../src/DependencyInjection/'
            - '../src/Entity/'
            - '../src/Kernel.php'
            # 不要添加Serializer目录到排除列表

2. 验证resolveUri返回值

在MediaObjectNormalizer的normalize方法中添加日志,确认是否生成了正确的URL:

// 在$object->contentUrl赋值前添加
$uri = $this->storage->resolveUri($object, 'file');
error_log('Resolved media URI: ' . $uri); // 查看服务器日志确认值是否存在
$object->contentUrl = $uri;

3. 检查CreateMediaObjectAction控制器返回值

确保上传控制器返回的是MediaObject实例,且未覆盖序列化上下文:

public function __invoke(Request $request): MediaObject
{
    $mediaObject = new MediaObject();
    $form = $this->createForm(MediaObjectType::class, $mediaObject);
    $form->handleRequest($request);

    if ($form->isValid()) {
        $this->entityManager->persist($mediaObject);
        $this->entityManager->flush();
        return $mediaObject; // 必须返回实体实例,ApiPlatform才会自动序列化
    }

    throw new ValidationException($form);
}

4. 提高序列化器优先级

若存在其他自定义序列化器,可能导致当前normalizer未被调用,在services.yaml中设置优先级:

App\Serializer\MediaObjectNormalizer:
    tags:
        - { name: serializer.normalizer, priority: 100 }

5. 清除缓存并验证静态文件访问

执行命令清除缓存,确保配置生效:

php bin/console cache:clear

同时确认/media目录可访问,上传的文件能通过/media/[文件名]正常打开。


内容的提问来源于stack exchange,提问作者Shamshir Isroilov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 06:24:53