JMS Serializer交叉引用问题:过滤自关联实体的revision类型页面
children时排除type=revision的Page实体问题 看起来你是在Symfony项目里用LexikFormFilterBundle做过滤,但现在遇到了展开关联的children集合时,里面混进了type为revision的Page,想把这些排除掉对吧?我给你几个可行的方案,你可以根据自己的业务场景选择:
方案1:修改Doctrine关联映射(一劳永逸)
如果你的业务逻辑里,children集合永远不应该包含revision类型的Page,那直接在实体的关联注解里加条件是最简单的:
// src/Entity/Page.php use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity() */ class Page { // ...其他属性和方法 /** * @ORM\OneToMany(targetEntity="Page", mappedBy="parent") * @ORM\Where(clause="type != 'revision'") */ private $children; // ... }
这样Doctrine在加载children集合的时候,会自动过滤掉type=revision的记录,不管是通过API展开还是其他地方调用,都不会拿到这些数据。不过要注意,如果之后有场景需要获取包含revision的子页面,你得再定义一个新的关联(比如allChildren),不带这个WHERE条件。
方案2:序列化阶段过滤(不影响原始数据)
如果只是在API响应里需要过滤,不想修改Doctrine的默认加载行为,可以用序列化器的事件来处理。比如用Symfony自带的Serializer:
先写一个序列化订阅器:
// src/Serializer/PageSerializerSubscriber.php use Symfony\Component\Serializer\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Serializer\EventDispatcher\PreSerializeEvent; use Doctrine\Common\Collections\ArrayCollection; use App\Entity\Page; use Symfony\Component\HttpFoundation\RequestStack; class PageSerializerSubscriber implements EventSubscriberInterface { private $requestStack; public function __construct(RequestStack $requestStack) { $this->requestStack = $requestStack; } public static function getSubscribedEvents() { return [ ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerialize', 'class' => Page::class], ]; } public function onPreSerialize(PreSerializeEvent $event) { /** @var Page $page */ $page = $event->getObject(); $request = $this->requestStack->getCurrentRequest(); // 只在请求需要展开children的时候才过滤 if ($request && $request->query->has('expand') && strpos($request->query->get('expand'), 'page_children') !== false) { $filteredChildren = array_filter( $page->getChildren()->toArray(), fn($child) => $child->getType() !== 'revision' ); $page->setChildren(new ArrayCollection($filteredChildren)); } } }
然后在services.yaml里注册这个订阅器:
services: App\Serializer\PageSerializerSubscriber: tags: - { name: serializer.event_subscriber }
这个方法的好处是不会影响Doctrine加载的原始数据,只是在序列化输出的时候过滤掉不需要的元素,灵活性更高。
方案3:结合LexikFormFilterBundle在查询阶段过滤
如果你的API已经在用LexikFormFilterBundle做列表过滤,也可以在FilterType里针对children关联添加过滤条件,确保查询时只加载符合要求的子页面:
// src/Form/Filter/PageFilterType.php use Lexik\Bundle\FormFilterBundle\Filter\Form\Type as Filters; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; class PageFilterType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder // 其他过滤字段... ->add('children', Filters\EntityFilterType::class, [ 'class' => Page::class, 'filter_fields' => [ 'type' => [ 'operator' => 'neq', 'value' => 'revision' ], ], ]); } public function getParent() { return Filters\FilterType::class; } }
然后在处理查询的时候,确保这个过滤条件被应用。核心思路就是在QueryBuilder里给children关联加上type != 'revision'的条件,和Lexik的过滤逻辑结合起来。
方案4:手动修改QueryBuilder(灵活可控)
如果以上方法都不符合你的需求,还可以在Repository或者控制器里手动修改查询构建器,当检测到expand=page_children参数时,添加过滤条件:
// src/Repository/PageRepository.php use Doctrine\ORM\EntityRepository; use Symfony\Component\HttpFoundation\RequestStack; class PageRepository extends EntityRepository { private $requestStack; public function __construct($em, $class, RequestStack $requestStack) { parent::__construct($em, $class); $this->requestStack = $requestStack; } public function getFilteredPagesQueryBuilder() { $qb = $this->createQueryBuilder('p'); $request = $this->requestStack->getCurrentRequest(); // 检查是否需要展开children并过滤 if ($request && $request->query->has('expand') && strpos($request->query->get('expand'), 'page_children') !== false) { $qb->leftJoin('p.children', 'c') ->andWhere('c.type != :revision') ->setParameter('revision', 'revision') ->addSelect('c'); } return $qb; } }
这样在查询页面列表的时候,只要请求带了expand=page_children,就会自动过滤掉子页面中的revision类型。
内容的提问来源于stack exchange,提问作者Łukasz D. Tulikowski

