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

时间:2015-04-23 14:11:33

标签: php forms symfony collections filter

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

其他组件(Symfony bundle)定义实现Dateable接口的模型,并设置可以查找和返回这些模型的提供程序。代码很像这样:

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不应该返回任何消息,因为博客帖子没有位置。但我无法在过滤器中检查location,因为BlogPostBundle不应该了解其他提供商及其过滤选项。

关于如何设计这样的过滤器表单的任何想法?

2 个答案:

答案 0 :(得分:0)

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

所有提供商都应该知道他们使用哪些过滤器(通过过滤器名称)。也是DI中的处理程序。

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

当过滤调用$provider->filter($requestedFiltersWithValues)时,它会检查它使用的过滤器是否实际被请求并注册(通过注入的处理程序)并根据需要返回结果。

答案 1 :(得分:0)

这就是我最终解决的问题。

首先,我有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类和提供商使用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')
        ;
    }
}

还修改了Timeline类的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;
    }
}

在这种情况下,与表单相关联的数据类。 $provider->createFilter()只返回一个具有与过滤器匹配的属性的对象。然后,填充并验证的过滤器对象将传递给提供者的find()方法。 E.g:

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()方法
  • 处理该属性