Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate not registering filters #6976

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions UPGRADE-3.x.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ UPGRADE 3.x
UPGRADE FROM 3.xx to 3.xx
=========================

### `Sonata\AdminBundle\Filter\FilterFactory`

Deprecated passing a value which is not registered as a service as argument 2 for `create()` method.

### Deprecated MenuBuilderInterface

Deprecated `AbstractAdmin::getSideMenu()`
Expand Down
34 changes: 34 additions & 0 deletions src/DependencyInjection/Compiler/AddFilterTypeCompilerPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@

namespace Sonata\AdminBundle\DependencyInjection\Compiler;

use Sonata\AdminBundle\Filter\FilterInterface;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This blank line should be removed

Copy link
Member Author

@franmomu franmomu Mar 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is php-cs-fixer who adds that line.

// NEXT_MAJOR: Uncomment next line.
// use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

/**
* @final since sonata-project/admin-bundle 3.52
*
Expand All @@ -25,6 +29,10 @@ class AddFilterTypeCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->has('sonata.admin.builder.filter.factory')) {
return;
}

$definition = $container->getDefinition('sonata.admin.builder.filter.factory');
$types = [];

Expand All @@ -34,6 +42,32 @@ public function process(ContainerBuilder $container)
$serviceDefinition->setShared(false);
$serviceDefinition->setPublic(true); // Temporary fix until we can support service locators

$class = $serviceDefinition->getClass();
$reflectionClass = $container->getReflectionClass($class);

if (null === $reflectionClass) {
// NEXT_MAJOR: Remove "trigger_error" and uncomment the exception below.
@trigger_error(sprintf(
'Not declaring a filter with an existing class name is deprecated since'
.' sonata-project/admin-bundle 3.x and will not work in 4.0.'
.' You MUST register a service with an existing class name for service "%s".',
$id,
), \E_USER_DEPRECATED);

//throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
}

if (null !== $reflectionClass && !$reflectionClass->isSubclassOf(FilterInterface::class)) {
// NEXT_MAJOR: Remove "trigger_error" and uncomment the exception below.
@trigger_error(sprintf(
'Registering service "%s" without implementing interface "%s" is deprecated since'
.' sonata-project/admin-bundle 3.x and will be mandatory in 4.0.',
$id,
FilterInterface::class,
), \E_USER_DEPRECATED);
// throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, FilterInterface::class));
}

$types[$serviceDefinition->getClass()] = $id;

// NEXT_MAJOR: Remove this loop, only FQCN will be supported
Expand Down
9 changes: 8 additions & 1 deletion src/Filter/FilterFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,19 @@ public function create($name, $type, array $options = [])
if ($filter && !class_exists($type)) {
@trigger_error(sprintf(
'Referencing a filter by name (%s) is deprecated since version 3.57 and will be removed in 4.0.'
.' Use the fully-qualified type class name instead (%s)',
.' Use the fully-qualified type class name instead (%s).',
$type,
\get_class($filter)
), \E_USER_DEPRECATED);
}
} elseif (class_exists($type)) {
// NEXT_MAJOR: Remove this "elseif" block
@trigger_error(sprintf(
'Not declaring a filter as service is deprecated since sonata-project/admin-bundle 3.x'
.' and will not work in 4.0.'
.' You MUST register a service with class name (%s) instead.',
$type,
), \E_USER_DEPRECATED);
$filter = new $type();
} else {
throw new \RuntimeException(sprintf('No attached service to type named `%s`', $type));
Expand Down
147 changes: 88 additions & 59 deletions tests/DependencyInjection/Compiler/AddFilterTypeCompilerPassTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,83 +13,112 @@

namespace Sonata\AdminBundle\Tests\DependencyInjection\Compiler;

use PHPUnit\Framework\TestCase;
use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractCompilerPassTestCase;
use Sonata\AdminBundle\DependencyInjection\Compiler\AddFilterTypeCompilerPass;
use Sonata\AdminBundle\Filter\FilterFactoryInterface;
use Sonata\AdminBundle\Tests\Fixtures\Filter\BarFilter;
use Sonata\AdminBundle\Tests\Fixtures\Filter\FooFilter;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

class AddFilterTypeCompilerPassTest extends TestCase
{
private $filterFactory;

private $fooFilter;

private $barFilter;
// NEXT_MAJOR: Uncomment next line.
//use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

private $bazFilter;
final class AddFilterTypeCompilerPassTest extends AbstractCompilerPassTestCase
{
use ExpectDeprecationTrait;

protected function setUp(): void
{
$this->filterFactory = $this->createMock(Definition::class);
$this->fooFilter = $this->createMock(Definition::class);
$this->barFilter = $this->createMock(Definition::class);
$this->bazFilter = $this->createMock(Definition::class);
parent::setUp();

$filterFactoryDefinition = new Definition(FilterFactoryInterface::class, [
null,
[],
]);

$this->container
->setDefinition('sonata.admin.builder.filter.factory', $filterFactoryDefinition);
}

public function testProcess(): void
{
$containerBuilderMock = $this->createMock(ContainerBuilder::class);

$containerBuilderMock
->method('getDefinition')
->with($this->anything())
->willReturnMap([
['sonata.admin.builder.filter.factory', $this->filterFactory],
['acme.demo.foo_filter', $this->fooFilter],
['acme.demo.bar_filter', $this->barFilter],
['acme.demo.baz_filter', $this->bazFilter],
$fooFilter = new Definition(FooFilter::class);
$fooFilter
->addTag('sonata.admin.filter.type', [
'alias' => 'foo_filter_alias',
]);

$containerBuilderMock->expects($this->once())
->method('findTaggedServiceIds')
->with($this->equalTo('sonata.admin.filter.type'))
->willReturn([
'acme.demo.foo_filter' => [
'tag1' => [
'alias' => 'foo_filter_alias',
],
],
'acme.demo.bar_filter' => [
'tag1' => [
'alias' => 'bar_filter_alias',
],
],
'acme.demo.baz_filter' => [
'tag1' => [
],
],
]);
$this->container
->setDefinition('acme.demo.foo_filter', $fooFilter);

$this->fooFilter->method('getClass')
->willReturn('Acme\Filter\FooFilter');
$barFilter = new Definition(BarFilter::class);
$barFilter
->addTag('sonata.admin.filter.type');

$this->barFilter->method('getClass')
->willReturn('Acme\Filter\BarFilter');
$this->container
->setDefinition('acme.demo.bar_filter', $barFilter);

$this->bazFilter->method('getClass')
->willReturn('Acme\Filter\BazFilter');
$this->compile();

$this->filterFactory->expects($this->once())
->method('replaceArgument')
->with(1, $this->equalTo([
$this->assertContainerBuilderHasServiceDefinitionWithArgument(
'sonata.admin.builder.filter.factory',
1,
[
'foo_filter_alias' => 'acme.demo.foo_filter',
'Acme\Filter\FooFilter' => 'acme.demo.foo_filter',
'bar_filter_alias' => 'acme.demo.bar_filter',
'Acme\Filter\BarFilter' => 'acme.demo.bar_filter',
'Acme\Filter\BazFilter' => 'acme.demo.baz_filter',
]));

$extensionsPass = new AddFilterTypeCompilerPass();
$extensionsPass->process($containerBuilderMock);
FooFilter::class => 'acme.demo.foo_filter',
BarFilter::class => 'acme.demo.bar_filter',
]
);
}

/**
* NEXT_MAJOR: Remove legacy group.
*
* @group legacy
*/
public function testServicesMustHaveAClassName(): void
{
$filter = new Definition('not_existing_class');
$filter
->addTag('sonata.admin.filter.type');

$this->container
->setDefinition('acme.demo.foo_filter', $filter);

// NEXT_MAJOR: Remove deprecation and uncomment exception.
$this->expectDeprecation('Not declaring a filter with an existing class name is deprecated since sonata-project/admin-bundle 3.x and will not work in 4.0. You MUST register a service with an existing class name for service "acme.demo.foo_filter".');
// $this->expectException(InvalidArgumentException::class);
// $this->expectExceptionMessage('Class "not_existing_class" used for service "acme.demo.foo_filter" cannot be found.');

$this->compile();
}

