Skip to content

Commit

Permalink
Add ParamConverter for Admins
Browse files Browse the repository at this point in the history
  • Loading branch information
franmomu authored and VincentLanglet committed May 2, 2021
1 parent e72c53c commit 105dad3
Show file tree
Hide file tree
Showing 18 changed files with 676 additions and 1 deletion.
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
85 changes: 85 additions & 0 deletions docs/cookbook/recipe_decouple_crud_controller.rst
Original file line number Diff line number Diff line change
@@ -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,
]);
}
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/DependencyInjection/SonataAdminExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
69 changes: 69 additions & 0 deletions src/Request/AdminFetcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Sonata Project package.
*
* (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
*
* 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;
}
}
22 changes: 22 additions & 0 deletions src/Request/AdminFetcherInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Sonata Project package.
*
* (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
*
* 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;
}
62 changes: 62 additions & 0 deletions src/Request/ParamConverter/AdminParamConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Sonata Project package.
*
* (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
*
* 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);
}
}
11 changes: 10 additions & 1 deletion src/Resources/config/core.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
};
27 changes: 27 additions & 0 deletions src/Resources/config/param_converter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Sonata Project package.
*
* (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
*
* 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'),
]);
};
39 changes: 39 additions & 0 deletions tests/App/Admin/TestingParamConverterAdmin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Sonata Project package.
*
* (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
*
* 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,
]);
}
}
2 changes: 2 additions & 0 deletions tests/App/AppKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -42,6 +43,7 @@ public function registerBundles()
{
$bundles = [
new FrameworkBundle(),
new SensioFrameworkExtraBundle(),
new TwigBundle(),
new SecurityBundle(),
new KnpMenuBundle(),
Expand Down
Loading

0 comments on commit 105dad3

Please sign in to comment.