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) {
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