Skip to content

Commit 4716e81

Browse files
lukaslueckejaviereguiluz
authored andcommitted
Add option to modify query of AssociationField (with autocomplete)
1 parent 6d0170d commit 4716e81

File tree

6 files changed

+136
-62
lines changed

6 files changed

+136
-62
lines changed

src/Controller/AbstractCrudController.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
1717
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Controller\CrudControllerInterface;
1818
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
19+
use EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto;
1920
use EasyCorp\Bundle\EasyAdminBundle\Dto\SearchDto;
2021
use EasyCorp\Bundle\EasyAdminBundle\Event\AfterCrudActionEvent;
2122
use EasyCorp\Bundle\EasyAdminBundle\Event\AfterEntityDeletedEvent;
@@ -29,10 +30,12 @@
2930
use EasyCorp\Bundle\EasyAdminBundle\Exception\ForbiddenActionException;
3031
use EasyCorp\Bundle\EasyAdminBundle\Exception\InsufficientEntityPermissionException;
3132
use EasyCorp\Bundle\EasyAdminBundle\Factory\ActionFactory;
33+
use EasyCorp\Bundle\EasyAdminBundle\Factory\ControllerFactory;
3234
use EasyCorp\Bundle\EasyAdminBundle\Factory\EntityFactory;
3335
use EasyCorp\Bundle\EasyAdminBundle\Factory\FilterFactory;
3436
use EasyCorp\Bundle\EasyAdminBundle\Factory\FormFactory;
3537
use EasyCorp\Bundle\EasyAdminBundle\Factory\PaginatorFactory;
38+
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
3639
use EasyCorp\Bundle\EasyAdminBundle\Form\Type\FiltersFormType;
3740
use EasyCorp\Bundle\EasyAdminBundle\Orm\EntityRepository;
3841
use EasyCorp\Bundle\EasyAdminBundle\Orm\EntityUpdater;
@@ -88,6 +91,7 @@ public static function getSubscribedServices()
8891
'event_dispatcher' => '?'.EventDispatcherInterface::class,
8992
ActionFactory::class => '?'.ActionFactory::class,
9093
AdminContextProvider::class => '?'.AdminContextProvider::class,
94+
ControllerFactory::class => '?'.ControllerFactory::class,
9195
CrudUrlGenerator::class => '?'.CrudUrlGenerator::class,
9296
EntityFactory::class => '?'.EntityFactory::class,
9397
EntityRepository::class => '?'.EntityRepository::class,
@@ -389,6 +393,21 @@ public function delete(AdminContext $context)
389393
public function autocomplete(AdminContext $context): JsonResponse
390394
{
391395
$queryBuilder = $this->createIndexQueryBuilder($context->getSearch(), $context->getEntity(), FieldCollection::new([]), FilterCollection::new());
396+
397+
$autocompleteContext = $context->getRequest()->get(AssociationField::PARAM_AUTOCOMPLETE_CONTEXT);
398+
399+
/** @var CrudControllerInterface $controller */
400+
$controller = $this->get(ControllerFactory::class)->getCrudControllerInstance($autocompleteContext['crudId'], Action::INDEX, $context->getRequest());
401+
/** @var FieldDto $field */
402+
$field = FieldCollection::new($controller->configureFields(Crud::PAGE_INDEX))->get($autocompleteContext['propertyName']);
403+
/** @var \Closure|null $modify */
404+
$modify = $field->getCustomOption(AssociationField::OPTION_MODIFY_QUERY);
405+
406+
if(null !== $modify)
407+
{
408+
$modify($queryBuilder);
409+
}
410+
392411
$paginator = $this->get(PaginatorFactory::class)->create($queryBuilder);
393412

394413
return JsonResponse::fromJsonString($paginator->getResultsAsJson());

src/EventListener/AdminContextListener.php

Lines changed: 6 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Controller\DashboardControllerInterface;
88
use EasyCorp\Bundle\EasyAdminBundle\EasyAdminBundle;
99
use EasyCorp\Bundle\EasyAdminBundle\Factory\AdminContextFactory;
10+
use EasyCorp\Bundle\EasyAdminBundle\Factory\ControllerFactory;
1011
use EasyCorp\Bundle\EasyAdminBundle\Registry\CrudControllerRegistry;
1112
use EasyCorp\Bundle\EasyAdminBundle\Registry\DashboardControllerRegistry;
1213
use Symfony\Component\HttpFoundation\Request;
@@ -25,17 +26,13 @@
2526
class AdminContextListener
2627
{
2728
private $adminContextFactory;
28-
private $dashboardControllers;
29-
private $crudControllers;
30-
private $controllerResolver;
29+
private $controllerFactory;
3130
private $twig;
3231

33-
public function __construct(AdminContextFactory $adminContextFactory, DashboardControllerRegistry $dashboardControllers, CrudControllerRegistry $crudControllers, ControllerResolverInterface $controllerResolver, Environment $twig)
32+
public function __construct(AdminContextFactory $adminContextFactory, ControllerFactory $controllerFactory, Environment $twig)
3433
{
3534
$this->adminContextFactory = $adminContextFactory;
36-
$this->dashboardControllers = $dashboardControllers;
37-
$this->crudControllers = $crudControllers;
38-
$this->controllerResolver = $controllerResolver;
35+
$this->controllerFactory = $controllerFactory;
3936
$this->twig = $twig;
4037
}
4138

@@ -49,7 +46,7 @@ public function onKernelController(ControllerEvent $event): void
4946

5047
$dashboardControllerInstance = $currentControllerInstance instanceof DashboardControllerInterface
5148
? $currentControllerInstance
52-
: $this->getDashboardControllerInstanceFromContextId($contextId, $event->getRequest());
49+
: $this->controllerFactory->getDashboardControllerInstanceFromContextId($contextId, $event->getRequest());
5350
if (null === $dashboardControllerInstance) {
5451
// this can only happen when a malicious user tries to hack the contextId value in the query string
5552
// don't throw an exception to prevent hackers from causing lots of exceptions in applications using EasyAdmin
@@ -59,7 +56,7 @@ public function onKernelController(ControllerEvent $event): void
5956

6057
$crudId = $event->getRequest()->query->get('crudId');
6158
$crudAction = $event->getRequest()->query->get('crudAction');
62-
$crudControllerInstance = $this->getCrudControllerInstance($crudId, $crudAction, $event->getRequest());
59+
$crudControllerInstance = $this->controllerFactory->getCrudControllerInstance($crudId, $crudAction, $event->getRequest());
6360

6461
if (null !== $crudId && null === $dashboardControllerInstance) {
6562
// this can only happen when a malicious user tries to hack the crudId value in the query string
@@ -122,55 +119,6 @@ private function getCurrentControllerInstance(ControllerEvent $event)
122119
return $controller[0];
123120
}
124121

125-
private function getDashboardControllerInstanceFromContextId(string $contextId, Request $request): ?DashboardControllerInterface
126-
{
127-
$dashboardControllerFqcn = $this->dashboardControllers->getControllerFqcnByContextId($contextId);
128-
if (null === $dashboardControllerFqcn) {
129-
return null;
130-
}
131-
132-
$newRequest = $request->duplicate(null, null, ['_controller' => [$dashboardControllerFqcn, 'index']]);
133-
$dashboardControllerCallable = $this->controllerResolver->getController($newRequest);
134-
135-
if (false === $dashboardControllerCallable) {
136-
throw new NotFoundHttpException(sprintf('Unable to find the controller "%s::%s".', $dashboardControllerFqcn, 'index'));
137-
}
138-
139-
if (!\is_array($dashboardControllerCallable)) {
140-
return null;
141-
}
142-
143-
$dashboardControllerInstance = $dashboardControllerCallable[0];
144-
145-
return $dashboardControllerInstance instanceof DashboardControllerInterface ? $dashboardControllerInstance : null;
146-
}
147-
148-
private function getCrudControllerInstance(?string $crudId, ?string $crudAction, Request $request): ?CrudControllerInterface
149-
{
150-
if (null === $crudId || null === $crudAction) {
151-
return null;
152-
}
153-
154-
if (null === $crudControllerFqcn = $this->crudControllers->findCrudFqcnByCrudId($crudId)) {
155-
return null;
156-
}
157-
158-
$newRequest = $request->duplicate(null, null, ['_controller' => [$crudControllerFqcn, $crudAction]]);
159-
$crudControllerCallable = $this->controllerResolver->getController($newRequest);
160-
161-
if (false === $crudControllerCallable) {
162-
throw new NotFoundHttpException(sprintf('Unable to find the controller "%s::%s".', $crudControllerFqcn, $crudAction));
163-
}
164-
165-
if (!\is_array($crudControllerCallable)) {
166-
return null;
167-
}
168-
169-
$crudControllerInstance = $crudControllerCallable[0];
170-
171-
return $crudControllerInstance instanceof CrudControllerInterface ? $crudControllerInstance : null;
172-
}
173-
174122
private function createAdminContext(Request $request, DashboardControllerInterface $dashboardController, ?CrudControllerInterface $crudController): AdminContext
175123
{
176124
return $this->adminContextFactory->create($request, $dashboardController, $crudController);

src/Factory/ControllerFactory.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
4+
namespace EasyCorp\Bundle\EasyAdminBundle\Factory;
5+
6+
7+
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Controller\CrudControllerInterface;
8+
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Controller\DashboardControllerInterface;
9+
use EasyCorp\Bundle\EasyAdminBundle\Registry\CrudControllerRegistry;
10+
use EasyCorp\Bundle\EasyAdminBundle\Registry\DashboardControllerRegistry;
11+
use Symfony\Component\HttpFoundation\Request;
12+
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
13+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
14+
15+
/**
16+
* @author Lukas Lücke <lukas@luecke.me>
17+
*/
18+
final class ControllerFactory
19+
{
20+
private $dashboardControllers;
21+
private $crudControllers;
22+
private $controllerResolver;
23+
24+
public function __construct(DashboardControllerRegistry $dashboardControllers, CrudControllerRegistry $crudControllers, ControllerResolverInterface $controllerResolver)
25+
{
26+
$this->dashboardControllers = $dashboardControllers;
27+
$this->crudControllers = $crudControllers;
28+
$this->controllerResolver = $controllerResolver;
29+
}
30+
31+
public function getDashboardControllerInstanceFromContextId(string $contextId, Request $request): ?DashboardControllerInterface
32+
{
33+
return $this->getDashboardController($this->dashboardControllers->getControllerFqcnByContextId($contextId), $request);
34+
}
35+
36+
public function getCrudControllerInstance(?string $crudId, ?string $crudAction, Request $request): ?CrudControllerInterface
37+
{
38+
if (null === $crudId) {
39+
return null;
40+
}
41+
42+
return $this->getCrudController($this->crudControllers->findCrudFqcnByCrudId($crudId), $crudAction, $request);
43+
}
44+
45+
private function getDashboardController(?string $dashboardControllerFqcn, Request $request): ?DashboardControllerInterface
46+
{
47+
return $this->getController(DashboardControllerInterface::class, $dashboardControllerFqcn, 'index', $request);
48+
}
49+
50+
public function getCrudController(?string $crudControllerFqcn, ?string $crudAction, Request $request): ?CrudControllerInterface
51+
{
52+
return $this->getController(CrudControllerInterface::class, $crudControllerFqcn, $crudAction, $request);
53+
}
54+
55+
private function getController(string $controllerInterface, ?string $controllerFqcn, ?string $controllerAction, Request $request)
56+
{
57+
if (null === $controllerFqcn || null === $controllerAction) {
58+
return null;
59+
}
60+
61+
$newRequest = $request->duplicate(null, null, ['_controller' => [$controllerFqcn, $controllerAction]]);
62+
$controllerCallable = $this->controllerResolver->getController($newRequest);
63+
64+
if (false === $controllerCallable) {
65+
throw new NotFoundHttpException(sprintf('Unable to find the controller "%s::%s".', $controllerFqcn, $controllerAction));
66+
}
67+
68+
if (!\is_array($controllerCallable)) {
69+
return null;
70+
}
71+
72+
$controllerInstance = $controllerCallable[0];
73+
74+
return is_subclass_of($controllerInstance, $controllerInterface) ? $controllerInstance : null;
75+
}
76+
}

src/Field/AssociationField.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,15 @@ final class AssociationField implements FieldInterface
1414

1515
public const OPTION_AUTOCOMPLETE = 'autocomplete';
1616
public const OPTION_CRUD_CONTROLLER = 'crudControllerFqcn';
17+
public const OPTION_MODIFY_QUERY = 'modifyQuery';
1718
/** @internal this option is intended for internal use only */
1819
public const OPTION_RELATED_URL = 'relatedUrl';
1920
/** @internal this option is intended for internal use only */
2021
public const OPTION_DOCTRINE_ASSOCIATION_TYPE = 'associationType';
2122

23+
/** @internal this option is intended for internal use only */
24+
public const PARAM_AUTOCOMPLETE_CONTEXT = 'autocompleteContext';
25+
2226
public static function new(string $propertyName, ?string $label = null): self
2327
{
2428
return (new self())
@@ -29,6 +33,7 @@ public static function new(string $propertyName, ?string $label = null): self
2933
->addCssClass('field-association')
3034
->setCustomOption(self::OPTION_AUTOCOMPLETE, false)
3135
->setCustomOption(self::OPTION_CRUD_CONTROLLER, null)
36+
->setCustomOption(self::OPTION_MODIFY_QUERY, null)
3237
->setCustomOption(self::OPTION_RELATED_URL, null)
3338
->setCustomOption(self::OPTION_DOCTRINE_ASSOCIATION_TYPE, null);
3439
}
@@ -46,4 +51,11 @@ public function setCrudController(string $crudControllerFqcn): self
4651

4752
return $this;
4853
}
54+
55+
public function modifyQuery(\Closure $modify): self
56+
{
57+
$this->setCustomOption(self::OPTION_MODIFY_QUERY, $modify);
58+
59+
return $this;
60+
}
4961
}

src/Field/Configurator/AssociationConfigurator.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace EasyCorp\Bundle\EasyAdminBundle\Field\Configurator;
44

5+
use Doctrine\ORM\EntityRepository;
56
use Doctrine\ORM\PersistentCollection;
67
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
78
use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
@@ -70,9 +71,23 @@ public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $c
7071
->setController($field->getCustomOption(AssociationField::OPTION_CRUD_CONTROLLER))
7172
->setAction('autocomplete')
7273
->setEntityId(null)
74+
->set(AssociationField::PARAM_AUTOCOMPLETE_CONTEXT, [
75+
'crudId' => $context->getRequest()->query->get('crudId'),
76+
'propertyName' => $propertyName
77+
])
7378
->generateUrl();
7479

7580
$field->setFormTypeOption('attr.data-ea-autocomplete-endpoint-url', $autocompleteEndpointUrl);
81+
} else {
82+
$field->setFormTypeOption('query_builder', static function(EntityRepository $repository) use($field) {
83+
// TODO: should this use `createIndexQueryBuilder` instead, so we get the default ordering etc.?
84+
// it would then be identical to the one used in autocomplete action, but it is a bit complex getting it in here
85+
$queryBuilder = $repository->createQueryBuilder('entity');
86+
if($modify = $field->getCustomOption(AssociationField::OPTION_MODIFY_QUERY)) {
87+
$modify($queryBuilder);
88+
}
89+
return $queryBuilder;
90+
});
7691
}
7792
}
7893

