Closed landure closed 1 month ago
This is an example implementation I used to fix the issue in my project.
<?php
declare(strict_types=1);
/*
* This file is part of the Runroom package.
*
* (c) Runroom <runroom@runroom.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Runroom\SortableBehaviorBundle\Service;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ManagerRegistry;
use Gedmo\Sortable\SortableListener;
/**
* Position handler for Gedmo Sortable behavior.
*/
final class GedmoPositionHandler extends AbstractPositionHandler
{
/**
* @var array<string, int>
*/
private array $cacheLastPosition = [];
private ?EntityManagerInterface $entityManager = null;
/**
* Constructor, for dependency injection.
*/
public function __construct(
private readonly ManagerRegistry $registry,
private readonly SortableListener $listener
) {
}
/**
* Get the entity previous position.
*
* @param object $entity
*
* @return int
*/
public function getLastPosition(object $entity): int
{
$meta = $this->getEntityManager($entity::class)->getClassMetadata($entity::class);
/**
* @var array{ useObjectClass: string, position: string, groups?: class-string[] }
*/
$config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName());
/** @var array<string,mixed> $groups */
$groups = [];
if (isset($config['groups'])) {
foreach ($config['groups'] as $groupName) {
/** @psalm-suppress MixedAssignment */
$groups[$groupName] = $meta->getReflectionProperty($groupName)->getValue($entity);
}
}
$hash = $this->getHash($config, $groups);
if (!isset($this->cacheLastPosition[$hash])) {
$this->cacheLastPosition[$hash] = $this->queryLastPosition($config, $groups);
}
return $this->cacheLastPosition[$hash];
}
/**
* Get the position field name for the given entity.
*/
public function getPositionFieldByEntity($entity): string
{
if (\is_object($entity)) {
$entity = $entity::class;
}
$meta = $this->getEntityManager($entity)->getClassMetadata($entity);
/**
* @var array{position: string}
*/
$config = $this->listener->getConfiguration($this->getEntityManager($entity), $meta->getName());
return $config['position'];
}
/**
* @param array<string,string|class-string[]> $config
* @param array<string, mixed> $groups
* @psalm-param array{
* useObjectClass: string,
* position: string,
* groups?: class-string[]
* } $config
*/
private function getHash(array $config, array $groups): string
{
$data = $config['useObjectClass'];
/** @psalm-suppress MixedAssignment */
foreach ($groups as $groupName => $value) {
if ($value instanceof \DateTime) {
$value = $value->format('c');
}
if (\is_object($value)) {
$value = spl_object_hash($value);
}
if (is_scalar($value)) {
$data .= $groupName.$value;
continue;
}
// If value can't be converted to string, ignore it for hash.
$data .= $groupName;
}
return md5($data);
}
/**
* @param array<string,string|class-string[]> $config
* @param array<string, mixed> $groups
* @psalm-param array{
* useObjectClass: string,
* position: string,
* groups?: class-string[]
* } $config
*/
private function queryLastPosition(array $config, array $groups): int
{
$queryBuilder = $this->getEntityManager()->createQueryBuilder();
$queryBuilder->select(sprintf('MAX(n.%s)', $config['position']))
->from($config['useObjectClass'], 'n');
$index = 0;
/** @psalm-suppress MixedAssignment */
foreach ($groups as $groupName => $value) {
++$index;
if (null === $value) {
$queryBuilder->andWhere(sprintf('n.%s IS NULL', $groupName));
continue;
}
$queryBuilder->andWhere(sprintf('n.%s = :group_%s', $groupName, $index));
$queryBuilder->setParameter(sprintf('group_%s', $index), $value);
}
$query = $queryBuilder->getQuery();
$query->disableResultCache();
$lastPosition = $query->getSingleScalarResult();
\assert(is_numeric($lastPosition));
return (int) $lastPosition;
}
/**
* @param class-string|null $class
*
* @throws \RuntimeException if the entity manager is not found
*/
private function getEntityManager(string $class = null): EntityManagerInterface
{
if (null === $class) {
if (null === $this->entityManager) {
throw new \RuntimeException('Entity manager not set');
}
return $this->entityManager;
}
$entityManager = $this->registry->getManagerForClass($class);
if (!$entityManager instanceof EntityManagerInterface) {
throw new \RuntimeException(sprintf('Unable to find the entity manager for class "%s".', $class));
}
// Cache the entity manager for later use
$this->entityManager = $entityManager;
return $this->entityManager;
}
}
Your proposal is interesting. Isn't the Manager Registry aleready doing some sort of cache for getManagerForClass? Why do you need to add an extra cache layer?
I cache the entity manager because queryLastPosition method doesn't know of the entity class. It should probably be passed to the method from getLastPosition method, but I not sufficiently expert on this subject to predict side effects.
When using sortable-behavior-bundle package with Sonata Admin as described by 8. Sortable behavior in admin listing in a project with several entity managers, the following error appear:
This error is raised by
vendor/runroom-packages/sortable-behavior-bundle/src/Resources/views/sort.html.twig
line 2 :{% set current_position = currentObjectPosition(object) %}
The app use a modular monolith architecture with one database per module. The issue lies with the
ORMPositionHandler
andGedmoPositionHandler
services. They useEntityManagerInterface
, that is auto-wired with the first of default entity manager.It should auto-wire the Doctrine
ManagerRegistry
to get the correctEntityManager
for the class using thegetManagerForClass
method.Or the documentation should provide an example on how to create a custom Position Handler service for each entity manager.
Thank you for your work.