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__"},