Persisting other entities inside preUpdate of Doctrine Entity Listener

I give all the credits to Richard for pointing me into the right direction, so I'm accepting his answer. Nevertheless I also publish my answer with the complete code for future visitors.

class ProjectEntitySubscriber implements EventSubscriber
{
    public function getSubscribedEvents()
    {
        return array(
            'onFlush',
        );
    }

    public function onFlush(OnFlushEventArgs  $args)
    {
        $em = $args->getEntityManager();
        $uow = $em->getUnitOfWork();

        foreach ($uow->getScheduledEntityUpdates() as $keyEntity => $entity) {
            if ($entity instanceof ProjectTolerances) {
                foreach ($uow->getEntityChangeSet($entity) as $keyField => $field) {
                    $notification = new ProjectNotification();
                    // place here all the setters
                    $em->persist($notification);
                    $classMetadata = $em->getClassMetadata('AppBundle\Entity\ProjectNotification');
                    $uow->computeChangeSet($classMetadata, $notification);
                }
            }
        }
    }
}

Don't use preUpdate, use onFlush - this allows you to access the UnitOfWork API & you can then persist entities.

E.g. (this is how I do it in 2.3, might be changed in newer versions)

    $this->getEntityManager()->persist($entity);
    $metaData = $this->getEntityManager()->getClassMetadata($className);
    $this->getUnitOfWork()->computeChangeSet($metaData, $entity);

As David Baucum stated, the initial question referred to Doctrine Entity Listeners, but as a solution, the op ended up using an Event Listener.

I am sure many more will stumble upon this topic, because of the infinite loop problem. For those that adopt the accepted answer, TAKE NOTE that the onFlush event (when using an Event Listener like above) is executed with ALL the entities that might be in queue for an update, whereas an Entity Listener is used only when doing something with the entity it was "assigned" to.

I setup a custom auditing system with symfony 4.4 and API Platform, and i managed to achieve the desired result with just an Entity Listener.

NOTE: Tested and working however, the namespaces and functions have been modified, and this is purely to demonstrate how to manipulate another entity inside a Doctrine Entity Listener.

// this goes into the main entity
/**
* @ORM\EntityListeners({"App\Doctrine\MyEntityListener"})
*/
<?
// App\Doctrine\MyEntityListener.php

namespace App\Doctrine;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\Security;

// whenever an Employee record is inserted/updated
// log changes to EmployeeAudit
use App\Entity\Employee;
use App\Entity\EmployeeAudit;

private $security;
private $currentUser;
private $em;
private $audit;

public function __construct(Security $security, EntityManagerInterface $em) {
    $this->security = $security;
    $this->currentUser = $security->getUser();
    $this->em = $em;
}

// HANDLING NEW RECORDS

/**
 * since prePersist is called only when inserting a new record, the only purpose of this method
 * is to mark our object as a new entry
 * this method might not be necessary, but for some reason, if we set something like
 * $this->isNewEntry = true, the postPersist handler will not pick up on that
 * might be just me doing something wrong
 *
 * @param Employee $obj
 * @ORM\PrePersist()
 */
public function prePersist(Employee $obj){
    if(!($obj instanceof Employee)){
        return;
    }
    $isNewEntry = !$obj->getId();
    $obj->markAsNewEntry($isNewEntry);// custom Employee method (just sets an internal var to true or false, which can later be retrieved)
}

/**
 * @param Employee $obj
 * @ORM\PostPersist()
 */
public function postPersist(Employee $obj){
    // in this case, we can flush our EmployeeAudit object safely
    $this->prepareAuditEntry($obj);
}

// END OF NEW RECORDS HANDLING

// HANDLING UPDATES

/**
 * @see {https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/events.html}
 * @param Employee $obj
 * @param PreUpdateEventArgs $args
 * @ORM\PreUpdate()
 */
public function preUpdate(Employee $obj, PreUpdateEventArgs $args){
    $entity = $args->getEntity();
    $changeset = $args->getEntityChangeSet();

    // we just prepare our EmployeeAudit obj but don't flush anything
    $this->audit = $this->prepareAuditEntry($obj, $changeset, $flush = false);
}

/**
 * @ORM\PostUpdate()
 */
public function postUpdate(){
    // if the preUpdate handler was called, $this->audit should exist
    // NOTE: the preUpdate handler DOES NOT get called, if nothing changed
    if($this->audit){
        $this->em->persist($this->audit);
        $this->em->flush();
    }
    // don't forget to unset this
    $this->audit = null;
}

// END OF HANDLING UPDATES

// AUDITOR

private function prepareAuditEntry(Employee $obj, $changeset = [], $flush = true){
    if(!($obj instanceof Employee) || !$obj->getId()){
        // at this point, we need a DB id
        return;
    }

    $audit = new EmployeeAudit();
    // this part was cut out, since it is custom
    // here you would set things to your EmployeeAudit object
    // either get them from $obj, compare with the changeset, etc...

    // setting some custom fields
    // in case it is a new insert, the changedAt datetime will be identical to the createdAt datetime
    $changedAt = $obj->isNewInsert() ? $obj->getCreatedAt() : new \DateTime('@'.strtotime('now'));
    $changedFields = array_keys($changeset);
    $changedCount = count($changedFields);
    $changedBy = $this->currentUser->getId();
    $entryId = $obj->getId();

    $audit->setEntryId($entryId);
    $audit->setChangedFields($changedFields);
    $audit->setChangedCount($changedCount);
    $audit->setChangedBy($changedBy);
    $audit->setChangedAt($changedAt);

    if(!$flush){
        return $audit;
    }
    else{
        $this->em->persist($audit);
        $this->em->flush();
    }
}

The idea is to NOT persist/flush anything inside preUpdate (except prepare your data, because you have access to the changeset and stuff), and do it postUpdate in case of updates, or postPersist in case of new inserts.