我正在开发一个具有时间轴的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不应该了解其他提供商及其过滤选项。
关于如何设计这样的过滤器表单的任何想法?
答案 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;
}
这一切使得管理过滤器非常容易。
添加新类型的过滤器:
form.type
,将标记为filter.type
开始使用过滤器:
configureFilters()
find()
方法