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

Symfony Serializer未序列化Event类的attendees字段问题

Symfony序列化Event时缺失attendees字段问题排查与解决

我定义了Event和User两个实体类,用户可参与事件并成为Attendee(参与者)。但使用Symfony Serializer序列化包含参与者的Event实例时,响应结果中缺失attendees字段。

Event类代码

<?php

namespace App\Entity;

use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\CustomIdGenerator;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\ManyToMany;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\Table;
use Gedmo\Mapping\Annotation\Timestampable;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints\NotBlank;
use Vich\UploaderBundle\Mapping\Annotation\Uploadable;
use Vich\UploaderBundle\Mapping\Annotation\UploadableField;

#[Entity]
#[Table(name: 'event')]
#[Uploadable]
class Event
{
    #[Id]
    #[GeneratedValue(strategy: 'CUSTOM')]
    #[CustomIdGenerator(class: UUIDGenerator::class)]
    #[Column]
    #[Groups(['apiExposed'])]
    private string $id;

    #[Column]
    #[Groups(['apiExposed', 'searchable'])]
    #[NotBlank]
    private string $title;

    #[Column(type: 'point', nullable: true)]
    #[Groups(['apiExposed', 'searchable'])]
    private ?Point $location;

    #[ManyToOne(targetEntity: User::class)]
    #[Groups('apiExposed')]
    #[NotBlank]
    private User $organizer;

    #[ManyToMany(targetEntity: User::class, inversedBy: 'participatedEvents')]
    #[Groups('apiExposed')]
    private Collection $attendees;

    #[Column(type: 'text')]
    #[Groups(['apiExposed', 'searchable'])]
    #[NotBlank]
    private string $description;

    #[Column]
    #[Groups(['aîExposed', 'searchable'])]
    #[NotBlank]
    private string $address;

    #[UploadableField(mapping: 'event_picture', fileNameProperty: 'pictureName')]
    private ?File $pictureFile = null;

    #[Column(nullable: true)]
    #[Groups(['apiExposed', 'searchable'])]
    private ?string $pictureName = 'event.jpg';

    #[Column]
    #[Groups(['apiExposed', 'searchable'])]
    private bool $isSecret = false;

    #[Column]
    #[Groups(['apiExposed', 'searchable'])]
    private \DateTimeImmutable $startDate;

    #[Column]
    #[Groups('apiExposed')]
    #[Timestampable(on: 'create')]
    private \DateTimeImmutable $createdAt;

    #[Column]
    #[Groups('apiExposed')]
    #[Timestampable(on: 'update')]
    private \DateTimeImmutable $updatedAt;

    // Getters and setters
    /**
     * @return Collection
     */
     public function getAttendees(): Collection
     {
         return $this->attendees;
     }

     /**
      * @param User $user
      * @return $this
      */
     public function addAttendee(User $user): Event
     {
         $this -> attendees -> add($user);
         return $this;
     }

     /**
      * @param User $user
      * @return $this
      */
     public function removeAttendee(User $user): Event
     {
         $this -> attendees -> removeElement($user);
         return $this;
      }

}

User类代码

<?php

namespace App\Entity;

use App\Repository\UserRepository;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation\Timestampable;
use OpenApi\Attributes\Items;
use OpenApi\Attributes\Property;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Vich\UploaderBundle\Mapping\Annotation\Uploadable;
use Vich\UploaderBundle\Mapping\Annotation\UploadableField;

#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
#[Uploadable]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    #[Groups('apiExposed')]
    private ?int $id = null;

    #[ORM\Column(length: 180, unique: true)]
    #[Assert\NotBlank]
    #[Groups('apiExposed')]
    private ?string $username = null;

    #[ORM\Column]
    #[Groups('apiExposed')]
    #[Property(type: 'array', items: new Items(type: 'string'))]
    private array $roles = [];

    /**
     * @var string The hashed password
     */
    #[ORM\Column]
    #[Assert\NotBlank]
    private ?string $password = null;

    #[ORM\Column(nullable: true)]
    #[Groups('apiExposed')]
    private ?string $bio = null;

    #[ORM\Column(unique: true)]
    #[Assert\Email]
    #[Assert\NotBlank]
    #[Groups('apiExposed')]
    private ?string $email = null;

    #[UploadableField(mapping: 'user_picture', fileNameProperty: 'pictureName')]
    private ?File $pictureFile = null;

    #[ORM\Column(nullable: true)]
    #[Groups('apiExposed')]
    private ?string $pictureName = null;

    #[ORM\OneToMany(targetEntity: Car::class, mappedBy: 'user')]
    private Collection $cars;

    #[ORM\ManyToMany(targetEntity: Event::class, mappedBy: 'attendees')]
    private Collection $participatedEvents;

    #[ORM\Column]
    #[Timestampable(on: 'create')]
    #[Groups('apiExposed')]
    private ?\DateTimeImmutable $createdAt;

    #[ORM\Column]
    #[Timestampable(on: 'update')]
    #[Groups('apiExposed')]
    private ?\DateTimeImmutable $updatedAt;

    // Getters and setters

}

