diff --git a/composer.json b/composer.json index c0fc50d4b6..93b6ee985a 100644 --- a/composer.json +++ b/composer.json @@ -67,12 +67,14 @@ }, "conflict": { "doctrine/doctrine-bundle": ">=3", + "sensio/framework-extra-bundle": "<5.6", "sonata-project/core-bundle": "<3.20", "sonata-project/doctrine-extensions": "<1.8", "sonata-project/media-bundle": "<3.7", "sonata-project/user-bundle": "<3.3" }, "require-dev": { + "doctrine/annotations": "^1.7", "jms/translation-bundle": "^1.4", "matthiasnoback/symfony-config-test": "^4.2", "matthiasnoback/symfony-dependency-injection-test": "^4.2", @@ -81,6 +83,7 @@ "phpstan/phpstan-symfony": "^0.12.10", "psalm/plugin-symfony": "^2.0", "psr/event-dispatcher": "^1.0", + "sensio/framework-extra-bundle": "^5.6 || ^6.1", "sonata-project/intl-bundle": "^2.4", "symfony/browser-kit": "^4.4 || ^5.1", "symfony/css-selector": "^4.4 || ^5.1", diff --git a/docs/cookbook/recipe_decouple_crud_controller.rst b/docs/cookbook/recipe_decouple_crud_controller.rst new file mode 100644 index 0000000000..51bcd9ba98 --- /dev/null +++ b/docs/cookbook/recipe_decouple_crud_controller.rst @@ -0,0 +1,85 @@ +Decouple from CRUDController +============================ + +.. versionadded:: 3.x + + The ability to inject an Admin to an action and ``AdminFetcherInterface`` service were introduced in 3.x. + +When creating custom actions, we can create our controllers without extending ``CRUDController``. What we usually need +is to access the ``admin`` instance associated to the action, to do so we can use a param converter or +the ``AdminFetcherInterface`` service. + +If you are using ``SensioFrameworkExtraBundle``, then you can add your Admin as parameter of the action:: + + // src/Controller/CarAdminController.php + + namespace App\Controller; + + use Symfony\Component\HttpFoundation\RedirectResponse; + + final class CarAdminController + { + public function clone(CarAdmin $admin, Request $request) + { + $object = $admin->getSubject(); + + // ... + + $request->getSession()->getFlashBag()->add('sonata_flash_success', 'Cloned successfully'); + + return new RedirectResponse($admin->generateUrl('list')); + } + } + +Or you can use ``AdminFetcherInterface`` service to fetch the admin from the request, in this example we transformed +the controller to make it Invokable:: + + // src/Controller/CarAdminController.php + + namespace App\Controller; + + use Symfony\Component\HttpFoundation\RedirectResponse; + + final class CarAdminSoldAction + { + /** + * @var AdminFetcherInterface + */ + private $adminFetcher; + + public function __construct(AdminFetcherInterface $adminFetcher) + { + $this->adminFetcher = $adminFetcher; + } + + public function __invoke(Request $request) + { + $admin = $this->adminFetcher->get($request); + + $object = $admin->getSubject(); + + // ... + + $request->getSession()->getFlashBag()->add('sonata_flash_success', 'Sold successfully'); + + return new RedirectResponse($admin->generateUrl('list')); + } + } + +Now we only need to add the new route in ``configureRoutes``:: + + use App\Controller\CarAdminCloneAction; + use Sonata\AdminBundle\Route\RouteCollection; + + protected function configureRoutes(RouteCollection $collection) + { + $collection + ->add('clone', $this->getRouterIdParameter().'/clone', [ + '_controller' => 'App\Controller\CarAdminController::clone', + ]) + + // Using invokable controller: + ->add('sold', $this->getRouterIdParameter().'/sold', [ + '_controller' => CarAdminSoldAction::class, + ]); + } diff --git a/docs/index.rst b/docs/index.rst index 28c4f5257a..fa276f7cfa 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -83,6 +83,7 @@ The demo website can be found at https://demo.sonata-project.org. cookbook/recipe_sortable_listing cookbook/recipe_dynamic_form_modification cookbook/recipe_custom_action + cookbook/recipe_decouple_crud_controller cookbook/recipe_customizing_a_mosaic_list cookbook/recipe_overwrite_admin_configuration cookbook/recipe_improve_performance_large_datasets diff --git a/src/DependencyInjection/SonataAdminExtension.php b/src/DependencyInjection/SonataAdminExtension.php index 600c87beeb..9abbe445dd 100644 --- a/src/DependencyInjection/SonataAdminExtension.php +++ b/src/DependencyInjection/SonataAdminExtension.php @@ -84,6 +84,10 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('exporter.php'); } + if (isset($bundles['SensioFrameworkExtraBundle'])) { + $loader->load('param_converter.php'); + } + $configuration = $this->getConfiguration($configs, $container); $config = $this->processConfiguration($configuration, $configs); diff --git a/src/Request/AdminFetcher.php b/src/Request/AdminFetcher.php new file mode 100644 index 0000000000..007bc3d436 --- /dev/null +++ b/src/Request/AdminFetcher.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\AdminBundle\Request; + +use Sonata\AdminBundle\Admin\AdminInterface; +use Sonata\AdminBundle\Admin\Pool; +use Sonata\AdminBundle\Exception\AdminCodeNotFoundException; +use Symfony\Component\HttpFoundation\Request; + +final class AdminFetcher implements AdminFetcherInterface +{ + /** + * @var Pool + */ + private $pool; + + public function __construct(Pool $pool) + { + $this->pool = $pool; + } + + public function get(Request $request): AdminInterface + { + $adminCode = (string) $request->get('_sonata_admin'); + + if ('' === $adminCode) { + throw new \InvalidArgumentException(sprintf( + 'There is no `_sonata_admin` defined for the current route `%s`.', + (string) $request->get('_route') + )); + } + + $admin = $this->pool->getAdminByAdminCode($adminCode); + + // NEXT_MAJOR: Remove this block. + if (false === $admin) { + throw new AdminCodeNotFoundException(sprintf( + 'Unable to find the admin class related to the admin code: "%s".', + $adminCode + )); + } + + $rootAdmin = $admin; + + while ($rootAdmin->isChild()) { + $rootAdmin->setCurrentChild(true); + $rootAdmin = $rootAdmin->getParent(); + } + + $rootAdmin->setRequest($request); + + if ($request->get('uniqid')) { + $admin->setUniqid($request->get('uniqid')); + } + + return $admin; + } +} diff --git a/src/Request/AdminFetcherInterface.php b/src/Request/AdminFetcherInterface.php new file mode 100644 index 0000000000..ec0875fc52 --- /dev/null +++ b/src/Request/AdminFetcherInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\AdminBundle\Request; + +use Sonata\AdminBundle\Admin\AdminInterface; +use Symfony\Component\HttpFoundation\Request; + +interface AdminFetcherInterface +{ + public function get(Request $request): AdminInterface; +} diff --git a/src/Request/ParamConverter/AdminParamConverter.php b/src/Request/ParamConverter/AdminParamConverter.php new file mode 100644 index 0000000000..81400fec91 --- /dev/null +++ b/src/Request/ParamConverter/AdminParamConverter.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\AdminBundle\Request\ParamConverter; + +use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; +use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface; +use Sonata\AdminBundle\Admin\AdminInterface; +use Sonata\AdminBundle\Exception\AdminCodeNotFoundException; +use Sonata\AdminBundle\Request\AdminFetcherInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +final class AdminParamConverter implements ParamConverterInterface +{ + /** + * @var AdminFetcherInterface + */ + private $adminFetcher; + + public function __construct(AdminFetcherInterface $adminFetcher) + { + $this->adminFetcher = $adminFetcher; + } + + public function apply(Request $request, ParamConverter $configuration): bool + { + try { + $admin = $this->adminFetcher->get($request); + } catch (AdminCodeNotFoundException $exception) { + throw new NotFoundHttpException($exception->getMessage()); + } + + if (!is_a($admin, $configuration->getClass())) { + throw new \LogicException(sprintf( + '"%s" MUST be an instance of "%s", "%s" given.', + $configuration->getName(), + $configuration->getClass(), + \get_class($admin) + )); + } + + $request->attributes->set($configuration->getName(), $admin); + + return true; + } + + public function supports(ParamConverter $configuration): bool + { + return is_subclass_of($configuration->getClass(), AdminInterface::class); + } +} diff --git a/src/Resources/config/core.php b/src/Resources/config/core.php index 022d019598..3df590c5c4 100644 --- a/src/Resources/config/core.php +++ b/src/Resources/config/core.php @@ -26,6 +26,8 @@ use Sonata\AdminBundle\Filter\Persister\SessionFilterPersister; use Sonata\AdminBundle\Model\AuditManager; use Sonata\AdminBundle\Model\AuditManagerInterface; +use Sonata\AdminBundle\Request\AdminFetcher; +use Sonata\AdminBundle\Request\AdminFetcherInterface; use Sonata\AdminBundle\Route\AdminPoolLoader; use Sonata\AdminBundle\Search\SearchHandler; use Sonata\AdminBundle\SonataConfiguration; @@ -384,5 +386,12 @@ )) // NEXT_MAJOR: remove this alias, global template registry SHOULD NOT be mutable - ->alias(MutableTemplateRegistryInterface::class, 'sonata.admin.global_template_registry'); + ->alias(MutableTemplateRegistryInterface::class, 'sonata.admin.global_template_registry') + + ->set('sonata.admin.request.fetcher', AdminFetcher::class) + ->args([ + new ReferenceConfigurator('sonata.admin.pool'), + ]) + + ->alias(AdminFetcherInterface::class, 'sonata.admin.request.fetcher'); }; diff --git a/src/Resources/config/param_converter.php b/src/Resources/config/param_converter.php new file mode 100644 index 0000000000..43b8e7955a --- /dev/null +++ b/src/Resources/config/param_converter.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Sonata\AdminBundle\Request\ParamConverter\AdminParamConverter; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\DependencyInjection\Loader\Configurator\ReferenceConfigurator; + +return static function (ContainerConfigurator $containerConfigurator): void { + // Use "service" function for creating references to services when dropping support for Symfony 4.4 + $containerConfigurator->services() + + ->set('sonata.admin.param_converter', AdminParamConverter::class) + ->tag('request.param_converter', ['converter' => 'sonata_admin']) + ->args([ + new ReferenceConfigurator('sonata.admin.request.fetcher'), + ]); +}; diff --git a/tests/App/Admin/TestingParamConverterAdmin.php b/tests/App/Admin/TestingParamConverterAdmin.php new file mode 100644 index 0000000000..a6ed8f5cae --- /dev/null +++ b/tests/App/Admin/TestingParamConverterAdmin.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\AdminBundle\Tests\App\Admin; + +use Sonata\AdminBundle\Admin\AbstractAdmin; +use Sonata\AdminBundle\Route\RouteCollection; +use Sonata\AdminBundle\Tests\App\Controller\InvokableController; + +final class TestingParamConverterAdmin extends AbstractAdmin +{ + protected $baseRoutePattern = 'tests/app/testing-param-converter'; + protected $baseRouteName = 'admin_testing_param_converter'; + + protected function configureRoutes(RouteCollection $collection): void + { + $collection->add('withAnnotation', null, [ + '_controller' => 'Sonata\AdminBundle\Tests\App\Controller\ParamConverterController::withAnnotation', + ]); + + $collection->add('withoutAnnotation', null, [ + '_controller' => 'Sonata\AdminBundle\Tests\App\Controller\ParamConverterController::withoutAnnotation', + ]); + + $collection->add('invokable', null, [ + '_controller' => InvokableController::class, + ]); + } +} diff --git a/tests/App/AppKernel.php b/tests/App/AppKernel.php index f69e3c8add..8b3516c687 100644 --- a/tests/App/AppKernel.php +++ b/tests/App/AppKernel.php @@ -14,6 +14,7 @@ namespace Sonata\AdminBundle\Tests\App; use Knp\Bundle\MenuBundle\KnpMenuBundle; +use Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle; use Sonata\AdminBundle\SonataAdminBundle; use Sonata\BlockBundle\SonataBlockBundle; use Sonata\CoreBundle\SonataCoreBundle; @@ -42,6 +43,7 @@ public function registerBundles() { $bundles = [ new FrameworkBundle(), + new SensioFrameworkExtraBundle(), new TwigBundle(), new SecurityBundle(), new KnpMenuBundle(), diff --git a/tests/App/Controller/InvokableController.php b/tests/App/Controller/InvokableController.php new file mode 100644 index 0000000000..c7d497cab3 --- /dev/null +++ b/tests/App/Controller/InvokableController.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\AdminBundle\Tests\App\Controller; + +use Sonata\AdminBundle\Tests\App\Admin\TestingParamConverterAdmin; +use Symfony\Component\HttpFoundation\Response; + +final class InvokableController +{ + public function __invoke(TestingParamConverterAdmin $admin): Response + { + return new Response(); + } +} diff --git a/tests/App/Controller/ParamConverterController.php b/tests/App/Controller/ParamConverterController.php new file mode 100644 index 0000000000..55ff940380 --- /dev/null +++ b/tests/App/Controller/ParamConverterController.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\AdminBundle\Tests\App\Controller; + +use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; +use Sonata\AdminBundle\Tests\App\Admin\TestingParamConverterAdmin; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +final class ParamConverterController +{ + /** + * @ParamConverter("admin", class="Sonata\AdminBundle\Tests\App\Admin\TestingParamConverterAdmin") + */ + public function withAnnotation($admin): Response + { + if (!$admin instanceof TestingParamConverterAdmin) { + throw new NotFoundHttpException(); + } + + return new Response(); + } + + public function withoutAnnotation(TestingParamConverterAdmin $admin): Response + { + return new Response(); + } +} diff --git a/tests/App/config/services.yml b/tests/App/config/services.yml index 09a4e179af..868a2c42f0 100644 --- a/tests/App/config/services.yml +++ b/tests/App/config/services.yml @@ -50,6 +50,11 @@ services: tags: - {name: sonata.admin, manager_type: test, label: Foo} + Sonata\AdminBundle\Tests\App\Admin\TestingParamConverterAdmin: + arguments: [~, Sonata\AdminBundle\Tests\App\Model\Foo, ~] + tags: + - {name: sonata.admin, manager_type: test} + Sonata\AdminBundle\Tests\App\Admin\EmptyAdmin: arguments: [~, Sonata\AdminBundle\Tests\App\Model\Foo, ~] tags: diff --git a/tests/DependencyInjection/SonataAdminExtensionTest.php b/tests/DependencyInjection/SonataAdminExtensionTest.php index c0ee2e07fa..8e97c5d6e6 100644 --- a/tests/DependencyInjection/SonataAdminExtensionTest.php +++ b/tests/DependencyInjection/SonataAdminExtensionTest.php @@ -32,6 +32,7 @@ use Sonata\AdminBundle\Model\AuditManagerInterface; use Sonata\AdminBundle\Model\AuditReaderInterface; use Sonata\AdminBundle\Model\ModelManagerInterface; +use Sonata\AdminBundle\Request\ParamConverter\AdminParamConverter; use Sonata\AdminBundle\Route\AdminPoolLoader; use Sonata\AdminBundle\Search\SearchHandler; use Sonata\AdminBundle\Templating\MutableTemplateRegistryInterface; @@ -132,6 +133,21 @@ public function testLoadsExporterServiceDefinitionWhenExporterBundleIsRegistered ); } + public function testLoadsParamConverterServiceDefinitionWhenSensioFrameworkExtraBundleIsRegistered(): void + { + $this->container->setParameter('kernel.bundles', ['SensioFrameworkExtraBundle' => 'whatever']); + $this->load(); + $this->assertContainerBuilderHasService( + 'sonata.admin.param_converter', + AdminParamConverter::class + ); + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'sonata.admin.param_converter', + 'request.param_converter', + ['converter' => 'sonata_admin'] + ); + } + public function testHasSecurityRoleParameters(): void { $this->container->setParameter('kernel.bundles', []); diff --git a/tests/Functional/Controller/ParamConverterControllerTest.php b/tests/Functional/Controller/ParamConverterControllerTest.php new file mode 100644 index 0000000000..91e223eb8e --- /dev/null +++ b/tests/Functional/Controller/ParamConverterControllerTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\AdminBundle\Tests\Functional\Controller; + +use Sonata\AdminBundle\Tests\App\AppKernel; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +final class ParamConverterControllerTest extends WebTestCase +{ + /** + * @dataProvider urlIsSuccessfulDataProvider + */ + public function testUrlIsSuccessful(string $url): void + { + $client = static::createClient(); + $client->request(Request::METHOD_GET, $url); + + $this->assertSame(Response::HTTP_OK, $client->getResponse()->getStatusCode()); + } + + public function urlIsSuccessfulDataProvider(): iterable + { + return [ + ['/admin/tests/app/testing-param-converter/withAnnotation'], + ['/admin/tests/app/testing-param-converter/withoutAnnotation'], + ['/admin/tests/app/testing-param-converter/invokable'], + ]; + } + + protected static function getKernelClass() + { + return AppKernel::class; + } +} diff --git a/tests/Request/AdminFetcherTest.php b/tests/Request/AdminFetcherTest.php new file mode 100644 index 0000000000..fd7392e5b4 --- /dev/null +++ b/tests/Request/AdminFetcherTest.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\AdminBundle\Tests\Request; + +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Sonata\AdminBundle\Admin\AdminInterface; +use Sonata\AdminBundle\Admin\Pool; +use Sonata\AdminBundle\Exception\AdminCodeNotFoundException; +use Sonata\AdminBundle\Request\AdminFetcher; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\HttpFoundation\Request; + +final class AdminFetcherTest extends TestCase +{ + /** + * @var AdminFetcher + */ + private $adminFetcher; + + /** + * @var MockObject&AdminInterface + */ + private $admin; + + protected function setUp(): void + { + $this->admin = $this->createMock(AdminInterface::class); + + $container = new Container(); + $container->set('sonata.admin.post', $this->admin); + + $this->adminFetcher = new AdminFetcher(new Pool($container, ['sonata.admin.post'])); + } + + public function testGetItThrowsAnExceptionWithoutAdminCode(): void + { + $request = new Request(); + + $this->expectException(\InvalidArgumentException::class); + + $this->adminFetcher->get($request); + } + + public function testGetItThrowsAnExceptionIfThereIsNoAdminWithAdminCodeGiven(): void + { + $request = new Request(); + $request->attributes->set('_sonata_admin', 'non_existing_admin_code'); + + $this->expectException(AdminCodeNotFoundException::class); + + $this->adminFetcher->get($request); + } + + public function testSetsUniqidToAdmin(): void + { + $request = new Request(); + $request->attributes->set('_sonata_admin', 'sonata.admin.post'); + $uniqueId = 'uniqid_post_id'; + $request->query->set('uniqid', $uniqueId); + + $this->admin + ->expects($this->once()) + ->method('setUniqid') + ->with($uniqueId); + + $this->adminFetcher->get($request); + } + + public function testSetsRequestToRootAdmin(): void + { + $request = new Request(); + $request->attributes->set('_sonata_admin', 'sonata.admin.post'); + + $this->admin + ->expects($this->once()) + ->method('isChild') + ->willReturn(true); + + $adminParent = $this->createMock(AdminInterface::class); + + $this->admin + ->expects($this->once()) + ->method('getParent') + ->willReturn($adminParent); + + $this->admin + ->expects($this->once()) + ->method('setCurrentChild') + ->with(true); + + $adminParent + ->expects($this->once()) + ->method('setRequest') + ->with($request); + + $this->adminFetcher->get($request); + } +} diff --git a/tests/Request/ParamConverter/AdminParamConverterTest.php b/tests/Request/ParamConverter/AdminParamConverterTest.php new file mode 100644 index 0000000000..e49ed6932e --- /dev/null +++ b/tests/Request/ParamConverter/AdminParamConverterTest.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\AdminBundle\Tests\Request\ParamConverter; + +use PHPUnit\Framework\MockObject\Stub; +use PHPUnit\Framework\TestCase; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; +use Sonata\AdminBundle\Admin\AbstractAdmin; +use Sonata\AdminBundle\Admin\AdminInterface; +use Sonata\AdminBundle\Admin\Pool; +use Sonata\AdminBundle\Request\AdminFetcher; +use Sonata\AdminBundle\Request\ParamConverter\AdminParamConverter; +use Sonata\AdminBundle\Tests\Fixtures\Admin\PostAdmin; +use Sonata\AdminBundle\Tests\Fixtures\Admin\TagAdmin; +use Sonata\AdminBundle\Tests\Fixtures\Bundle\Entity\Post; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +final class AdminParamConverterTest extends TestCase +{ + /** + * @var Stub|AdminInterface + */ + private $admin; + + /** + * @var AdminParamConverter + */ + private $converter; + + protected function setUp(): void + { + $this->admin = new PostAdmin('sonata.admin.post', Post::class, ''); + + $container = new Container(); + $container->set('sonata.admin.post', $this->admin); + + $adminFetcher = new AdminFetcher(new Pool($container, ['sonata.admin.post'])); + + $this->converter = new AdminParamConverter($adminFetcher); + } + + public function testSupports(): void + { + $config = $this->createConfiguration(AbstractAdmin::class); + $this->assertTrue($this->converter->supports($config)); + + $config = $this->createConfiguration(__CLASS__); + $this->assertFalse($this->converter->supports($config)); + } + + public function testThrows404WhenAdminCodeNotFound(): void + { + $request = new Request(); + $request->attributes->set('_sonata_admin', 'not_existing_admin_code'); + + $this->expectException(NotFoundHttpException::class); + $this->converter->apply($request, $this->createConfiguration(PostAdmin::class)); + } + + public function testThrowsLogicExceptionWhenAdminClassDoesNotMatchTheConfiguredOne(): void + { + $request = new Request(); + $request->attributes->set('_sonata_admin', 'sonata.admin.post'); + + $this->expectException(\LogicException::class); + $this->converter->apply($request, $this->createConfiguration(TagAdmin::class)); + } + + public function testItSetsTheAdminInTheRequest(): void + { + $request = new Request(); + $request->attributes->set('_sonata_admin', 'sonata.admin.post'); + + $variableName = 'myAdmin'; + $this->assertTrue($this->converter->apply($request, $this->createConfiguration(PostAdmin::class, $variableName))); + $this->assertSame($this->admin, $request->attributes->get($variableName)); + } + + /** + * @return Stub&ParamConverter + * @phpstan-param class-string $class + */ + private function createConfiguration(string $class, string $name = 'admin'): Stub + { + $config = $this->createStub(ParamConverter::class); + + $config + ->method('getName') + ->willReturn($name); + + $config + ->method('getClass') + ->willReturn($class); + + return $config; + } +}