Skip to content

Commit cde8ccf

Browse files
Improve custom actions and custom routes in CRUD controllers (#4794)
2 parents 28653db + 460c729 commit cde8ccf

17 files changed

Lines changed: 224 additions & 46 deletions

config/services.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ services:
2020
$crudControllers: '%shopsys.admin.crud_controllers%'
2121
$crudControllerExtensions: '%shopsys.admin.crud_controllers_extensions%'
2222

23+
Shopsys\AdministrationBundle\Component\Crud\CrudRoleConstantProvider:
24+
arguments:
25+
$customRoleConstants: '%shopsys.admin.crud_role_constants%'
26+
2327
Shopsys\AdministrationBundle\Component\Menu\CrudMenuSubscriber:
2428
tags:
2529
- { name: kernel.event_subscriber }

src/Component/Action/AbstractAction.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ public function setIcon(string $icon): static
4545
/**
4646
* Set function that will determine if action should be displayed
4747
*
48-
* @param \Closure(): bool $function Function must return boolean value. If function returns false, action will not be displayed
48+
* @param \Closure(mixed): bool $function Function must return boolean value. If function returns false, action will not be displayed.
49+
* The closure receives the data the action is built with, depending on the action kind:
50+
* a top action (Action) gets the data of the current page — the entity on the edit page, null on the list, create, and detail pages;
51+
* a row action (RowAction) gets the data of the grid row it is rendered in
4952
*/
5053
public function displayIf(Closure $function): static
5154
{

src/Component/Config/CrudConfig.php

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,6 @@ final class CrudConfig
4141

4242
private ?string $routePrefix = null;
4343

44-
private ?string $customRoleConstant = null;
45-
4644
private ?string $customRoleSection = null;
4745

4846
private ?string $menuIcon = null;
@@ -63,8 +61,13 @@ final class CrudConfig
6361
ActionType::CREATE->value => null,
6462
];
6563

66-
public function __construct(private readonly string $entityName)
67-
{
64+
/**
65+
* @param string|null $customRoleConstant role declared by the ForRole attribute on the CRUD controller (or its extension), resolved at compile time
66+
*/
67+
public function __construct(
68+
private readonly string $entityName,
69+
private readonly ?string $customRoleConstant = null,
70+
) {
6871
$this->enabledActions = new ArrayCollection([
6972
ActionType::LIST,
7073
]);
@@ -210,19 +213,6 @@ public function setRoutePrefix(?string $routePrefix): self
210213
return $this;
211214
}
212215

213-
/**
214-
* Set custom role constant for the CRUD controller. This will be used for access control checks.
215-
* If not set, role constant will be generated from the controller name automatically.
216-
*
217-
* @return $this
218-
*/
219-
public function setCustomRoleConstant(?string $roleConstant): self
220-
{
221-
$this->customRoleConstant = $roleConstant;
222-
223-
return $this;
224-
}
225-
226216
/**
227217
* Set role section for role constant. If not set, role section will be got from menu section automatically.
228218
*

src/Component/Crud/CrudControllerRegistry.php

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
use Shopsys\AdministrationBundle\Component\Config\CrudConfig;
1010
use Shopsys\AdministrationBundle\Component\Config\CrudConfigData;
1111
use Shopsys\FrameworkBundle\Component\EntityExtension\EntityNameResolver;
12-
use SplPriorityQueue;
1312
use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
1413
use Symfony\Component\DependencyInjection\ServiceLocator;
1514
use Webmozart\Assert\Assert;
@@ -36,10 +35,11 @@ final class CrudControllerRegistry
3635

3736
/**
3837
* @param array<int, array{class: class-string<\Shopsys\AdministrationBundle\Controller\AbstractCrudController>, entityClass: string}> $crudControllers
39-
* @param array<int, array{extensionClass: class-string<\Shopsys\AdministrationBundle\Controller\AbstractCrudControllerExtension>, controllerClass: class-string<\Shopsys\AdministrationBundle\Controller\AbstractCrudController>, priority: int}> $crudControllerExtensions
38+
* @param array<int, array{extensionClass: class-string<\Shopsys\AdministrationBundle\Controller\AbstractCrudControllerExtension>, controllerClass: class-string<\Shopsys\AdministrationBundle\Controller\AbstractCrudController>, priority: int}> $crudControllerExtensions sorted by ascending priority
4039
*/
4140
public function __construct(
4241
private readonly EntityNameResolver $entityNameResolver,
42+
private readonly CrudRoleConstantProvider $crudRoleConstantProvider,
4343
#[TaggedLocator('shopsys.admin.crud_controllers')]
4444
private readonly ServiceLocator $controllers,
4545
#[TaggedLocator('shopsys.admin.crud_handler')]
@@ -113,7 +113,7 @@ private function buildConfig(string $controllerClass): CrudConfigData
113113
/** @var \Shopsys\AdministrationBundle\Controller\AbstractCrudController $crudController */
114114
$crudController = $this->controllers->get($controllerClass);
115115

116-
$config = new CrudConfig($meta['entityName']);
116+
$config = new CrudConfig($meta['entityName'], $this->crudRoleConstantProvider->findCustomRoleConstant($controllerClass));
117117
$crudController->configure($config);
118118

119119
foreach ($extensions as $extension) {
@@ -153,23 +153,9 @@ private function getResolvedExtensions(): array
153153
{
154154
if ($this->resolvedExtensions === null) {
155155
$this->resolvedExtensions = [];
156-
$queueByController = [];
157156

158157
foreach ($this->crudControllerExtensions as $extension) {
159-
if (isset($queueByController[$extension['controllerClass']]) === false) {
160-
$queueByController[$extension['controllerClass']] = new SplPriorityQueue();
161-
}
162-
163-
$queueByController[$extension['controllerClass']]->insert(
164-
$this->controllers->get($extension['extensionClass']),
165-
$extension['priority'],
166-
);
167-
}
168-
169-
foreach ($queueByController as $controller => $queue) {
170-
$queue->top();
171-
$queue->setExtractFlags(SplPriorityQueue::EXTR_DATA);
172-
$this->resolvedExtensions[$controller] = array_reverse(iterator_to_array($queue));
158+
$this->resolvedExtensions[$extension['controllerClass']][] = $this->controllers->get($extension['extensionClass']);
173159
}
174160
}
175161

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Shopsys\AdministrationBundle\Component\Crud;
6+
7+
/**
8+
* Reads the custom role constants of CRUD controllers resolved at compile time by ResolveCrudRoleConstantsCompilerPass
9+
*/
10+
final class CrudRoleConstantProvider
11+
{
12+
public const string CRUD_ROLE_CONSTANTS_PARAMETER = 'shopsys.admin.crud_role_constants';
13+
14+
/**
15+
* @param array<class-string<\Shopsys\AdministrationBundle\Controller\AbstractCrudController>, string|null> $customRoleConstants role declared by the ForRole attribute, or null for the generated role, indexed by controller class
16+
*/
17+
public function __construct(
18+
private readonly array $customRoleConstants = [],
19+
) {
20+
}
21+
22+
/**
23+
* Returns the role declared by the ForRole attribute on the CRUD controller or one of its extensions, or null when the controller uses its generated role
24+
*
25+
* @param class-string<\Shopsys\AdministrationBundle\Controller\AbstractCrudController> $controllerClass
26+
*/
27+
public function findCustomRoleConstant(string $controllerClass): ?string
28+
{
29+
return $this->customRoleConstants[$controllerClass] ?? null;
30+
}
31+
}

src/Component/Security/Attribute/AttributeProcessor.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
use InvalidArgumentException;
88
use ReflectionClass;
99
use ReflectionMethod;
10+
use Shopsys\AdministrationBundle\Component\Crud\CrudRoleConstantProvider;
11+
use Shopsys\AdministrationBundle\Component\Crud\Helper\CrudTransformationHelper;
1012
use Shopsys\AdministrationBundle\Component\Security\AccessControl\AccessControlRuleFactory;
13+
use Shopsys\AdministrationBundle\Controller\AbstractCrudController;
1114
use Shopsys\FrameworkBundle\Component\Security\Attribute\CanCreate;
1215
use Shopsys\FrameworkBundle\Component\Security\Attribute\CanDelete;
1316
use Shopsys\FrameworkBundle\Component\Security\Attribute\CanEdit;
@@ -27,6 +30,7 @@ final class AttributeProcessor
2730
{
2831
public function __construct(
2932
private readonly AccessControlRuleFactory $accessControlRuleFactory,
33+
private readonly CrudRoleConstantProvider $crudRoleConstantProvider,
3034
) {
3135
}
3236

@@ -75,8 +79,13 @@ public function processMethod(ReflectionClass $class, ReflectionMethod $method):
7579
$rules[] = $this->accessControlRuleFactory->create($roleWithPermission, $attribute->getMethods());
7680
}
7781

78-
// Get class-level role if ForRole attribute is present
79-
$classRole = $this->getClassAttribute($class, ForRole::class)?->role;
82+
// CRUD controllers use their role constant (generated, or set by ForRole — resolved at compile time including extension overrides) so custom routes are guarded by the same role as the built-in actions, other classes read the ForRole attribute directly
83+
$classRole = $class->isSubclassOf(AbstractCrudController::class)
84+
? CrudTransformationHelper::generateRoleConstant(
85+
$class->getShortName(),
86+
$this->crudRoleConstantProvider->findCustomRoleConstant($class->getName()),
87+
)
88+
: $this->getClassAttribute($class, ForRole::class)?->role;
8089

8190
// Process CRUD attributes
8291
$this->processPermissionAttributes($method, $rules, CanView::class, $classRole);

src/Controller/AbstractCrudController.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ public function editAction(Request $request, int $id): Response
298298
return $this->render($this->getEditTemplate(), [
299299
'title' => $config->getTitle(ActionType::EDIT, $recordName),
300300
'topActions' => $this->getConfiguredActions(ActionType::EDIT),
301+
'entity' => $entity,
301302
'form' => $form->createView(),
302303
...$this->getEditViewData($entity),
303304
]);

src/Controller/BlogArticleAuthorController.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Shopsys\FrameworkBundle\Component\Grid\Grid;
1616
use Shopsys\FrameworkBundle\Component\Grid\GridFactory;
1717
use Shopsys\FrameworkBundle\Component\Grid\QueryBuilderDataSourceFactory;
18+
use Shopsys\FrameworkBundle\Component\Security\Attribute\ForRole;
1819
use Shopsys\FrameworkBundle\Component\Security\Role\AdminRoleConstant;
1920
use Shopsys\FrameworkBundle\Form\Admin\Blog\BlogArticleAuthorFormType;
2021
use Shopsys\FrameworkBundle\Model\AdminNavigation\SideMenuBuilder;
@@ -23,6 +24,7 @@
2324
use Shopsys\FrameworkBundle\Model\Localization\Localization;
2425

2526
#[CrudController(BlogArticleAuthor::class)]
27+
#[ForRole(AdminRoleConstant::ROLE_BLOG_ARTICLE_AUTHOR)]
2628
class BlogArticleAuthorController extends AbstractCrudController
2729
{
2830
protected const int ARTICLES_GRID_DEFAULT_LIMIT = 10;
@@ -40,8 +42,7 @@ public function configure(CrudConfig $config): void
4042
{
4143
$config
4244
->registerHandler(BlogArticleAuthorCrudHandler::class)
43-
->setMenuSection(SideMenuBuilder::SECTION_BLOG)
44-
->setCustomRoleConstant(AdminRoleConstant::ROLE_BLOG_ARTICLE_AUTHOR);
45+
->setMenuSection(SideMenuBuilder::SECTION_BLOG);
4546
}
4647

4748
#[Override]

src/Controller/TransportGroupController.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010
use Shopsys\AdministrationBundle\Component\Crud\Form\CrudFormConfigurator;
1111
use Shopsys\AdministrationBundle\Component\Datagrid\Datagrid;
1212
use Shopsys\AdministrationBundle\Model\Transport\TransportGroupCrudHandler;
13+
use Shopsys\FrameworkBundle\Component\Security\Attribute\ForRole;
1314
use Shopsys\FrameworkBundle\Component\Security\Role\AdminRoleConstant;
1415
use Shopsys\FrameworkBundle\Form\Admin\Transport\TransportGroupFormType;
1516
use Shopsys\FrameworkBundle\Model\AdminNavigation\SideMenuBuilder;
1617
use Shopsys\FrameworkBundle\Model\Transport\TransportGroup;
1718

1819
#[CrudController(TransportGroup::class)]
20+
#[ForRole(AdminRoleConstant::ROLE_TRANSPORT_AND_PAYMENT)]
1921
class TransportGroupController extends AbstractCrudController
2022
{
2123
#[Override]
@@ -28,7 +30,6 @@ public function configure(CrudConfig $config): void
2830
SideMenuBuilder::SECTION_LISTS,
2931
['after' => SideMenuBuilder::LIST_TRANSPORT_AND_PAYMENT],
3032
)
31-
->setCustomRoleConstant(AdminRoleConstant::ROLE_TRANSPORT_AND_PAYMENT)
3233
->registerHandler(TransportGroupCrudHandler::class);
3334
}
3435

src/DependencyInjection/Compiler/LoadControllersExtensionCompilerPass.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ public function process(ContainerBuilder $container): void
3030
}
3131
}
3232

33+
// ascending priority is the order the extensions are applied in, consumers rely on it instead of sorting again
34+
usort($extensions, static fn (array $a, array $b): int => $a['priority'] <=> $b['priority']);
35+
3336
$container->setParameter(
3437
CrudControllerRegistry::CRUD_CONTROLLERS_EXTENSIONS_PARAMETER,
3538
$extensions,

0 commit comments

Comments
 (0)