petkopara/multi-search-bundle
Symfony bundle that adds a Multi Search service and form type for Doctrine. Build a QueryBuilder to search across all or selected entity columns using a single term, with optional wildcard matching, and reuse it in your controllers or forms.
Installation:
composer require petkopara/multi-search-bundle
Register the bundle in config/bundles.php (Symfony 4+) or AppKernel.php (Symfony 3):
Petkopara\MultiSearchBundle\PetkoparaMultiSearchBundle::class => ['all' => true],
First Use Case:
Inject the service into a controller and apply multi-search to a QueryBuilder:
use Petkopara\MultiSearchBundle\Service\MultiSearchBuilder;
public function indexAction(Request $request, MultiSearchBuilder $multiSearch)
{
$searchTerm = $request->query->get('search');
$qb = $this->getDoctrine()->getRepository('AppBundle:Post')->createQueryBuilder('p');
if ($searchTerm) {
$qb = $multiSearch->searchEntity($qb, 'AppBundle:Post', $searchTerm);
}
return $this->render('post/index.html.twig', [
'posts' => $qb->getQuery()->getResult()
]);
}
Form Integration:
Create a form type with MultiSearchType:
use Petkopara\MultiSearchBundle\Form\Type\MultiSearchType;
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('search', MultiSearchType::class, [
'class' => 'AppBundle:Post',
'search_fields' => ['title', 'content'], // Optional
'search_comparison_type' => 'wildcard' // Optional
]);
}
Dynamic Query Building:
Use the service to dynamically modify QueryBuilder instances:
$qb = $this->getDoctrine()->getRepository('AppBundle:Product')->createQueryBuilder('p');
$qb = $multiSearch->searchEntity($qb, 'AppBundle:Product', $searchTerm, ['name', 'description'], 'wildcard');
Combining with Other Criteria: Chain the multi-search with other query modifications:
$qb->andWhere('p.isActive = :active')
->setParameter('active', true);
$qb = $multiSearch->searchEntity($qb, 'AppBundle:Product', $searchTerm);
Pagination Integration:
Use the modified QueryBuilder with KnpPaginatorBundle or native Doctrine pagination:
$paginator = $this->get('knp_paginator');
$results = $paginator->paginate(
$qb,
$request->query->getInt('page', 1),
10
);
Form Handling: Bind the form in the controller and apply the search:
$form = $this->createForm(PostFilterType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$qb = $this->getDoctrine()->getRepository('AppBundle:Post')->createQueryBuilder('p');
$qb = $multiSearch->searchForm($qb, $form->get('search'));
}
Customizing Search Fields: Override search fields per form type:
$builder->add('search', MultiSearchType::class, [
'class' => 'AppBundle:User',
'search_fields' => ['username', 'email', 'fullName']
]);
Reusing Forms: Extend base form types to avoid repetition:
class BaseSearchType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('search', MultiSearchType::class, [
'class' => $options['entity_class'],
'search_fields' => $options['search_fields'] ?? [],
]);
}
}
Event Listeners:
Attach multi-search to entity events (e.g., kernel.request):
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if ($request->isMethod('GET') && $request->query->has('search')) {
// Modify global query logic here
}
}
API Integration: Use the service in API controllers for search endpoints:
$qb = $this->getDoctrine()->getRepository('AppBundle:ApiResource')->createQueryBuilder('r');
$qb = $multiSearch->searchEntity($qb, 'AppBundle:ApiResource', $request->query->get('q'));
$serializer = $this->get('serializer');
return new JsonResponse($serializer->serialize($qb->getQuery()->getResult(), 'json'));
Dynamic Entity Handling: Pass entity classes dynamically (e.g., from routes):
$entityClass = $request->attributes->get('entity');
$qb = $this->getDoctrine()->getRepository($entityClass)->createQueryBuilder('e');
$qb = $multiSearch->searchEntity($qb, $entityClass, $searchTerm);
Case Sensitivity:
The bundle uses LIKE by default, which is case-insensitive in most databases. For case-sensitive searches, use ILIKE (PostgreSQL) or modify the generated DQL:
// Override the service to customize the comparison
$this->container->get('petkopara_multi_search.builder')->setComparisonType('custom');
Performance with Large Datasets:
Avoid searching all columns on large tables. Explicitly define search_fields:
$qb = $multiSearch->searchEntity($qb, 'AppBundle:Post', $searchTerm, ['title', 'content']);
Reserved Keywords:
If search terms contain SQL reserved words (e.g., order), escape them or use parameter binding:
// Ensure the service escapes inputs (check the source for `addLike` method)
Form Validation:
The MultiSearchType does not validate the search term by default. Add constraints if needed:
$builder->add('search', MultiSearchType::class, [
'constraints' => [new Length(['max' => 100])]
]);
Inspect Generated DQL:
Log the QueryBuilder before execution to debug:
$query = $qb->getQuery();
$this->logger->debug('Generated DQL: ' . $query->getDQL());
Check for Typos:
Ensure search_fields match entity property names (case-sensitive in some Doctrine configurations).
Service Overrides: Extend the service to add logging or custom logic:
class CustomMultiSearchBuilder extends MultiSearchBuilder
{
public function searchEntity(QueryBuilder $qb, $entity, $searchTerm, array $fields = [], $comparisonType = 'wildcard')
{
$this->logger->info('Searching entity: ' . $entity . ' with term: ' . $searchTerm);
return parent::searchEntity($qb, $entity, $searchTerm, $fields, $comparisonType);
}
}
Register the override in services.yaml:
services:
petkopara_multi_search.builder:
class: AppBundle\Service\CustomMultiSearchBuilder
public: true
Custom Comparison Types:
Extend the bundle to support new comparison types (e.g., regex):
// Override the service and add a new method
public function addRegex(QueryBuilder $qb, $alias, $field, $searchTerm)
{
$qb->andWhere("$alias.$field REGEXP :searchTerm")
->setParameter('searchTerm', $searchTerm);
}
Entity Metadata Integration: Dynamically fetch searchable fields from entity metadata (e.g., annotations):
$metadata = $this->getDoctrine()->getManager()->getMetadataFactory()->getMetadataFor($entity);
$searchFields = array_keys($metadata->fieldMappings);
Caching Search Results:
Cache the modified QueryBuilder or its results:
$cacheKey = md5($searchTerm . serialize($fields));
if (!$results = $cache->get($cacheKey)) {
$results = $qb->getQuery()->getResult();
$cache->set($cacheKey, $results, 3600);
}
How can I help you explore Laravel packages today?