/**
* NEXT_MAJOR: Remove legacy group.
*
* @group legacy
*/
public function testServicesMustImplementFilterInterface(): void
{
$filter = new Definition(\stdClass::class);
$filter
->addTag('sonata.admin.filter.type');

$this->container
->setDefinition('acme.demo.foo_filter', $filter);

// NEXT_MAJOR: Remove deprecation and uncomment exception.
$this->expectDeprecation('Registering service "acme.demo.foo_filter" without implementing interface "Sonata\AdminBundle\Filter\FilterInterface" is deprecated since sonata-project/admin-bundle 3.x and will be mandatory in 4.0.');
// $this->expectException(InvalidArgumentException::class);
// $this->expectExceptionMessage('Service "acme.demo.foo_filter" MUST implement interface "Sonata\AdminBundle\Filter\FilterInterface".');

$this->compile();
}

protected function registerCompilerPass(ContainerBuilder $container): void
{
$container->addCompilerPass(new AddFilterTypeCompilerPass());
}
}
31 changes: 27 additions & 4 deletions tests/Filter/FilterFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,20 @@
use Sonata\AdminBundle\Filter\FilterFactory;
use Sonata\AdminBundle\Filter\FilterInterface;
use Sonata\AdminBundle\Form\Type\Filter\DefaultType;
use Sonata\AdminBundle\Tests\Fixtures\Filter\FooFilter;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\DependencyInjection\Container;

