doctrine/DoctrineMongoDBBundle

Symfony ODM reset manager

shubaivan opened this issue · 7 comments

I using

"doctrine/mongodb-odm-bundle": "^4.2",
"symfony/proxy-manager-bridge": "5.1.*",

and when I try reset manager I faced with error about lazy property
this my conf

doctrine_mongodb:
    auto_generate_proxy_classes: true
    auto_generate_hydrator_classes: true
    default_database: '%env(resolve:MONGODB_DB)%'
    default_connection: application_conn
    default_document_manager: application_manager

    connections:
        application_conn:
            server: '%env(resolve:MONGODB_URL)%'
            options: {}
        alert_conn:
            server: '%mongodb_server%'
            options:
                username: '%env(MONGO_INITDB_ROOT_USERNAME)%'
                password: '%env(MONGO_INITDB_ROOT_USERNAME)%'
                authSource: 'admin'

    document_managers:
        application_manager:
            connection: application_conn
            mappings:
                App:
                    is_bundle: false
                    type: annotation
                    dir: '%kernel.project_dir%/src/Document'
                    prefix: 'App\Document'
                    alias: App
        alert_manager:
            connection: alert_conn
            database: '%env(MONGO_INITDB_DATABASE)%'
            mappings:
                App:
                    is_bundle: false
                    type: annotation
                    dir: '%kernel.project_dir%/src/AlertDocument'
                    prefix: 'App\AlertDocument'
                    alias: App
    $this->registry->resetManager('alert_manager');

this is exception message:

#    Resetting a non-lazy manager service is not supported. Declare the "doctrine_mongodb.odm.alert_manager_document_manager" service as lazy.

After I instaled proxy manager bridge nothing changed

bin/console debug:container doctrine_mongodb.odm.alert_manager_document_manager
 The DocumentManager class is the central access point for managing the persistence of documents.

 ---------------- ----------------------------------------------------- 
  Option           Value                                                
 ---------------- ----------------------------------------------------- 
  Service ID       doctrine_mongodb.odm.alert_manager_document_manager  
  Class            Doctrine\ODM\MongoDB\DocumentManager                 
  Tags             doctrine_mongodb.odm.document_manager                
  Public           yes                                                  
  Synthetic        no                                                   
  Lazy             no                                                   
  Shared           yes                                                  
  Abstract         no                                                   
  Autowired        no                                                   
  Autoconfigured   no                                                   
  Factory Class    Doctrine\ODM\MongoDB\DocumentManager                 
  Factory Method   create                                               
 ---------------- ------------

does it posible?

Excuse me if this sounds daft, but why are you trying to reset the manager?

The error comes from symfony/doctrine-bridge@f54227d which implies that document managers are not lazy. I believe that Symfony is making ORM's entity manager lazy by default but I can't figure out how

Excuse me if this sounds daft, but why are you trying to reset the manager?

Look, if exist this function resetManager, perhaps this is actuall functionality. Example after change dbdefault name for switch exist entity manager to work witj another db, with the same set of models. Maybe will be better way create some service where will be function like get entity manager by dbaname where will be creating new object of dm by static function, BUT, if develop process include reset manager way...

Example after change dbdefault name for switch exist entity manager to work witj another db, with the same set of models.

I've been working with multi-tenant apps for few years, I believe the approach we developed is way better than having only one DM and resetting it: https://stackoverflow.com/a/42142666/5982920

Look, if exist this function resetManager, perhaps this is actuall functionality.

Resetting the manager is necessary if the entity manager was closed due to an error. It is a scenario from which the entity manager is not able to recover safely, which is why there is no reset directly on the manager. The only way to safely recover is to recreate the manager entirely.

In Symfony's Doctrine Bridge, you have the option to reset the manager in a different way. When marking the services as lazy, you don't get an actual manager instance, but a manager that decorates the manager being used. This means that you can change the decorated service without changing the decorator. Why does this matter? Your manager will have been injected into a number of other services via constructor injection. As explained before, the only way to safely reset the manager is to dump the object into the garbage bin and recreate it. However, this would also require resetting every service that depends on it, as you need to let it know about the new object instance. Symfony works around this by using lazy services, where a reset means that you can create a new manager without having to let every other service that depends on it know.

As @malarzm mentioned, I would not recommend using this kind of functionality for a multi-tenant application. Depending on when you need to change connection parameters, it may be sensible to create a separate manager.

Maybe will be better way create some service where will be function like get entity manager by dbaname

