Skip to content

Commit

Permalink
Allow to disable the global search by admin
Browse files Browse the repository at this point in the history
  • Loading branch information
phansys committed Nov 21, 2020
1 parent b1ba37f commit 1670087
Show file tree
Hide file tree
Showing 7 changed files with 289 additions and 10 deletions.
17 changes: 17 additions & 0 deletions docs/reference/search.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
<service id="app.admin.post" class="App\Admin\PostAdmin">
<tag name="sonata.admin" global_search="false" manager_type="orm" group="Content" label="Post"/>
<argument/>
<argument>App\Entity\Post</argument>
<argument/>
</service>
Customization
-------------

Expand Down
77 changes: 77 additions & 0 deletions src/DependencyInjection/Compiler/AdminSearchCompilerPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Sonata Project package.
*
* (c) Thomas Rabaix <[email protected]>
*
* 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 <[email protected]>
*/
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]);
}
}
34 changes: 34 additions & 0 deletions src/Search/SearchHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ class SearchHandler
*/
private $caseSensitive;

/**
* @var array<string, bool>
*/
private $adminsSearchConfig = [];

/**
* NEXT_MAJOR: Change signature to __construct(bool $caseSensitive) and remove pool property.
*
Expand Down Expand Up @@ -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;
Expand All @@ -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<string, bool> $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;
}
}
}
2 changes: 2 additions & 0 deletions src/SonataAdminBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down
76 changes: 76 additions & 0 deletions tests/DependencyInjection/Compiler/AdminSearchCompilerPassTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Sonata Project package.
*
* (c) Thomas Rabaix <[email protected]>
*
* 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 <[email protected]>
*/
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);
}
}
85 changes: 76 additions & 9 deletions tests/Search/SearchHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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()];
}
}
Loading

0 comments on commit 1670087

Please sign in to comment.