JMS Serializer与Gedmo SoftDeleteable关联实体失效及配置问题
我来帮你搞定这两个棘手的问题,咱们一步步拆解:
Doctrine的SoftDeleteable过滤器是在查询层面生效的,但JMS序列化是拿到实体后才处理的,所以得让序列化过程“知道”哪些实体是被软删除的,或者从源头就只加载未删除的实体。这里有两个靠谱的方案:
方案1:从查询入手,确保只加载未软删除的实体
首先得确认你的SoftDeleteable过滤器已经在Doctrine里启用了,一般在config.yml里这么配置:
doctrine: orm: filters: softdeleteable: class: Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter enabled: true
然后,在SubCoursesRepository的getEntitiesByParams方法里,构建查询时要把关联的Courses也带上,并且确保只加载未被软删除的:
public function getEntitiesByParams($paramFetcher, $count = false) { $qb = $this->createQueryBuilder('sc'); if (!$count) { // 左连接关联的Courses,同时过滤掉已删除的 $qb->leftJoin('sc.courses', 'c') ->addSelect('c') ->andWhere('c.deletedAt IS NULL'); } // 加上你的其他查询条件... return $count ? $qb->select('COUNT(sc.id)')->getSingleScalarResult() : $qb->getQuery()->getResult(); }
这样查出来的SubCourses关联的Courses都是未被软删除的,序列化时自然不会碰到已删除的实体。
方案2:用JMS序列化事件监听器,跳过已软删除的实体
如果你需要保留已软删除的SubCourses,但序列化时要跳过它们(或者它们关联的已删除实体),可以写个JMS的事件监听器:
// src/AppBundle/Serializer/SoftDeleteableListener.php namespace AppBundle\Serializer; use JMS\Serializer\EventDispatcher\EventSubscriberInterface; use JMS\Serializer\EventDispatcher\ObjectEvent; class SoftDeleteableListener implements EventSubscriberInterface { public static function getSubscribedEvents() { return [ [ 'event' => 'serializer.pre_serialize', 'method' => 'onPreSerialize', 'format' => 'json', ], ]; } public function onPreSerialize(ObjectEvent $event) { $entity = $event->getObject(); // 检查实体是否有deletedAt方法,且已被软删除 if (method_exists($entity, 'getDeletedAt') && $entity->getDeletedAt() !== null) { // 停止序列化这个实体 $event->getVisitor()->stopVisiting(); } } }
然后在services.yml里注册这个监听器:
services: app.serializer.soft_deleteable_listener: class: AppBundle\Serializer\SoftDeleteableListener tags: - { name: jms_serializer.event_subscriber }
这样不管是SubCourses还是关联的Courses,只要是被软删除的,序列化时就会被跳过,不会出问题。
这个错误的根源是:Doctrine的SoftDeleteable过滤器会把已软删除的Courses过滤掉,当SubCourses的$courses关联尝试延迟加载时,Doctrine找不到对应的实体,所以抛错。解决方法有这几种:
方案1:查询时左连接,允许关联为null
修改SubCoursesRepository的查询,用左连接加载Courses,并且允许它为null,同时过滤掉已删除的:
public function getEntitiesByParams($paramFetcher, $count = false) { $qb = $this->createQueryBuilder('sc'); if (!$count) { $qb->leftJoin('sc.courses', 'c') ->addSelect('c') ->andWhere('c.deletedAt IS NULL OR c.id IS NULL'); } // 其他查询逻辑... return $count ? $qb->select('COUNT(sc.id)')->getSingleScalarResult() : $qb->getQuery()->getResult(); }
这样查出来的SubCourses的$courses要么是未删除的实体,要么是null,序列化时就不会因为找不到实体而报错。
方案2:把关联设为可null,序列化时处理null
首先在SubCourses的$courses注解里加上nullable=true,允许这个关联为null:
/** * @var Courses * * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Courses", inversedBy="subCourses", nullable=true) * @Annotation\Groups({ * "get_sub_courses" * }) * @Annotation\Type("AppBundle\Entity\Courses") */ private $courses;
然后你的createSuccessResponse方法里已经设置了setSerializeNull(true),这样如果$courses是null,序列化时会输出null,而不是报错。
方案3:改用即时加载,避免延迟加载的问题
把关联的加载方式改成EAGER,这样查询SubCourses时会同时加载关联的Courses,不会出现延迟加载时找不到的情况:
/** * @var Courses * * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Courses", inversedBy="subCourses", fetch="EAGER") * @Annotation\Groups({ * "get_sub_courses" * }) * @Annotation\Type("AppBundle\Entity\Courses") */ private $courses;
当然,这个方案要配合查询时的过滤条件,确保只加载未删除的Courses。
针对你的场景,我建议优先用查询时左连接并过滤已删除实体的方案解决第二个问题,从源头避免加载已删除的关联;然后用JMS序列化的事件监听器处理第一个问题,确保序列化过程符合预期。这样两个问题都能完美解决。
内容的提问来源于stack exchange,提问作者shuba.ivan

