. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ namespace Espo\Core\ORM; use Espo\Core\Field\Link; use Espo\Core\Field\LinkParent; use Espo\Core\Name\Field; use Espo\Core\ORM\Defs\AttributeParam; use Espo\ORM\BaseEntity; use Espo\ORM\Defs\Params\RelationParam; use Espo\ORM\Entity as OrmEntity; use Espo\ORM\Name\Attribute; use Espo\ORM\Query\Part\Order; use Espo\ORM\Type\AttributeType; use Espo\ORM\Type\RelationType; use LogicException; use stdClass; /** * An entity. */ class Entity extends BaseEntity { /** * Has a link-multiple field. */ public function hasLinkMultipleField(string $field): bool { return $this->hasRelation($field) && $this->getAttributeParam($field . 'Ids', AttributeParam::IS_LINK_MULTIPLE_ID_LIST); } /** * Has a link field. */ public function hasLinkField(string $field): bool { return $this->hasAttribute($field . 'Id') && $this->hasRelation($field); } /** * Has a link-parent field. */ public function hasLinkParentField(string $field): bool { return $this->getAttributeType($field . 'Type') === AttributeType::FOREIGN_TYPE && $this->hasAttribute($field . 'Id'); } /** * Load a parent-name field. */ public function loadParentNameField(string $field): void { if (!$this->hasLinkParentField($field)) { throw new LogicException("Called `loadParentNameField` on non-link-parent field `$field`."); } $idAttribute = $field . 'Id'; $nameAttribute = $field . 'Name'; $parentId = $this->get($idAttribute); $parentType = $this->get($field . 'Type'); if (!$this->entityManager) { throw new LogicException("No entity-manager."); } $toSetFetched = !$this->isNew() && !$this->isAttributeChanged($idAttribute); if (!$parentId || !$parentType) { /** @noinspection PhpRedundantOptionalArgumentInspection */ $this->set($nameAttribute, null); if ($toSetFetched) { $this->setFetched($nameAttribute, null); } return; } if (!$this->entityManager->hasRepository($parentType)) { return; } $repository = $this->entityManager->getRDBRepository($parentType); $select = [Attribute::ID, Field::NAME]; $foreignEntity = $repository ->select($select) ->where([Attribute::ID => $parentId]) ->findOne(); $entityName = $foreignEntity ? $foreignEntity->get(Field::NAME) : null; $this->set($nameAttribute, $entityName); if ($toSetFetched) { $this->setFetched($nameAttribute, $entityName); } } /** * @param string $link * @return ?array{ * orderBy: string|array|null, * order: ?string, * } */ protected function getRelationOrderParams(string $link): ?array { $field = $link; $idsAttribute = $field . 'Ids'; $foreignEntityType = $this->getRelationParam($field, RelationParam::ENTITY); if ($this->getAttributeParam($idsAttribute, 'orderBy')) { $defs = [ 'orderBy' => $this->getAttributeParam($idsAttribute, 'orderBy'), 'order' => Order::ASC, ]; if ($this->getAttributeParam($idsAttribute, 'orderDirection')) { $defs['order'] = $this->getAttributeParam($idsAttribute, 'orderDirection'); } return $defs; } if ($this->getRelationParam($link, 'orderBy')) { $defs = [ 'orderBy' => $this->getRelationParam($link, 'orderBy'), 'order' => Order::ASC, ]; if ($this->getRelationParam($link, 'order')) { $defs['order'] = strtoupper($this->getRelationParam($link, 'order')); } return $defs; } if (!$foreignEntityType || !$this->entityManager) { return null; } $ormDefs = $this->entityManager->getMetadata()->getDefs(); if (!$ormDefs->hasEntity($foreignEntityType)) { return null; } $entityDefs = $ormDefs->getEntity($foreignEntityType); $collectionDefs = $entityDefs->getParam('collection') ?? []; $orderBy = $collectionDefs['orderBy'] ?? null; $order = $collectionDefs['order'] ?? 'ASC'; if (!$orderBy) { return null; } if (!$entityDefs->hasAttribute($orderBy)) { return null; } return [ 'orderBy' => $orderBy, 'order' => $order, ]; } /** * Load a link-multiple field. Should be used wisely. Consider using `getLinkMultipleIdList` instead. * * @param ?array $columns Deprecated as of v9.0. * @todo Add a method to load and set only fetched values? * @internal */ public function loadLinkMultipleField(string $field, ?array $columns = null): void { if (!$this->hasLinkMultipleField($field)) { throw new LogicException("Called `loadLinkMultipleField` on non-link-multiple field `$field`."); } if (!$this->entityManager) { throw new LogicException("No entity-manager."); } $select = [Attribute::ID, Field::NAME]; $hasType = $this->hasAttribute($field . 'Types'); if ($hasType) { $select[] = 'type'; } $columns ??= $this->getLinkMultipleColumnsFromDefs($field); if ($columns) { foreach ($columns as $it) { $select[] = $it; } } $selectBuilder = $this->entityManager ->getRDBRepository($this->getEntityType()) ->getRelation($this, $field) ->select($select); $orderBy = null; $order = null; $orderParams = $this->getRelationOrderParams($field); if ($orderParams) { $orderBy = $orderParams['orderBy'] ?? null; /** @var string|bool|null $order */ $order = $orderParams['order'] ?? null; } if ($orderBy) { if (is_string($orderBy) && !in_array($orderBy, $select)) { $selectBuilder->select($orderBy); } if (is_string($order)) { $order = strtoupper($order); if ($order !== Order::ASC && $order !== Order::DESC) { $order = Order::ASC; } } $selectBuilder->order($orderBy, $order); } $collection = $selectBuilder->find(); $ids = []; $names = (object) []; $types = (object) []; $columnsData = (object) []; foreach ($collection as $e) { $id = $e->getId(); $ids[] = $id; $names->$id = $e->get(Field::NAME); if ($hasType) { $types->$id = $e->get('type'); } if (!$columns) { continue; } $columnsData->$id = (object) []; foreach ($columns as $column => $foreignAttribute) { $columnsData->$id->$column = $e->get($foreignAttribute); } } $idsAttribute = $field . 'Ids'; $namesAttribute = $field . 'Names'; $typesAttribute = $field . 'Types'; $columnsAttribute = $field . 'Columns'; $toSetFetched = !$this->isNew() && !$this->hasFetched($idsAttribute); $this->setInContainerNotWritten($idsAttribute, $ids); $this->setInContainerNotWritten($namesAttribute, $names); if ($toSetFetched) { $this->setFetched($idsAttribute, $ids); $this->setFetched($namesAttribute, $names); } if ($hasType) { $this->set($typesAttribute, $types); if ($toSetFetched) { $this->setFetched($typesAttribute, $types); } } if ($columns) { $this->setInContainerNotWritten($columnsAttribute, $columnsData); if ($toSetFetched) { $this->setFetched($columnsAttribute, $columnsData); } } } /** * Load a link field. If a value is already set, it will set only a fetched value. */ public function loadLinkField(string $field): void { if (!$this->hasLinkField($field)) { throw new LogicException("Called `loadLinkField` on non-link field '$field'."); } if ( $this->getRelationType($field) !== RelationType::HAS_ONE && $this->getRelationType($field) !== RelationType::BELONGS_TO ) { throw new LogicException("Can't load link '$field'."); } if (!$this->entityManager) { throw new LogicException("No entity-manager."); } $select = [Attribute::ID, Field::NAME]; $entity = $this->entityManager ->getRelation($this, $field) ->select($select) ->findOne(); $entityId = null; $entityName = null; if ($entity) { $entityId = $entity->getId(); $entityName = $entity->get(Field::NAME); } $idAttribute = $field . 'Id'; $nameAttribute = $field . 'Name'; if (!$this->isNew() && !$this->hasFetched($idAttribute)) { $this->setFetched($idAttribute, $entityId); $this->setFetched($nameAttribute, $entityName); } if ($this->has($idAttribute)) { return; } $this->setInContainerNotWritten($idAttribute, $entityId); $this->setInContainerNotWritten($nameAttribute, $entityName); } /** * Get a link-multiple name. */ public function getLinkMultipleName(string $field, string $id): ?string { $namesAttribute = $field . 'Names'; if (!$this->hasAttribute($namesAttribute)) { throw new LogicException("Called `getLinkMultipleName` on non-link-multiple field `$field."); } if (!$this->has($namesAttribute) && !$this->isNew()) { $this->loadLinkMultipleField($field); } $object = $this->get($namesAttribute) ?? (object) []; if (!$object instanceof stdClass) { throw new LogicException("Non-object value in `$namesAttribute`."); } return $object->$id ?? null; } /** * Set a link-multiple name. * * @return static As of v9.2. */ public function setLinkMultipleName(string $field, string $id, ?string $value): static { $namesAttribute = $field . 'Names'; if (!$this->hasAttribute($namesAttribute)) { throw new LogicException("Called `setLinkMultipleName` on non-link-multiple field `$field."); } if (!$this->has($namesAttribute)) { return $this; } $object = $this->get($namesAttribute) ?? (object) []; if (!$object instanceof stdClass) { throw new LogicException("Non-object value in `$namesAttribute`."); } $object->$id = $value; $this->set($namesAttribute, $object); return $this; } /** * Get a link-multiple column value. */ public function getLinkMultipleColumn(string $field, string $column, string $id): mixed { $columnsAttribute = $field . 'Columns'; if (!$this->hasAttribute($columnsAttribute)) { throw new LogicException("Called `getLinkMultipleColumn` on not supported field `$field."); } if (!$this->has($columnsAttribute) && !$this->isNew()) { $this->loadLinkMultipleField($field); } $object = $this->get($columnsAttribute) ?? (object) []; if (!$object instanceof stdClass) { throw new LogicException("Non-object value in `$columnsAttribute`."); } return $object->$id->$column ?? null; } /** * Set a link-multiple column value. * * @return static As of v9.2. */ public function setLinkMultipleColumn(string $field, string $column, string $id, mixed $value): static { $columnsAttribute = $field . 'Columns'; if (!$this->hasAttribute($columnsAttribute)) { throw new LogicException("Called `setLinkMultipleColumn` on non-link-multiple field `$field."); } if (!$this->has($columnsAttribute) && !$this->isNew()) { $this->loadLinkMultipleField($field); } $object = $this->get($columnsAttribute) ?? (object) []; if (!$object instanceof stdClass) { throw new LogicException("Non-object value in `$columnsAttribute`."); } $object->$id ??= (object) []; $object->$id->$column = $value; $this->set($columnsAttribute, $object); return $this; } /** * Set link-multiple IDs. * * @param string[] $idList * @return static As of v9.2. */ public function setLinkMultipleIdList(string $field, array $idList): static { $idsAttribute = $field . 'Ids'; if (!$this->hasAttribute($idsAttribute)) { throw new LogicException("Called `setLinkMultipleIdList` on non-link-multiple field `$field."); } $this->set($idsAttribute, $idList); return $this; } /** * Add an ID to a link-multiple field. * * @return static As of v9.2. */ public function addLinkMultipleId(string $field, string $id): static { $idsAttribute = $field . 'Ids'; if (!$this->hasAttribute($idsAttribute)) { throw new LogicException("Called `addLinkMultipleId` on non-link-multiple field `$field."); } if (!$this->has($idsAttribute)) { if (!$this->isNew()) { $this->loadLinkMultipleField($field); } else { $this->set($idsAttribute, []); } } if (!$this->has($idsAttribute)) { return $this; } $idList = $this->get($idsAttribute); if ($idList === null) { throw new LogicException("Null value set in `$idsAttribute`."); } if (!is_array($idList)) { throw new LogicException("Non-array value set in `$idsAttribute`."); } if (in_array($id, $idList)) { return $this; } $idList[] = $id; $this->set($idsAttribute, $idList); return $this; } /** * Remove an ID from link-multiple field. * * @return static As of v9.2. */ public function removeLinkMultipleId(string $field, string $id): static { if (!$this->hasLinkMultipleId($field, $id)) { return $this; } $list = $this->getLinkMultipleIdList($field); $index = array_search($id, $list); if ($index !== false) { unset($list[$index]); $list = array_values($list); } $this->setLinkMultipleIdList($field, $list); return $this; } /** * Get link-multiple field IDs. * * @return string[] */ public function getLinkMultipleIdList(string $field): array { $idsAttribute = $field . 'Ids'; if (!$this->hasAttribute($idsAttribute)) { throw new LogicException("Called `getLinkMultipleIdList` for non-link-multiple field `$field."); } if (!$this->has($idsAttribute) && !$this->isNew()) { $this->loadLinkMultipleField($field); } /** @var string[] */ return $this->get($idsAttribute) ?? []; } /** * Get previous link-multiple field IDs. * * @return string[] * @since 9.1.0 */ public function getFetchedLinkMultipleIdList(string $field): array { $idsAttribute = $field . 'Ids'; if (!$this->hasAttribute($idsAttribute)) { throw new LogicException("Called `getFetchedLinkMultipleIdList` for non-link-multiple field `$field."); } if (!$this->isNew()) { if (!$this->has($idsAttribute)) { $this->loadLinkMultipleField($field); } else if (!$this->hasFetched($field)) { // Set but not loaded. $attributes = [ $field . 'Ids', $field . 'Names', $field . 'Types', $field . 'Columns', ]; $map = array_reduce($attributes, function ($p, $item) { if (!$this->has($item)) { return $p; } $p[$item] = $this->get($item); return $p; }, []); $this->loadLinkMultipleField($field); // Restore set values. $this->setMultiple($map); } } if (!$this->has($idsAttribute) && !$this->isNew()) { $this->loadLinkMultipleField($field); } /** @var string[] */ return $this->getFetched($idsAttribute) ?? []; } /** * Has an ID in a link-multiple field. */ public function hasLinkMultipleId(string $field, string $id): bool { $idsAttribute = $field . 'Ids'; if (!$this->hasAttribute($idsAttribute)) { throw new LogicException("Called `hasLinkMultipleId` for non-link-multiple field `$field."); } if (!$this->has($idsAttribute) && !$this->isNew()) { $this->loadLinkMultipleField($field); } if (!$this->has($idsAttribute)) { return false; } /** @var string[] $idList */ $idList = $this->get($idsAttribute) ?? []; return in_array($id, $idList); } /** * @return string[]|null */ private function getLinkMultipleColumnsFromDefs(string $field): ?array { if (!$this->entityManager) { return null; } $entityDefs = $this->entityManager->getDefs()->getEntity($this->entityType); /** @var ?array $columns */ $columns = $entityDefs->tryGetField($field)?->getParam('columns'); if (!$columns) { return $columns; } $foreignEntityType = $entityDefs->tryGetRelation($field)?->tryGetForeignEntityType(); if ($foreignEntityType) { $foreignEntityDefs = $this->entityManager->getDefs()->getEntity($foreignEntityType); foreach ($columns as $column => $attribute) { if (!$foreignEntityDefs->hasAttribute($attribute)) { // For backward compatibility. If foreign attributes defined in the field do not exist. unset($columns[$column]); } } } return $columns; } /** * @since 9.0.0 */ protected function setRelatedLinkOrEntity(string $relation, Link|LinkParent|OrmEntity|null $related): static { if ($related instanceof Entity || $related === null) { $this->relations->set($relation, $related); return $this; } $this->setValueObject($relation, $related); return $this; } }