730 lines
21 KiB
PHP
730 lines
21 KiB
PHP
<?php
|
||
/************************************************************************
|
||
* This file is part of EspoCRM.
|
||
*
|
||
* EspoCRM – Open Source CRM application.
|
||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||
* Website: https://www.espocrm.com
|
||
*
|
||
* This program is free software: you can redistribute it and/or modify
|
||
* it under the terms of the GNU Affero General Public License as published by
|
||
* the Free Software Foundation, either version 3 of the License, or
|
||
* (at your option) any later version.
|
||
*
|
||
* This program is distributed in the hope that it will be useful,
|
||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
* GNU Affero General Public License for more details.
|
||
*
|
||
* You should have received a copy of the GNU Affero General Public License
|
||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
*
|
||
* 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<int, array{string, string}>|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<string, string> $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<string, string> $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;
|
||
}
|
||
}
|