-
-
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.'); | |
} | |
} | |
} | |
} |
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.
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.