Update a doctrine ManyToOne relationship
Is there a way to update a secondary entity that has a ManyToOne relationship with the entity that was directly updated? For example, there are multiple project entities that have a many to one relationship to a user entity. When the user updates their entity, we need a means to run a script and update values on the project entity based on values changed in the user entity. I can provide more details if this is generally possible.
Yes, with PHP 8 and Symfony 7.2, you can use more modern approaches to handle the relationship between entities. Here are the most current options:
Option 1: Use PHP 8 attributes and event listener with automatic dependency injection
// src/EventListener/UserUpdateListener.php
namespace App\EventListener;
use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Event\AfterEntityUpdatedEvent;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
#[AsEventListener(event: AfterEntityUpdatedEvent::class, method: 'onAfterEntityUpdated')]
final class UserUpdateListener
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
) {}
public function onAfterEntityUpdated(AfterEntityUpdatedEvent $event): void
{
$entity = $event->getEntityInstance();
if (!$entity instanceof User) {
return;
}
$projects = $entity->getProjects();
foreach ($projects as $project) {
$project->updateFromUser($entity);
// Other necessary updates
}
$this->entityManager->flush();
}
}
The #[AsEventListener] attribute replaces the need to implement EventSubscriberInterface and Symfony 7.2's autoconfiguration takes care of the rest.
Option 2: Override the controller with property promotions and return types
// src/Controller/Admin/UserCrudController.php
namespace App\Controller\Admin;
use App\Entity\User;
use App\Repository\ProjectRepository;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use Doctrine\ORM\EntityManagerInterface;
class UserCrudController extends AbstractCrudController
{
public function __construct(
private readonly ProjectRepository $projectRepository,
) {}
public static function getEntityFqcn(): string
{
return User::class;
}
// Other configuration...
public function updateEntity(EntityManagerInterface $entityManager, $entityInstance): void
{
// Update the User entity
parent::updateEntity($entityManager, $entityInstance);
// Use typehint to avoid type checking
assert($entityInstance instanceof User);
// Use repository methods for optimized queries
$this->projectRepository->updateProjectsForUser($entityInstance);
// Or direct update if needed
foreach ($entityInstance->getProjects() as $project) {
$project->updateFromUser($entityInstance);
}
$entityManager->flush();
}
}
Option 3: Use Doctrine attributes for lifecycle callbacks
// src/Entity/User.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Event\PostUpdateEventArgs;
#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
class User
{
// Existing properties and methods
#[ORM\PostUpdate]
public function onPostUpdate(PostUpdateEventArgs $args): void
{
// Access EntityManager via arguments
$entityManager = $args->getObjectManager();
// Update associated projects
foreach ($this->getProjects() as $project) {
$project->updateFromUser($this);
}
// Make sure to flush only if necessary
// Since we're in a PostUpdate event, a flush isn't always necessary
// but may be required for certain operations
$entityManager->flush();
}
}
Option 4: Use Symfony 7.2 State Processors (API Platform)
If you're also using API Platform with EasyAdmin, you can leverage State Processors:
// src/State/UserProcessor.php
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
final class UserProcessor implements ProcessorInterface
{
public function __construct(
private readonly ProcessorInterface $persistProcessor,
private readonly EntityManagerInterface $entityManager,
) {}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): User
{
if (!$data instanceof User) {
return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
}
// Process the user with the default processor
$result = $this->persistProcessor->process($data, $operation, $uriVariables, $context);
// If it's an update (not a creation)
if (isset($context['previous_data'])) {
// Update associated projects
foreach ($data->getProjects() as $project) {
$project->updateFromUser($data);
}
$this->entityManager->flush();
}
return $result;
}
}
Then apply it to your User entity with the appropriate attribute.
These approaches take full advantage of PHP 8's modern features (attributes, promoted properties, return types) and Symfony 7.2 (advanced autoconfiguration, dependency injection, etc.).