diff --git a/application/Espo/Core/Utils/Database/Orm/RelationConverter.php b/application/Espo/Core/Utils/Database/Orm/RelationConverter.php index 11d5c5f3a7..08fdbd9dc2 100644 --- a/application/Espo/Core/Utils/Database/Orm/RelationConverter.php +++ b/application/Espo/Core/Utils/Database/Orm/RelationConverter.php @@ -61,6 +61,12 @@ class RelationConverter RelationParam::INDEXES, ]; + /** @var string[] */ + private $manyMergeParams = [ + RelationParam::ORDER_BY, + RelationParam::ORDER, + ]; + public function __construct( private Metadata $metadata, private InjectableFactory $injectableFactory, @@ -112,7 +118,7 @@ class RelationConverter $raw = $convertedEntityDefs->toAssoc(); if (isset($raw[EntityParam::RELATIONS][$name])) { - $this->mergeParams($raw[EntityParam::RELATIONS][$name], $params, $foreignParams ?? []); + $this->mergeParams($raw[EntityParam::RELATIONS][$name], $params, $foreignParams ?? [], $linkType); $this->correct($raw[EntityParam::RELATIONS][$name]); } @@ -172,9 +178,18 @@ class RelationConverter * @param array $params * @param array $foreignParams */ - private function mergeParams(array &$relationDefs, array $params, array $foreignParams): void + private function mergeParams(array &$relationDefs, array $params, array $foreignParams, string $linkType): void { - foreach ($this->mergeParams as $name) { + $mergeParams = $this->mergeParams; + + if ( + $linkType === RelationType::HAS_MANY || + $linkType === RelationType::HAS_CHILDREN + ) { + $mergeParams = array_merge($mergeParams, $this->manyMergeParams); + } + + foreach ($mergeParams as $name) { $additionalParam = $this->getMergedParam($name, $params, $foreignParams); if ($additionalParam === null) { @@ -227,6 +242,13 @@ class RelationConverter */ private function correct(array &$relationDefs): void { + if ( + isset($relationDefs[RelationParam::ORDER]) && + is_string($relationDefs[RelationParam::ORDER]) + ) { + $relationDefs[RelationParam::ORDER] = strtoupper($relationDefs[RelationParam::ORDER]); + } + if (!isset($relationDefs[RelationParam::ADDITIONAL_COLUMNS])) { return; } diff --git a/application/Espo/ORM/Defs/Params/RelationParam.php b/application/Espo/ORM/Defs/Params/RelationParam.php index 72e4d2a098..da94ad9e95 100644 --- a/application/Espo/ORM/Defs/Params/RelationParam.php +++ b/application/Espo/ORM/Defs/Params/RelationParam.php @@ -93,4 +93,18 @@ class RelationParam * Deferred load. */ public const DEFERRED_LOAD = 'deferredLoad'; + + /** + * Default order by. Applied on the entity level. + * + * @since 9.2.5 + */ + public const ORDER_BY = 'orderBy'; + + /** + * Default order. Applied on the entity level. + * + * @since 9.2.5 + */ + public const ORDER = 'order'; } diff --git a/application/Espo/ORM/Relation/RDBRelations.php b/application/Espo/ORM/Relation/RDBRelations.php index 8fed528acf..e4004f1e71 100644 --- a/application/Espo/ORM/Relation/RDBRelations.php +++ b/application/Espo/ORM/Relation/RDBRelations.php @@ -32,12 +32,14 @@ namespace Espo\ORM\Relation; use Espo\ORM\BaseEntity; use Espo\ORM\Defs\Defs; use Espo\ORM\Defs\Params\RelationParam; +use Espo\ORM\Defs\RelationDefs; use Espo\ORM\Entity; use Espo\ORM\EntityCollection; use Espo\ORM\EntityManager; use Espo\ORM\Mapper\RDBMapper; use Espo\ORM\Name\Attribute; use Espo\ORM\Query\Part\Order; +use Espo\ORM\Repository\RDBRelationSelectBuilder; use Espo\ORM\Type\RelationType; use LogicException; use RuntimeException; @@ -293,22 +295,11 @@ class RDBRelations implements Relations ->getEntity($this->getEntity()->getEntityType()) ->getRelation($relation); - $orderBy = null; - $order = null; + $builder = $this->entityManager + ->getRelation($this->getEntity(), $relation) + ->createBuilder(); - if ($relationDefs->getParam('orderBy')) { - $orderBy = $relationDefs->getParam('orderBy'); - - if ($relationDefs->getParam('order')) { - $order = strtoupper($relationDefs->getParam('order')) === Order::DESC ? Order::DESC : Order::ASC; - } - } - - $builder = $this->entityManager->getRelation($this->getEntity(), $relation); - - if ($orderBy) { - $builder->order($orderBy, $order); - } + $this->applyOrder($relationDefs, $builder); $collection = $builder->find(); @@ -398,4 +389,21 @@ class RDBRelations implements Relations return $foreignEntity; } + + private function applyOrder(RelationDefs $relationDefs, RDBRelationSelectBuilder $builder): void + { + $orderBy = $relationDefs->getParam(RelationParam::ORDER_BY); + + if (!$orderBy) { + return; + } + + $order = $relationDefs->getParam(RelationParam::ORDER); + + if ($order !== null) { + $order = strtoupper($order) === Order::DESC ? Order::DESC : Order::ASC; + } + + $builder->order($orderBy, $order); + } } diff --git a/application/Espo/ORM/Repository/RDBRelation.php b/application/Espo/ORM/Repository/RDBRelation.php index 235bbd84c3..235961cb74 100644 --- a/application/Espo/ORM/Repository/RDBRelation.php +++ b/application/Espo/ORM/Repository/RDBRelation.php @@ -51,7 +51,7 @@ use RuntimeException; /** * An access point for a specific relation of a record. * - * @template TEntity of Entity + * @template TEntity of Entity = Entity */ class RDBRelation { @@ -95,6 +95,18 @@ class RDBRelation } } + /** + * Create a select builder. + * + * @return Builder + * + * @since 9.2.5 + */ + public function createBuilder(): Builder + { + return $this->createSelectBuilder(); + } + /** * Create a select builder. * diff --git a/application/Espo/ORM/Repository/RDBRelationSelectBuilder.php b/application/Espo/ORM/Repository/RDBRelationSelectBuilder.php index 4988cf98fd..4df205feb9 100644 --- a/application/Espo/ORM/Repository/RDBRelationSelectBuilder.php +++ b/application/Espo/ORM/Repository/RDBRelationSelectBuilder.php @@ -53,7 +53,7 @@ use InvalidArgumentException; /** * Builds select parameters for related records for RDB repository. * - * @template TEntity of Entity + * @template TEntity of Entity = Entity */ class RDBRelationSelectBuilder { diff --git a/application/Espo/ORM/Repository/RDBRepository.php b/application/Espo/ORM/Repository/RDBRepository.php index a5a8dabcae..48c59434b7 100644 --- a/application/Espo/ORM/Repository/RDBRepository.php +++ b/application/Espo/ORM/Repository/RDBRepository.php @@ -234,7 +234,7 @@ class RDBRepository implements Repository */ public function find(): EntityCollection|SthCollection { - return $this->createSelectBuilder()->find(); + return $this->createBuilder()->find(); } /** @@ -273,7 +273,7 @@ class RDBRepository implements Repository */ public function count(): int { - return $this->createSelectBuilder()->count(); + return $this->createBuilder()->count(); } /** @@ -283,7 +283,7 @@ class RDBRepository implements Repository */ public function max(string $attribute) { - return $this->createSelectBuilder()->max($attribute); + return $this->createBuilder()->max($attribute); } /** @@ -293,7 +293,7 @@ class RDBRepository implements Repository */ public function min(string $attribute) { - return $this->createSelectBuilder()->min($attribute); + return $this->createBuilder()->min($attribute); } /** @@ -303,7 +303,7 @@ class RDBRepository implements Repository */ public function sum(string $attribute) { - return $this->createSelectBuilder()->sum($attribute); + return $this->createBuilder()->sum($attribute); } /** @@ -334,7 +334,7 @@ class RDBRepository implements Repository */ public function join($target, ?string $alias = null, $conditions = null): RDBSelectBuilder { - return $this->createSelectBuilder()->join($target, $alias, $conditions); + return $this->createBuilder()->join($target, $alias, $conditions); } /** @@ -348,7 +348,7 @@ class RDBRepository implements Repository */ public function leftJoin($target, ?string $alias = null, $conditions = null): RDBSelectBuilder { - return $this->createSelectBuilder()->leftJoin($target, $alias, $conditions); + return $this->createBuilder()->leftJoin($target, $alias, $conditions); } /** @@ -358,7 +358,7 @@ class RDBRepository implements Repository */ public function distinct(): RDBSelectBuilder { - return $this->createSelectBuilder()->distinct(); + return $this->createBuilder()->distinct(); } /** @@ -368,7 +368,7 @@ class RDBRepository implements Repository */ public function forUpdate(): RDBSelectBuilder { - return $this->createSelectBuilder()->forUpdate(); + return $this->createBuilder()->forUpdate(); } /** @@ -378,7 +378,7 @@ class RDBRepository implements Repository */ public function sth(): RDBSelectBuilder { - return $this->createSelectBuilder()->sth(); + return $this->createBuilder()->sth(); } /** @@ -395,7 +395,7 @@ class RDBRepository implements Repository */ public function where($clause = [], $value = null): RDBSelectBuilder { - return $this->createSelectBuilder()->where($clause, $value); + return $this->createBuilder()->where($clause, $value); } /** @@ -412,7 +412,7 @@ class RDBRepository implements Repository */ public function having($clause = [], $value = null): RDBSelectBuilder { - return $this->createSelectBuilder()->having($clause, $value); + return $this->createBuilder()->having($clause, $value); } /** @@ -432,7 +432,7 @@ class RDBRepository implements Repository */ public function order($orderBy = Attribute::ID, $direction = null): RDBSelectBuilder { - return $this->createSelectBuilder()->order($orderBy, $direction); + return $this->createBuilder()->order($orderBy, $direction); } /** @@ -442,7 +442,7 @@ class RDBRepository implements Repository */ public function limit(?int $offset = null, ?int $limit = null): RDBSelectBuilder { - return $this->createSelectBuilder()->limit($offset, $limit); + return $this->createBuilder()->limit($offset, $limit); } /** @@ -462,7 +462,7 @@ class RDBRepository implements Repository */ public function select($select = [], ?string $alias = null): RDBSelectBuilder { - return $this->createSelectBuilder()->select($select, $alias); + return $this->createBuilder()->select($select, $alias); } /** @@ -479,15 +479,17 @@ class RDBRepository implements Repository */ public function group($groupBy): RDBSelectBuilder { - return $this->createSelectBuilder()->group($groupBy); + return $this->createBuilder()->group($groupBy); } /** * Create a select builder. * * @return RDBSelectBuilder + * + * @since 9.2.5 */ - protected function createSelectBuilder(): RDBSelectBuilder + public function createBuilder(): RDBSelectBuilder { /** @var RDBSelectBuilder $builder */ $builder = new RDBSelectBuilder($this->entityManager, $this->entityType); @@ -495,6 +497,19 @@ class RDBRepository implements Repository return $builder; } + /** + * Create a select builder. + * + * @return RDBSelectBuilder + * + * @deprecated Since v9.2.5. Use `createBuilder` instead. + * @todo Remove in v10.0. + */ + protected function createSelectBuilder(): RDBSelectBuilder + { + return $this->createBuilder(); + } + /** * Use hooks instead. * diff --git a/tests/integration/Espo/ORM/RelationsTest.php b/tests/integration/Espo/ORM/RelationsTest.php index 03c222e00a..dce0552ec8 100644 --- a/tests/integration/Espo/ORM/RelationsTest.php +++ b/tests/integration/Espo/ORM/RelationsTest.php @@ -29,8 +29,10 @@ namespace tests\integration\Espo\ORM; +use Espo\Modules\Crm\Entities\Contact; use Espo\Modules\Crm\Entities\Lead; use Espo\Modules\Crm\Entities\Task; +use Espo\ORM\Defs\Params\RelationParam; use Espo\ORM\EntityCollection; use Espo\Modules\Crm\Entities\Account; use Espo\Modules\Crm\Entities\Opportunity; @@ -232,4 +234,71 @@ class RelationsTest extends BaseTestCase $this->assertEquals(null, $lead2->get('createdAccountId')); } + + public function testOrder(): void + { + $em = $this->getEntityManager(); + + $account1 = $em->createEntity(Account::ENTITY_TYPE, ['name' => 'a1']); + $account2 = $em->createEntity(Account::ENTITY_TYPE, ['name' => 'a2']); + + $contact = $em->createEntity(Contact::ENTITY_TYPE); + + $this->assertInstanceOf(Contact::class, $contact); + + $em->getRelation($contact, 'accounts')->relate($account1); + $em->getRelation($contact, 'accounts')->relate($account2); + + // + + $metadata = $this->getMetadata(); + $metadata->set('entityDefs', Contact::ENTITY_TYPE, [ + 'links' => [ + 'accounts' => [ + RelationParam::ORDER_BY => 'name', + RelationParam::ORDER => 'desc', + ] + ] + ]); + $metadata->save(); + + $this->reCreateApplication(); + + $em = $this->getEntityManager(); + + $contact = $em->getRDBRepositoryByClass(Contact::class)->getById($contact->getId()); + $this->assertNotNull($contact); + + $accounts = $contact->getAccounts(); + + $this->assertCount(2, $accounts); + $this->assertEquals('a2', $accounts[0]->getName()); + $this->assertEquals('a1', $accounts[1]->getName()); + + // + + $metadata = $this->getMetadata(); + $metadata->set('entityDefs', Contact::ENTITY_TYPE, [ + 'links' => [ + 'accounts' => [ + RelationParam::ORDER_BY => 'name', + RelationParam::ORDER => 'asc', + ] + ] + ]); + $metadata->save(); + + $this->reCreateApplication(); + + $em = $this->getEntityManager(); + + $contact = $em->getRDBRepositoryByClass(Contact::class)->getById($contact->getId()); + $this->assertNotNull($contact); + + $accounts = $contact->getAccounts(); + + $this->assertCount(2, $accounts); + $this->assertEquals('a1', $accounts[0]->getName()); + $this->assertEquals('a2', $accounts[1]->getName()); + } }