Skip to content

Instantly share code, notes, and snippets.

@ragboyjr
Last active February 15, 2024 13:16
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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.');
}
}
}
}
@ovidals
Copy link

ovidals commented Jun 30, 2020

This does not work if I don't comment line 27.

static::getEntityFqcn() return entity we want to create but line 25 returns the DTO. Is that correct?

@ragboyjr
Copy link
Author

@ovidals, thanks great catch. I actually had fixed this internally, but didn't update the gist. Check now sir!

@ovidals
Copy link

ovidals commented Jun 30, 2020

Thank you @ragboyjr!

@ovidals
Copy link

ovidals commented Jan 4, 2021

Hi @ragboyjr,

Are you planning to compatibilize this files with the last version of EasyAdmin (3.2.1)?

They made some changes and now this error is triggered on the "new" action:

The new entity instance must be of the same type as the previous instance (original instance: "App\Api\Domain\Entity\Activity\ActivityType", new instance: "App\BackOffice\Infrastructure\Controller\Activity\ActivityTypeRequest").

@mitalcoi
Copy link

mitalcoi commented Jan 4, 2021

Yes, they changed way of getting id from entity, and now I getting error when updating resource:
Снимок экрана 2021-01-04 в 20 20 41

(original: entity, new instance: DTO)

@mitalcoi
Copy link

mitalcoi commented Jan 4, 2021

Снимок экрана 2021-01-04 в 19 39 31

What exactly changed in ea 3.2

@ovidals
Copy link

ovidals commented Mar 9, 2021

Did anyone manage to make this work?

@dragosprotung
Copy link

Did anyone manage to make this work?

Only for edit action. As of version 3.2.8 there is no way to make it for new action because there are not enough extension points

@ovidals
Copy link

ovidals commented Apr 11, 2021

I manage to make it work overriding the 'new' method from AbstractController on the dtoController and commenting the line before the check if form is submitted. Also the dto's should have an id property. Create action works well.

Did anyone manage to make this work?

Only for edit action. As of version 3.2.8 there is no way to make it for new action because there are not enough extension points

@dragosprotung
Copy link

I manage to make it work overriding the 'new' method from AbstractController on the dtoController and commenting the line before the check if form is submitted. Also the dto's should have an id property. Create action works well.

Did anyone manage to make this work?

Only for edit action. As of version 3.2.8 there is no way to make it for new action because there are not enough extension points

Yes, that is basically the main issue. While this works, i do not consider this to be a good solution as it requires overwriting the action and at this point you might as well implement whatever you want there.

@ragboyjr
Copy link
Author

Ya, I agree, it's not a great solution considering that the easy admin maintainers made it clear that don't plan on supporting DTOs going forward. This would have been acceptable if there was going to be first class support from EA.

@ovidals
Copy link

ovidals commented Apr 14, 2021

Not the best solution I know, but it works and will allow us to migrate to PHP 8. Otherwise we are not able to do.

@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