diff --git a/application/Espo/ORM/BaseEntity.php b/application/Espo/ORM/BaseEntity.php index c0f538b1d8..ba637e3030 100644 --- a/application/Espo/ORM/BaseEntity.php +++ b/application/Espo/ORM/BaseEntity.php @@ -31,6 +31,7 @@ namespace Espo\ORM; use Espo\ORM\Relation\EmptyRelations; use Espo\ORM\Relation\Relations; +use Espo\ORM\Type\AttributeType; use Espo\ORM\Type\RelationType; use Espo\ORM\Value\ValueAccessorFactory; use Espo\ORM\Value\ValueAccessor; @@ -139,6 +140,8 @@ class BaseEntity implements Entity public function reset(): void { $this->valuesContainer = []; + + $this->relations->resetAll(); } /** @@ -458,6 +461,29 @@ class BaseEntity implements Entity } $this->setInContainer($attribute, $preparedValue); + + $type = $this->getAttributeType($attribute); + + if ( + $type === AttributeType::FOREIGN_ID || + $type === AttributeType::FOREIGN && + $this->isAttributeHasOneForeignId($attribute) + // @todo Move the logic for hasOne to Espo\Core\ORM\Entity ? + ) { + $this->relations->reset(substr($attribute, 0, -2)); + } else if ($type === AttributeType::FOREIGN_TYPE) { + $this->relations->reset(substr($attribute, 0, -4)); + } + } + + private function isAttributeHasOneForeignId(string $attribute): bool + { + $type = $this->getAttributeType($attribute); + + return $type === AttributeType::FOREIGN && + $this->getAttributeParam($attribute, 'relation') === substr($attribute, 0, -2) && + $this->getAttributeParam($attribute, 'foreign') === 'id' && + str_ends_with($attribute, 'Id'); } protected function prepareAttributeValue(string $attribute, mixed $value): mixed @@ -945,7 +971,7 @@ class BaseEntity implements Entity } /** - * Copy all current values to fetched values. All current attribute values will beset as those + * Copy all current values to fetched values. All current attribute values will be set as those * that are fetched from DB. */ public function updateFetchedValues(): void @@ -1117,12 +1143,13 @@ class BaseEntity implements Entity continue; } - if ($attribute == 'id') { + if ($attribute === 'id') { $this->id = $data[$attribute]; continue; } + // @todo Revise. To remove? if ($onlyAccessible && $this->getAttributeParam($attribute, 'notAccessible')) { continue; } diff --git a/application/Espo/ORM/EntityManager.php b/application/Espo/ORM/EntityManager.php index ae2788e5d5..145c9fe2a4 100644 --- a/application/Espo/ORM/EntityManager.php +++ b/application/Espo/ORM/EntityManager.php @@ -281,7 +281,7 @@ class EntityManager throw new RuntimeException("Can't refresh a non-existent entity."); } - $this->relationsMap->get($entity)?->reset(); + $this->relationsMap->get($entity)?->resetAll(); $entity->set($fetchedEntity->getValueMap()); $entity->setAsFetched(); diff --git a/application/Espo/ORM/Relation/EmptyRelations.php b/application/Espo/ORM/Relation/EmptyRelations.php index 803f91355d..6b76e18278 100644 --- a/application/Espo/ORM/Relation/EmptyRelations.php +++ b/application/Espo/ORM/Relation/EmptyRelations.php @@ -31,17 +31,60 @@ namespace Espo\ORM\Relation; use Espo\ORM\Entity; use Espo\ORM\EntityCollection; +use LogicException; +use RuntimeException as RuntimeExceptionAlias; class EmptyRelations implements Relations { + /** @var array|null> */ + private array $setData = []; + public function __construct() {} - public function reset(): void - {} + public function resetAll(): void + { + $this->setData = []; + } + + public function reset(string $relation): void + { + unset($this->setData[$relation]); + } + + /** + * @param Entity|EntityCollection|null $related + */ + public function set(string $relation, Entity|EntityCollection|null $related): void + { + $this->setData[$relation] = $related; + } + + public function isSet(string $relation): bool + { + return array_key_exists($relation, $this->setData); + } + + /** + * @return Entity|EntityCollection|null + */ + public function getSet(string $relation): Entity|EntityCollection|null + { + if (!array_key_exists($relation, $this->setData)) { + throw new RuntimeExceptionAlias("Relation '$relation' is not set."); + } + + return $this->setData[$relation]; + } public function getOne(string $relation): ?Entity { - return null; + $entity = $this->setData[$relation] ?? null; + + if ($entity instanceof EntityCollection) { + throw new LogicException("Not an entity."); + } + + return $entity; } /*** @@ -49,7 +92,13 @@ class EmptyRelations implements Relations */ public function getMany(string $relation): EntityCollection { + $collection = $this->setData[$relation] ?? new EntityCollection(); + + if (!$collection instanceof EntityCollection) { + throw new LogicException("Not a collection."); + } + /** @var EntityCollection */ - return new EntityCollection(); + return $collection; } } diff --git a/application/Espo/ORM/Relation/RDBRelations.php b/application/Espo/ORM/Relation/RDBRelations.php index 5a4d810f31..13dd0b23d8 100644 --- a/application/Espo/ORM/Relation/RDBRelations.php +++ b/application/Espo/ORM/Relation/RDBRelations.php @@ -29,23 +29,33 @@ namespace Espo\ORM\Relation; -use Espo\ORM\Collection; use Espo\ORM\Entity; use Espo\ORM\EntityCollection; use Espo\ORM\EntityManager; use Espo\ORM\Query\Part\Order; use Espo\ORM\Type\RelationType; +use InvalidArgumentException; use LogicException; +use RuntimeException; /** * @internal */ class RDBRelations implements Relations { - /** @var array|null> */ + /** @var array|null> */ private array $data = []; + /** @var array|null> */ + private array $setData = []; private ?Entity $entity = null; + /** @var string[] */ + private array $manyTypeList = [ + RelationType::MANY_MANY, + RelationType::HAS_MANY, + RelationType::HAS_CHILDREN, + ]; + public function __construct( private EntityManager $entityManager, ) {} @@ -59,16 +69,68 @@ class RDBRelations implements Relations $this->entity = $entity; } - public function reset(): void + public function reset(string $relation): void + { + unset($this->data[$relation]); + unset($this->setData[$relation]); + } + + public function resetAll(): void { $this->data = []; + $this->setData = []; + } + + public function isSet(string $relation): bool + { + return array_key_exists($relation, $this->setData); + } + + /** + * @return Entity|EntityCollection|null + */ + public function getSet(string $relation): Entity|EntityCollection|null + { + if (!array_key_exists($relation, $this->setData)) { + throw new RuntimeException("Relation '$relation' is not set."); + } + + return $this->setData[$relation]; + } + + /** + * @param Entity|EntityCollection|null $related + */ + public function set(string $relation, Entity|EntityCollection|null $related): void + { + $type = $this->getRelationType($relation); + + if (!$type) { + throw new LogicException("Relation '$relation' does not exist."); + } + + if (in_array($type, $this->manyTypeList) && !$related instanceof EntityCollection) { + throw new InvalidArgumentException("Non-collection passed for relation '$relation'."); + } + + if ( + !in_array($type, [ + RelationType::BELONGS_TO, + RelationType::BELONGS_TO_PARENT, + RelationType::HAS_ONE, + ]) + ) { + throw new LogicException("Relation type '$type' is not supported for setting."); + } + + $this->setData[$relation] = $related; } public function getOne(string $relation): ?Entity { $entity = $this->get($relation); - if ($entity instanceof Collection) { + if ($entity instanceof EntityCollection) { throw new LogicException("Not an entity. Use `getMany` instead."); } @@ -96,16 +158,16 @@ class RDBRelations implements Relations */ private function get(string $relation): Entity|EntityCollection|null { + if (array_key_exists($relation, $this->setData)) { + return $this->setData[$relation]; + } + if (!array_key_exists($relation, $this->data)) { if (!$this->entity) { throw new LogicException("No entity set."); } - $isMany = in_array($this->getRelationType($relation), [ - RelationType::MANY_MANY, - RelationType::HAS_MANY, - RelationType::HAS_CHILDREN, - ]); + $isMany = in_array($this->getRelationType($relation), $this->manyTypeList); $this->data[$relation] = $isMany ? $this->findMany($relation) : @@ -114,7 +176,7 @@ class RDBRelations implements Relations $object = $this->data[$relation]; - if ($object instanceof Collection) { + if ($object instanceof EntityCollection) { /** @var EntityCollection $object */ $object = new EntityCollection(iterator_to_array($object)); } @@ -183,7 +245,7 @@ class RDBRelations implements Relations return $collection; } - private function getRelationType(string $relation): string + private function getRelationType(string $relation): ?string { if (!$this->entity) { throw new LogicException(); @@ -192,7 +254,7 @@ class RDBRelations implements Relations return $this->entityManager ->getDefs() ->getEntity($this->entity->getEntityType()) - ->getRelation($relation) - ->getType(); + ->tryGetRelation($relation) + ?->getType(); } } diff --git a/application/Espo/ORM/Relation/Relations.php b/application/Espo/ORM/Relation/Relations.php index 5e84d02d9f..a5138854fe 100644 --- a/application/Espo/ORM/Relation/Relations.php +++ b/application/Espo/ORM/Relation/Relations.php @@ -33,14 +33,31 @@ use Espo\ORM\Entity; use Espo\ORM\EntityCollection; /** - * @internal + * @internal Not ready for production. */ interface Relations { /** - * Reset. + * Reset a specific relation. */ - public function reset(): void; + public function reset(string $relation): void; + + /** + * Reset all. + */ + public function resetAll(): void; + + /** + * @param Entity|EntityCollection|null $related + */ + public function set(string $relation, Entity|EntityCollection|null $related): void; + + public function isSet(string $relation): bool; + + /** + * @return Entity|EntityCollection|null + */ + public function getSet(string $relation): Entity|EntityCollection|null; /** * Get one related record. For has-one, belongs-to. diff --git a/application/Espo/ORM/Repository/RDBRepository.php b/application/Espo/ORM/Repository/RDBRepository.php index cb57281b70..d0b047f5cf 100644 --- a/application/Espo/ORM/Repository/RDBRepository.php +++ b/application/Espo/ORM/Repository/RDBRepository.php @@ -182,7 +182,7 @@ class RDBRepository implements Repository $entity->setAsNotBeingSaved(); } - $this->relationsMap?->get($entity)?->reset(); + $this->relationsMap?->get($entity)?->resetAll(); } /**