The Select2 component of the AdminBundle simplifies the creation of JSON endpoints compatible with the Select2 JavaScript component. It uses a Data Providers system to decouple the data retrieval logic from your controllers.
The component relies on three main elements:
Select2 class: The main service to inject into your controllers.Select2DataProviderInterface interface: To be implemented for each data source.For each entity or data source you want to expose via Select2, you must create a class implementing Select2DataProviderInterface.
namespace App\Select2;
use App\Entity\Customer;
use App\Repository\CustomerRepository;
use Aropixel\AdminBundle\Component\Select2\Select2DataProviderInterface;
use Doctrine\ORM\QueryBuilder;
class CustomerSelect2Provider implements Select2DataProviderInterface
{
public function __construct(private CustomerRepository $repository) {}
public function supports(string $alias): bool
{
// The alias used in the controller to call this provider
return $alias === 'customer';
}
public function getRootAlias(): string
{
// The alias used in the QueryBuilder (must match the one in createQueryBuilder)
return 'c';
}
public function getQueryBuilder(string $searchTerm): QueryBuilder
{
// Returns the base QueryBuilder with the search logic
return $this->repository->createQueryBuilder('c')
->where('c.email LIKE :q OR c.firstName LIKE :q OR c.lastName LIKE :q')
->setParameter('q', '%'.$searchTerm.'%');
}
}
Thanks to autoconfiguration, this service will be automatically detected by the AdminBundle.
Once your provider is created, you can use it fluently in your controller.
use Aropixel\AdminBundle\Component\Select2\Select2;
use App\Entity\Customer;
public function select2(Select2 $select2): Response
{
return $select2
->withProvider('customer') // Uses the alias defined in the provider
->filter(function(QueryBuilder $qb) {
// Optional: Add additional contextual filters
$qb->andWhere('c.active = :active')->setParameter('active', true);
})
->render(); // By default, uses getId() and __toString() for full_name
}
If you need a custom format, you can provide a transformer.
If the transformer returns a string, it will be used as the full_name and the default id will be used.
If it returns an array, you can omit the id to use the default one. The text key is also supported and automatically mapped to full_name for backward compatibility.
// Transformer returning a string
->render(fn(Customer $customer) => $customer->getFullName())
// Transformer returning an array without the 'id' key
->render(fn(Customer $customer) => [
'full_name' => sprintf('%s (%s)', $customer->getFullName(), $customer->getEmail()),
])
// Transformer returning an array with the legacy 'text' key
->render(fn(Customer $customer) => [
'id' => $customer->getId(),
'text' => $customer->getFullName(),
])
// Transformer returning an indexed array (id, full_name)
->render(fn(Customer $customer) => [
$customer->getId(),
$customer->getFullName(),
]);
searchIn)If you don't want to create a dedicated provider, you can use the automatic search on specific fields.
use Aropixel\AdminBundle\Component\Select2\Select2;
use App\Entity\Customer;
public function select2(Select2 $select2): Response
{
return $select2
->withEntity(Customer::class)
->searchIn(['firstName', 'lastName', 'email'])
->render(); // Uses getId() and __toString() by default
}
You can combine searchIn and filter for a flexible "out-of-the-box" experience.
use Aropixel\AdminBundle\Component\Select2\Select2;
use App\Entity\Customer;
public function select2(Select2 $select2): Response
{
return $select2
->withEntity(Customer::class)
->searchIn(['firstName', 'lastName', 'email'])
->filter(function(QueryBuilder $qb) {
$qb->andWhere('e.active = :active')->setParameter('active', true);
})
->render();
}
The Select2Type form type allows you to easily integrate a Select2 input in your Symfony forms.
use Aropixel\AdminBundle\Form\Type\Select2Type;
use App\Entity\Category;
$builder->add('category', Select2Type::class, [
'label' => 'Category',
'class' => Category::class,
'route' => 'admin_category_ajax_search',
'route_params' => ['active' => 1], // Optional route parameters
'choice_label' => 'title',
'multiple' => false, // Set to true for multiple selection
'placeholder' => 'Select a category',
]);
class (required): The entity class.route (required): The AJAX route name to fetch results.route_params (default: []): The AJAX route parameters.choice_label (default: 'label'): The property name or callback to display as label.multiple (default: false): Whether to allow multiple selection.placeholder (default: null): The input placeholder.withProvider(string $alias): selfSets the data provider to use based on its alias.
withEntity(string $className): selfSets the entity class to use when no provider is defined.
searchIn(array $fields): selfEnables automatic search on the specified fields (using LIKE %search%). Works both with withEntity() and withProvider().
filter(callable $callback): selfAllows applying additional filters on the QueryBuilder. The closure receives the QueryBuilder object as a parameter. It can be used in combination with searchIn() for a mix of automatic search and custom filtering.
render(callable $transformer = null): ResponseExecutes the search (automatically handling pagination and counting) and returns a JsonResponse. If no transformer is provided, it uses id and __toString() to format the results as ['id' => ..., 'full_name' => ...].
The response generated by render() follows the standard Select2 format:
{
"results": [
{ "id": 1, "full_name": "John Doe (john@example.com)" },
{ "id": 2, "full_name": "Jane Smith (jane@example.com)" }
],
"pagination": {
"more": true
},
"total_count": 45
}
How can I help you explore Laravel packages today?