Last active
February 15, 2024 13:16
-
-
Save ragboyjr/2ed5734eb839483ca22892f6955b2792 to your computer and use it in GitHub Desktop.
Easy Admin 3 DTO Crud Controller
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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(), | |
]); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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.'); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.