diff --git a/application/Espo/Core/Formula/Functions/EntityGroup/IsRelatedType.php b/application/Espo/Core/Formula/Functions/EntityGroup/IsRelatedType.php index 1ebdac7df5..e533621b11 100644 --- a/application/Espo/Core/Formula/Functions/EntityGroup/IsRelatedType.php +++ b/application/Espo/Core/Formula/Functions/EntityGroup/IsRelatedType.php @@ -61,6 +61,7 @@ class IsRelatedType extends \Espo\Core\Formula\Functions\Base implements return $this->entityManager ->getRDBRepository($this->getEntity()->getEntityType()) - ->isRelated($this->getEntity(), $link, $id); + ->getRelation($this->getEntity(), $link) + ->isRelatedById($id); } } diff --git a/application/Espo/Core/Portal/Acl/DefaultOwnershipChecker.php b/application/Espo/Core/Portal/Acl/DefaultOwnershipChecker.php index 8b3c22193f..c4cf3f87c6 100644 --- a/application/Espo/Core/Portal/Acl/DefaultOwnershipChecker.php +++ b/application/Espo/Core/Portal/Acl/DefaultOwnershipChecker.php @@ -105,7 +105,11 @@ class DefaultOwnershipChecker implements $repository = $this->entityManager->getRDBRepository($entity->getEntityType()); foreach ($accountIdList as $accountId) { - if ($repository->isRelated($entity, self::FIELD_ACCOUNTS, $accountId)) { + if ( + $repository + ->getRelation($entity, self::FIELD_ACCOUNTS) + ->isRelatedById($accountId) + ) { return true; } } @@ -146,7 +150,11 @@ class DefaultOwnershipChecker implements ) { $repository = $this->entityManager->getRDBRepository($entity->getEntityType()); - if ($repository->isRelated($entity, self::FIELD_CONTACTS, $contactId)) { + if ( + $repository + ->getRelation($entity, self::FIELD_CONTACTS) + ->isRelatedById($contactId) + ) { return true; } } diff --git a/application/Espo/ORM/Mapper/BaseMapper.php b/application/Espo/ORM/Mapper/BaseMapper.php index 6777ce4d3b..dd44be2011 100644 --- a/application/Espo/ORM/Mapper/BaseMapper.php +++ b/application/Espo/ORM/Mapper/BaseMapper.php @@ -318,7 +318,7 @@ class BaseMapper implements RDBMapper case Entity::BELONGS_TO: /** @var Entity $relEntity */ - $params['whereClause'][$foreignKey] = $entity->get($key); + $params['whereClause'][] = [$foreignKey =>$entity->get($key)]; $params['offset'] = 0; $params['limit'] = 1; $params['from'] = $relEntity->getEntityType(); @@ -358,7 +358,7 @@ class BaseMapper implements RDBMapper $params['from'] = $relEntity->getEntityType(); $params['fromAlias'] ??= lcfirst($relEntity->getEntityType()); - $params['whereClause'][$foreignKey] = $entity->get($key); + $params['whereClause'][] = [$foreignKey => $entity->get($key)]; if ($relType == Entity::HAS_CHILDREN) { $foreignType = $keySet['foreignType'] ?? null; @@ -367,7 +367,7 @@ class BaseMapper implements RDBMapper throw new RuntimeException("Bad relation key."); } - $params['whereClause'][$foreignType] = $entity->getEntityType(); + $params['whereClause'][] = [$foreignType => $entity->getEntityType()]; } $relConditions = $this->getRelationParam($entity, $relationName, 'conditions'); @@ -456,7 +456,7 @@ class BaseMapper implements RDBMapper return null; } - $params['whereClause'][$foreignKey] = $foreignEntityId; + $params['whereClause'][] = [$foreignKey => $foreignEntityId]; $params['offset'] = 0; $params['limit'] = 1; diff --git a/application/Espo/ORM/Repository/RDBRelation.php b/application/Espo/ORM/Repository/RDBRelation.php index 0b4b5af74f..525d46f603 100644 --- a/application/Espo/ORM/Repository/RDBRelation.php +++ b/application/Espo/ORM/Repository/RDBRelation.php @@ -74,7 +74,7 @@ class RDBRelation } if (!$entity->hasRelation($relationName)) { - throw new RuntimeException("Entity does not have a relation '{$relationName}'."); + throw new RuntimeException("Entity does not have a relation '$relationName'."); } $this->relationName = $relationName; @@ -103,7 +103,7 @@ class RDBRelation private function createSelectBuilder(?Select $query = null): Builder { if ($this->noBuilder) { - throw new RuntimeException("Can't use query builder for the '{$this->relationType}' relation type."); + throw new RuntimeException("Can't use query builder for the '$this->relationType' relation type."); } return new Builder($this->entityManager, $this->entity, $this->relationName, $query); @@ -115,7 +115,7 @@ class RDBRelation public function clone(Select $query): Builder { if ($this->noBuilder) { - throw new RuntimeException("Can't use clone for the '{$this->relationType}' relation type."); + throw new RuntimeException("Can't use clone for the '$this->relationType' relation type."); } if ($query->getFrom() !== $this->foreignEntityType) { @@ -134,6 +134,7 @@ class RDBRelation { $mapper = $this->entityManager->getMapper(); + /** @noinspection PhpConditionAlreadyCheckedInspection */ if (!$mapper instanceof RDBMapper) { throw new LogicException(); } @@ -251,7 +252,7 @@ class RDBRelation * * `where(string $key, string $value)` * * @param WhereItem|array|string $clause A key or where clause. - * @param mixed[]|scalar|null $value A value. Should be omitted if the first argument is not string. + * @param array|scalar|null $value A value. Should be omitted if the first argument is not string. */ public function where($clause = [], $value = null): Builder { @@ -267,7 +268,7 @@ class RDBRelation * * `having(string $key, string $value)` * * @param WhereItem|array|string $clause A key or where clause. - * @param mixed[]|string|null $value A value. Should be omitted if the first argument is not string. + * @param array|string|null $value A value. Should be omitted if the first argument is not string. */ public function having($clause = [], $value = null): Builder { @@ -396,6 +397,22 @@ class RDBRelation ->findOne(); } + /** + * Whether related with another entity. An entity is specified by an ID. + * Does not work with 'belongsToParent' relations. + */ + public function isRelatedById(string $id): bool + { + if ($this->isBelongsToParentType()) { + throw new LogicException("Can't use isRelatedById for 'belongsToParent'."); + } + + return (bool) $this->createSelectBuilder() + ->select(['id']) + ->where(['id' => $id]) + ->findOne(); + } + private function isRelatedBelongsToParent(Entity $entity): bool { $fromEntity = $this->entity; diff --git a/application/Espo/ORM/Repository/RDBRelationSelectBuilder.php b/application/Espo/ORM/Repository/RDBRelationSelectBuilder.php index 9f548234cc..34e2b1a47f 100644 --- a/application/Espo/ORM/Repository/RDBRelationSelectBuilder.php +++ b/application/Espo/ORM/Repository/RDBRelationSelectBuilder.php @@ -106,15 +106,16 @@ class RDBRelationSelectBuilder return $this->createSelectBuilder()->clone($newQuery); } - protected function createSelectBuilder(): SelectBuilder + private function createSelectBuilder(): SelectBuilder { return new SelectBuilder(); } - protected function getMapper(): RDBMapper + private function getMapper(): RDBMapper { $mapper = $this->entityManager->getMapper(); + /** @noinspection PhpConditionAlreadyCheckedInspection */ if (!$mapper instanceof RDBMapper) { throw new LogicException(); } @@ -155,7 +156,7 @@ class RDBRelationSelectBuilder * @param array $where * @return array */ - protected function applyMiddleAliasToWhere(array $where): array + private function applyMiddleAliasToWhere(array $where): array { $transformedWhere = []; @@ -293,7 +294,7 @@ class RDBRelationSelectBuilder * * `where(string $key, string $value)` * * @param WhereItem|array|string $clause A key or where clause. - * @param mixed[]|scalar|null $value A value. Should be omitted if the first argument is not string. + * @param array|scalar|null $value A value. Should be omitted if the first argument is not string. */ public function where($clause = [], $value = null): self { @@ -322,8 +323,8 @@ class RDBRelationSelectBuilder * * `having(array $clause)` * * `having(string $key, string $value)` * - * @param WhereItem|array|string $clause A key or where clause. - * @param mixed[]|string|null $value A value. Should be omitted if the first argument is not string. + * @param WhereItem|array|string $clause A key or where clause. + * @param array|string|null $value A value. Should be omitted if the first argument is not string. */ public function having($clause = [], $value = null): self { @@ -411,7 +412,7 @@ class RDBRelationSelectBuilder return $this->group($groupBy); } - protected function getMiddleTableAlias(): ?string + private function getMiddleTableAlias(): ?string { if (!$this->isManyMany()) { return null; @@ -430,7 +431,7 @@ class RDBRelationSelectBuilder return $this->middleTableAlias; } - protected function applyRelationAliasToWhereClauseKey(string $item): string + private function applyRelationAliasToWhereClauseKey(string $item): string { if (!$this->isManyMany()) { return $item; @@ -445,7 +446,7 @@ class RDBRelationSelectBuilder * @param array $where * @return array */ - protected function applyRelationAliasToWhereClause(array $where): array + private function applyRelationAliasToWhereClause(array $where): array { if (!$this->isManyMany()) { return $where; @@ -457,10 +458,6 @@ class RDBRelationSelectBuilder $transformedKey = $key; $transformedValue = $value; - if (is_int($key)) { - $transformedKey = $key; - } - if (is_string($key)) { $transformedKey = $this->applyRelationAliasToWhereClauseKey($key); } @@ -475,7 +472,7 @@ class RDBRelationSelectBuilder return $transformedWhere; } - protected function isManyMany(): bool + private function isManyMany(): bool { return $this->relationType === Entity::MANY_MANY; } @@ -484,7 +481,7 @@ class RDBRelationSelectBuilder * @param Collection $collection * @return Collection */ - protected function handleReturnCollection(Collection $collection): Collection + private function handleReturnCollection(Collection $collection): Collection { if (!$collection instanceof SthCollection) { return $collection; @@ -499,6 +496,7 @@ class RDBRelationSelectBuilder /** * @return mixed + * @noinspection PhpSameParameterValueInspection */ private function getRelationParam(string $param) { diff --git a/application/Espo/Tools/EmailNotification/Processor.php b/application/Espo/Tools/EmailNotification/Processor.php index 0520bd5c1b..d7738c4e4f 100644 --- a/application/Espo/Tools/EmailNotification/Processor.php +++ b/application/Espo/Tools/EmailNotification/Processor.php @@ -790,8 +790,8 @@ class Processor foreach ($eaList as $ea) { if ( - $emailRepository->isRelated($emailSubject, 'toEmailAddresses', $ea) || - $emailRepository->isRelated($emailSubject, 'ccEmailAddresses', $ea) + $emailRepository->getRelation($emailSubject, 'toEmailAddresses')->isRelated($ea) || + $emailRepository->getRelation($emailSubject, 'ccEmailAddresses')->isRelated($ea) ) { return; } diff --git a/tests/integration/Espo/Core/Formula/FormulaTest.php b/tests/integration/Espo/Core/Formula/FormulaTest.php index cf68db79a1..4d6e2f6967 100644 --- a/tests/integration/Espo/Core/Formula/FormulaTest.php +++ b/tests/integration/Espo/Core/Formula/FormulaTest.php @@ -33,7 +33,9 @@ use Espo\Core\Field\DateTime; use Espo\Core\Field\DateTimeOptional; use Espo\Core\Formula\Manager; use Espo\Entities\User; +use Espo\Modules\Crm\Entities\Account; use Espo\Modules\Crm\Entities\Meeting; +use Espo\Modules\Crm\Entities\Opportunity; use Espo\ORM\EntityManager; class FormulaTest extends \tests\integration\Core\BaseTestCase @@ -871,4 +873,26 @@ class FormulaTest extends \tests\integration\Core\BaseTestCase ); $this->assertFalse($fm->run($script)); } + + public function testIsRelated(): void + { + $em = $this->getEntityManager(); + $fm = $this->getContainer()->getByClass(Manager::class); + + $account = $em->createEntity(Account::ENTITY_TYPE, []); + $opp = $em->createEntity(Opportunity::ENTITY_TYPE, []); + + $em + ->getRDBRepositoryByClass(Account::class) + ->getRelation($account, 'opportunities') + ->relate($opp); + + $script = sprintf( + "entity\\isRelated('opportunities', '%s')", + $opp->getId() + ); + + /** @noinspection PhpUnhandledExceptionInspection */ + $this->assertTrue($fm->run($script, $account)); + } } diff --git a/tests/integration/Espo/ORM/RepositoryTest.php b/tests/integration/Espo/ORM/RepositoryTest.php index 031840844b..512df8b7c7 100644 --- a/tests/integration/Espo/ORM/RepositoryTest.php +++ b/tests/integration/Espo/ORM/RepositoryTest.php @@ -30,6 +30,8 @@ namespace tests\integration\Espo\ORM; use Espo\Modules\Crm\Entities\Account; +use Espo\Modules\Crm\Entities\Contact; +use Espo\Modules\Crm\Entities\Opportunity; use Espo\ORM\EntityManager; class RepositoryTest extends \tests\integration\Core\BaseTestCase @@ -70,4 +72,54 @@ class RepositoryTest extends \tests\integration\Core\BaseTestCase $this->assertEquals($user2->getId(), $account->get('modifiedById')); } + + public function testIsRelated(): void + { + $em = $this->getEntityManager(); + + $acc1 = $em->createEntity(Account::ENTITY_TYPE, []); + $acc2 = $em->createEntity(Account::ENTITY_TYPE, []); + + $opp1 = $em->createEntity(Opportunity::ENTITY_TYPE, []); + + $contact1 = $em->createEntity(Contact::ENTITY_TYPE, []); + $contact2 = $em->createEntity(Contact::ENTITY_TYPE, []); + + $oppRepo = $em->getRDBRepositoryByClass(Opportunity::class); + + $oppRepo->getRelation($opp1, 'account')->relate($acc1); + $oppRepo->getRelation($opp1, 'contacts')->relate($contact1); + + $this->assertTrue( + $oppRepo->getRelation($opp1, 'account')->isRelatedById($acc1->getId()) + ); + + $this->assertTrue( + $oppRepo->getRelation($opp1, 'account')->isRelated($acc1) + ); + + $this->assertFalse( + $oppRepo->getRelation($opp1, 'account')->isRelatedById($acc2->getId()) + ); + + $this->assertFalse( + $oppRepo->getRelation($opp1, 'account')->isRelated($acc2) + ); + + $this->assertTrue( + $oppRepo->getRelation($opp1, 'contacts')->isRelatedById($contact1->getId()) + ); + + $this->assertTrue( + $oppRepo->getRelation($opp1, 'contacts')->isRelated($contact1) + ); + + $this->assertFalse( + $oppRepo->getRelation($opp1, 'contacts')->isRelatedById($contact2->getId()) + ); + + $this->assertFalse( + $oppRepo->getRelation($opp1, 'contacts')->isRelated($contact2) + ); + } } diff --git a/tests/unit/Espo/Core/Formula/FormulaTest.php b/tests/unit/Espo/Core/Formula/FormulaTest.php index 9ee5ccef31..8da400ae1b 100644 --- a/tests/unit/Espo/Core/Formula/FormulaTest.php +++ b/tests/unit/Espo/Core/Formula/FormulaTest.php @@ -297,46 +297,6 @@ class FormulaTest extends \PHPUnit\Framework\TestCase $this->assertTrue($result); } - function testHasValue() - { - $item = new Argument(self::stringToNode(' - { - "type": "entity\\\\isRelated", - "value": [ - { - "type": "value", - "value": "teams" - }, - { - "type": "value", - "value": "1" - } - ] - } - ')); - - $this->entity - ->expects($this->any()) - ->method('getEntityType') - ->will($this->returnValue('Test')); - - $this->repository - ->expects($this->any()) - ->method('isRelated') - ->will($this->returnValueMap([ - [$this->entity, 'teams', '1', true] - ])); - - $this->entityManager - ->expects($this->any()) - ->method('getRDBRepository') - ->will($this->returnValue($this->repository)); - - $result = $this->createProcessor()->process($item); - - $this->assertTrue($result); - } - function testAddLinkMultipleId() { $item = new Argument(self::stringToNode(' diff --git a/tests/unit/Espo/ORM/MapperTest.php b/tests/unit/Espo/ORM/MapperTest.php index 711f573e0c..0c91efb10c 100644 --- a/tests/unit/Espo/ORM/MapperTest.php +++ b/tests/unit/Espo/ORM/MapperTest.php @@ -525,8 +525,8 @@ class MapperTest extends \PHPUnit\Framework\TestCase $query = SelectBuilder::create() ->from('Note', 'note') ->where([ - 'parentId' => '1', - 'parentType' => 'Post' + ['parentId' => '1'], + ['parentType' => 'Post'], ]) ->build(); @@ -560,7 +560,7 @@ class MapperTest extends \PHPUnit\Framework\TestCase "post.created_by_id AS `createdById`, post.deleted AS `deleted` ". "FROM `post` AS `post` ". "LEFT JOIN `user` AS `createdBy` ON post.created_by_id = createdBy.id " . - "WHERE post.id = '1' AND post.deleted = 0 ". + "WHERE (post.id = '1') AND post.deleted = 0 ". "LIMIT 0, 1"; $return = [ [