src/Resources/config/services.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use EasyCorp\Bundle\EasyAdminBundle\EventListener\ExceptionListener;
1616
use EasyCorp\Bundle\EasyAdminBundle\Factory\ActionFactory;
1717
use EasyCorp\Bundle\EasyAdminBundle\Factory\AdminContextFactory;
18+
use EasyCorp\Bundle\EasyAdminBundle\Factory\ControllerFactory;
1819
use EasyCorp\Bundle\EasyAdminBundle\Factory\EntityFactory;
1920
use EasyCorp\Bundle\EasyAdminBundle\Factory\FieldFactory;
2021
use EasyCorp\Bundle\EasyAdminBundle\Factory\FilterFactory;
@@ -150,12 +151,15 @@
150151

151152
->set(AdminContextListener::class)
152153
->arg(0, new Reference(AdminContextFactory::class))
153-
->arg(1, new Reference(DashboardControllerRegistry::class))
154-
->arg(2, new Reference(CrudControllerRegistry::class))
155-
->arg(3, new Reference('controller_resolver'))
156-
->arg(4, new Reference('twig'))
154+
->arg(1, new Reference(ControllerFactory::class))
155+
->arg(2, new Reference('twig'))
157156
->tag('kernel.event_listener', ['event' => ControllerEvent::class])
158157

158+
->set(ControllerFactory::class)
159+
->arg(0, new Reference(DashboardControllerRegistry::class))
160+
->arg(1, new Reference(CrudControllerRegistry::class))
161+
->arg(2, new Reference('controller_resolver'))
162+
159163
->set(CrudResponseListener::class)
160164
->arg(0, new Reference(AdminContextProvider::class))
161165
->arg(1, new Reference('twig'))

0 commit comments

Comments
 (0)