How to create filters for a universal, pluggable collection?

I am developing a web application that has a timeline very similar to the Facebook timeline. The timeline itself is fully shared and plug-in. This is just a general set of items. Everything that has the proper interface ( Dateable ) can be added to the collection and displayed on the timeline.

Other components (Symfony packages) identify models that implement the Dateable interface and configure a provider that can find and return these models. The code looks something like this:

 class Timeline { private $providers = []; // Filled by DI public function find(/* ... */) { $result = []; foreach ($this->providers as $provider) { $result = array_merge($result, $provider->find(/* ... */)); } return $result; } } 

The problem is that there should be a set of filters next to the timeline. Some filter options (for example, date ) apply to all providers. But most options do not. For example, most providers will be able to use the filter parameter author , but not all. Some notification elements are dynamically generated and do not have an author.

Some filter options apply to only one provider. For example, only event elements have a location property.

I can't figure out how to create a filter form as modular as the timeline itself. Where should the available filter parameters be determined? The filter options, depending on the type of Bundle, may have come from the package itself, but what about filter options (like user ) that can be used by multiple packages? And what if some filter parameters subsequently become available for use by multiple bundles? For example, only events have location now, but what if another module is added that also has elements with location?

And how will each supplier determine if the submitted filter form contains only the parameters that it understands? If I set the location in the filter, then BlogPostProvider should not return any posts, because there is no place in the blog posts. But I can’t check the location in the filter, because BlogPostBundle should not know about other providers and their filtering options.

Any ideas on how I can create such a filter form?

+6
source share
2 answers

This is how I solved it at the end.

Firstly, I have a FilterRegistry . Any package can add filters to it using the Symfony DI tag. A filter is just a form type. Filter example:

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

DI configuration:

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

FilterRegistry knows how to get these types of forms from a DI container:

 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]); } } 

The Timeline class and providers use FilterBuilder to add new filters to the filter form. The constructor is as follows:

 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; } } 

To display the form, a filter is created using the options of all suppliers. This happens in Timeline->getFilterForm() . Note that the data object is not bound to a form:

 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(); } } 

Each provider implements the configureFilter method:

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

The find method of the Timeline class is also used. Instead of creating a filter with all parameters, it creates a new filter form using only the parameters of this provider. If the form validation fails, the provider will not be able to process the combination of filters at this time. This usually happens because a filter parameter is set that the provider does not understand. In this case, the form check is not performed due to the installation of additional data.

 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; } } 

In this case, there is a data class bound to the form. $provider->createFilter() simply returns an object that has properties matching the filters. The filled and checked filter object is then passed to the provider's find() method. For instance:

 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; } 

All this together simplifies filter management.

To add a new filter type:

  • Implementing FormType
  • Mark it in DI as form.type and as filter.type

To start using the filter:

  • Add it to FilterBuilder in configureFilters()
  • Add property to filter model
  • Process property in find() method
0
source

Add a central FilterHandler where you can register each available filter. General filters can be stored in one set with the processor and registered from there, and packets can also register filters.

All suppliers should know whether and when which filters they use (by filter name). Also a DI handler in them.

From the handler, you can get a complete list of registered filters, and then create your own filter form.

When filtering a call, it will be $provider->filter($requestedFiltersWithValues) , which will check whether the filters it uses are actually requested and registered (through the injection handler) and return the results as necessary.

0
source

Source: https://habr.com/ru/post/985791/


All Articles