EasyAdminBundle icon indicating copy to clipboard operation
EasyAdminBundle copied to clipboard

Update a doctrine ManyToOne relationship

Open scadergit opened this issue 1 year ago • 1 comments

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.

scadergit avatar Feb 06 '25 16:02 scadergit

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.).

nissim94 avatar Mar 07 '25 10:03 nissim94