如何为通用的、可插入的集合设计过滤器?

How to design filters for a generic, pluggable collection?

我正在开发一个具有时间线功能的 Web 应用程序,很像 Facebook 时间线。时间线本身是完全通用和可插入的。它只是一个通用的项目集合。任何具有适当界面 (Dateable) 的内容都可以添加到集合中并显示在时间线上。

其他组件(Symfony 捆绑包)定义了实现 Dateable 接口的模型,并设置了可以找到和 return 这些模型的提供者。代码大致是这样的:

class Timeline
{
    private $providers = []; // Filled by DI

    public function find(/* ... */)
    {
        $result = [];
        foreach ($this->providers as $provider) {
            $result = array_merge($result, $provider->find(/* ... */));
        }

        return $result;
    }
}

问题是时间轴旁边需要有一组过滤器。一些过滤器选项(如 date)适用于所有提供商。但大多数选项没有。例如,大多数提供商都可以使用 author 过滤器选项,但不是全部。一些通知项目是动态生成的,没有作者。

一些过滤器选项仅适用于单个提供商。例如,只有事件项具有 location 属性.

我不知道如何设计一个与时间轴本身一样模块化的过滤器表单。应该在哪里定义可用的过滤器选项?特定于捆绑包的过滤器选项可能来自捆绑包本身,但是可以由多个捆绑包使用的过滤器选项(如 user)怎么样?如果一些过滤器选项后来可以被多个包使用怎么办?例如,现在只有事件有 location,但如果添加另一个模块也有带位置的项目怎么办?

每个提供商如何确定提交的过滤器表单是否只包含它理解的选项?如果我在过滤器中设置位置,那么 BlogPostProvider 不应 return 任何消息,因为博文没有位置。但我无法在过滤器中检查 location,因为 BlogPostBundle 不应该知道其他提供商及其过滤选项。

关于如何设计这样的过滤器表单有什么想法吗?

添加一个中心 FilterHandler,可以在其中注册每个可用的过滤器。通用过滤器可以与处理程序保存在同一个包中并从那里注册,并且包也可以注册过滤器。

所有提供者都应该知道他们是否以及何时使用了哪些过滤器(通过过滤器名称)。还要在其中 DI 处理程序。

从处理程序中,您可以获得已注册过滤器的完整列表,然后在此基础上构建您的过滤器表单。

过滤调用时 $provider->filter($requestedFiltersWithValues) 将检查它使用的过滤器是否实际被请求和注册(通过注入的处理程序)并且 return 根据需要得到结果。

最后我是这样解决的

首先,我有一个 FilterRegistry。任何包都可以使用 Symfony DI 标签添加过滤器。过滤器只是一种表单类型。过滤器示例:

class LocationFilterType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('location', 'choice', [ /* ... */ ]);
    }
}

DI 配置:

<service id="filter.location" class="My\Bundle\Form\LocationFilterType">
    <tag name="form.type" alias="filter_location" />
    <tag name="filter.type" alias="location" />
</service>

FilterRegistry 知道如何从 DI 容器中获取那些表单类型:

class FilterRegistry
{
    public function getFormType($name)
    {
        if (!isset($this->types[$name])) {
            throw new \InvalidArgumentException(sprintf('Unknown filter type "%s"', $name));
        }

        return $this->container->get($this->types[$name]);
    }
}

Timeline class 和提供者使用 FilterBuilder 向过滤器表单添加新过滤器。生成器看起来像这样:

class FilterBuilder
{
    public function __construct(FilterRegistry $filterRegistry, FormBuilderInterface $formBuilder)
    {
        $this->filterRegistry = $filterRegistry;
        $this->formBuilder = $formBuilder;
    }

    public function add($name)
    {
        if ($this->formBuilder->has($name)) {
            return;
        }

        $type = $this->filterRegistry->getFormType($name);
        $type->buildForm($this->formBuilder, $this->formBuilder->getOptions());

        return $this;
    }
}

为了显示表单,使用所有提供程序的选项构建了一个过滤器。这发生在 Timeline->getFilterForm()。请注意,没有绑定到表单的数据对象:

class Timeline
{
    public function getFilterForm()
    {
        $formBuilder = $this->formFactory->createNamedBuilder('', 'base_filter_type');

        foreach ($this->providers as $provider) {
            $provider->configureFilter(new FilterBuilder($this->filterRegistry, $formBuilder));
        }

        return $formBuilder->getForm();
    }
}

每个提供者都实现 configureFilter 方法:

class EventProvider
{
    public function configureFilter(FilterBuilder $builder)
    {
        $builder
            ->add('location')
            ->add('author')
        ;
    }
}

时间轴class的find方法也进行了修改。它不是构建一个包含所有选项的过滤器,而是构建一个仅包含该提供者选项的新过滤器表单。如果表单验证失败,则提供者无法处理当前提交的过滤器组合。通常这是因为设置了提供者不理解的过滤器选项。在这种情况下,由于设置了额外的数据,表单验证失败。

class Timeline
{
    public function find(Request $request)
    {
        $result = [];

        foreach ($this->providers as $provider) {
            $filter = $provider->createFilter();
            $formBuilder = $this->formFactory->createNamedBuilder('', 'base_filter_type', $filter);

            $provider->configureFilter(new FilterBuilder($this->filterRegistry, $formBuilder));

            $form = $formBuilder->getForm();
            $form->handleRequest($request);

            if (!$form->isSubmitted() || $form->isValid()) {
                $result = array_merge($result, $provider->find($filter));
            }
        }

        return $result;
    }
}

在这种情况下,一个数据class绑定到表单。 $provider->createFilter() 只是 returns 一个具有与过滤器匹配的属性的对象。然后将填充和验证的过滤器对象传递给提供者的 find() 方法。例如:

class EventProvider
{
    public function createFilter()
    {
        return new EventFilter();
    }

    public function find(EventFilter $filter)
    {
        // Do something with $filter and return events
    }
}

class EventFilter
{
    public $location;
    public $author;
}

这一切使管理过滤器变得非常容易。

添加新类型的过滤器:

  • 实现 FormType
  • 在 DI 中将其标记为 form.type 作为 filter.type

要开始使用过滤器:

  • 将其添加到 configureFilters()
  • 中的 FilterBuilder
  • 在过滤器模型中添加一个属性
  • 处理find()方法中的属性