Please have a look at manager registries: they keep track of managers by name and allow you to fetch a manager by its name or fetch a manager for a particular entity. Since all your managers manage the same entities the latter won't be interesting to you, but the first portion should be what you're looking for. The bundle creates a registry for all your managers and exposes it via the @doctrine_mongodb service. You can add your own managers to this class when the container is compiled so there will be no runtime impact by having a manager for every tenant out there.

yep, thank you - 'Depending on when you need to change connection parameters, it may be sensible to create a separate manage'
I decided chosse approach based on one dm, becuase I have the same documants in each db, db name will be dynamic and just for try how it will be useful

I fire event when needed

class IdentityCompanyDBEvent extends Event
{
    /**
     * @var string|null 
     */
    private $dbName;

    /**
     * IdentityCompanyDBEvent constructor.
     * @param $dbName
     */
    public function __construct(?string $dbName = null)
    {
        $this->dbName = $dbName;
    }

    /**
     * @return string|null
     */
    public function getDbName()
    {
        return $this->dbName;
    }
}

and subscribe it like that

class IdentityCompanyDBSubscriber implements EventSubscriberInterface
{
    const IDENTITY_COMPANY_DB = 'identity_company_db';
    /**
     * @var DocumentManager
     */
    private $alertManager;

    /**
     * @var TokenStorage
     */
    private $storage;

    /**
     * @var ManagerRegistry
     */
    private $registry;

    /**
     * @var SessionInterface
     */
    private $session;

    /**
     * IdentityCompanyDBSubscriber constructor.
     * @param ManagerRegistry $registry
     * @param TokenStorageInterface $storage
     */
    public function __construct(
        SessionInterface $session,
        ManagerRegistry $registry,
        TokenStorageInterface $storage)
    {
        $this->session = $session;
        $this->registry = $registry;
        $this->storage = $storage;
        $this->alertManager = $registry->getManager('alert_manager');
    }


    public static function getSubscribedEvents()
    {
        return [
            IdentityCompanyDBEvent::class => 'identityCompanyDB'
        ];
    }

    /**
     * @param IdentityCompanyDBEvent $event
     * @throws \ReflectionException
     */
    public function identityCompanyDB(IdentityCompanyDBEvent $event)
    {
        $companyDB = $this->identityDDName($event);
        $this->alertManager->getConfiguration()->setDefaultDB($companyDB);
        
        foreach ($this->alertManager->getDocumentCollections() as $key=>$collection)
        {
            if (is_subclass_of($key, CompanyDocument::class)) {
                $reflection = new \ReflectionClass($collection);
                $property = $reflection->getProperty('databaseName');
                $property->setAccessible(true);
                $property->setValue($collection, $companyDB);
                $property->setAccessible(false);   
            }
        }

        $event->stopPropagation();
    }
    
    private function identityDDName(IdentityCompanyDBEvent $event)
    {
        if ($event->getDbName()) {
            return $event->getDbName();
        }
        if ($this->session->has(self::IDENTITY_COMPANY_DB)) {
            return $this->session->get(self::IDENTITY_COMPANY_DB);
        }
        $user = $this->storage->getToken()->getUser();
        if (!$user instanceof User) {
            throw new BadRequestException('user should be auth');
        }

        if (!$user->getCompany() || !$user->getCompany()->getName()) {
            throw new BadRequestException('company is necessary for user');
        }
        
        return $user->getCompany()->getName();
    }
}

and in parent repo

    /**
     * @return DocumentManager
     */
    public function getDocumentManager(): DocumentManager
    {
        $this->fireIdentityCompanyDBEvent();
        return parent::getDocumentManager();
    }

    /**
     * @return DocumentPersister
     */
    protected function getDocumentPersister(): DocumentPersister
    {
        $this->fireIdentityCompanyDBEvent();
        return parent::getDocumentPersister();
    }

    /**
     * @return void
     */
    protected function fireIdentityCompanyDBEvent(): void
    {
        if (is_subclass_of($this->getDocumentName(), CompanyDocument::class)) {
            $this->eventDispatcher->dispatch(new IdentityCompanyDBEvent());
        } else {
            $this->eventDispatcher->dispatch(new IdentityCompanyDBEvent($this->params->get('mongo_initdb_database')));
        }
    }

maybe look like bad approch but I want to test it

stale commented

This issue has been automatically marked as stale because it has not had any recent activity. It will be closed in a week if no further activity occurs. Thank you for your contributions.