-
-
Save ragboyjr/2ed5734eb839483ca22892f6955b2792 to your computer and use it in GitHub Desktop.
<?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, thanks great catch. I actually had fixed this internally, but didn't update the gist. Check now sir!
Thank you @ragboyjr!
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").
Did anyone manage to make this work?
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
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 fornew
action because there are not enough extension points
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 fornew
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.
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.
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.
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;
}
I managed to make it work without overwriting the
new
method, but by extending thecreateNewForm
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.
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.
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?