class FilterFactoryTest extends TestCase
{
use ExpectDeprecationTrait;

public function testEmptyType(): void
{
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('The type must be defined');

$filter = new FilterFactory(new Container(), []);
$filter = new FilterFactory(new Container());
$filter->create('test', null);
}

Expand All @@ -35,7 +39,7 @@ public function testUnknownType(): void
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('No attached service to type named `mytype`');

$filter = new FilterFactory(new Container(), []);
$filter = new FilterFactory(new Container());
$filter->create('test', 'mytype');
}

Expand All @@ -44,13 +48,18 @@ public function testUnknownClassType(): void
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('No attached service to type named `Sonata\AdminBundle\Form\Type\Filter\FooType`');

$filter = new FilterFactory(new Container(), []);
$filter = new FilterFactory(new Container());
$filter->create('test', 'Sonata\AdminBundle\Form\Type\Filter\FooType');
}

public function testClassType(): void
{
$filter = new FilterFactory(new Container(), []);
$container = new Container();
$container
->set(DefaultType::class, new DefaultType());
$filter = new FilterFactory($container, [
DefaultType::class => DefaultType::class,
]);

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage(
Expand All @@ -60,6 +69,20 @@ public function testClassType(): void
$filter->create('test', DefaultType::class);
}

/**
* NEXT_MAJOR: Remove this test.
*
* @group legacy
*/
public function testItTriggersDeprecationWithoutTheService(): void
{
$filter = new FilterFactory(new Container());

$this->expectDeprecation('Not declaring a filter as service is deprecated since sonata-project/admin-bundle 3.x and will not work in 4.0. You MUST register a service with class name (Sonata\AdminBundle\Tests\Fixtures\Filter\FooFilter) instead.');

$filter->create('test', FooFilter::class);
}

/**
* NEXT_MAJOR: Remove this test.
*
Expand Down
45 changes: 45 additions & 0 deletions tests/Fixtures/Filter/BarFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?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\Fixtures\Filter;

use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
use Sonata\AdminBundle\Filter\Filter;
use Sonata\AdminBundle\Form\Type\Filter\DefaultType;

final class BarFilter extends Filter
{
/**
* NEXT_MAJOR: Remove this method.
*/
public function filter(ProxyQueryInterface $query, $alias, $field, $value): void
{
}

// NEXT_MAJOR: Add type declarations
public function apply($query, $value): void
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Next_major, add typehint ?

{
}

public function getDefaultOptions(): array
{
return ['bar' => 'bar'];
}

public function getRenderSettings(): array
{
return [DefaultType::class, [
'label' => 'label',
]];
}
}