Skip to content

Instantly share code, notes, and snippets.

@pauci
Created July 3, 2020 14:15
Show Gist options
  • Save pauci/b34dde239659390646bd01303fa25547 to your computer and use it in GitHub Desktop.
Save pauci/b34dde239659390646bd01303fa25547 to your computer and use it in GitHub Desktop.
<?php
declare(strict_types=1);
use Doctrine\ORM\EntityManagerInterface;
use ProxyManager\Factory\LazyLoadingValueHolderFactory;
use ProxyManager\Proxy\VirtualProxyInterface;
use Psr\Container\ContainerInterface;
use Swoole\ArrayObject;
use Swoole\Coroutine;
final class CoroutineEntityManagerDelegator
{
public function __invoke(ContainerInterface $container, string $name, callable $callback): VirtualProxyInterface
{
$factory = new LazyLoadingValueHolderFactory();
return $factory->createProxy(
EntityManagerInterface::class,
static function (?EntityManagerInterface &$wrappedObject) use ($name, $callback): bool {
static $globalContext;
$context = Coroutine::getContext() ?? ($globalContext ??= new ArrayObject());
// Return existing entity manager for current coroutine or create one
$wrappedObject = $context[$name] ??= $callback();
return true;
}
);
}
}
@Radiergummi
Copy link

I assume you'd configure this to decorate the EntityManager?

@pauci
Copy link
Author

pauci commented Feb 2, 2022

Actually this class decorates EntityManager's factory, which in turn produces proxy-decorated EntityManager. I'm using it as a delegator in laminas-servicemanager.

Proxy creates a fresh EntityManager instance for every new coroutine (new request). After coroutine terminates, its EntityManager is destroyed along with coroutine context. It works even outside coroutine context thanks to fallback to $globalContext, so I can use same proxied EntityManage outside request, e.g. during bootstrapping the swoole server or in CLI scripts.

@pauci
Copy link
Author

pauci commented Feb 2, 2022

I use similar approach with DBAL Connection and Redis doctrine cache, but instead of creating a new connection every time, I pick connection from connection-pool and then return it back to the pool after coroutine terminates.

@diego-ninja
Copy link

Hey,

I am fighting similar things in a Symfony Console CLI application, can you point me to a working example of this?

Thanks in advance.

@Radiergummi
Copy link

Radiergummi commented Feb 14, 2022

@pauci Sorry, I didn't get a notification until now? Seems to be a GitHub issue. After pondering a bit, I realised the pattern used here uses Laminas ServiceManager, not Symfony DI. I created a Symfony variant, which may also be interesting to @diego-ninja:

// src/Doctrine/CoroutineEntityManagerDecorator.php
<?php

declare(strict_types=1);

namespace App\Doctrine;

use ArrayObject;
use Doctrine\ORM\EntityManagerInterface as EM;
use ProxyManager\Factory\LazyLoadingValueHolderFactory;
use ProxyManager\Proxy\VirtualProxyInterface;
use Swoole\Coroutine;

class CoroutineEntityManagerDecorator
{
    public function __invoke(EM $entityManager): VirtualProxyInterface
    {
        return (new LazyLoadingValueHolderFactory())->createProxy(
            EM::class,
            static function (EM|null &$wrapped) use ($entityManager): bool {
                static $globalContext;

                // Use the global coroutine context, or create a new one
                $context = Coroutine::getContext()
                           ?? ($globalContext ??= new ArrayObject());

                // Return existing entity manager for current coroutine or create one
                $wrapped = $context[EM::class] ??= $entityManager;

                return true;
            }
        );
    }
}

Configuring it involves creating a combination of a factory and a decorator:

# config/services.yaml
services:
    your_app.entity_manager:
        class: Doctrine\ORM\EntityManagerInterface
        factory: '@App\Doctrine\CoroutineEntityManagerDecorator'
        decorates: Doctrine\ORM\EntityManagerInterface
        arguments:
            - '@.inner'

I'm pretty sure that works, although I restructured the application to not rely on Swoole before fully validating this.

@pauci
Copy link
Author

pauci commented Feb 15, 2022

@Radiergummi does it work for you with enabled coroutines Swoole\Coroutine::enableCoroutine()? I do not know how Symfony DI works, but it seems to me that your entity-manager proxy is using same entity-manager instance that was initially passed to __invoke(), for each coroutine. In my example, the $callback is actual entity-manager factory and is called directly from proxy for every coroutine, so every coroutine gets separate entity-manager instance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment