Skip to content

Instantly share code, notes, and snippets.

@ragboyjr
Last active February 15, 2024 13:16
Show Gist options
  • Save ragboyjr/2ed5734eb839483ca22892f6955b2792 to your computer and use it in GitHub Desktop.
Save ragboyjr/2ed5734eb839483ca22892f6955b2792 to your computer and use it in GitHub Desktop.
Easy Admin 3 DTO Crud Controller
<?php
namespace App\EasyAdminExtensions\Controller;
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeCrudActionEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityPersistedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityUpdatedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateField;
use App\EasyAdminExtensions\Util;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Enables support for using DTOs for the Create/Edit forms to prevent easy admin from directly updating your entities.
*/
abstract class AbstractCrudDTOController extends AbstractCrudController implements EventSubscriberInterface
{
protected $temporaryEntityForEdit;
abstract public static function getDTOFqcn(): string;
abstract public function createEntityFromDTO(object $dto): object;
abstract public function createDTOFromEntity(object $entity): object;
abstract public function updateEntityFromDTO(object $entity, object $dto): void;
public function convertDTOToEntityFromBeforeEntityPersistedEvent(BeforeEntityPersistedEvent $event): void {
$dto = $event->getEntityInstance();
if (get_class($dto) !== static::getDTOFqcn()) {
return;
}
$entity = $this->createEntityFromDTO($dto);
\Closure::bind(function($entity) {
$this->entityInstance = $entity;
}, $event, BeforeEntityPersistedEvent::class)($entity);
}
public function convertEntityToDTOFromBeforeCrudActionEvent(BeforeCrudActionEvent $event): void {
if ($event->getAdminContext()->getCrud()->getCurrentAction() !== Action::EDIT) {
return;
}
$entity = $event->getAdminContext()->getEntity();
if ($entity->getInstance() === null || get_class($entity->getInstance()) !== static::getEntityFqcn()) {
return;
}
// hack: this triggers the primary key value to be cached on the entity dto. This is required because we
// won't have access to the actual entity with it's pkey from the entityDto.
$entity->getPrimaryKeyValue();
$this->temporaryEntityForEdit = $entity->getInstance();
$dto = $this->createDTOFromEntity($entity->getInstance());
\Closure::bind(function($dto) {
$this->instance = $dto;
}, $entity, EntityDto::class)($dto);
}
public function convertDTOToUpdatedEntityFromBeforeEntityUpdatedEvent(BeforeEntityUpdatedEvent $event): void {
$dto = $event->getEntityInstance();
if (get_class($dto) !== static::getDTOFqcn()) {
return;
}
if (!$this->temporaryEntityForEdit) {
throw new \RuntimeException('Temporary entity for edit variable was not set, something went wrong with the edit process.');
}
$entity = $this->temporaryEntityForEdit;
$this->updateEntityFromDTO($entity, $dto);
\Closure::bind(function($entity) {
$this->entityInstance = $entity;
}, $event, BeforeEntityUpdatedEvent::class)($entity);
}
/**
* The entityId query param will get left in the generated urls when you click around the admin. For example, if you visit
* show, and then hit the Back To Listings button, the url will have the recently shown entityId still in the url.
* When creating a new entity, it causes the admin context provider to try and load the old entity by id and set it on the
* EntityDto. This then causes an exception when we later try to setInstance on the EntityDto to a Dto class and not the
* Entity.
*/
public function removeEntityFromContextOnBeforeCrudActionEvent(BeforeCrudActionEvent $event): void {
if ($event->getAdminContext()->getCrud()->getCurrentAction() !== Action::NEW) {
return;
}
$entity = $event->getAdminContext()->getEntity();
\Closure::bind(function() {
$this->instance = null;
}, $entity, EntityDto::class)();
}
public function createEntity(string $entityFqcn) {
$className = static::getDTOFqcn();
return new $className();
}
/** @codeCoverageIgnore */
public static function getSubscribedEvents() {
return [
BeforeEntityPersistedEvent::class => 'convertDTOToEntityFromBeforeEntityPersistedEvent',
BeforeCrudActionEvent::class => [['convertEntityToDTOFromBeforeCrudActionEvent'], ['removeEntityFromContextOnBeforeCrudActionEvent']],
BeforeEntityUpdatedEvent::class => 'convertDTOToUpdatedEntityFromBeforeEntityUpdatedEvent',
];
}
public function configureFields(string $pageName): iterable {
return Util::mapFields(parent::configureFields($pageName), [
'createdAt' => DateField::new('createdAt')->hideOnForm(),
]);
}
}
<?php
namespace App\EasyAdminExtensions;
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
final class Util
{
/**
* @param FieldInterface[] $fields
* @psalm-param array<string, \Closure|FieldInterface> $fieldMap
* @return FieldInterface[]
*/
public static function mapFields(iterable $fields, array $fieldMap): iterable {
foreach ($fields as $field) {
$map = $fieldMap[$field->getAsDto()->getProperty()] ?? null;
if ($map === null) {
yield $field;
} else if ($map instanceof FieldInterface) {
yield $map;
} else if ($map instanceof \Closure) {
yield $map($field);
} else {
throw new \RuntimeException('Unexpected value for fieldMap.');
}
}
}
}
@dragosprotung
Copy link

I managed to make it work without overwriting the new method, but by extending the createNewForm method:

public function createNewForm(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormInterface
{
    $form = parent::createNewForm($entityDto, $formOptions, $context);

    $entity = $context->getEntity();
    Closure::bind(function (): void {
        $this->instance = null;
    }, $entity, EntityDto::class)();

    return $form;
}

@ovidals
Copy link

ovidals commented Apr 14, 2021

I managed to make it work without overwriting the new method, but by extending the createNewForm method:

public function createNewForm(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormInterface
{
    $form = parent::createNewForm($entityDto, $formOptions, $context);

    $entity = $context->getEntity();
    Closure::bind(function (): void {
        $this->instance = null;
    }, $entity, EntityDto::class)();

    return $form;
}

Great! will try it.

@andreicio
Copy link

On the subject of moderately hacky ways to conform to DDD and still use easyadmin 3, one idea is to add a trait to relevant entities with magic getter/setter that checks if the caller is easyadmin by using debug_backtrace limited to 3-4 levels and checking the filename.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment