EasyCorp / EasyAdminBundle

EasyAdmin is a fast, beautiful and modern admin generator for Symfony applications.
MIT License
4.04k stars 1.02k forks source link

Paginator handler. #5057

Open maxkain opened 2 years ago

maxkain commented 2 years ago

Some times is needed to calculate entities virtual fields on index page. But it's difficult to do now, the way is to decorate PaginatorFactory for each controller. But there is another way to do this feature in PaginatorFactory for easy using it for all contollers.

Usage in crud controller:

    //override method of abstract controller
    protected function indexPaginatorHandler(EntityPaginator $paginator)
    {
        foreach ($paginator->getResults() as $entity) {
            if ($entity instanceof MyEntity) {
                $entity->setMyProperty1($someValue1);
                $entity->setMyProperty2($someValue2);
                //etc...

                //or
                $this->myEntityService->calculateFields($entity);
            }
        }
    }

Abstract crud controller:

    use EasyCorp\Bundle\EasyAdminBundle\Factory\PaginatorFactory as BasePaginatorFactory;
    //...

    public static function getSubscribedServices(): array
    {
        return array_merge(parent::getSubscribedServices(), [
            BasePaginatorFactory::class => PaginatorFactory::class,
        ]);
    }

    //override index action
    public function index(AdminContext $context): KeyValueStore|Response
    {
        $paginatorFactory = $this->container->get(BasePaginatorFactory::class);
        if ($paginatorFactory instanceof PaginatorFactory) {
            $paginatorFactory->setPaginatorHandler($this->indexPaginatorHandler(...));
        }

        try {
            return parent::index($context);
        } finally {
            if ($paginatorFactory instanceof PaginatorFactory) {
                $paginatorFactory->setPaginatorHandler(null);
            }
        }
    }

    protected function indexPaginatorHandler(EntityPaginator $paginator)
    {
    }

PaginatorFactory Decorator:

use Doctrine\ORM\QueryBuilder;
use EasyCorp\Bundle\EasyAdminBundle\Factory\PaginatorFactory as BasePaginatorFactory;
use EasyCorp\Bundle\EasyAdminBundle\Orm\EntityPaginator;

final class PaginatorFactory
{
    private BasePaginatorFactory $paginatorFactory;

    /** @var callable|null  */
    private mixed $paginatorHandler = null;

    public function __construct(BasePaginatorFactory $paginatorFactory)
    {
        $this->paginatorFactory = $paginatorFactory;
    }

    public function create(QueryBuilder $queryBuilder): EntityPaginator
    {
        $paginator = $this->paginatorFactory->create($queryBuilder);

        if ($this->paginatorHandler) {
            //call handler
            ($this->paginatorHandler)($paginator);
        }

        return $paginator;
    }

    public function getPaginatorHandler(): ?callable
    {
        return $this->paginatorHandler;
    }

    public function setPaginatorHandler(?callable $paginatorHandler): void
    {
        $this->paginatorHandler = $paginatorHandler;
    }
}
Ang3 commented 2 years ago

Hi,

I'm happy to know I'm not the only one to have this need. Indeed, the index query builder locked allowed entities and there is no way to create a query builder somewhere else to perform action on these entities.

@maxkain It's interesting, but this is a major change I think.

In my app, I created a QueryBuilderFactory. It would be interesting to move the logic of the method createIndexQueryBuilder to an independant service like the factory below to be able to create a query builder based on CRUD configuration.

<?php

namespace App\EasyAdmin\Factory;

use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Config\Option\EA;
use EasyCorp\Bundle\EasyAdminBundle\Factory\AdminContextFactory;
use EasyCorp\Bundle\EasyAdminBundle\Factory\ControllerFactory;
use EasyCorp\Bundle\EasyAdminBundle\Factory\EntityFactory;
use EasyCorp\Bundle\EasyAdminBundle\Factory\FilterFactory;
use EasyCorp\Bundle\EasyAdminBundle\Factory\FormFactory;
use EasyCorp\Bundle\EasyAdminBundle\Orm\EntityRepository;
use EasyCorp\Bundle\EasyAdminBundle\Provider\AdminContextProvider;
use LogicException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;

class QueryBuilderFactory
{
    public function __construct(private ManagerRegistry $doctrine, private AdminContextFactory $adminContextFactory, private ControllerFactory $controllerFactory, private EntityFactory $entityFactory, private FormFactory $formFactory, private FilterFactory $filterFactory, private EventDispatcherInterface $eventDispatcher)
    {
    }

    public function create(string $crudControllerFqcn, string $dashboardControllerFqcn, string $query = null, array $customSort = [], array $appliedFilters = []): QueryBuilder
    {
        $request = new Request([
            EA::CRUD_ACTION => Action::INDEX,
            EA::QUERY => $query,
            EA::SORT => $customSort,
            EA::FILTERS => $appliedFilters,
        ]);

        $dashboardController = $this->controllerFactory->getDashboardControllerInstance($dashboardControllerFqcn, $request);

        if (!$dashboardController) {
            throw new LogicException(sprintf('The dashboard controller "%s" was not found.', $dashboardControllerFqcn));
        }

        $crudController = $this->controllerFactory->getCrudControllerInstance($crudControllerFqcn, Action::INDEX, $request);

        if (!$crudController) {
            throw new LogicException(sprintf('The CRUD controller "%s" was not found.', $crudControllerFqcn));
        }

        $adminContext = $this->adminContextFactory->create($request, $dashboardController, $crudController);
        $request->attributes->set(EA::CONTEXT_REQUEST_ATTRIBUTE, $adminContext);

        $requestStack = new RequestStack();
        $requestStack->push($request);

        $adminContextProvider = new AdminContextProvider($requestStack);
        $entityRepository = new EntityRepository($adminContextProvider, $this->doctrine, $this->entityFactory, $this->formFactory, $this->eventDispatcher);
        $fields = FieldCollection::new($crudController->configureFields(Crud::PAGE_INDEX));
        $crudDto = $adminContext->getCrud();

        if (!$crudDto) {
            throw new LogicException('Missing CRUD DTO.');
        }

        $filters = $this->filterFactory->create($crudDto->getFiltersConfig(), $fields, $adminContext->getEntity());
        $searchDto = $adminContext->getSearch();

        if (!$searchDto) {
            throw new LogicException('Missing Search DTO.');
        }

        return $entityRepository->createQueryBuilder($searchDto, $adminContext->getEntity(), $fields, $filters);
    }
}

As you can see, I have to create a fake request to be able to create a fake admin context so as to get the CrudDto of another entity than the current request. Pretty dirty 😢 But it works (for now) 😄