diff --git a/.idea/jsonSchemas.xml b/.idea/jsonSchemas.xml index 2318c1f133..9a57f9a1e3 100644 --- a/.idea/jsonSchemas.xml +++ b/.idea/jsonSchemas.xml @@ -797,6 +797,25 @@ + + + + + + + diff --git a/.vscode/settings.json b/.vscode/settings.json index 669ac82cbf..eee86920e9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -334,6 +334,12 @@ ], "url": "./schema/metadata/app/linkManager.json" }, + { + "fileMatch": [ + "*/Resources/metadata/app/entityManager.json" + ], + "url": "./schema/metadata/app/entityManager.json" + }, { "fileMatch": [ "*/Resources/metadata/app/massActions.json" diff --git a/application/Espo/Controllers/EntityManager.php b/application/Espo/Controllers/EntityManager.php index 2304d2a9c7..cb22aab826 100644 --- a/application/Espo/Controllers/EntityManager.php +++ b/application/Espo/Controllers/EntityManager.php @@ -39,10 +39,12 @@ use Espo\Core\Exceptions\BadRequest; use Espo\Core\Exceptions\Forbidden; use Espo\Tools\ExportCustom\ExportCustom; use Espo\Tools\ExportCustom\Params as ExportCustomParams; - use Espo\Tools\ExportCustom\Service as ExportCustomService; +use Espo\Tools\LinkManager\LinkManager; use stdClass; +use const FILTER_SANITIZE_STRING; + class EntityManager { /** @@ -51,6 +53,7 @@ class EntityManager public function __construct( private User $user, private EntityManagerTool $entityManagerTool, + private LinkManager $linkManager, private InjectableFactory $injectableFactory ) { if (!$this->user->isAdmin()) { @@ -76,8 +79,8 @@ class EntityManager $name = $data['name']; $type = $data['type']; - $name = filter_var($name, \FILTER_SANITIZE_STRING); - $type = filter_var($type, \FILTER_SANITIZE_STRING); + $name = filter_var($name, FILTER_SANITIZE_STRING); + $type = filter_var($type, FILTER_SANITIZE_STRING); if (!is_string($name) || !is_string($type)) { throw new BadRequest(); @@ -160,7 +163,7 @@ class EntityManager $name = $data['name']; - $name = filter_var($name, \FILTER_SANITIZE_STRING); + $name = filter_var($name, FILTER_SANITIZE_STRING); if (!is_string($name)) { throw new BadRequest(); @@ -188,7 +191,7 @@ class EntityManager $name = $data['name']; - $name = filter_var($name, \FILTER_SANITIZE_STRING); + $name = filter_var($name, FILTER_SANITIZE_STRING); if (!is_string($name)) { throw new BadRequest(); @@ -231,11 +234,11 @@ class EntityManager throw new BadRequest(); } - $params[$item] = filter_var($data[$item], \FILTER_SANITIZE_STRING); + $params[$item] = filter_var($data[$item], FILTER_SANITIZE_STRING); } foreach ($additionalParamList as $item) { - $params[$item] = filter_var($data[$item] ?? null, \FILTER_SANITIZE_STRING); + $params[$item] = filter_var($data[$item] ?? null, FILTER_SANITIZE_STRING); } $params['labelForeign'] = $params['labelForeign'] ?? $params['linkForeign']; @@ -290,7 +293,7 @@ class EntityManager * } $params */ - $this->entityManagerTool->createLink($params); + $this->linkManager->create($params); return true; } @@ -318,7 +321,7 @@ class EntityManager foreach ($paramList as $item) { if (array_key_exists($item, $data)) { - $params[$item] = filter_var($data[$item], \FILTER_SANITIZE_STRING); + $params[$item] = filter_var($data[$item], FILTER_SANITIZE_STRING); } } @@ -372,11 +375,15 @@ class EntityManager * } $params */ - $this->entityManagerTool->updateLink($params); + $this->linkManager->update($params); return true; } + /** + * @throws BadRequest + * @throws Error + */ public function postActionRemoveLink(Request $request): bool { $data = $request->getParsedBody(); @@ -391,7 +398,7 @@ class EntityManager $params = []; foreach ($paramList as $item) { - $params[$item] = filter_var($data[$item], \FILTER_SANITIZE_STRING); + $params[$item] = filter_var($data[$item], FILTER_SANITIZE_STRING); } /** @@ -401,7 +408,7 @@ class EntityManager * } $params */ - $this->entityManagerTool->deleteLink($params); + $this->linkManager->delete($params); return true; } @@ -448,6 +455,10 @@ class EntityManager return true; } + /** + * @throws BadRequest + * @throws Error + */ public function postActionResetToDefault(Request $request): bool { $data = $request->getParsedBody(); diff --git a/application/Espo/Core/Utils/Metadata.php b/application/Espo/Core/Utils/Metadata.php index af3024c67a..50ed41d756 100644 --- a/application/Espo/Core/Utils/Metadata.php +++ b/application/Espo/Core/Utils/Metadata.php @@ -67,6 +67,11 @@ class Metadata ['app', 'api', 'routeMiddlewareClassNameListMap', self::ANY_KEY], ['app', 'api', 'controllerMiddlewareClassNameListMap', self::ANY_KEY], ['app', 'api', 'controllerActionMiddlewareClassNameListMap', self::ANY_KEY], + ['app', 'entityManager', 'createHookClassNameList'], + ['app', 'entityManager', 'deleteHookClassNameList'], + ['app', 'entityManager', 'updateHookClassNameList'], + ['app', 'linkManager', 'createHookClassNameList'], + ['app', 'linkManager', 'deleteHookClassNameList'], ['recordDefs', self::ANY_KEY, 'readLoaderClassNameList'], ['recordDefs', self::ANY_KEY, 'listLoaderClassNameList'], ['recordDefs', self::ANY_KEY, 'saverClassNameList'], diff --git a/application/Espo/Resources/metadata/app/entityManager.json b/application/Espo/Resources/metadata/app/entityManager.json new file mode 100644 index 0000000000..1655c3aaec --- /dev/null +++ b/application/Espo/Resources/metadata/app/entityManager.json @@ -0,0 +1,9 @@ +{ + "createHookClassNameList": [ + "Espo\\Tools\\EntityManager\\Hook\\Hooks\\PlusCreateHook" + ], + "deleteHookClassNameList": [ + "Espo\\Tools\\EntityManager\\Hook\\Hooks\\PlusDeleteHook" + ], + "updateHookClassNameList": [] +} diff --git a/application/Espo/Resources/metadata/app/linkManager.json b/application/Espo/Resources/metadata/app/linkManager.json index 9bf2b5a21d..4f4534d698 100644 --- a/application/Espo/Resources/metadata/app/linkManager.json +++ b/application/Espo/Resources/metadata/app/linkManager.json @@ -1,10 +1,10 @@ { "createHookClassNameList": [ - "Espo\\Tools\\EntityManager\\Link\\Hooks\\TargetListCreate", - "Espo\\Tools\\EntityManager\\Link\\Hooks\\AssignedUsersCreate" + "Espo\\Tools\\LinkManager\\Hook\\Hooks\\TargetListCreate", + "Espo\\Tools\\LinkManager\\Hook\\Hooks\\AssignedUsersCreate" ], "deleteHookClassNameList": [ - "Espo\\Tools\\EntityManager\\Link\\Hooks\\TargetListDelete", - "Espo\\Tools\\EntityManager\\Link\\Hooks\\ForeignFieldDelete" + "Espo\\Tools\\LinkManager\\Hook\\Hooks\\TargetListDelete", + "Espo\\Tools\\LinkManager\\Hook\\Hooks\\ForeignFieldDelete" ] } diff --git a/application/Espo/Tools/EntityManager/EntityManager.php b/application/Espo/Tools/EntityManager/EntityManager.php index ed5bd2e4f7..0a1c8d12a6 100644 --- a/application/Espo/Tools/EntityManager/EntityManager.php +++ b/application/Espo/Tools/EntityManager/EntityManager.php @@ -33,11 +33,8 @@ use Espo\Core\Exceptions\Error; use Espo\Core\Exceptions\BadRequest; use Espo\Core\Exceptions\Forbidden; use Espo\Core\Exceptions\Conflict; -use Espo\ORM\Entity; -use Espo\Tools\EntityManager\Link\Params as LinkParams; -use Espo\Tools\EntityManager\Link\HookProcessor as LinkHookProcessor; -use Espo\Tools\EntityManager\Link\Type as LinkType; -use Espo\Core\Utils\Route; +use Espo\Tools\EntityManager\Hook\CreateHook; +use Espo\Tools\EntityManager\Hook\DeleteHook; use Espo\Core\DataManager; use Espo\Core\InjectableFactory; use Espo\Core\Utils\Config; @@ -47,8 +44,10 @@ use Espo\Core\Utils\Json; use Espo\Core\Utils\Language; use Espo\Core\Utils\Metadata; use Espo\Core\Utils\Util; - +use Espo\Tools\EntityManager\Hook\UpdateHook; +use Espo\Tools\LinkManager\LinkManager; use Exception; +use RuntimeException; /** * Administration > Entity Manager. @@ -66,14 +65,13 @@ class EntityManager private ConfigWriter $configWriter, private DataManager $dataManager, private InjectableFactory $injectableFactory, - private LinkHookProcessor $linkHookProcessor, private NameUtil $nameUtil, - private Route $routeUtil + private LinkManager $linkManager ) {} /** * @param array $params - * @param array $replaceData + * @param array $replaceData @todo Revise. * @throws BadRequest * @throws Error * @throws Conflict @@ -272,7 +270,9 @@ class EntityManager $this->fileManager->copy($layoutsPath, 'custom/Espo/Custom/Resources/layouts/' . $name); } - $this->processHook('afterCreate', $type, $name, $params); + $entityTypeParams = new Params($name, $type, $params); + + $this->processCreateHook($entityTypeParams); $tabList = $this->config->get('tabList', []); @@ -280,7 +280,6 @@ class EntityManager $tabList[] = $name; $this->configWriter->set('tabList', $tabList); - $this->configWriter->save(); } @@ -343,6 +342,7 @@ class EntityManager } $isCustom = $this->metadata->get(['scopes', $name, 'isCustom']); + $type = $this->metadata->get(['scopes', $name, 'type']); if ($this->metadata->get(['scopes', $name, 'statusFieldLocked'])) { unset($data['statusField']); @@ -355,6 +355,9 @@ class EntityManager $this->metadata->get(['entityDefs', $name, 'collection', 'fullTextSearch']) ?? false, ]; + $entityTypeParams = new Params($name, $type, array_merge($this->getCurrentParams($name), $data)); + $previousEntityTypeParams = new Params($name, $type, $this->getCurrentParams($name)); + if (array_key_exists('stream', $data)) { $this->metadata->set('scopes', $name, ['stream' => (bool) $data['stream']]); } @@ -448,6 +451,8 @@ class EntityManager } } + $this->processUpdateHook($entityTypeParams, $previousEntityTypeParams); + $this->dataManager->clearCache(); if ( @@ -482,6 +487,8 @@ class EntityManager throw new Error('Type \''.$type.'\' is not removable.'); } + $entityTypeParams = new Params($name, $type, $this->getCurrentParams($name)); + $this->metadata->delete('entityDefs', $name); $this->metadata->delete('clientDefs', $name); $this->metadata->delete('recordDefs', $name); @@ -490,7 +497,7 @@ class EntityManager foreach ($this->metadata->get(['entityDefs', $name, 'links'], []) as $link => $item) { try { - $this->deleteLink(['entity' => $name, 'link' => $link]); + $this->linkManager->delete(['entity' => $name, 'link' => $link]); } catch (Exception) {} } @@ -542,7 +549,7 @@ class EntityManager } if ($type) { - $this->processHook('afterRemove', $type, $name); + $this->processDeleteHook($entityTypeParams); } $this->deleteEntityTypeFromConfigParams($name); @@ -574,867 +581,11 @@ class EntityManager $this->configWriter->set($param, $list); } - protected function isCustom(string $name): bool + private function isCustom(string $name): bool { return (bool) $this->metadata->get('scopes.' . $name . '.isCustom'); } - /** - * @param array{ - * linkType: string, - * entity: string, - * link: string, - * entityForeign: string, - * linkForeign: string, - * label: string, - * labelForeign: string, - * relationName?: ?string, - * linkMultipleField?: bool, - * linkMultipleFieldForeign?: bool, - * audited?: bool, - * auditedForeign?: bool, - * layout?: string, - * layoutForeign?: string, - * } $params - * @throws BadRequest - * @throws Error - * @throws Conflict - */ - public function createLink(array $params): void - { - $linkType = $params['linkType']; - - $entity = $params['entity']; - $link = trim($params['link']); - - $entityForeign = $params['entityForeign']; - $linkForeign = trim($params['linkForeign']); - - $label = $params['label']; - $labelForeign = $params['labelForeign']; - - $relationName = null; - $dataRight = null; - - if (empty($linkType)) { - throw new BadRequest("No link type."); - } - - if (empty($entity)) { - throw new BadRequest("No entity."); - } - - if (empty($link) || empty($linkForeign)) { - throw new BadRequest("No link or link-foreign."); - } - - if ($linkType === 'manyToMany') { - $relationName = !empty($params['relationName']) ? - $params['relationName'] : - lcfirst($entity) . $entityForeign; - - if ( - strlen($relationName) > NameUtil::MAX_LINK_NAME_LENGTH - ) { - throw new Error("Relation name should not be longer than " . NameUtil::MAX_LINK_NAME_LENGTH . "."); - } - - if (preg_match('/[^a-z]/', $relationName[0])) { - throw new Error("Relation name should start with a lower case letter."); - } - - if ($this->metadata->get(['scopes', ucfirst($relationName)])) { - throw new Conflict("Entity with the same name '$relationName' exists."); - } - - if ($this->nameUtil->relationshipExists($relationName)) { - throw new Conflict("Relationship with the same name '$relationName' exists."); - } - } - - $linkParams = LinkParams::createBuilder() - ->setType($linkType) - ->setEntityType($entity) - ->setForeignEntityType($entityForeign) - ->setLink($link) - ->setForeignLink($linkForeign) - ->setName($relationName) - ->build(); - - if (strlen($link) > NameUtil::MAX_LINK_NAME_LENGTH || strlen($linkForeign) > NameUtil::MAX_LINK_NAME_LENGTH) { - throw new Error("Link name should not be longer than " . NameUtil::MAX_LINK_NAME_LENGTH . "."); - } - - if (is_numeric($link[0]) || is_numeric($linkForeign[0])) { - throw new Error('Bad link name.'); - } - - if (preg_match('/[^a-z]/', $link[0])) { - throw new Error("Link name should start with a lower case letter."); - } - - if (preg_match('/[^a-z]/', $linkForeign[0])) { - throw new Error("Link name should start with a lower case letter."); - } - - if (in_array($link, NameUtil::LINK_FORBIDDEN_NAME_LIST)) { - throw new Conflict("Link name '$link' is not allowed."); - } - - if (in_array($linkForeign, NameUtil::LINK_FORBIDDEN_NAME_LIST)) { - throw new Conflict("Link name '$linkForeign' is not allowed."); - } - - foreach ($this->routeUtil->getFullList() as $route) { - if ($route->getRoute() === "/$entity/:id/$link") { - throw new Conflict("Link name '$link' conflicts with existing API endpoint."); - } - } - - if ($entityForeign) { - foreach ($this->routeUtil->getFullList() as $route) { - if ($route->getRoute() === "/$entityForeign/:id/$linkForeign") { - throw new Conflict("Link name '$linkForeign' conflicts with existing API endpoint."); - } - } - } - - $linkMultipleField = false; - - if (!empty($params['linkMultipleField'])) { - $linkMultipleField = true; - } - - $linkMultipleFieldForeign = false; - - if (!empty($params['linkMultipleFieldForeign'])) { - $linkMultipleFieldForeign = true; - } - - $audited = false; - - if (!empty($params['audited'])) { - $audited = true; - } - - $auditedForeign = false; - - if (!empty($params['auditedForeign'])) { - $auditedForeign = true; - } - - if ($linkType !== 'childrenToParent') { - if (empty($entityForeign)) { - throw new Error(); - } - } - - if ($this->metadata->get('entityDefs.' . $entity . '.links.' . $link)) { - throw new Conflict("Link $entity::$link already exists."); - } - - if ($entityForeign) { - if ($this->metadata->get('entityDefs.' . $entityForeign . '.links.' . $linkForeign)) { - throw new Conflict("Link $entityForeign::$linkForeign already exists."); - } - } - - if ($entity === $entityForeign) { - if ( - $link === lcfirst($entity) || - $linkForeign === lcfirst($entity) || - $link === $linkForeign - ) { - throw new Conflict("Link names $entityForeign, $linkForeign conflict."); - } - } - - if ($linkForeign === lcfirst($entityForeign)) { - throw new Conflict("Link $entityForeign::$linkForeign must not match entity type name."); - } - - if ($link === lcfirst($entity)) { - throw new Conflict("Link $entity::$link must not match entity type name."); - } - - switch ($linkType) { - case 'oneToOneRight': - case 'oneToOneLeft': - - if ( - $this->metadata->get('entityDefs.' . $entityForeign . '.fields.' . $linkForeign) || - $this->metadata->get('entityDefs.' . $entityForeign . '.fields.' . $linkForeign . 'Id') || - $this->metadata->get('entityDefs.' . $entityForeign . '.fields.' . $linkForeign . 'Name') - ) { - throw new Conflict("Field $entityForeign::$linkForeign already exists."); - } - - if ( - $this->metadata->get('entityDefs.' . $entity . '.fields.' . $link) || - $this->metadata->get('entityDefs.' . $entity . '.fields.' . $link . 'Id') || - $this->metadata->get('entityDefs.' . $entity . '.fields.' . $link . 'Name') - ) { - throw new Conflict("Field $entity::$link already exists."); - } - - if ($linkType === 'oneToOneLeft') { - $dataLeft = [ - 'fields' => [ - $link => [ - 'type' => 'linkOne', - ], - ], - 'links' => [ - $link => [ - 'type' => Entity::HAS_ONE, - 'foreign' => $linkForeign, - 'entity' => $entityForeign, - 'isCustom' => true, - ], - ], - ]; - - $dataRight = [ - 'fields' => [ - $linkForeign => [ - 'type' => 'link', - ], - ], - 'links' => [ - $linkForeign => [ - 'type' => Entity::BELONGS_TO, - 'foreign' => $link, - 'entity' => $entity, - 'isCustom' => true, - ], - ], - ]; - } - else { - $dataLeft = [ - 'fields' => [ - $link => [ - 'type' => 'link', - 'isCustom' => true, - ], - ], - 'links' => [ - $link => [ - 'type' => Entity::BELONGS_TO, - 'foreign' => $linkForeign, - 'entity' => $entityForeign, - 'isCustom' => true, - ], - ], - ]; - - $dataRight = [ - 'fields' => [ - $linkForeign => [ - 'type' => 'linkOne', - 'isCustom' => true, - ], - ], - 'links' => [ - $linkForeign => [ - 'type' => Entity::HAS_ONE, - 'foreign' => $link, - 'entity' => $entity, - 'isCustom' => true, - ], - ], - ]; - } - - break; - - case 'oneToMany': - - if ( - $this->metadata->get('entityDefs.' . $entityForeign . '.fields.' . $linkForeign) || - $this->metadata->get('entityDefs.' . $entityForeign . '.fields.' . $linkForeign . 'Id') || - $this->metadata->get('entityDefs.' . $entityForeign . '.fields.' . $linkForeign . 'Name') - ) { - throw new Conflict("Field $entityForeign::$linkForeign already exists."); - } - - $dataLeft = [ - 'fields' => [ - $link => [ - 'type' => 'linkMultiple', - 'layoutDetailDisabled' => !$linkMultipleField, - 'layoutMassUpdateDisabled' => !$linkMultipleField, - 'layoutListDisabled' => !$linkMultipleField, - 'noLoad' => !$linkMultipleField, - 'importDisabled' => !$linkMultipleField, - 'exportDisabled' => !$linkMultipleField, - 'customizationDisabled' => !$linkMultipleField, - 'isCustom' => true, - ], - ], - 'links' => [ - $link => [ - 'type' => Entity::HAS_MANY, - 'foreign' => $linkForeign, - 'entity' => $entityForeign, - 'audited' => $auditedForeign, - 'isCustom' => true, - ], - ], - ]; - - $dataRight = [ - 'fields' => [ - $linkForeign => [ - 'type' => 'link', - ], - ], - 'links' => [ - $linkForeign => [ - 'type' => Entity::BELONGS_TO, - 'foreign' => $link, - 'entity' => $entity, - 'audited' => $audited, - 'isCustom' => true, - ], - ], - ]; - - break; - - case 'manyToOne': - - if ( - $this->metadata->get('entityDefs.' . $entity . '.fields.' . $link) || - $this->metadata->get('entityDefs.' . $entity . '.fields.' . $link . 'Id') || - $this->metadata->get('entityDefs.' . $entity . '.fields.' . $link . 'Name') - ) { - throw new Conflict("Field $entity::$link already exists."); - } - - $dataLeft = [ - 'fields' => [ - $link => [ - 'type' => 'link', - ], - ], - 'links' => [ - $link => [ - 'type' => Entity::BELONGS_TO, - 'foreign' => $linkForeign, - 'entity' => $entityForeign, - 'audited' => $auditedForeign, - 'isCustom' => true, - ], - ], - ]; - - $dataRight = [ - 'fields' => [ - $linkForeign => [ - 'type' => 'linkMultiple', - 'layoutDetailDisabled' => !$linkMultipleFieldForeign, - 'layoutMassUpdateDisabled' => !$linkMultipleFieldForeign, - 'layoutListDisabled' => !$linkMultipleFieldForeign, - 'noLoad' => !$linkMultipleFieldForeign, - 'importDisabled' => !$linkMultipleFieldForeign, - 'exportDisabled' => !$linkMultipleFieldForeign, - 'customizationDisabled' => !$linkMultipleField, - 'isCustom' => true, - ] - ], - 'links' => [ - $linkForeign => [ - 'type' => Entity::HAS_MANY, - 'foreign' => $link, - 'entity' => $entity, - 'audited' => $audited, - 'isCustom' => true, - ], - ], - ]; - - break; - - case 'manyToMany': - $dataLeft = [ - 'fields' => [ - $link => [ - 'type' => 'linkMultiple', - 'layoutDetailDisabled' => !$linkMultipleField, - 'layoutMassUpdateDisabled' => !$linkMultipleField, - 'layoutListDisabled' => !$linkMultipleField, - 'noLoad' => !$linkMultipleField, - 'importDisabled' => !$linkMultipleField, - 'exportDisabled' => !$linkMultipleField, - 'customizationDisabled' => !$linkMultipleField, - 'isCustom' => true, - ] - ], - 'links' => [ - $link => [ - 'type' => Entity::HAS_MANY, - 'relationName' => $relationName, - 'foreign' => $linkForeign, - 'entity' => $entityForeign, - 'audited' => $auditedForeign, - 'isCustom' => true, - ], - ], - ]; - - $dataRight = [ - 'fields' => [ - $linkForeign => [ - 'type' => 'linkMultiple', - 'layoutDetailDisabled' => !$linkMultipleFieldForeign, - 'layoutMassUpdateDisabled' => !$linkMultipleFieldForeign, - 'layoutListDisabled' => !$linkMultipleFieldForeign, - 'noLoad' => !$linkMultipleFieldForeign, - 'importDisabled' => !$linkMultipleFieldForeign, - 'exportDisabled' => !$linkMultipleFieldForeign, - 'customizationDisabled' => !$linkMultipleField, - 'isCustom' => true, - ] - ], - 'links' => [ - $linkForeign => [ - 'type' => Entity::HAS_MANY, - 'relationName' => $relationName, - 'foreign' => $link, - 'entity' => $entity, - 'audited' => $audited, - 'isCustom' => true, - ] - ] - ]; - - if ($entityForeign == $entity) { - $dataLeft['links'][$link]['midKeys'] = ['leftId', 'rightId']; - - $dataRight['links'][$linkForeign]['midKeys'] = ['rightId', 'leftId']; - } - - break; - - case 'childrenToParent': - $dataLeft = [ - 'fields' => [ - $link => [ - 'type' => 'linkParent', - 'entityList' => $params['parentEntityTypeList'] ?? null, - ], - ], - 'links' => [ - $link => [ - 'type' => Entity::BELONGS_TO_PARENT, - 'foreign' => $linkForeign, - 'isCustom' => true, - ], - ], - ]; - - break; - - default: - throw new BadRequest(); - } - - $this->metadata->set('entityDefs', $entity, $dataLeft); - - if ($entityForeign) { - $this->metadata->set('entityDefs', $entityForeign, $dataRight); - } - - $this->setLayouts($params); - - $this->metadata->save(); - - $this->language->set($entity, 'fields', $link, $label); - $this->language->set($entity, 'links', $link, $label); - - if ($entityForeign) { - $this->language->set($entityForeign, 'fields', $linkForeign, $labelForeign); - $this->language->set($entityForeign, 'links', $linkForeign, $labelForeign); - } - - $this->language->save(); - - if ($this->isLanguageNotBase()) { - $this->baseLanguage->set($entity, 'fields', $link, $label); - $this->baseLanguage->set($entity, 'links', $link, $label); - - if ($entityForeign) { - $this->baseLanguage->set($entityForeign, 'fields', $linkForeign, $labelForeign); - $this->baseLanguage->set($entityForeign, 'links', $linkForeign, $labelForeign); - } - - $this->baseLanguage->save(); - } - - if ($linkType === 'childrenToParent') { - $foreignLinkEntityTypeList = $params['foreignLinkEntityTypeList'] ?? null; - - if (is_array($foreignLinkEntityTypeList)) { - $this->updateParentForeignLinks($entity, $link, $linkForeign, $foreignLinkEntityTypeList); - } - } - - $this->linkHookProcessor->processCreate($linkParams); - - $this->dataManager->rebuild(); - } - - /** - * @param array{ - * entity: string, - * link: string, - * entityForeign?: ?string, - * linkForeign?: ?string, - * label?: string, - * labelForeign?: string, - * linkMultipleField?: bool, - * linkMultipleFieldForeign?: bool, - * audited?: bool, - * auditedForeign?: bool, - * parentEntityTypeList?: string[], - * foreignLinkEntityTypeList?: string[], - * layout?: string, - * layoutForeign?: string, - * } $params - * @throws BadRequest - * @throws Error - */ - public function updateLink(array $params): void - { - $entity = $params['entity']; - $link = $params['link']; - $entityForeign = $params['entityForeign'] ?? null; - $linkForeign = $params['linkForeign'] ?? null; - - if (empty($link)) { - throw new BadRequest(); - } - - if (empty($entity)) { - throw new BadRequest(); - } - - $linkType = $this->metadata->get("entityDefs.$entity.links.$link.type"); - $isCustom = $this->metadata->get("entityDefs.$entity.links.$link.isCustom"); - - if ($linkType !== Entity::BELONGS_TO_PARENT) { - if (empty($entityForeign)) { - throw new BadRequest(); - } - - if (empty($linkForeign)) { - throw new BadRequest(); - } - } - - if ( - $this->metadata->get("entityDefs.$entity.links.$link.type") == Entity::HAS_MANY && - $this->metadata->get("entityDefs.$entity.links.$link.isCustom") - ) { - if (array_key_exists('linkMultipleField', $params)) { - $linkMultipleField = $params['linkMultipleField']; - - $dataLeft = [ - 'fields' => [ - $link => [ - 'type' => 'linkMultiple', - 'layoutDetailDisabled' => !$linkMultipleField, - 'layoutMassUpdateDisabled' => !$linkMultipleField, - 'layoutListDisabled' => !$linkMultipleField, - 'noLoad' => !$linkMultipleField, - 'importDisabled' => !$linkMultipleField, - 'exportDisabled' => !$linkMultipleField, - 'customizationDisabled' => !$linkMultipleField, - 'isCustom' => true, - ] - ] - ]; - - $this->metadata->set('entityDefs', $entity, $dataLeft); - - $this->metadata->save(); - } - } - - if ( - $this->metadata->get("entityDefs.$entityForeign.links.$linkForeign.type") == Entity::HAS_MANY && - $this->metadata->get("entityDefs.$entityForeign.links.$linkForeign.isCustom") - ) { - /** @var string $entityForeign */ - - if (array_key_exists('linkMultipleFieldForeign', $params)) { - $linkMultipleFieldForeign = $params['linkMultipleFieldForeign']; - - $dataRight = [ - 'fields' => [ - $linkForeign => [ - 'type' => 'linkMultiple', - 'layoutDetailDisabled' => !$linkMultipleFieldForeign, - 'layoutMassUpdateDisabled' => !$linkMultipleFieldForeign, - 'layoutListDisabled' => !$linkMultipleFieldForeign, - 'noLoad' => !$linkMultipleFieldForeign, - 'importDisabled' => !$linkMultipleFieldForeign, - 'exportDisabled' => !$linkMultipleFieldForeign, - 'customizationDisabled' => !$linkMultipleFieldForeign, - 'isCustom' => true, - ] - ] - ]; - - $this->metadata->set('entityDefs', $entityForeign, $dataRight); - $this->metadata->save(); - } - } - - if ( - in_array($this->metadata->get("entityDefs.$entity.links.$link.type"), [ - Entity::HAS_MANY, - Entity::HAS_CHILDREN, - ]) - ) { - if (array_key_exists('audited', $params)) { - $audited = $params['audited']; - - $dataLeft = [ - 'links' => [ - $link => [ - "audited" => $audited, - ], - ], - ]; - $this->metadata->set('entityDefs', $entity, $dataLeft); - $this->metadata->save(); - } - } - - if ( - $linkForeign && - in_array( - $this->metadata->get("entityDefs.$entityForeign.links.$linkForeign.type"), - [ - Entity::HAS_MANY, - Entity::HAS_CHILDREN, - ] - ) - ) { - /** @var string $entityForeign */ - - if (array_key_exists('auditedForeign', $params)) { - $auditedForeign = $params['auditedForeign']; - - $dataRight = [ - 'links' => [ - $linkForeign => [ - "audited" => $auditedForeign, - ], - ], - ]; - - $this->metadata->set('entityDefs', $entityForeign, $dataRight); - $this->metadata->save(); - } - } - - if ($linkType === Entity::BELONGS_TO_PARENT) { - $parentEntityTypeList = $params['parentEntityTypeList'] ?? null; - - if (is_array($parentEntityTypeList)) { - $data = [ - 'fields' => [ - $link => [ - 'entityList' => $parentEntityTypeList, - ], - ], - ]; - - $this->metadata->set('entityDefs', $entity, $data); - $this->metadata->save(); - } - - $foreignLinkEntityTypeList = $params['foreignLinkEntityTypeList'] ?? null; - - if ($linkForeign && is_array($foreignLinkEntityTypeList)) { - $this->updateParentForeignLinks($entity, $link, $linkForeign, $foreignLinkEntityTypeList); - } - } - - $this->setLayouts($params); - $this->metadata->save(); - - $label = null; - - if (isset($params['label'])) { - $label = $params['label']; - } - - if ($label) { - $this->language->set($entity, 'fields', $link, $label); - $this->language->set($entity, 'links', $link, $label); - } - - $labelForeign = null; - - if ($linkType !== Entity::BELONGS_TO_PARENT) { - /** @var string $linkForeign */ - /** @var string $entityForeign */ - - if (isset($params['labelForeign'])) { - $labelForeign = $params['labelForeign']; - } - - if ($labelForeign) { - $this->language->set($entityForeign, 'fields', $linkForeign, $labelForeign); - $this->language->set($entityForeign, 'links', $linkForeign, $labelForeign); - } - } - - $this->language->save(); - - if ($isCustom) { - if ($this->language->getLanguage() !== $this->baseLanguage->getLanguage()) { - - if ($label) { - $this->baseLanguage->set($entity, 'fields', $link, $label); - $this->baseLanguage->set($entity, 'links', $link, $label); - } - - if ($labelForeign && $linkType !== Entity::BELONGS_TO_PARENT) { - /** @var string $linkForeign */ - /** @var string $entityForeign */ - - $this->baseLanguage->set($entityForeign, 'fields', $linkForeign, $labelForeign); - $this->baseLanguage->set($entityForeign, 'links', $linkForeign, $labelForeign); - } - - $this->baseLanguage->save(); - } - } - - $this->dataManager->clearCache(); - } - - /** - * @param array{ - * entity?: string, - * link?: string, - * } $params - * @throws Error - * @throws BadRequest - */ - public function deleteLink(array $params): void - { - $entity = $params['entity'] ?? null; - $link = $params['link'] ?? null; - - if (!$this->metadata->get("entityDefs.$entity.links.$link.isCustom")) { - throw new Error("Could not delete link $entity.$link. Not isCustom."); - } - - if (empty($entity) || empty($link)) { - throw new BadRequest(); - } - - $entityForeign = $this->metadata->get("entityDefs.$entity.links.$link.entity"); - $linkForeign = $this->metadata->get("entityDefs.$entity.links.$link.foreign"); - $linkType = $this->metadata->get("entityDefs.$entity.links.$link.type"); - - if (!$this->metadata->get(['entityDefs', $entity, 'links', $link, 'isCustom'])) { - throw new Error("Can't remove not custom link."); - } - - if ($linkType === Entity::HAS_CHILDREN) { - $this->metadata->delete('entityDefs', $entity, [ - 'links.' . $link, - ]); - - $this->metadata->save(); - - return; - } - - if ($linkType === Entity::BELONGS_TO_PARENT) { - $this->metadata->delete('entityDefs', $entity, [ - 'fields.' . $link, - 'links.' . $link, - ]); - - $this->metadata->save(); - - if ($linkForeign) { - $this->updateParentForeignLinks($entity, $link, $linkForeign, []); - } - - return; - } - - if (empty($entityForeign) || empty($linkForeign)) { - throw new BadRequest(); - } - - $foreignLinkType = $this->metadata->get(['entityDefs', $entityForeign, 'links', $linkForeign, 'type']); - - $type = null; - - if ($linkType === Entity::HAS_MANY && $foreignLinkType === Entity::HAS_MANY) { - $type = LinkType::MANY_TO_MANY; - } - else if ($linkType === Entity::HAS_MANY && $foreignLinkType === Entity::BELONGS_TO) { - $type = LinkType::ONE_TO_MANY; - } - else if ($linkType === Entity::BELONGS_TO && $foreignLinkType === Entity::HAS_MANY) { - $type = LinkType::MANY_TO_ONE; - } - else if ($linkType === Entity::HAS_ONE && $foreignLinkType === Entity::BELONGS_TO) { - $type = LinkType::ONE_TO_ONE_LEFT; - } - else if ($linkType === Entity::BELONGS_TO && $foreignLinkType === Entity::HAS_ONE) { - $type = LinkType::ONE_TO_ONE_RIGHT; - } - - $name = $this->metadata->get(['entityDefs', $entity, $link, 'relationName']) ?? - $this->metadata->get(['entityDefs', $entityForeign, $linkForeign, 'relationName']); - - $linkParams = null; - - if ($type) { - $linkParams = LinkParams::createBuilder() - ->setType($type) - ->setName($name) - ->setEntityType($entity) - ->setForeignEntityType($entityForeign) - ->setLink($link) - ->setForeignLink($linkForeign) - ->build(); - } - - $this->metadata->delete('entityDefs', $entity, [ - 'fields.' . $link, - 'links.' . $link - ]); - - $this->metadata->delete('entityDefs', $entityForeign, [ - 'fields.' . $linkForeign, - 'links.' . $linkForeign - ]); - - $this->metadata->delete('clientDefs', $entity, ['relationshipPanels.' . $link]); - $this->metadata->delete('clientDefs', $entityForeign, ['relationshipPanels.' . $linkForeign]); - - $this->metadata->save(); - - if ($linkParams) { - $this->linkHookProcessor->processDelete($linkParams); - } - - $this->dataManager->clearCache(); - } - /** * @param array $data * @throws Error @@ -1447,47 +598,40 @@ class EntityManager $this->dataManager->clearCache(); } - /** - * @param ?array $params - */ - protected function processHook(string $methodName, string $type, string $name, &$params = null): void + private function processUpdateHook(Params $params, Params $previousParams): void { - $hook = $this->getHook($type); + /** @var class-string[] $classNameList */ + $classNameList = $this->metadata->get(['app', 'entityManager', 'updateHookClassNameList']) ?? []; - if (!$hook) { - return; + foreach ($classNameList as $className) { + $hook = $this->injectableFactory->create($className); + + $hook->process($params, $previousParams); } - - if (!method_exists($hook, $methodName)) { - return; - } - - $hook->$methodName($name, $params); } - protected function getHook(string $type): ?object + private function processDeleteHook(Params $params): void { - $templateDefs = $this->metadata->get(['app', 'entityTemplates', $type], []); + /** @var class-string[] $classNameList */ + $classNameList = $this->metadata->get(['app', 'entityManager', 'deleteHookClassNameList']) ?? []; - $className = 'Espo\\Tools\\EntityManager\\Hooks\\' . $type . 'Type'; + foreach ($classNameList as $className) { + $hook = $this->injectableFactory->create($className); - if (!empty($templateDefs['module'])) { - $templateModuleName = $templateDefs['module']; - - $normalizedTemplateModuleName = Util::normalizeClassName($templateModuleName); - - $className = - 'Espo\\Modules\\' . $normalizedTemplateModuleName . - '\\Tools\\EntityManager\\Hooks\\' . $type . 'Type'; + $hook->process($params); } + } - $className = $this->metadata->get(['app', 'entityTemplates', $type, 'hookClassName'], $className); + private function processCreateHook(Params $params): void + { + /** @var class-string[] $classNameList */ + $classNameList = $this->metadata->get(['app', 'entityManager', 'createHookClassNameList']) ?? []; - if (class_exists($className)) { - return $this->injectableFactory->create($className); + foreach ($classNameList as $className) { + $hook = $this->injectableFactory->create($className); + + $hook->process($params); } - - return null; } /** @@ -1534,114 +678,8 @@ class EntityManager $this->dataManager->clearCache(); } - /** - * @param string[] $foreignLinkEntityTypeList - */ - protected function updateParentForeignLinks( - string $entityType, - string $link, - string $linkForeign, - array $foreignLinkEntityTypeList - ): void { - - $toCreateList = []; - - foreach ($foreignLinkEntityTypeList as $foreignEntityType) { - $linkDefs = $this->metadata->get(['entityDefs', $foreignEntityType, 'links']) ?? []; - - foreach ($linkDefs as $kLink => $defs) { - $kForeign = $defs['foreign'] ?? null; - $kIsCustom = $defs['isCustom'] ?? false; - $kEntity = $defs['entity'] ?? null; - - if ( - $kForeign === $link && !$kIsCustom && $kEntity == $entityType - ) { - continue 2; - } - - if ($kLink == $linkForeign) { - if ($defs['type'] !== Entity::HAS_CHILDREN) { - continue 2; - } - } - } - - $toCreateList[] = $foreignEntityType; - } - - /** @var string[] $entityTypeList */ - $entityTypeList = array_keys($this->metadata->get('entityDefs') ?? []); - - foreach ($entityTypeList as $itemEntityType) { - $linkDefs = $this->metadata->get(['entityDefs', $itemEntityType, 'links']) ?? []; - - foreach ($linkDefs as $kLink => $defs) { - $kForeign = $defs['foreign'] ?? null; - $kIsCustom = $defs['isCustom'] ?? false; - $kEntity = $defs['entity'] ?? null; - - if ( - $kForeign === $link && $kIsCustom && $kEntity == $entityType && - $defs['type'] == Entity::HAS_CHILDREN && $kLink === $linkForeign - ) { - if (!in_array($itemEntityType, $toCreateList)) { - $this->metadata->delete('entityDefs', $itemEntityType, [ - 'links.' . $linkForeign, - ]); - - $this->language->delete($itemEntityType, 'links', $linkForeign); - - if ( - $this->isLanguageNotBase() - ) { - $this->baseLanguage->delete($itemEntityType, 'links', $linkForeign); - } - } - - break; - } - } - } - - foreach ($toCreateList as $itemEntityType) { - $this->metadata->set('entityDefs', $itemEntityType, [ - 'links' => [ - $linkForeign => [ - 'type' => Entity::HAS_CHILDREN, - 'foreign' => $link, - 'entity' => $entityType, - 'isCustom' => true, - ], - ], - ]); - - $label = $this->language->translate($entityType, 'scopeNamesPlural'); - - $this->language->set($itemEntityType, 'links', $linkForeign, $label); - - if ($this->isLanguageNotBase()) { - $this->baseLanguage->set($itemEntityType, 'links', $linkForeign, $label); - } - } - - $this->metadata->save(); - - $this->language->save(); - - if ($this->isLanguageNotBase()) { - $this->baseLanguage->save(); - } - } - - private function isLanguageNotBase(): bool - { - return $this->language->getLanguage() !== $this->baseLanguage->getLanguage(); - } - /** * @param array $data - * @throws Error */ private function setAdditionalParamsInMetadata(string $entityType, array $data): void { @@ -1658,9 +696,26 @@ class EntityManager } } + /** + * @return array + */ + private function getCurrentParams(string $entityType): array + { + $data = []; + + foreach ($this->getAdditionalParamLocationMap($entityType) as $param => $location) { + $data[$param] = $this->metadata->get([$location, $entityType, $param]); + } + + $data['statusField'] = $this->metadata->get(['scopes', $entityType, 'statusField']); + $data['kanbanViewMode'] = $this->metadata->get(['scopes', $entityType, 'kanbanViewMode']); + $data['disabled'] = $this->metadata->get(['scopes', $entityType, 'disabled']); + + return $data; + } + /** * @return array - * @throws Error */ private function getAdditionalParamLocationMap(string $entityType): array { @@ -1688,7 +743,7 @@ class EntityManager $location = $defs['location'] ?? self::DEFAULT_PARAM_LOCATION; if (!in_array($location, ['scopes', 'entityDefs', 'clientDefs', 'recordDefs'])) { - throw new Error("Param location `$location` is not supported."); + throw new RuntimeException("Param location `$location` is not supported."); } $result[$param] = $location; @@ -1697,34 +752,9 @@ class EntityManager return $result; } - /** - * @param array{ - * entity: string, - * link: string, - * entityForeign?: ?string, - * linkForeign?: ?string, - * layout?: string, - * layoutForeign?: string, - * } $params - */ - private function setLayouts(array $params): void + private function isLanguageNotBase(): bool { - $this->setLayout($params['entity'], $params['link'], $params['layout'] ?? null); - - if (isset($params['entityForeign']) && isset($params['linkForeign'])) { - $this->setLayout($params['entityForeign'], $params['linkForeign'], $params['layoutForeign'] ?? null); - } - } - - private function setLayout(string $entityType, string $link, ?string $layout): void - { - $this->metadata->set('clientDefs', $entityType, [ - 'relationshipPanels' => [ - $link => [ - 'layout' => $layout, - ] - ] - ]); + return $this->language->getLanguage() !== $this->baseLanguage->getLanguage(); } public function resetFormulaToDefault(string $scope, string $type): void diff --git a/application/Espo/Tools/EntityManager/Hooks/CompanyType.php b/application/Espo/Tools/EntityManager/Hook/CreateHook.php similarity index 89% rename from application/Espo/Tools/EntityManager/Hooks/CompanyType.php rename to application/Espo/Tools/EntityManager/Hook/CreateHook.php index 4013256552..4ad6b89800 100644 --- a/application/Espo/Tools/EntityManager/Hooks/CompanyType.php +++ b/application/Espo/Tools/EntityManager/Hook/CreateHook.php @@ -27,9 +27,11 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Tools\EntityManager\Hooks; +namespace Espo\Tools\EntityManager\Hook; -class CompanyType extends BasePlusType +use Espo\Tools\EntityManager\Params; + +interface CreateHook { - -} \ No newline at end of file + public function process(Params $params): void; +} diff --git a/application/Espo/Tools/EntityManager/Hooks/PersonType.php b/application/Espo/Tools/EntityManager/Hook/DeleteHook.php similarity index 89% rename from application/Espo/Tools/EntityManager/Hooks/PersonType.php rename to application/Espo/Tools/EntityManager/Hook/DeleteHook.php index 9db5037fac..aae9b4e664 100644 --- a/application/Espo/Tools/EntityManager/Hooks/PersonType.php +++ b/application/Espo/Tools/EntityManager/Hook/DeleteHook.php @@ -27,9 +27,11 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Tools\EntityManager\Hooks; +namespace Espo\Tools\EntityManager\Hook; -class PersonType extends BasePlusType +use Espo\Tools\EntityManager\Params; + +interface DeleteHook { - -} \ No newline at end of file + public function process(Params $params): void; +} diff --git a/application/Espo/Tools/EntityManager/Hook/Hooks/PlusCreateHook.php b/application/Espo/Tools/EntityManager/Hook/Hooks/PlusCreateHook.php new file mode 100644 index 0000000000..0718f50422 --- /dev/null +++ b/application/Espo/Tools/EntityManager/Hook/Hooks/PlusCreateHook.php @@ -0,0 +1,91 @@ +getType(), [ + BasePlus::TEMPLATE_TYPE, + Company::TEMPLATE_TYPE, + Person::TEMPLATE_TYPE, + ]) + ) { + return; + } + + $name = $params->getName(); + + $activitiesEntityTypeList = $this->config->get('activitiesEntityList', []); + $historyEntityTypeList = $this->config->get('historyEntityList', []); + + $entityTypeList = array_merge($activitiesEntityTypeList, $historyEntityTypeList); + $entityTypeList[] = Task::ENTITY_TYPE; + $entityTypeList = array_unique($entityTypeList); + + foreach ($entityTypeList as $entityType) { + if (!$this->metadata->get(['entityDefs', $entityType, 'fields', 'parent', 'entityList'])) { + continue; + } + + $list = $this->metadata->get(['entityDefs', $entityType, 'fields', 'parent', 'entityList'], []); + + if (!in_array($name, $list)) { + $list[] = $name; + + $data = [ + 'fields' => [ + 'parent' => ['entityList' => $list] + ] + ]; + + $this->metadata->set('entityDefs', $entityType, $data); + } + } + + $this->metadata->save(); + } +} diff --git a/application/Espo/Tools/EntityManager/Hooks/BasePlusType.php b/application/Espo/Tools/EntityManager/Hook/Hooks/PlusDeleteHook.php similarity index 64% rename from application/Espo/Tools/EntityManager/Hooks/BasePlusType.php rename to application/Espo/Tools/EntityManager/Hook/Hooks/PlusDeleteHook.php index d0b8ff071f..e6e8c7a2b4 100644 --- a/application/Espo/Tools/EntityManager/Hooks/BasePlusType.php +++ b/application/Espo/Tools/EntityManager/Hook/Hooks/PlusDeleteHook.php @@ -27,53 +27,38 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Tools\EntityManager\Hooks; +namespace Espo\Tools\EntityManager\Hook\Hooks; -use Espo\Core\Di; +use Espo\Core\Templates\Entities\BasePlus; +use Espo\Core\Templates\Entities\Company; +use Espo\Core\Templates\Entities\Person; +use Espo\Core\Utils\Config; +use Espo\Core\Utils\Metadata; use Espo\Modules\Crm\Entities\Task; +use Espo\Tools\EntityManager\Hook\CreateHook; +use Espo\Tools\EntityManager\Params; -class BasePlusType implements Di\ConfigAware, Di\MetadataAware +class PlusDeleteHook implements CreateHook { - use Di\ConfigSetter; - use Di\MetadataSetter; + public function __construct( + private Config $config, + private Metadata $metadata + ) {} - /** - * @param array $params - */ - public function afterCreate(string $name, array $params): void + public function process(Params $params): void { - $activitiesEntityTypeList = $this->config->get('activitiesEntityList', []); - $historyEntityTypeList = $this->config->get('historyEntityList', []); - - $entityTypeList = array_merge($activitiesEntityTypeList, $historyEntityTypeList); - $entityTypeList[] = Task::ENTITY_TYPE; - $entityTypeList = array_unique($entityTypeList); - - foreach ($entityTypeList as $entityType) { - if (!$this->metadata->get(['entityDefs', $entityType, 'fields', 'parent', 'entityList'])) { - continue; - } - - $list = $this->metadata->get(['entityDefs', $entityType, 'fields', 'parent', 'entityList'], []); - - if (!in_array($name, $list)) { - $list[] = $name; - - $data = [ - 'fields' => [ - 'parent' => ['entityList' => $list] - ] - ]; - - $this->metadata->set('entityDefs', $entityType, $data); - } + if ( + !in_array($params->getType(), [ + BasePlus::TEMPLATE_TYPE, + Company::TEMPLATE_TYPE, + Person::TEMPLATE_TYPE, + ]) + ) { + return; } - $this->metadata->save(); - } + $name = $params->getName(); - public function afterRemove(string $name): void - { $activitiesEntityTypeList = $this->config->get('activitiesEntityList', []); $historyEntityTypeList = $this->config->get('historyEntityList', []); diff --git a/application/Espo/Tools/EntityManager/Hook/UpdateHook.php b/application/Espo/Tools/EntityManager/Hook/UpdateHook.php new file mode 100644 index 0000000000..2bb169590d --- /dev/null +++ b/application/Espo/Tools/EntityManager/Hook/UpdateHook.php @@ -0,0 +1,37 @@ + $params + */ + public function __construct( + private string $name, + private ?string $type, + private array $params + ) {} + + public function get(string $name): mixed + { + return $this->params[$name] ?? null; + } + + public function getType(): ?string + { + return $this->type; + } + + public function getName(): string + { + return $this->name; + } +} diff --git a/application/Espo/Tools/EntityManager/Link/CreateHook.php b/application/Espo/Tools/LinkManager/Hook/CreateHook.php similarity index 95% rename from application/Espo/Tools/EntityManager/Link/CreateHook.php rename to application/Espo/Tools/LinkManager/Hook/CreateHook.php index 76b40b7b87..e237fad5f1 100644 --- a/application/Espo/Tools/EntityManager/Link/CreateHook.php +++ b/application/Espo/Tools/LinkManager/Hook/CreateHook.php @@ -27,7 +27,9 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Tools\EntityManager\Link; +namespace Espo\Tools\LinkManager\Hook; + +use Espo\Tools\LinkManager\Params; interface CreateHook { diff --git a/application/Espo/Tools/EntityManager/Link/DeleteHook.php b/application/Espo/Tools/LinkManager/Hook/DeleteHook.php similarity index 95% rename from application/Espo/Tools/EntityManager/Link/DeleteHook.php rename to application/Espo/Tools/LinkManager/Hook/DeleteHook.php index 3c099bbdda..93c3219cfe 100644 --- a/application/Espo/Tools/EntityManager/Link/DeleteHook.php +++ b/application/Espo/Tools/LinkManager/Hook/DeleteHook.php @@ -27,7 +27,9 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Tools\EntityManager\Link; +namespace Espo\Tools\LinkManager\Hook; + +use Espo\Tools\LinkManager\Params; interface DeleteHook { diff --git a/application/Espo/Tools/EntityManager/Link/HookProcessor.php b/application/Espo/Tools/LinkManager/Hook/HookProcessor.php similarity index 89% rename from application/Espo/Tools/EntityManager/Link/HookProcessor.php rename to application/Espo/Tools/LinkManager/Hook/HookProcessor.php index 7905edf9b8..fa9fa07948 100644 --- a/application/Espo/Tools/EntityManager/Link/HookProcessor.php +++ b/application/Espo/Tools/LinkManager/Hook/HookProcessor.php @@ -27,22 +27,18 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Tools\EntityManager\Link; +namespace Espo\Tools\LinkManager\Hook; use Espo\Core\Utils\Metadata; use Espo\Core\InjectableFactory; +use Espo\Tools\LinkManager\Params; class HookProcessor { - private Metadata $metadata; - - private InjectableFactory $injectableFactory; - - public function __construct(Metadata $metadata, InjectableFactory $injectableFactory) - { - $this->metadata = $metadata; - $this->injectableFactory = $injectableFactory; - } + public function __construct( + private Metadata $metadata, + private InjectableFactory $injectableFactory + ) {} public function processCreate(Params $params): void { diff --git a/application/Espo/Tools/EntityManager/Link/Hooks/AssignedUsersCreate.php b/application/Espo/Tools/LinkManager/Hook/Hooks/AssignedUsersCreate.php similarity index 89% rename from application/Espo/Tools/EntityManager/Link/Hooks/AssignedUsersCreate.php rename to application/Espo/Tools/LinkManager/Hook/Hooks/AssignedUsersCreate.php index cd819a1fcc..97feec2ec3 100644 --- a/application/Espo/Tools/EntityManager/Link/Hooks/AssignedUsersCreate.php +++ b/application/Espo/Tools/LinkManager/Hook/Hooks/AssignedUsersCreate.php @@ -27,26 +27,20 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Tools\EntityManager\Link\Hooks; - -use Espo\Tools\EntityManager\Link\CreateHook; -use Espo\Tools\EntityManager\Link\Params; -use Espo\Tools\EntityManager\Link\Type; +namespace Espo\Tools\LinkManager\Hook\Hooks; +use Espo\Tools\LinkManager\Hook\CreateHook; +use Espo\Tools\LinkManager\Params; +use Espo\Tools\LinkManager\Type; use Espo\Core\Utils\Metadata; - use Espo\Entities\User; class AssignedUsersCreate implements CreateHook { private const LINK_NAME = 'assignedUsers'; - private Metadata $metadata; - - public function __construct(Metadata $metadata) - { - $this->metadata = $metadata; - } + public function __construct(private Metadata $metadata) + {} public function process(Params $params): void { @@ -75,8 +69,6 @@ class AssignedUsersCreate implements CreateHook $params->getLink() === self::LINK_NAME ) { $this->processInternal($entityType); - - return; } } diff --git a/application/Espo/Tools/EntityManager/Link/Hooks/ForeignFieldDelete.php b/application/Espo/Tools/LinkManager/Hook/Hooks/ForeignFieldDelete.php similarity index 88% rename from application/Espo/Tools/EntityManager/Link/Hooks/ForeignFieldDelete.php rename to application/Espo/Tools/LinkManager/Hook/Hooks/ForeignFieldDelete.php index a07b725503..0b5c723df1 100644 --- a/application/Espo/Tools/EntityManager/Link/Hooks/ForeignFieldDelete.php +++ b/application/Espo/Tools/LinkManager/Hook/Hooks/ForeignFieldDelete.php @@ -27,26 +27,20 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Tools\EntityManager\Link\Hooks; - -use Espo\Tools\EntityManager\Link\DeleteHook; -use Espo\Tools\EntityManager\Link\Params; +namespace Espo\Tools\LinkManager\Hook\Hooks; +use Espo\Tools\LinkManager\Hook\DeleteHook; +use Espo\Tools\LinkManager\Params; use Espo\Core\Utils\Metadata; use Espo\ORM\Defs; class ForeignFieldDelete implements DeleteHook { - private Metadata $metadata; - - private Defs $defs; - - public function __construct(Metadata $metadata, Defs $defs) - { - $this->metadata = $metadata; - $this->defs = $defs; - } + public function __construct( + private Metadata $metadata, + private Defs $defs + ) {} public function process(Params $params): void { diff --git a/application/Espo/Tools/EntityManager/Link/Hooks/TargetListCreate.php b/application/Espo/Tools/LinkManager/Hook/Hooks/TargetListCreate.php similarity index 94% rename from application/Espo/Tools/EntityManager/Link/Hooks/TargetListCreate.php rename to application/Espo/Tools/LinkManager/Hook/Hooks/TargetListCreate.php index 013a3cfaed..f91dc4493b 100644 --- a/application/Espo/Tools/EntityManager/Link/Hooks/TargetListCreate.php +++ b/application/Espo/Tools/LinkManager/Hook/Hooks/TargetListCreate.php @@ -27,25 +27,20 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Tools\EntityManager\Link\Hooks; +namespace Espo\Tools\LinkManager\Hook\Hooks; use Espo\Core\Templates\Entities\Company; use Espo\Core\Templates\Entities\Person; -use Espo\Tools\EntityManager\Link\CreateHook; -use Espo\Tools\EntityManager\Link\Params; -use Espo\Tools\EntityManager\Link\Type; +use Espo\Tools\LinkManager\Hook\CreateHook; +use Espo\Tools\LinkManager\Params; +use Espo\Tools\LinkManager\Type; use Espo\Modules\Crm\Entities\TargetList; - use Espo\Core\Utils\Metadata; class TargetListCreate implements CreateHook { - private Metadata $metadata; - - public function __construct(Metadata $metadata) - { - $this->metadata = $metadata; - } + public function __construct(private Metadata $metadata) + {} public function process(Params $params): void { diff --git a/application/Espo/Tools/EntityManager/Link/Hooks/TargetListDelete.php b/application/Espo/Tools/LinkManager/Hook/Hooks/TargetListDelete.php similarity index 91% rename from application/Espo/Tools/EntityManager/Link/Hooks/TargetListDelete.php rename to application/Espo/Tools/LinkManager/Hook/Hooks/TargetListDelete.php index c384f02eeb..c674630a4b 100644 --- a/application/Espo/Tools/EntityManager/Link/Hooks/TargetListDelete.php +++ b/application/Espo/Tools/LinkManager/Hook/Hooks/TargetListDelete.php @@ -27,25 +27,21 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Tools\EntityManager\Link\Hooks; +namespace Espo\Tools\LinkManager\Hook\Hooks; use Espo\Core\Templates\Entities\Company; use Espo\Core\Templates\Entities\Person; -use Espo\Tools\EntityManager\Link\DeleteHook; -use Espo\Tools\EntityManager\Link\Params; -use Espo\Tools\EntityManager\Link\Type; +use Espo\Tools\LinkManager\Hook\DeleteHook; +use Espo\Tools\LinkManager\Params; +use Espo\Tools\LinkManager\Type; use Espo\Modules\Crm\Entities\TargetList; use Espo\Core\Utils\Metadata; class TargetListDelete implements DeleteHook { - private Metadata $metadata; - - public function __construct(Metadata $metadata) - { - $this->metadata = $metadata; - } + public function __construct(private Metadata $metadata) + {} public function process(Params $params): void { diff --git a/application/Espo/Tools/LinkManager/LinkManager.php b/application/Espo/Tools/LinkManager/LinkManager.php new file mode 100644 index 0000000000..0fed7c271a --- /dev/null +++ b/application/Espo/Tools/LinkManager/LinkManager.php @@ -0,0 +1,1050 @@ + Entity Manager > {Entity Type} > Relationships. + */ +class LinkManager +{ + public function __construct( + private Metadata $metadata, + private Language $language, + private Language $baseLanguage, + private DataManager $dataManager, + private LinkHookProcessor $linkHookProcessor, + private NameUtil $nameUtil, + private Route $routeUtil + ) {} + + /** + * @param array{ + * linkType: string, + * entity: string, + * link: string, + * entityForeign: string, + * linkForeign: string, + * label: string, + * labelForeign: string, + * relationName?: ?string, + * linkMultipleField?: bool, + * linkMultipleFieldForeign?: bool, + * audited?: bool, + * auditedForeign?: bool, + * layout?: string, + * layoutForeign?: string, + * } $params + * @throws BadRequest + * @throws Error + * @throws Conflict + */ + public function create(array $params): void + { + $linkType = $params['linkType']; + + $entity = $params['entity']; + $link = trim($params['link']); + + $entityForeign = $params['entityForeign']; + $linkForeign = trim($params['linkForeign']); + + $label = $params['label']; + $labelForeign = $params['labelForeign']; + + $relationName = null; + $dataRight = null; + + if (empty($linkType)) { + throw new BadRequest("No link type."); + } + + if (empty($entity)) { + throw new BadRequest("No entity."); + } + + if (empty($link) || empty($linkForeign)) { + throw new BadRequest("No link or link-foreign."); + } + + if ($linkType === 'manyToMany') { + $relationName = !empty($params['relationName']) ? + $params['relationName'] : + lcfirst($entity) . $entityForeign; + + if ( + strlen($relationName) > NameUtil::MAX_LINK_NAME_LENGTH + ) { + throw new Error("Relation name should not be longer than " . NameUtil::MAX_LINK_NAME_LENGTH . "."); + } + + if (preg_match('/[^a-z]/', $relationName[0])) { + throw new Error("Relation name should start with a lower case letter."); + } + + if ($this->metadata->get(['scopes', ucfirst($relationName)])) { + throw new Conflict("Entity with the same name '$relationName' exists."); + } + + if ($this->nameUtil->relationshipExists($relationName)) { + throw new Conflict("Relationship with the same name '$relationName' exists."); + } + } + + $linkParams = LinkParams::createBuilder() + ->setType($linkType) + ->setEntityType($entity) + ->setForeignEntityType($entityForeign) + ->setLink($link) + ->setForeignLink($linkForeign) + ->setName($relationName) + ->build(); + + if (strlen($link) > NameUtil::MAX_LINK_NAME_LENGTH || strlen($linkForeign) > NameUtil::MAX_LINK_NAME_LENGTH) { + throw new Error("Link name should not be longer than " . NameUtil::MAX_LINK_NAME_LENGTH . "."); + } + + if (is_numeric($link[0]) || is_numeric($linkForeign[0])) { + throw new Error('Bad link name.'); + } + + if (preg_match('/[^a-z]/', $link[0])) { + throw new Error("Link name should start with a lower case letter."); + } + + if (preg_match('/[^a-z]/', $linkForeign[0])) { + throw new Error("Link name should start with a lower case letter."); + } + + if (in_array($link, NameUtil::LINK_FORBIDDEN_NAME_LIST)) { + throw new Conflict("Link name '$link' is not allowed."); + } + + if (in_array($linkForeign, NameUtil::LINK_FORBIDDEN_NAME_LIST)) { + throw new Conflict("Link name '$linkForeign' is not allowed."); + } + + foreach ($this->routeUtil->getFullList() as $route) { + if ($route->getRoute() === "/$entity/:id/$link") { + throw new Conflict("Link name '$link' conflicts with existing API endpoint."); + } + } + + if ($entityForeign) { + foreach ($this->routeUtil->getFullList() as $route) { + if ($route->getRoute() === "/$entityForeign/:id/$linkForeign") { + throw new Conflict("Link name '$linkForeign' conflicts with existing API endpoint."); + } + } + } + + $linkMultipleField = false; + + if (!empty($params['linkMultipleField'])) { + $linkMultipleField = true; + } + + $linkMultipleFieldForeign = false; + + if (!empty($params['linkMultipleFieldForeign'])) { + $linkMultipleFieldForeign = true; + } + + $audited = false; + + if (!empty($params['audited'])) { + $audited = true; + } + + $auditedForeign = false; + + if (!empty($params['auditedForeign'])) { + $auditedForeign = true; + } + + if ($linkType !== 'childrenToParent') { + if (empty($entityForeign)) { + throw new Error(); + } + } + + if ($this->metadata->get('entityDefs.' . $entity . '.links.' . $link)) { + throw new Conflict("Link $entity::$link already exists."); + } + + if ($entityForeign) { + if ($this->metadata->get('entityDefs.' . $entityForeign . '.links.' . $linkForeign)) { + throw new Conflict("Link $entityForeign::$linkForeign already exists."); + } + } + + if ($entity === $entityForeign) { + if ( + $link === lcfirst($entity) || + $linkForeign === lcfirst($entity) || + $link === $linkForeign + ) { + throw new Conflict("Link names $entityForeign, $linkForeign conflict."); + } + } + + if ($linkForeign === lcfirst($entityForeign)) { + throw new Conflict("Link $entityForeign::$linkForeign must not match entity type name."); + } + + if ($link === lcfirst($entity)) { + throw new Conflict("Link $entity::$link must not match entity type name."); + } + + switch ($linkType) { + case 'oneToOneRight': + case 'oneToOneLeft': + + if ( + $this->metadata->get('entityDefs.' . $entityForeign . '.fields.' . $linkForeign) || + $this->metadata->get('entityDefs.' . $entityForeign . '.fields.' . $linkForeign . 'Id') || + $this->metadata->get('entityDefs.' . $entityForeign . '.fields.' . $linkForeign . 'Name') + ) { + throw new Conflict("Field $entityForeign::$linkForeign already exists."); + } + + if ( + $this->metadata->get('entityDefs.' . $entity . '.fields.' . $link) || + $this->metadata->get('entityDefs.' . $entity . '.fields.' . $link . 'Id') || + $this->metadata->get('entityDefs.' . $entity . '.fields.' . $link . 'Name') + ) { + throw new Conflict("Field $entity::$link already exists."); + } + + if ($linkType === 'oneToOneLeft') { + $dataLeft = [ + 'fields' => [ + $link => [ + 'type' => 'linkOne', + ], + ], + 'links' => [ + $link => [ + 'type' => Entity::HAS_ONE, + 'foreign' => $linkForeign, + 'entity' => $entityForeign, + 'isCustom' => true, + ], + ], + ]; + + $dataRight = [ + 'fields' => [ + $linkForeign => [ + 'type' => 'link', + ], + ], + 'links' => [ + $linkForeign => [ + 'type' => Entity::BELONGS_TO, + 'foreign' => $link, + 'entity' => $entity, + 'isCustom' => true, + ], + ], + ]; + } + else { + $dataLeft = [ + 'fields' => [ + $link => [ + 'type' => 'link', + 'isCustom' => true, + ], + ], + 'links' => [ + $link => [ + 'type' => Entity::BELONGS_TO, + 'foreign' => $linkForeign, + 'entity' => $entityForeign, + 'isCustom' => true, + ], + ], + ]; + + $dataRight = [ + 'fields' => [ + $linkForeign => [ + 'type' => 'linkOne', + 'isCustom' => true, + ], + ], + 'links' => [ + $linkForeign => [ + 'type' => Entity::HAS_ONE, + 'foreign' => $link, + 'entity' => $entity, + 'isCustom' => true, + ], + ], + ]; + } + + break; + + case 'oneToMany': + + if ( + $this->metadata->get('entityDefs.' . $entityForeign . '.fields.' . $linkForeign) || + $this->metadata->get('entityDefs.' . $entityForeign . '.fields.' . $linkForeign . 'Id') || + $this->metadata->get('entityDefs.' . $entityForeign . '.fields.' . $linkForeign . 'Name') + ) { + throw new Conflict("Field $entityForeign::$linkForeign already exists."); + } + + $dataLeft = [ + 'fields' => [ + $link => [ + 'type' => 'linkMultiple', + 'layoutDetailDisabled' => !$linkMultipleField, + 'layoutMassUpdateDisabled' => !$linkMultipleField, + 'layoutListDisabled' => !$linkMultipleField, + 'noLoad' => !$linkMultipleField, + 'importDisabled' => !$linkMultipleField, + 'exportDisabled' => !$linkMultipleField, + 'customizationDisabled' => !$linkMultipleField, + 'isCustom' => true, + ], + ], + 'links' => [ + $link => [ + 'type' => Entity::HAS_MANY, + 'foreign' => $linkForeign, + 'entity' => $entityForeign, + 'audited' => $auditedForeign, + 'isCustom' => true, + ], + ], + ]; + + $dataRight = [ + 'fields' => [ + $linkForeign => [ + 'type' => 'link', + ], + ], + 'links' => [ + $linkForeign => [ + 'type' => Entity::BELONGS_TO, + 'foreign' => $link, + 'entity' => $entity, + 'audited' => $audited, + 'isCustom' => true, + ], + ], + ]; + + break; + + case 'manyToOne': + + if ( + $this->metadata->get('entityDefs.' . $entity . '.fields.' . $link) || + $this->metadata->get('entityDefs.' . $entity . '.fields.' . $link . 'Id') || + $this->metadata->get('entityDefs.' . $entity . '.fields.' . $link . 'Name') + ) { + throw new Conflict("Field $entity::$link already exists."); + } + + $dataLeft = [ + 'fields' => [ + $link => [ + 'type' => 'link', + ], + ], + 'links' => [ + $link => [ + 'type' => Entity::BELONGS_TO, + 'foreign' => $linkForeign, + 'entity' => $entityForeign, + 'audited' => $auditedForeign, + 'isCustom' => true, + ], + ], + ]; + + $dataRight = [ + 'fields' => [ + $linkForeign => [ + 'type' => 'linkMultiple', + 'layoutDetailDisabled' => !$linkMultipleFieldForeign, + 'layoutMassUpdateDisabled' => !$linkMultipleFieldForeign, + 'layoutListDisabled' => !$linkMultipleFieldForeign, + 'noLoad' => !$linkMultipleFieldForeign, + 'importDisabled' => !$linkMultipleFieldForeign, + 'exportDisabled' => !$linkMultipleFieldForeign, + 'customizationDisabled' => !$linkMultipleField, + 'isCustom' => true, + ] + ], + 'links' => [ + $linkForeign => [ + 'type' => Entity::HAS_MANY, + 'foreign' => $link, + 'entity' => $entity, + 'audited' => $audited, + 'isCustom' => true, + ], + ], + ]; + + break; + + case 'manyToMany': + $dataLeft = [ + 'fields' => [ + $link => [ + 'type' => 'linkMultiple', + 'layoutDetailDisabled' => !$linkMultipleField, + 'layoutMassUpdateDisabled' => !$linkMultipleField, + 'layoutListDisabled' => !$linkMultipleField, + 'noLoad' => !$linkMultipleField, + 'importDisabled' => !$linkMultipleField, + 'exportDisabled' => !$linkMultipleField, + 'customizationDisabled' => !$linkMultipleField, + 'isCustom' => true, + ] + ], + 'links' => [ + $link => [ + 'type' => Entity::HAS_MANY, + 'relationName' => $relationName, + 'foreign' => $linkForeign, + 'entity' => $entityForeign, + 'audited' => $auditedForeign, + 'isCustom' => true, + ], + ], + ]; + + $dataRight = [ + 'fields' => [ + $linkForeign => [ + 'type' => 'linkMultiple', + 'layoutDetailDisabled' => !$linkMultipleFieldForeign, + 'layoutMassUpdateDisabled' => !$linkMultipleFieldForeign, + 'layoutListDisabled' => !$linkMultipleFieldForeign, + 'noLoad' => !$linkMultipleFieldForeign, + 'importDisabled' => !$linkMultipleFieldForeign, + 'exportDisabled' => !$linkMultipleFieldForeign, + 'customizationDisabled' => !$linkMultipleField, + 'isCustom' => true, + ] + ], + 'links' => [ + $linkForeign => [ + 'type' => Entity::HAS_MANY, + 'relationName' => $relationName, + 'foreign' => $link, + 'entity' => $entity, + 'audited' => $audited, + 'isCustom' => true, + ] + ] + ]; + + if ($entityForeign == $entity) { + $dataLeft['links'][$link]['midKeys'] = ['leftId', 'rightId']; + + $dataRight['links'][$linkForeign]['midKeys'] = ['rightId', 'leftId']; + } + + break; + + case 'childrenToParent': + $dataLeft = [ + 'fields' => [ + $link => [ + 'type' => 'linkParent', + 'entityList' => $params['parentEntityTypeList'] ?? null, + ], + ], + 'links' => [ + $link => [ + 'type' => Entity::BELONGS_TO_PARENT, + 'foreign' => $linkForeign, + 'isCustom' => true, + ], + ], + ]; + + break; + + default: + throw new BadRequest(); + } + + $this->metadata->set('entityDefs', $entity, $dataLeft); + + if ($entityForeign) { + $this->metadata->set('entityDefs', $entityForeign, $dataRight); + } + + $this->setLayouts($params); + + $this->metadata->save(); + + $this->language->set($entity, 'fields', $link, $label); + $this->language->set($entity, 'links', $link, $label); + + if ($entityForeign) { + $this->language->set($entityForeign, 'fields', $linkForeign, $labelForeign); + $this->language->set($entityForeign, 'links', $linkForeign, $labelForeign); + } + + $this->language->save(); + + if ($this->isLanguageNotBase()) { + $this->baseLanguage->set($entity, 'fields', $link, $label); + $this->baseLanguage->set($entity, 'links', $link, $label); + + if ($entityForeign) { + $this->baseLanguage->set($entityForeign, 'fields', $linkForeign, $labelForeign); + $this->baseLanguage->set($entityForeign, 'links', $linkForeign, $labelForeign); + } + + $this->baseLanguage->save(); + } + + if ($linkType === 'childrenToParent') { + $foreignLinkEntityTypeList = $params['foreignLinkEntityTypeList'] ?? null; + + if (is_array($foreignLinkEntityTypeList)) { + $this->updateParentForeignLinks($entity, $link, $linkForeign, $foreignLinkEntityTypeList); + } + } + + $this->linkHookProcessor->processCreate($linkParams); + + $this->dataManager->rebuild(); + } + + /** + * @param array{ + * entity: string, + * link: string, + * entityForeign?: ?string, + * linkForeign?: ?string, + * label?: string, + * labelForeign?: string, + * linkMultipleField?: bool, + * linkMultipleFieldForeign?: bool, + * audited?: bool, + * auditedForeign?: bool, + * parentEntityTypeList?: string[], + * foreignLinkEntityTypeList?: string[], + * layout?: string, + * layoutForeign?: string, + * } $params + * @throws BadRequest + * @throws Error + */ + public function update(array $params): void + { + $entity = $params['entity']; + $link = $params['link']; + $entityForeign = $params['entityForeign'] ?? null; + $linkForeign = $params['linkForeign'] ?? null; + + if (empty($link)) { + throw new BadRequest(); + } + + if (empty($entity)) { + throw new BadRequest(); + } + + $linkType = $this->metadata->get("entityDefs.$entity.links.$link.type"); + $isCustom = $this->metadata->get("entityDefs.$entity.links.$link.isCustom"); + + if ($linkType !== Entity::BELONGS_TO_PARENT) { + if (empty($entityForeign)) { + throw new BadRequest(); + } + + if (empty($linkForeign)) { + throw new BadRequest(); + } + } + + if ( + $this->metadata->get("entityDefs.$entity.links.$link.type") == Entity::HAS_MANY && + $this->metadata->get("entityDefs.$entity.links.$link.isCustom") + ) { + if (array_key_exists('linkMultipleField', $params)) { + $linkMultipleField = $params['linkMultipleField']; + + $dataLeft = [ + 'fields' => [ + $link => [ + 'type' => 'linkMultiple', + 'layoutDetailDisabled' => !$linkMultipleField, + 'layoutMassUpdateDisabled' => !$linkMultipleField, + 'layoutListDisabled' => !$linkMultipleField, + 'noLoad' => !$linkMultipleField, + 'importDisabled' => !$linkMultipleField, + 'exportDisabled' => !$linkMultipleField, + 'customizationDisabled' => !$linkMultipleField, + 'isCustom' => true, + ] + ] + ]; + + $this->metadata->set('entityDefs', $entity, $dataLeft); + + $this->metadata->save(); + } + } + + if ( + $this->metadata->get("entityDefs.$entityForeign.links.$linkForeign.type") == Entity::HAS_MANY && + $this->metadata->get("entityDefs.$entityForeign.links.$linkForeign.isCustom") + ) { + /** @var string $entityForeign */ + + if (array_key_exists('linkMultipleFieldForeign', $params)) { + $linkMultipleFieldForeign = $params['linkMultipleFieldForeign']; + + $dataRight = [ + 'fields' => [ + $linkForeign => [ + 'type' => 'linkMultiple', + 'layoutDetailDisabled' => !$linkMultipleFieldForeign, + 'layoutMassUpdateDisabled' => !$linkMultipleFieldForeign, + 'layoutListDisabled' => !$linkMultipleFieldForeign, + 'noLoad' => !$linkMultipleFieldForeign, + 'importDisabled' => !$linkMultipleFieldForeign, + 'exportDisabled' => !$linkMultipleFieldForeign, + 'customizationDisabled' => !$linkMultipleFieldForeign, + 'isCustom' => true, + ] + ] + ]; + + $this->metadata->set('entityDefs', $entityForeign, $dataRight); + $this->metadata->save(); + } + } + + if ( + in_array($this->metadata->get("entityDefs.$entity.links.$link.type"), [ + Entity::HAS_MANY, + Entity::HAS_CHILDREN, + ]) + ) { + if (array_key_exists('audited', $params)) { + $audited = $params['audited']; + + $dataLeft = [ + 'links' => [ + $link => [ + "audited" => $audited, + ], + ], + ]; + $this->metadata->set('entityDefs', $entity, $dataLeft); + $this->metadata->save(); + } + } + + if ( + $linkForeign && + in_array( + $this->metadata->get("entityDefs.$entityForeign.links.$linkForeign.type"), + [ + Entity::HAS_MANY, + Entity::HAS_CHILDREN, + ] + ) + ) { + /** @var string $entityForeign */ + + if (array_key_exists('auditedForeign', $params)) { + $auditedForeign = $params['auditedForeign']; + + $dataRight = [ + 'links' => [ + $linkForeign => [ + "audited" => $auditedForeign, + ], + ], + ]; + + $this->metadata->set('entityDefs', $entityForeign, $dataRight); + $this->metadata->save(); + } + } + + if ($linkType === Entity::BELONGS_TO_PARENT) { + $parentEntityTypeList = $params['parentEntityTypeList'] ?? null; + + if (is_array($parentEntityTypeList)) { + $data = [ + 'fields' => [ + $link => [ + 'entityList' => $parentEntityTypeList, + ], + ], + ]; + + $this->metadata->set('entityDefs', $entity, $data); + $this->metadata->save(); + } + + $foreignLinkEntityTypeList = $params['foreignLinkEntityTypeList'] ?? null; + + if ($linkForeign && is_array($foreignLinkEntityTypeList)) { + $this->updateParentForeignLinks($entity, $link, $linkForeign, $foreignLinkEntityTypeList); + } + } + + $this->setLayouts($params); + $this->metadata->save(); + + $label = null; + + if (isset($params['label'])) { + $label = $params['label']; + } + + if ($label) { + $this->language->set($entity, 'fields', $link, $label); + $this->language->set($entity, 'links', $link, $label); + } + + $labelForeign = null; + + if ($linkType !== Entity::BELONGS_TO_PARENT) { + /** @var string $linkForeign */ + /** @var string $entityForeign */ + + if (isset($params['labelForeign'])) { + $labelForeign = $params['labelForeign']; + } + + if ($labelForeign) { + $this->language->set($entityForeign, 'fields', $linkForeign, $labelForeign); + $this->language->set($entityForeign, 'links', $linkForeign, $labelForeign); + } + } + + $this->language->save(); + + if ($isCustom) { + if ($this->language->getLanguage() !== $this->baseLanguage->getLanguage()) { + + if ($label) { + $this->baseLanguage->set($entity, 'fields', $link, $label); + $this->baseLanguage->set($entity, 'links', $link, $label); + } + + if ($labelForeign && $linkType !== Entity::BELONGS_TO_PARENT) { + /** @var string $linkForeign */ + /** @var string $entityForeign */ + + $this->baseLanguage->set($entityForeign, 'fields', $linkForeign, $labelForeign); + $this->baseLanguage->set($entityForeign, 'links', $linkForeign, $labelForeign); + } + + $this->baseLanguage->save(); + } + } + + $this->dataManager->clearCache(); + } + + /** + * @param array{ + * entity?: string, + * link?: string, + * } $params + * @throws Error + * @throws BadRequest + */ + public function delete(array $params): void + { + $entity = $params['entity'] ?? null; + $link = $params['link'] ?? null; + + if (!$this->metadata->get("entityDefs.$entity.links.$link.isCustom")) { + throw new Error("Could not delete link $entity.$link. Not isCustom."); + } + + if (empty($entity) || empty($link)) { + throw new BadRequest(); + } + + $entityForeign = $this->metadata->get("entityDefs.$entity.links.$link.entity"); + $linkForeign = $this->metadata->get("entityDefs.$entity.links.$link.foreign"); + $linkType = $this->metadata->get("entityDefs.$entity.links.$link.type"); + + if (!$this->metadata->get(['entityDefs', $entity, 'links', $link, 'isCustom'])) { + throw new Error("Can't remove not custom link."); + } + + if ($linkType === Entity::HAS_CHILDREN) { + $this->metadata->delete('entityDefs', $entity, [ + 'links.' . $link, + ]); + + $this->metadata->save(); + + return; + } + + if ($linkType === Entity::BELONGS_TO_PARENT) { + $this->metadata->delete('entityDefs', $entity, [ + 'fields.' . $link, + 'links.' . $link, + ]); + + $this->metadata->save(); + + if ($linkForeign) { + $this->updateParentForeignLinks($entity, $link, $linkForeign, []); + } + + return; + } + + if (empty($entityForeign) || empty($linkForeign)) { + throw new BadRequest(); + } + + $foreignLinkType = $this->metadata->get(['entityDefs', $entityForeign, 'links', $linkForeign, 'type']); + + $type = null; + + if ($linkType === Entity::HAS_MANY && $foreignLinkType === Entity::HAS_MANY) { + $type = LinkType::MANY_TO_MANY; + } + else if ($linkType === Entity::HAS_MANY && $foreignLinkType === Entity::BELONGS_TO) { + $type = LinkType::ONE_TO_MANY; + } + else if ($linkType === Entity::BELONGS_TO && $foreignLinkType === Entity::HAS_MANY) { + $type = LinkType::MANY_TO_ONE; + } + else if ($linkType === Entity::HAS_ONE && $foreignLinkType === Entity::BELONGS_TO) { + $type = LinkType::ONE_TO_ONE_LEFT; + } + else if ($linkType === Entity::BELONGS_TO && $foreignLinkType === Entity::HAS_ONE) { + $type = LinkType::ONE_TO_ONE_RIGHT; + } + + $name = $this->metadata->get(['entityDefs', $entity, $link, 'relationName']) ?? + $this->metadata->get(['entityDefs', $entityForeign, $linkForeign, 'relationName']); + + $linkParams = null; + + if ($type) { + $linkParams = LinkParams::createBuilder() + ->setType($type) + ->setName($name) + ->setEntityType($entity) + ->setForeignEntityType($entityForeign) + ->setLink($link) + ->setForeignLink($linkForeign) + ->build(); + } + + $this->metadata->delete('entityDefs', $entity, [ + 'fields.' . $link, + 'links.' . $link + ]); + + $this->metadata->delete('entityDefs', $entityForeign, [ + 'fields.' . $linkForeign, + 'links.' . $linkForeign + ]); + + $this->metadata->delete('clientDefs', $entity, ['relationshipPanels.' . $link]); + $this->metadata->delete('clientDefs', $entityForeign, ['relationshipPanels.' . $linkForeign]); + + $this->metadata->save(); + + if ($linkParams) { + $this->linkHookProcessor->processDelete($linkParams); + } + + $this->dataManager->clearCache(); + } + + /** + * @param array{ + * entity: string, + * link: string, + * entityForeign?: ?string, + * linkForeign?: ?string, + * layout?: string, + * layoutForeign?: string, + * } $params + */ + private function setLayouts(array $params): void + { + $this->setLayout($params['entity'], $params['link'], $params['layout'] ?? null); + + if (isset($params['entityForeign']) && isset($params['linkForeign'])) { + $this->setLayout($params['entityForeign'], $params['linkForeign'], $params['layoutForeign'] ?? null); + } + } + + private function setLayout(string $entityType, string $link, ?string $layout): void + { + $this->metadata->set('clientDefs', $entityType, [ + 'relationshipPanels' => [ + $link => [ + 'layout' => $layout, + ] + ] + ]); + } + + /** + * @param string[] $foreignLinkEntityTypeList + */ + private function updateParentForeignLinks( + string $entityType, + string $link, + string $linkForeign, + array $foreignLinkEntityTypeList + ): void { + + $toCreateList = []; + + foreach ($foreignLinkEntityTypeList as $foreignEntityType) { + $linkDefs = $this->metadata->get(['entityDefs', $foreignEntityType, 'links']) ?? []; + + foreach ($linkDefs as $kLink => $defs) { + $kForeign = $defs['foreign'] ?? null; + $kIsCustom = $defs['isCustom'] ?? false; + $kEntity = $defs['entity'] ?? null; + + if ( + $kForeign === $link && !$kIsCustom && $kEntity == $entityType + ) { + continue 2; + } + + if ($kLink == $linkForeign) { + if ($defs['type'] !== Entity::HAS_CHILDREN) { + continue 2; + } + } + } + + $toCreateList[] = $foreignEntityType; + } + + /** @var string[] $entityTypeList */ + $entityTypeList = array_keys($this->metadata->get('entityDefs') ?? []); + + foreach ($entityTypeList as $itemEntityType) { + $linkDefs = $this->metadata->get(['entityDefs', $itemEntityType, 'links']) ?? []; + + foreach ($linkDefs as $kLink => $defs) { + $kForeign = $defs['foreign'] ?? null; + $kIsCustom = $defs['isCustom'] ?? false; + $kEntity = $defs['entity'] ?? null; + + if ( + $kForeign === $link && $kIsCustom && $kEntity == $entityType && + $defs['type'] == Entity::HAS_CHILDREN && $kLink === $linkForeign + ) { + if (!in_array($itemEntityType, $toCreateList)) { + $this->metadata->delete('entityDefs', $itemEntityType, [ + 'links.' . $linkForeign, + ]); + + $this->language->delete($itemEntityType, 'links', $linkForeign); + + if ( + $this->isLanguageNotBase() + ) { + $this->baseLanguage->delete($itemEntityType, 'links', $linkForeign); + } + } + + break; + } + } + } + + foreach ($toCreateList as $itemEntityType) { + $this->metadata->set('entityDefs', $itemEntityType, [ + 'links' => [ + $linkForeign => [ + 'type' => Entity::HAS_CHILDREN, + 'foreign' => $link, + 'entity' => $entityType, + 'isCustom' => true, + ], + ], + ]); + + $label = $this->language->translate($entityType, 'scopeNamesPlural'); + + $this->language->set($itemEntityType, 'links', $linkForeign, $label); + + if ($this->isLanguageNotBase()) { + $this->baseLanguage->set($itemEntityType, 'links', $linkForeign, $label); + } + } + + $this->metadata->save(); + + $this->language->save(); + + if ($this->isLanguageNotBase()) { + $this->baseLanguage->save(); + } + } + + private function isLanguageNotBase(): bool + { + return $this->language->getLanguage() !== $this->baseLanguage->getLanguage(); + } +} diff --git a/application/Espo/Tools/EntityManager/Link/Params.php b/application/Espo/Tools/LinkManager/Params.php similarity index 94% rename from application/Espo/Tools/EntityManager/Link/Params.php rename to application/Espo/Tools/LinkManager/Params.php index 17413472b9..824666f4bf 100644 --- a/application/Espo/Tools/EntityManager/Link/Params.php +++ b/application/Espo/Tools/LinkManager/Params.php @@ -27,7 +27,9 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Tools\EntityManager\Link; +namespace Espo\Tools\LinkManager; + +use Espo\Tools\LinkManager\ParamsBuilder; /** * @immutable @@ -38,8 +40,8 @@ class Params private string $entityType; private string $link; private string $foreignLink; - private ?string $foreignEntityType = null; - private ?string $name = null; + private ?string $foreignEntityType; + private ?string $name; public function __construct( string $type, diff --git a/application/Espo/Tools/EntityManager/Link/ParamsBuilder.php b/application/Espo/Tools/LinkManager/ParamsBuilder.php similarity index 98% rename from application/Espo/Tools/EntityManager/Link/ParamsBuilder.php rename to application/Espo/Tools/LinkManager/ParamsBuilder.php index eb6eb00e90..f47e3b3d9e 100644 --- a/application/Espo/Tools/EntityManager/Link/ParamsBuilder.php +++ b/application/Espo/Tools/LinkManager/ParamsBuilder.php @@ -27,20 +27,15 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Tools\EntityManager\Link; +namespace Espo\Tools\LinkManager; class ParamsBuilder { private string $type; - private string $entityType; - private string $link; - private string $foreignLink; - private ?string $foreignEntityType = null; - private ?string $name = null; public function setType(string $type): self diff --git a/application/Espo/Tools/EntityManager/Link/Type.php b/application/Espo/Tools/LinkManager/Type.php similarity index 97% rename from application/Espo/Tools/EntityManager/Link/Type.php rename to application/Espo/Tools/LinkManager/Type.php index bfc8468007..9c29a2bcaf 100644 --- a/application/Espo/Tools/EntityManager/Link/Type.php +++ b/application/Espo/Tools/LinkManager/Type.php @@ -27,19 +27,14 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Tools\EntityManager\Link; +namespace Espo\Tools\LinkManager; class Type { public const MANY_TO_MANY = 'manyToMany'; - public const MANY_TO_ONE = 'manyToOne'; - public const ONE_TO_MANY = 'oneToMany'; - public const ONE_TO_ONE_LEFT = 'oneToOneLeft'; - public const ONE_TO_ONE_RIGHT = 'oneToOneRight'; - public const CHILDREN_TO_PARENT = 'childrenToParent'; } diff --git a/schema/metadata/app/entityManager.json b/schema/metadata/app/entityManager.json new file mode 100644 index 0000000000..52fe1b9dd8 --- /dev/null +++ b/schema/metadata/app/entityManager.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://www.espocrm.com/schema/metadata/app/entityManager.json", + "title": "app/linkManager", + "description": "Entity manager definitions.", + "type": "object", + "properties": { + "createHookClassNameList": { + "type": "array", + "description": "Hooks called when creating a new entity (in the Entity Manager tool). Use __APPEND__ for extending. Should implement Espo\\Tools\\EntityManager\\Hook\\CreateHook.", + "items": { + "anyOf": [ + {"const": "__APPEND__"}, + { + "type": "string" + } + ] + } + }, + "deleteHookClassNameList": { + "type": "array", + "description": "Hooks called when deleting an entity (in the Entity Manager tool). Use __APPEND__ for extending. Should implement Espo\\Tools\\EntityManager\\Hook\\DeleteHook.", + "items": { + "anyOf": [ + {"const": "__APPEND__"}, + { + "type": "string" + } + ] + } + }, + "updateHookClassNameList": { + "type": "array", + "description": "Hooks called when updating an entity (in the Entity Manager tool). Use __APPEND__ for extending. Should implement Espo\\Tools\\EntityManager\\Hook\\UpdateHook.", + "items": { + "anyOf": [ + {"const": "__APPEND__"}, + { + "type": "string" + } + ] + } + } + } +} diff --git a/schema/metadata/app/linkManager.json b/schema/metadata/app/linkManager.json index ae6454e907..c1cac50132 100644 --- a/schema/metadata/app/linkManager.json +++ b/schema/metadata/app/linkManager.json @@ -7,7 +7,7 @@ "properties": { "createHookClassNameList": { "type": "array", - "description": "Hooks called when creating a new relationship (in the Entity Manager tool). Use __APPEND__ for extending. Should implement Espo\\Tools\\EntityManager\\Link\\CreateHook.", + "description": "Hooks called when creating a new relationship (in the Entity Manager tool). Use __APPEND__ for extending. Should implement Espo\\Tools\\LinkManager\\Hook\\CreateHook.", "items": { "anyOf": [ {"const": "__APPEND__"}, @@ -19,7 +19,7 @@ }, "deleteHookClassNameList": { "type": "array", - "description": "Hooks called when deleting a relationship (in the Entity Manager tool). Use __APPEND__ for extending. Should implement Espo\\Tools\\EntityManager\\Link\\DeleteHook.", + "description": "Hooks called when deleting a relationship (in the Entity Manager tool). Use __APPEND__ for extending. Should implement Espo\\Tools\\LinkManager\\Hook\\DeleteHook.", "items": { "anyOf": [ {"const": "__APPEND__"},