diff --git a/src/DataSource/Adapters/Bitrix/AbstractD7Repository.php b/src/DataSource/Adapters/Bitrix/AbstractD7Repository.php index 953369a..dbf4a74 100755 --- a/src/DataSource/Adapters/Bitrix/AbstractD7Repository.php +++ b/src/DataSource/Adapters/Bitrix/AbstractD7Repository.php @@ -14,7 +14,6 @@ use spaceonfire\Collection\CollectionInterface; use spaceonfire\Criteria\Criteria; use spaceonfire\Criteria\CriteriaInterface; -use spaceonfire\DataSource\Adapters\Bitrix\Heap\Heap; use spaceonfire\DataSource\Adapters\Bitrix\Query\D7Query; use spaceonfire\DataSource\EntityInterface; use spaceonfire\DataSource\Exceptions\NotFoundException; @@ -27,16 +26,40 @@ abstract class AbstractD7Repository implements RepositoryInterface { /** - * @var Heap + * @var EntityManagerInterface */ - private static $heap; + private $em; + /** + * @var bool + */ + private $tryFindOneByCriteriaInEntityManagerFirst = false; - private static function getHeap(): Heap + /** + * AbstractD7Repository constructor. + * @param EntityManagerInterface $em + */ + public function __construct(EntityManagerInterface $em) { - if (self::$heap === null) { - self::$heap = new Heap(); - } - return self::$heap; + $em->registerRepository($this, $this->getRole()); + $this->em = $em; + } + + /** + * Returns entity role + * @return string|null + */ + public function getRole(): ?string + { + return null; + } + + /** + * Enable option to try find entity in manager by criteria before making actual request to database in `findOne()` + * @param bool $value + */ + final protected function tryFindOneByCriteriaInEntityManagerFirst(bool $value = true): void + { + $this->tryFindOneByCriteriaInEntityManagerFirst = $value; } /** @@ -47,7 +70,7 @@ abstract protected function getDataManager(): string; /** * @return Entity */ - protected function getEntity(): Entity + final protected function getD7Entity(): Entity { try { return $this->getDataManager()::getEntity(); @@ -67,7 +90,7 @@ abstract public function getMapper(): MapperInterface; final protected function query(): QueryInterface { try { - return new D7Query($this->getDataManager()::query(), $this->getMapper(), self::getHeap()); + return new D7Query($this->getDataManager()::query(), $this->getMapper(), $this->em); } catch (SystemException $e) { throw new RuntimeException($e->getMessage(), $e->getCode(), $e); } @@ -78,10 +101,12 @@ final protected function query(): QueryInterface */ public function save($entity): void { - $oldData = self::getHeap()->get($entity); + $oldData = $this->em->getEntityData($entity); $data = $this->getMapper()->extract($entity); - $primary = $oldData !== null ? ORMTools::extractPrimary($this->getEntity(), $oldData) : null; + $primary = $oldData !== null ? ORMTools::extractPrimary($this->getD7Entity(), $oldData) : null; + + // TODO: detect changes between $oldData and $data update only by diff ORMTools::wrapTransaction(function () use ($primary, &$data): void { $result = $primary @@ -96,7 +121,7 @@ public function save($entity): void }); $data = array_merge($oldData ?? [], $data); - self::getHeap()->attach($entity, $data); + $this->em->attachEntity($entity, $data); $this->getMapper()->hydrate($entity, $data); } @@ -105,12 +130,12 @@ public function save($entity): void */ public function remove($entity): void { - $oldData = self::getHeap()->get($entity); + $oldData = $this->em->getEntityData($entity); if ($oldData === null) { return; } - $primary = ORMTools::extractPrimary($this->getEntity(), $oldData); + $primary = ORMTools::extractPrimary($this->getD7Entity(), $oldData); ORMTools::wrapTransaction(function () use ($primary): void { $result = $this->getDataManager()::delete($primary); @@ -120,7 +145,7 @@ public function remove($entity): void } }); - self::getHeap()->detach($entity); + $this->em->detachEntity($entity); } /** @@ -128,7 +153,7 @@ public function remove($entity): void */ public function findByPrimary($primary) { - $primaryKeys = array_values($this->getEntity()->getPrimaryArray()); + $primaryKeys = array_values($this->getD7Entity()->getPrimaryArray()); $primary = is_array($primary) ? $primary : [$primary]; @@ -138,9 +163,11 @@ public function findByPrimary($primary) $criteria = new Criteria(); $expr = Criteria::expr(); + $mapper = $this->getMapper(); + $indexData = []; foreach ($primaryKeys as $i => $key) { - $domainKey = $this->getMapper()->convertNameToDomain($key); + $domainKey = $mapper->convertNameToDomain($key); foreach ([$domainKey, $key, $i] as $k) { if (isset($primary[$k])) { @@ -153,7 +180,12 @@ public function findByPrimary($primary) throw new InvalidArgumentException('Invalid primary'); } - $criteria->andWhere($expr->property($key, $expr->same($val))); + $criteria->andWhere($expr->property($domainKey, $expr->same($val))); + $indexData[$key] = $mapper->convertValueToStorage($domainKey, $val); + } + + if (null !== $entity = $this->em->getByIndex($this, $indexData)) { + return $entity; } $entity = $this->findOne($criteria); @@ -184,6 +216,14 @@ public function findAll(?CriteriaInterface $criteria = null): CollectionInterfac */ public function findOne(?CriteriaInterface $criteria = null) { + if ( + $this->tryFindOneByCriteriaInEntityManagerFirst && + $criteria !== null && + null !== $entity = $this->em->getByCriteria($this, $criteria) + ) { + return $entity; + } + $query = $this->query(); if ($criteria !== null) { diff --git a/src/DataSource/Adapters/Bitrix/AbstractMapper.php b/src/DataSource/Adapters/Bitrix/AbstractMapper.php index 105a066..e6215ef 100755 --- a/src/DataSource/Adapters/Bitrix/AbstractMapper.php +++ b/src/DataSource/Adapters/Bitrix/AbstractMapper.php @@ -27,7 +27,13 @@ public function __construct() * @param array $data * @return string */ - abstract protected function resolveClass(array $data): string; + abstract public function resolveClass(array $data): string; + + /** + * Returns array of keys which will be used to index entity by them + * @return string[] + */ + abstract public function getUniqueIndexes(): array; /** * @inheritDoc diff --git a/src/DataSource/Adapters/Bitrix/EntityManager.php b/src/DataSource/Adapters/Bitrix/EntityManager.php new file mode 100644 index 0000000..c5867e5 --- /dev/null +++ b/src/DataSource/Adapters/Bitrix/EntityManager.php @@ -0,0 +1,283 @@ +|mixed[] + */ + private $heap; + /** + * @var array|RepositoryInterface[] + */ + private $roleToRepositoryMap = []; + /** + * @var array|string[] + */ + private $roleToEntityClassMap = []; + /** + * @var array + */ + private $collections = []; + /** + * @var array + */ + private $indexes = []; + + /** + * EntityManager constructor. + */ + public function __construct() + { + $this->heap = new SplObjectStorage(); + } + + /** + * @inheritDoc + */ + public function resolveRole($role): string + { + if ($role instanceof MapperInterface) { + $role = $this->heap[$role]; + } + + if ($role instanceof RepositoryInterface) { + return $this->heap[$role]; + } + + if ($role instanceof EntityInterface) { + $role = get_class($role); + } + + if (is_subclass_of($role, EntityInterface::class)) { + $entityClassRoleMap = array_flip($this->roleToEntityClassMap); + + if (isset($entityClassRoleMap[$role])) { + return $entityClassRoleMap[$role]; + } + + foreach ($this->roleToEntityClassMap as $r => $entityClass) { + if (is_subclass_of($role, $entityClass)) { + return $r; + } + } + } + + if (is_string($role)) { + if (!isset($this->roleToRepositoryMap[$role])) { + throw new RuntimeException(sprintf('No repository registered for role "%s"', $role)); + } + + return $role; + } + + throw new RuntimeException(sprintf('Unable to resolve role from "%s"', get_debug_type($role))); + } + + /** + * @inheritDoc + */ + public function registerRepository(RepositoryInterface $repository, ?string $role = null): void + { + /** @var AbstractMapper $mapper */ + Assert::isInstanceOf($mapper = $repository->getMapper(), AbstractMapper::class); + $entityClass = $mapper->resolveClass([]); + + $role = $role ?? $entityClass; + + if (isset($this->roleToRepositoryMap[$role])) { + if ($this->roleToRepositoryMap[$role] === $repository) { + return; + } + + throw new RuntimeException(sprintf('Repository already registered for role "%s"', $role)); + } + + $this->roleToRepositoryMap[$role] = $repository; + $this->roleToEntityClassMap[$role] = $entityClass; + $this->indexes[$role] = []; + + $this->heap[$repository] = $role; + $this->heap[$mapper] = $repository; + } + + /** + * @inheritDoc + */ + public function attachEntity(EntityInterface $entity, array $data): void + { + $role = $this->resolveRole($entity); + + // Attach data to heap + $this->heap->offsetSet($entity, $data); + + // Attach entity to collection + $this->getEntityCollection($role)->offsetSet(null, $entity); + + // Index entity + [$indexKey, $indexValue] = $this->createIndex($role, $data); + if (!empty($indexKey) && !empty($indexValue)) { + $this->indexes[$role][$indexKey][$indexValue] = $entity; + } + } + + private function createIndex($role, array $data): array + { + $role = $this->resolveRole($role); + $mapper = $this->getMapper($role); + + $indexKey = array_map(static function (string $key) use ($mapper) { + return $mapper->convertNameToStorage($key); + }, $mapper->getUniqueIndexes()); + $indexValue = []; + foreach ($indexKey as $k) { + $indexValue[] = $data[$k]; + } + + $indexKey = implode(',', $indexKey); + $indexValue = implode(',', $indexValue); + + return [$indexKey, $indexValue]; + } + + private function getEntityCollection($role): CollectionInterface + { + $role = $this->resolveRole($role); + + if (!isset($this->collections[$role])) { + $this->collections[$role] = new TypedCollection( + new IndexedCollection([], static function ($object) { + return spl_object_hash($object); + }), + new InstanceOfType($this->roleToEntityClassMap[$role]) + ); + } + + return $this->collections[$role]; + } + + /** + * @inheritDoc + */ + public function detachEntity(EntityInterface $entity): void + { + $role = $this->resolveRole($entity); + + // Unset entity index + [$indexKey, $indexValue] = $this->createIndex($role, $this->getEntityData($entity)); + unset($this->indexes[$role][$indexKey][$indexValue]); + + // Remove entity from collection + $hash = spl_object_hash($entity); + $this->getEntityCollection($role)->offsetUnset($hash); + + // Remove data from heap + $this->heap->offsetUnset($entity); + } + + /** + * @inheritDoc + */ + public function getEntityData(EntityInterface $entity): ?array + { + try { + return $this->heap->offsetGet($entity); + } catch (UnexpectedValueException $e) { + return null; + } + } + + /** + * @inheritDoc + */ + public function getRepository($role): RepositoryInterface + { + $role = $this->resolveRole($role); + return $this->roleToRepositoryMap[$role]; + } + + /** + * @inheritDoc + */ + public function getMapper($role): MapperInterface + { + $role = $this->resolveRole($role); + return $this->roleToRepositoryMap[$role]->getMapper(); + } + + /** + * @inheritDoc + */ + public function getByIndex($role, array $data): ?EntityInterface + { + $role = $this->resolveRole($role); + + [$indexKey, $indexValue] = $this->createIndex($role, $data); + + if (!empty($indexKey) && !empty($indexValue)) { + return $this->indexes[$role][$indexKey][$indexValue] ?? null; + } + + return null; + } + + /** + * @inheritDoc + */ + public function make($role, array $data): EntityInterface + { + $role = $this->resolveRole($role); + + if (null !== $entity = $this->getByIndex($role, $data)) { + return $entity; + } + + $mapper = $this->getMapper($role); + + [$entity, $data] = $mapper->init($data); + + if (!$entity instanceof EntityInterface) { + throw new RuntimeException('Associated with repository class must implement ' . EntityInterface::class); + } + + $mapper->hydrate($entity, $data); + + $this->attachEntity($entity, $data); + + return $entity; + } + + /** + * @inheritDoc + */ + public function getByCriteria($role, CriteriaInterface $criteria): ?EntityInterface + { + if (null === $expr = $criteria->getWhere()) { + return null; + } + + $role = $this->resolveRole($role); + + $collection = $this->getEntityCollection($role); + + return $collection->find(static function ($object) use ($expr) { + return $expr->evaluate($object); + }); + } +} diff --git a/src/DataSource/Adapters/Bitrix/EntityManagerInterface.php b/src/DataSource/Adapters/Bitrix/EntityManagerInterface.php new file mode 100644 index 0000000..a8427c4 --- /dev/null +++ b/src/DataSource/Adapters/Bitrix/EntityManagerInterface.php @@ -0,0 +1,83 @@ +storage = new SplObjectStorage(); - } - - public function has(EntityInterface $entity): bool - { - return $this->storage->offsetExists($entity); - } - - public function get(EntityInterface $entity): ?array - { - try { - return $this->storage->offsetGet($entity); - } catch (UnexpectedValueException $e) { - return null; - } - } - - /** - * @param EntityInterface $entity - * @param array $data - */ - public function attach(EntityInterface $entity, array $data): void - { - $this->storage->offsetSet($entity, $data); - } - - /** - * @param EntityInterface $entity - */ - public function detach(EntityInterface $entity): void - { - $this->storage->offsetUnset($entity); - } - - /** - * @inheritDoc - * @return SplObjectStorage - */ - public function getIterator(): SplObjectStorage - { - return $this->storage; - } -} diff --git a/src/DataSource/Adapters/Bitrix/Query/D7Query.php b/src/DataSource/Adapters/Bitrix/Query/D7Query.php index 182eaa9..4a74e64 100755 --- a/src/DataSource/Adapters/Bitrix/Query/D7Query.php +++ b/src/DataSource/Adapters/Bitrix/Query/D7Query.php @@ -15,6 +15,8 @@ use spaceonfire\Collection\TypedCollection; use spaceonfire\Criteria\Adapter\SpiralPagination\PaginableCriteria; use spaceonfire\Criteria\CriteriaInterface; +use spaceonfire\DataSource\Adapters\Bitrix\EntityManager; +use spaceonfire\DataSource\Adapters\Bitrix\EntityManagerInterface; use spaceonfire\DataSource\Adapters\Bitrix\Heap\Heap; use spaceonfire\DataSource\EntityInterface; use spaceonfire\DataSource\MapperInterface; @@ -31,23 +33,23 @@ final class D7Query implements QueryInterface */ private $mapper; /** - * @var Heap + * @var EntityManagerInterface */ - private $heap; + private $em; /** * D7Query constructor. * @param Query $query * @param MapperInterface $mapper - * @param Heap $heap + * @param EntityManagerInterface $em */ - public function __construct(Query $query, MapperInterface $mapper, Heap $heap) + public function __construct(Query $query, MapperInterface $mapper, EntityManagerInterface $em) { $query->setSelect(['*']); $this->query = $query; $this->mapper = $mapper; - $this->heap = $heap; + $this->em = $em; } /** @@ -76,7 +78,7 @@ public function fetchOne(): ?EntityInterface try { $data = $this->query->fetch(); - return is_array($data) ? $this->createEntity($data) : null; + return is_array($data) ? $this->em->make($this->mapper, $data) : null; } catch (SystemException $e) { throw new RuntimeException($e->getMessage(), $e->getCode(), $e); } @@ -93,7 +95,7 @@ public function fetchAll(): CollectionInterface $entitiesCollection = new TypedCollection([], EntityInterface::class); foreach ($rawItems as $item) { - $entitiesCollection[] = $this->createEntity($item); + $entitiesCollection[] = $this->em->make($this->mapper, $item); } return $entitiesCollection; @@ -163,17 +165,4 @@ public function count(): int throw new RuntimeException($e->getMessage(), $e->getCode(), $e); } } - - private function createEntity(array $data): EntityInterface - { - [$entity, $data] = $this->mapper->init($data); - $this->mapper->hydrate($entity, $data); - - if ($entity instanceof EntityInterface) { - $this->heap->attach($entity, $data); - return $entity; - } - - throw new RuntimeException('Associated with repository class must implement ' . EntityInterface::class); - } }