bateman_ap
bateman_ap

Reputation: 1921

Stop Symfony 4 updating relationship entity via EventListener

I have a entity with a manyToOne relationship, simplified example here:

class Api
{
   /**
   * @ORM\OneToMany(targetEntity="App\Entity\Score", mappedBy="api")
   */
   private $scores;

   public function __construct()
   {
       $this->scores = new ArrayCollection();
   }

   /**
   * @ORM\Column(type="string", length=400, nullable=true)
   */
   private $apiKey;

   /**
   * @return mixed
   */
   public function getApiKey() {
      return $this->apiKey;
   }

   /**
   * @param mixed $apiKey
   */
   public function setApiKey( $apiKey ): void {
      $this->apiKey = $apiKey;
   }

The other side of the OneToMany looks like this:

 class Score
 {
     /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Api", inversedBy="scores")
     * @ORM\JoinColumn(nullable=true)
     */
     private $api;

     /**
     * @ORM\Column(type="decimal", precision=2, scale=5, nullable=true)
     */
     private $highScore;

     /**
     * @return mixed
     */
     public function getHighScore()
     {
         return $this->highScore;
     }

     /**
     * @param mixed $highScore
     */
     public function setHighScore($highScore): void
     {
         $this->highScore= $highScore;
     }

All this is good, however I wanted to put some simple encryption on the API Key so I am using openssl_encrypt and openssl_decrypt storing the iv in the Api table when encoding it to be able to decode. To do this automatically I have set up a a EventListener looking like this:

class ApiSubscriber implements EventSubscriber
{
    private $encryption;

    public function __construct(Encryption $encryption) {
        $this->encryption = $encryption;
    }

    public function getSubscribedEvents()
    {
        return array(
            'prePersist',
            'preUpdate',
            'postLoad'
        );
    }

    public function prePersist(LifecycleEventArgs $args)
    {
        $this->index($args);
    }

    public function preUpdate(LifecycleEventArgs $args)
    {
        $this->index($args);
    }

    public function postLoad(LifecycleEventArgs $args)
    {
        $entity = $args->getObject();

        if ($entity instanceof Api) {
            $apiSecret = $entity->getApiSecret();
            $iv = $entity->getIv();
            $encodedSecret = $this->encryption->decrypt($apiSecret, $iv);
            $entity->setApiSecret($encodedSecret);
        }
    }

    public function index(LifecycleEventArgs $args)
    {
        $entity = $args->getObject();

        if ($entity instanceof Api) {
            $apiSecret = $entity->getApiKey();
            $encodedSecret = $this->encryption->encrypt($apiSecret);

            $entity->setApiSecret($encodedSecret['encodedString']);
            $entity->setIv($encodedSecret['iv']);

            if($encodedSecret['success'])
            {
                $entity->setApiKey($encodedSecret['encodedString']);
                $entity->setIv($encodedSecret['iv']);
            }
        }
    }
}

Again all this works absolutely fine, however my problem comes when I update the Score entity. It looks as because it has a ManyToOne relationship with Api the Subscriber is called and updates the ApiKey to a new encoded value with a new iv key.

This is usually fine but I need to use this in a CLI command where it is looping through the Api entities and calling a API service with them, but this update is causing the loop to use old outdated info and fails.

Do you know how to only call the Subscriber when directly modifying the Api entity, not one that it has a relationship with?

Upvotes: 0

Views: 603

Answers (1)

Alan T.
Alan T.

Reputation: 1440

preUpdate lifecycle method receives a sub type of LifecycleEventArgs which is PreUpdateEventArgs. This class allows you to access the change set of the entity being updated.

As explained in doctrine's documentation you can use that to change the behaviour of your preUpdate callback based on what has actually changed in your entity.

In your case, you would need to use $args->hasChangedField to skip the encryption part if the update only concerns associations fields. You could also turn this logic around and execute the encryption part if and only if fields that are actually used in the encryption process have changed.


On a side note, the inverse side of a ManyToOne association is not supposed to have its preUpdate event triggered if the owning side is modified. This particular point is discussed here. Thus, if the preUpdate of your Api entity is triggered, that means that its change set is not empty, but it cannot be the cause of a change related to your scores field normally.

Upvotes: 2

Related Questions