doctrine / orm

Doctrine Object Relational Mapper (ORM)
https://www.doctrine-project.org/projects/orm.html
MIT License
9.94k stars 2.51k forks source link

Is there any way of using DEFERRED_EXPLICIT globally? #11155

Closed abcRede closed 9 months ago

abcRede commented 9 months ago

We would like to exert control over the application's processes and explicitly mark what should be persisted. However, this necessitates modifying all classes to include the DEFERRED_EXPLICIT flag, posing a potential risk of overlooking some classes and leaving them behind.

While Doctrine/ORM appears to be a promising tool that could replace our current in-house developed ORM, we are hesitant about the concept of the entity manager monitoring everything and handling it automatically.

Given that we occasionally make changes in the model that aren't meant to be stored (as we operate financial software and run simulations over these models to generate the actual results of operations before storing the output), we need to exercise extreme caution. Cleaning every state before flushing entities is crucial. Therefore, having DEFERRED_EXPLICIT set by default would be the most suitable approach for us.

Our current method for handling these classes is similar to the following example:

$model = $this->modelRepository->find($payload->model_id);
$anotherModel = $this->anotherModelRepository->find($model->relation_id);
$model->doSomething($anotherModel);
$this->modelRepository->save($model);

Setting DEFERRED_EXPLICIT as the default would also facilitate code refactoring for us. Every instance where the "save" method is used could be replaced with "persist," streamlining the entire refactoring process.

Is there a way to implement this globally?

ovrflo commented 9 months ago

Hi @abcRede

We implemented a subscriber that lets us know whenever we have an entity missing DEFERRED_EXPLICIT.

use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Mapping\MappingException;

#[AsDoctrineListener(event: Events::loadClassMetadata)]
final readonly class ChangeTrackingPolicySubscriber
{
    public function __construct(
        private array $includedNamespaces = [],
        private array $excludedNamespaces = [],
    ) {
    }

    public function loadClassMetadata(LoadClassMetadataEventArgs $event): void
    {
        $classMetadata = $event->getClassMetadata();
        if ($classMetadata->isEmbeddedClass || $classMetadata->isMappedSuperclass || $classMetadata->changeTrackingPolicy === ClassMetadataInfo::CHANGETRACKING_DEFERRED_EXPLICIT) {
            return;
        }

        if (!$classMetadata->getReflectionClass()) {
            return;
        }

        if (PHP_SAPI === 'cli') {
            foreach ($_SERVER['argv'] ?? [] as $arg) {
                if (str_starts_with(strtolower($arg), 'make:')) {
                    return;
                }
            }
        }

        $fqcn = $classMetadata->getReflectionClass()->getName();

        $isIncluded = false;
        foreach ($this->includedNamespaces as $includedNamespace) {
            if (str_starts_with($fqcn, $includedNamespace)) {
                $isIncluded = true;
                break;
            }
        }

        if (!$isIncluded) {
            return;
        }

        if ($this->excludedNamespaces) {
            foreach ($this->excludedNamespaces as $excludedNamespace) {
                if (str_starts_with($fqcn, $excludedNamespace)) {
                    return;
                }
            }
        }

        throw new MappingException(sprintf('Entity %s must set change tracking policy to DEFERRED_EXPLICIT', $fqcn));
    }
}
abcRede commented 9 months ago

This is great! I'll give it a try.

Thank you very much!