diff --git a/docs/reference/search.rst b/docs/reference/search.rst
index 8bf28698168..141961bc59a 100644
--- a/docs/reference/search.rst
+++ b/docs/reference/search.rst
@@ -5,6 +5,23 @@ The admin comes with a basic global search available in the upper navigation men
and look for filter with the option ``global_search`` set to true. If you are using the ``SonataDoctrineORMBundle``
any text filter will be set to ``true`` by default.
+Disabling the search by admin
+-----------------------------
+
+You can disable the search for a whole admin by setting the ``global_search`` attribute
+to ``false`` at your admin definition using the tag ``sonata.admin``.
+
+.. configuration-block::
+
+ .. code-block:: xml
+
+
+
+
+ App\Entity\Post
+
+
+
Customization
-------------
diff --git a/src/DependencyInjection/Compiler/AdminSearchCompilerPass.php b/src/DependencyInjection/Compiler/AdminSearchCompilerPass.php
new file mode 100644
index 00000000000..7be26d8e6be
--- /dev/null
+++ b/src/DependencyInjection/Compiler/AdminSearchCompilerPass.php
@@ -0,0 +1,77 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Sonata\AdminBundle\DependencyInjection\Compiler;
+
+use Sonata\AdminBundle\Admin\AdminInterface;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Exception\LogicException;
+
+/**
+ * This class configures which admins must be considered for global search at `SearchHandler`.
+ *
+ * @author Javier Spagnoletti
+ */
+final class AdminSearchCompilerPass implements CompilerPassInterface
+{
+ public const TAG_ATTRIBUTE_TOGGLE_SEARCH = 'global_search';
+
+ private const TAG_ADMIN = 'sonata.admin';
+
+ public function process(ContainerBuilder $container): void
+ {
+ $adminSearch = [];
+
+ foreach ($container->findTaggedServiceIds(self::TAG_ADMIN) as $id => $tags) {
+ $definition = $container->getDefinition($id);
+
+ // Trim possible parameter delimiters ("%") from the class name.
+ $adminClass = trim($definition->getClass(), '%');
+ if (!class_exists($adminClass) && $container->hasParameter($adminClass)) {
+ $adminClass = $container->getParameter($adminClass);
+ }
+
+ if (!is_subclass_of($adminClass, AdminInterface::class)) {
+ throw new LogicException(sprintf(
+ 'Service "%s" must implement `%s`.',
+ $id,
+ AdminInterface::class
+ ));
+ }
+
+ foreach ($tags as $attributes) {
+ if (!isset($attributes[self::TAG_ATTRIBUTE_TOGGLE_SEARCH])) {
+ continue;
+ }
+
+ $globalSearch = $attributes[self::TAG_ATTRIBUTE_TOGGLE_SEARCH];
+
+ if (!\is_bool($globalSearch)) {
+ throw new LogicException(sprintf(
+ 'Attribute "%s" in tag "%s" at service "%s" must be of type boolean, "%s" given.',
+ self::TAG_ATTRIBUTE_TOGGLE_SEARCH,
+ self::TAG_ADMIN,
+ $id,
+ \gettype($globalSearch)
+ ));
+ }
+
+ $adminSearch[$id] = $globalSearch;
+ }
+ }
+
+ $searchHandlerDefinition = $container->getDefinition('sonata.admin.search.handler');
+ $searchHandlerDefinition->addMethodCall('configureAdminSearch', [$adminSearch]);
+ }
+}
diff --git a/src/Search/SearchHandler.php b/src/Search/SearchHandler.php
index cb113b8620a..596b232b2fd 100644
--- a/src/Search/SearchHandler.php
+++ b/src/Search/SearchHandler.php
@@ -35,6 +35,11 @@ class SearchHandler
*/
private $caseSensitive;
+ /**
+ * @var array
+ */
+ private $adminsSearchConfig = [];
+
/**
* NEXT_MAJOR: Change signature to __construct(bool $caseSensitive) and remove pool property.
*
@@ -69,6 +74,11 @@ public function __construct($deprecatedPoolOrCaseSensitive, $caseSensitive = tru
*/
public function search(AdminInterface $admin, $term, $page = 0, $offset = 20)
{
+ // If the search is disabled for the whole admin, skip any further processing.
+ if (false === ($this->adminsSearchConfig[$admin->getCode()] ?? true)) {
+ return false;
+ }
+
$datagrid = $admin->getDatagrid();
$found = false;
@@ -95,4 +105,28 @@ public function search(AdminInterface $admin, $term, $page = 0, $offset = 20)
return $pager;
}
+
+ /**
+ * Sets whether the search must be enabled or not for the passed admin codes.
+ * Receives an array with the admin code as key and a boolean as value.
+ *
+ * @param array $adminsSearchConfig
+ */
+ final public function configureAdminSearch(array $adminsSearchConfig): void
+ {
+ $this->adminsSearchConfig = [];
+
+ foreach ($adminsSearchConfig as $adminCode => $adminSearchConfig) {
+ if (!\is_bool($adminSearchConfig)) {
+ throw new \InvalidArgumentException(sprintf(
+ 'Values in the array passed as argument 1 to "%s()" must be of type boolean, %s given in the offset "%s".',
+ __METHOD__,
+ \is_object($adminSearchConfig) ? 'instance of "'.\get_class($adminSearchConfig).'"' : '"'.\gettype($adminSearchConfig).'"',
+ $adminCode
+ ));
+ }
+
+ $this->adminsSearchConfig[$adminCode] = $adminSearchConfig;
+ }
+ }
}
diff --git a/src/SonataAdminBundle.php b/src/SonataAdminBundle.php
index 5c99f7235ba..4aa333d8d29 100644
--- a/src/SonataAdminBundle.php
+++ b/src/SonataAdminBundle.php
@@ -16,6 +16,7 @@
use Mopa\Bundle\BootstrapBundle\Form\Type\TabType;
use Sonata\AdminBundle\DependencyInjection\Compiler\AddDependencyCallsCompilerPass;
use Sonata\AdminBundle\DependencyInjection\Compiler\AddFilterTypeCompilerPass;
+use Sonata\AdminBundle\DependencyInjection\Compiler\AdminSearchCompilerPass;
use Sonata\AdminBundle\DependencyInjection\Compiler\ExtensionCompilerPass;
use Sonata\AdminBundle\DependencyInjection\Compiler\GlobalVariablesCompilerPass;
use Sonata\AdminBundle\DependencyInjection\Compiler\ModelManagerCompilerPass;
@@ -50,6 +51,7 @@ public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new AddDependencyCallsCompilerPass());
$container->addCompilerPass(new AddFilterTypeCompilerPass());
+ $container->addCompilerPass(new AdminSearchCompilerPass());
$container->addCompilerPass(new ExtensionCompilerPass());
$container->addCompilerPass(new GlobalVariablesCompilerPass());
$container->addCompilerPass(new ModelManagerCompilerPass());
diff --git a/tests/DependencyInjection/Compiler/AdminSearchCompilerPassTest.php b/tests/DependencyInjection/Compiler/AdminSearchCompilerPassTest.php
new file mode 100644
index 00000000000..0cce3d979b6
--- /dev/null
+++ b/tests/DependencyInjection/Compiler/AdminSearchCompilerPassTest.php
@@ -0,0 +1,76 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Sonata\AdminBundle\Tests\DependencyInjection\Compiler;
+
+use PHPUnit\Framework\TestCase;
+use Sonata\AdminBundle\DependencyInjection\Compiler\AdminSearchCompilerPass;
+use Sonata\AdminBundle\Tests\Fixtures\Admin\PostAdmin;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+
+/**
+ * @author Javier Spagnoletti
+ */
+final class AdminSearchCompilerPassTest extends TestCase
+{
+ public function testProcess(): void
+ {
+ $containerBuilderMock = $this->createMock(ContainerBuilder::class);
+ $containerBuilderMock->expects($this->once())
+ ->method('findTaggedServiceIds')
+ ->with($this->equalTo('sonata.admin'))
+ ->willReturn([
+ 'admin.foo' => [
+ ['global_search' => true],
+ ],
+ 'admin.bar' => [
+ ['global_search' => false],
+ ],
+ 'admin.baz' => [
+ ['some_attribute' => 42],
+ ],
+ ]);
+
+ $adminFooDefinition = $this->createMock(Definition::class);
+ $adminFooDefinition->expects($this->once())
+ ->method('getClass')
+ ->willReturn(PostAdmin::class);
+
+ $adminBarDefinition = $this->createMock(Definition::class);
+ $adminBarDefinition->expects($this->once())
+ ->method('getClass')
+ ->willReturn(PostAdmin::class);
+
+ $adminBazDefinition = $this->createMock(Definition::class);
+ $adminBazDefinition->expects($this->once())
+ ->method('getClass')
+ ->willReturn(PostAdmin::class);
+
+ $searchHandlerDefinition = $this->createMock(Definition::class);
+ $searchHandlerDefinition->expects($this->once())
+ ->method('addMethodCall')
+ ->with('configureAdminSearch', [['admin.foo' => true, 'admin.bar' => false]]);
+
+ $containerBuilderMock
+ ->method('getDefinition')
+ ->willReturnMap([
+ ['admin.foo', $adminFooDefinition],
+ ['admin.bar', $adminBarDefinition],
+ ['admin.baz', $adminBazDefinition],
+ ['sonata.admin.search.handler', $searchHandlerDefinition],
+ ]);
+
+ (new AdminSearchCompilerPass())->process($containerBuilderMock);
+ }
+}
diff --git a/tests/Search/SearchHandlerTest.php b/tests/Search/SearchHandlerTest.php
index a04ac4275dd..9a5c6129d8b 100644
--- a/tests/Search/SearchHandlerTest.php
+++ b/tests/Search/SearchHandlerTest.php
@@ -24,14 +24,14 @@ class SearchHandlerTest extends TestCase
{
public function testBuildPagerWithNoGlobalSearchField(): void
{
- $filter = $this->getMockForAbstractClass(FilterInterface::class);
- $filter->expects($this->once())->method('getOption')->willReturn(false);
+ $filter = $this->createMock(FilterInterface::class);
+ $filter->expects($this->once())->method('getOption')->with('global_search')->willReturn(false);
$filter->expects($this->never())->method('setOption');
- $datagrid = $this->getMockForAbstractClass(DatagridInterface::class);
+ $datagrid = $this->createMock(DatagridInterface::class);
$datagrid->expects($this->once())->method('getFilters')->willReturn([$filter]);
- $admin = $this->getMockForAbstractClass(AdminInterface::class);
+ $admin = $this->createMock(AdminInterface::class);
$admin->expects($this->once())->method('getDatagrid')->willReturn($datagrid);
$handler = new SearchHandler(true);
@@ -43,20 +43,20 @@ public function testBuildPagerWithNoGlobalSearchField(): void
*/
public function testBuildPagerWithGlobalSearchField(bool $caseSensitive): void
{
- $filter = $this->getMockForAbstractClass(FilterInterface::class);
- $filter->expects($this->once())->method('getOption')->willReturn(true);
+ $filter = $this->createMock(FilterInterface::class);
+ $filter->expects($this->once())->method('getOption')->with('global_search')->willReturn(true);
$filter->expects($this->once())->method('setOption')->with('case_sensitive', $caseSensitive);
- $pager = $this->getMockForAbstractClass(PagerInterface::class);
+ $pager = $this->createMock(PagerInterface::class);
$pager->expects($this->once())->method('setPage');
$pager->expects($this->once())->method('setMaxPerPage');
- $datagrid = $this->getMockForAbstractClass(DatagridInterface::class);
+ $datagrid = $this->createMock(DatagridInterface::class);
$datagrid->expects($this->once())->method('getFilters')->willReturn([$filter]);
$datagrid->expects($this->once())->method('setValue');
$datagrid->expects($this->once())->method('getPager')->willReturn($pager);
- $admin = $this->getMockForAbstractClass(AdminInterface::class);
+ $admin = $this->createMock(AdminInterface::class);
$admin->expects($this->once())->method('getDatagrid')->willReturn($datagrid);
$handler = new SearchHandler($caseSensitive);
@@ -70,4 +70,71 @@ public function buildPagerWithGlobalSearchFieldProvider(): array
[false],
];
}
+
+ /**
+ * @dataProvider provideAdminSearchConfigurations
+ */
+ public function testAdminSearch($expected, $filterCallsCount, ?bool $enabled, string $adminCode): void
+ {
+ $filter = $this->createMock(FilterInterface::class);
+ $filter->expects($this->exactly($filterCallsCount))->method('getOption')->with('global_search')->willReturn(true);
+ $filter->expects($this->exactly($filterCallsCount))->method('setOption')->with('case_sensitive', true);
+
+ $pager = $this->createMock(PagerInterface::class);
+ $pager->expects($this->exactly($filterCallsCount))->method('setPage');
+ $pager->expects($this->exactly($filterCallsCount))->method('setMaxPerPage');
+
+ $datagrid = $this->createMock(DatagridInterface::class);
+ $datagrid->expects($this->exactly($filterCallsCount))->method('getFilters')->willReturn([$filter]);
+ $datagrid->expects($this->exactly($filterCallsCount))->method('setValue');
+ $datagrid->expects($this->exactly($filterCallsCount))->method('getPager')->willReturn($pager);
+
+ $admin = $this->createMock(AdminInterface::class);
+ $admin->expects($this->exactly($filterCallsCount))->method('getDatagrid')->willReturn($datagrid);
+ $admin->expects($this->once())->method('getCode')->willReturn($adminCode);
+
+ $handler = new SearchHandler(true);
+
+ if (null !== $enabled) {
+ $handler->configureAdminSearch([$adminCode => $enabled]);
+ }
+
+ if (false === $expected) {
+ $this->assertFalse($handler->search($admin, 'myservice'));
+ } else {
+ $this->assertInstanceOf($expected, $handler->search($admin, 'myservice'));
+ }
+ }
+
+ public function provideAdminSearchConfigurations(): iterable
+ {
+ yield 'admin_search_enabled' => [PagerInterface::class, 1, true, 'admin.foo'];
+ yield 'admin_search_disabled' => [false, 0, false, 'admin.bar'];
+ yield 'admin_search_omitted' => [PagerInterface::class, 1, null, 'admin.baz'];
+ }
+
+ /**
+ * @dataProvider provideAdminSearchInvalidConfigurations
+ */
+ public function testAdminSearchInvalidConfig($enabled): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessageMatches(
+ '/^Values in the array passed as argument 1 to "Sonata\\\AdminBundle\\\Search\\\SearchHandler::configureAdminSearch\(\)"'
+ .' must be of type boolean, .+ given in the offset "admin\.foo"\.$/'
+ );
+
+ (new SearchHandler(true))->configureAdminSearch(['admin.foo' => $enabled]);
+ }
+
+ public function provideAdminSearchInvalidConfigurations(): iterable
+ {
+ yield [1];
+ yield [0];
+ yield [null];
+ yield [''];
+ yield ['a'];
+ yield [[]];
+ yield [new \stdClass()];
+ }
}
diff --git a/tests/SonataAdminBundleTest.php b/tests/SonataAdminBundleTest.php
index 74bd1d15532..c77d787dccf 100644
--- a/tests/SonataAdminBundleTest.php
+++ b/tests/SonataAdminBundleTest.php
@@ -16,6 +16,7 @@
use PHPUnit\Framework\TestCase;
use Sonata\AdminBundle\DependencyInjection\Compiler\AddDependencyCallsCompilerPass;
use Sonata\AdminBundle\DependencyInjection\Compiler\AddFilterTypeCompilerPass;
+use Sonata\AdminBundle\DependencyInjection\Compiler\AdminSearchCompilerPass;
use Sonata\AdminBundle\DependencyInjection\Compiler\ExtensionCompilerPass;
use Sonata\AdminBundle\DependencyInjection\Compiler\GlobalVariablesCompilerPass;
use Sonata\AdminBundle\DependencyInjection\Compiler\ModelManagerCompilerPass;
@@ -35,7 +36,7 @@ public function testBuild(): void
{
$containerBuilder = $this->createMock(ContainerBuilder::class);
- $containerBuilder->expects($this->exactly(7))
+ $containerBuilder->expects($this->exactly(8))
->method('addCompilerPass')
->willReturnCallback(function (CompilerPassInterface $pass, $type = PassConfig::TYPE_BEFORE_OPTIMIZATION): void {
if ($pass instanceof AddDependencyCallsCompilerPass) {
@@ -46,6 +47,10 @@ public function testBuild(): void
return;
}
+ if ($pass instanceof AdminSearchCompilerPass) {
+ return;
+ }
+
if ($pass instanceof ExtensionCompilerPass) {
return;
}
@@ -70,6 +75,7 @@ public function testBuild(): void
'CompilerPass is not one of the expected types. Expects "%s", "%s", "%s", "%s", "%s", "%s" or "%s", but got "%s".',
AddDependencyCallsCompilerPass::class,
AddFilterTypeCompilerPass::class,
+ AdminSearchCompilerPass::class,
ExtensionCompilerPass::class,
GlobalVariablesCompilerPass::class,
ModelManagerCompilerPass::class,