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 authored and VincentLanglet committed Nov 25, 2020
1 parent fd2cd04 commit 78b1db8
Show file tree
Hide file tree
Showing 7 changed files with 261 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
106 changes: 106 additions & 0 deletions src/DependencyInjection/Compiler/AdminSearchCompilerPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?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
{
if (!$container->hasDefinition('sonata.admin.search.handler')) {
return;
}

$adminSearch = [];

foreach ($container->findTaggedServiceIds(self::TAG_ADMIN) as $id => $tags) {
$this->validateAdminClass($container, $id);

foreach ($tags as $attributes) {
$globalSearch = $this->getGlobalSearchValue($attributes, $id);

if (null === $globalSearch) {
continue;
}

$adminSearch[$id] = $globalSearch;
}
}

$searchHandlerDefinition = $container->getDefinition('sonata.admin.search.handler');
$searchHandlerDefinition->addMethodCall('configureAdminSearch', [$adminSearch]);
}

/**
* @throws LogicException if the class in the given service definition is not
* a subclass of `AdminInterface`
*/
private function validateAdminClass(ContainerBuilder $container, string $id): void
{
$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
));
}
}

/**
* @param array<string, mixed> $attributes
*
* @throws LogicException if the attribute value is not of type boolean
*/
private function getGlobalSearchValue(array $attributes, string $id): ?bool
{
$globalSearch = $attributes[self::TAG_ATTRIBUTE_TOGGLE_SEARCH] ?? null;

if (null === $globalSearch) {
return null;
}

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)
));
}

return $globalSearch;
}
}
21 changes: 21 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,15 @@ 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 = $adminsSearchConfig;
}
}
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
57 changes: 57 additions & 0 deletions tests/DependencyInjection/Compiler/AdminSearchCompilerPassTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?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 Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractCompilerPassTestCase;
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 AbstractCompilerPassTestCase
{
public function testProcess(): void
{
$adminFooDefinition = new Definition(PostAdmin::class);
$adminFooDefinition->addTag('sonata.admin', ['global_search' => true]);
$this->setDefinition('admin.foo', $adminFooDefinition);

$adminBarDefinition = new Definition(PostAdmin::class);
$adminBarDefinition->addTag('sonata.admin', ['global_search' => false]);
$this->setDefinition('admin.bar', $adminBarDefinition);

$adminBazDefinition = new Definition(PostAdmin::class);
$adminBazDefinition->addTag('sonata.admin', ['some_attribute' => 42]);
$this->setDefinition('admin.baz', $adminBazDefinition);

$searchHandlerDefinition = new Definition();
$this->setDefinition('sonata.admin.search.handler', $searchHandlerDefinition);

$this->compile();

$this->assertContainerBuilderHasServiceDefinitionWithMethodCall(
'sonata.admin.search.handler',
'configureAdminSearch',
[['admin.foo' => true, 'admin.bar' => false]]
);
}

protected function registerCompilerPass(ContainerBuilder $container): void
{
$container->addCompilerPass(new AdminSearchCompilerPass());
}
}
60 changes: 51 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,46 @@ 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'];
}
}
8 changes: 7 additions & 1 deletion tests/SonataAdminBundleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -46,6 +47,10 @@ public function testBuild(): void
return;
}

if ($pass instanceof AdminSearchCompilerPass) {
return;
}

if ($pass instanceof ExtensionCompilerPass) {
return;
}
Expand All @@ -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,
Expand Down

0 comments on commit 78b1db8

Please sign in to comment.