序列化Event的控制器方法

#[OA\Response(response: 200, description: 'Returns an event given by its ID.', content: new Model(type: Event::class, groups: ['apiExposed']))]
#[OA\Response(response: 404, description: 'No event found with this ID.')]
#[OA\Response(response: 403, description: 'Event is secret for the current user.')]
#[OA\Tag(name: 'Event')]
public function getByID(EntityManagerInterface $entityManager,
                        SerializerInterface $serializer,
                        string $eventId): JsonResponse
{
    //getting event
    $event = $entityManager -> getRepository(Event::class) -> find($eventId);

    //checking if exists
    if (!$event) {
        return new JsonResponse([
            'code' => Response::HTTP_NOT_FOUND,
            'error' => 'Unknown event'
        ], Response::HTTP_NOT_FOUND);
    }

    //check if event is secret
    if ($event -> isSecret() && $event -> getOrganizer() != $this -> getUser()) {
        return new JsonResponse([
            'code' => Response::HTTP_FORBIDDEN,
            'error' => 'Event is secret'
        ], Response::HTTP_FORBIDDEN);
    }

    return JsonResponse::fromJsonString($serializer -> serialize($event, 'json', ['groups' => 'apiExposed']));
}

问题原因与解决方案

1. 集合未初始化

Event类的attendees字段是Collection类型,但没有在构造函数中初始化,导致它默认是null,Symfony Serializer无法序列化null值的集合。

修复:给Event类添加构造函数,初始化attendees为ArrayCollection:

use Doctrine\Common\Collections\ArrayCollection;

// ...

public function __construct()
{
    $this->attendees = new ArrayCollection();
}

2. Doctrine懒加载问题

ManyToMany关联默认是懒加载模式,当你用find()方法获取Event时,attendees集合并没有被实际加载(只是一个代理对象),Serializer无法读取未初始化的集合数据。

修复方式一:查询时主动加载关联
修改控制器中的查询逻辑,用QueryBuilder主动join并加载attendees:

$event = $entityManager->getRepository(Event::class)
    ->createQueryBuilder('e')
    ->leftJoin('e.attendees', 'a')
    ->addSelect('a')
    ->where('e.id = :eventId')
    ->setParameter('eventId', $eventId)
    ->getQuery()
    ->getOneOrNullResult();

修复方式二:配置Serializer处理懒加载
在实体类的关联字段添加@MaxDepth注解,避免循环引用,同时序列化时启用深度控制:

  • 在Event类的attendees字段添加:
use Symfony\Component\Serializer\Annotation\MaxDepth;

// ...

#[ManyToMany(targetEntity: User::class, inversedBy: 'participatedEvents')]
#[Groups('apiExposed')]
#[MaxDepth(1)]
private Collection $attendees;
  • 在User类的participatedEvents字段添加:
#[ORM\ManyToMany(targetEntity: Event::class, mappedBy: 'attendees')]
#[Groups('apiExposed')]
#[MaxDepth(1)]
private Collection $participatedEvents;
  • 序列化时添加enable_max_depth参数:
return JsonResponse::fromJsonString($serializer->serialize($event, 'json', [
    'groups' => 'apiExposed',
    'enable_max_depth' => true
]));

3. 序列化组拼写错误(附带修复)

Event类中address字段的Groups注解写错了:#[Groups(['aîExposed', 'searchable'])],这里的aîExposed应该是apiExposed,虽然不影响attendees字段,但会导致address字段无法被正确序列化,建议修正:

#[Column]
#[Groups(['apiExposed', 'searchable'])]
#[NotBlank]
private string $address;

内容的提问来源于stack exchange,提问作者Cyprien Aubry

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 10:09:51