diff --git a/application/Espo/ORM/Query/Part/Join.php b/application/Espo/ORM/Query/Part/Join.php new file mode 100644 index 0000000000..ec1d7bbdff --- /dev/null +++ b/application/Espo/ORM/Query/Part/Join.php @@ -0,0 +1,137 @@ +target = $target; + $this->alias = $alias; + + if ($target === '' || $alias === '') { + throw new RuntimeException("Bad join."); + } + } + + /** + * Get a join target. A relationName or table. + * A relationName is in camelCase, a table is in CamelCase. + */ + public function getTarget(): string + { + return $this->target; + } + + /** + * Get an alias. + */ + public function getAlias(): ?string + { + return $this->alias; + } + + /** + * Get join conditions. + */ + public function getConditions(): ?WhereItem + { + return $this->conditions; + } + + public function isTable(): bool + { + return $this->target[0] === ucfirst($this->target[0]); + } + + public function isRelation(): bool + { + return !$this->isTable(); + } + + /** + * Create. + * + * @param string $target + * A relation name or table. A relationName should be in camelCase, a table in CamelCase. + * When joining a table, conditions should be specified. + * When joining a relation, conditions will be applied automatically. + */ + public static function create(string $target, ?string $alias = null): self + { + return new self($target, $alias); + } + + /** + * Create with a table target. + */ + public static function createWithTableTarget(string $table, ?string $alias = null): self + { + return self::create(ucfirst($table), $alias); + } + + /** + * Create with a relation target. Conditions will be applied automatically. + */ + public static function createWithRelationTarget(string $relation, ?string $alias = null): self + { + return self::create(lcfirst($relation), $alias); + } + + /** + * Clone with an alias. + */ + public function withAlias(?string $alias): self + { + $obj = clone $this; + $obj->alias = $alias; + + return $obj; + } + + /** + * Clone with join conditions. + */ + public function withConditions(?WhereItem $conditions): self + { + $obj = clone $this; + $obj->conditions = $conditions; + + return $obj; + } +} diff --git a/application/Espo/ORM/Query/Part/SelectExpression.php b/application/Espo/ORM/Query/Part/SelectExpression.php index 3359469225..5d15fff80d 100644 --- a/application/Espo/ORM/Query/Part/SelectExpression.php +++ b/application/Espo/ORM/Query/Part/SelectExpression.php @@ -35,7 +35,7 @@ class SelectExpression private $alias = null; - public function __construct(Expression $expression, ?string $alias = null) + private function __construct(Expression $expression, ?string $alias = null) { $this->expression = $expression; $this->alias = $alias; diff --git a/application/Espo/ORM/Query/SelectingBuilderTrait.php b/application/Espo/ORM/Query/SelectingBuilderTrait.php index b1c413f8d4..3fb8328e00 100644 --- a/application/Espo/ORM/Query/SelectingBuilderTrait.php +++ b/application/Espo/ORM/Query/SelectingBuilderTrait.php @@ -32,6 +32,7 @@ namespace Espo\ORM\Query; use Espo\ORM\Query\Part\WhereItem; use Espo\ORM\Query\Part\Expression; use Espo\ORM\Query\Part\OrderExpression; +use Espo\ORM\Query\Part\Join; use InvalidArgumentException; @@ -152,13 +153,19 @@ trait SelectingBuilderTrait /** * Add JOIN. * - * @param string $relationName - * A relationName or table. A relationName is in camelCase, a table is in CamelCase. + * @param Join|string $target + * A relation name or table. A relation name should be in camelCase, a table in CamelCase. * @param string|null $alias An alias. * @param WhereItem|array|null $conditions Join conditions. */ - public function join($relationName, ?string $alias = null, $conditions = null): self + public function join($target, ?string $alias = null, $conditions = null): self { + if ($target instanceof Join) { + $alias = $alias ?? $target->getAlias(); + $conditions = $conditions ?? $target->getConditions(); + $target = $target->getTarget(); + } + if ($conditions !== null && !is_array($conditions) && !$conditions instanceof WhereItem) { throw new InvalidArgumentException("Conditions must be WhereItem or array."); } @@ -171,8 +178,8 @@ trait SelectingBuilderTrait $this->params['joins'] = []; } - if (is_array($relationName)) { - $joinList = $relationName; + if (is_array($target)) { + $joinList = $target; foreach ($joinList as $item) { $this->params['joins'][] = $item; @@ -181,23 +188,23 @@ trait SelectingBuilderTrait return $this; } - if (is_null($alias) && is_null($conditions) && $this->hasJoinAlias($relationName)) { + if (is_null($alias) && is_null($conditions) && $this->hasJoinAlias($target)) { return $this; } if (is_null($alias) && is_null($conditions)) { - $this->params['joins'][] = $relationName; + $this->params['joins'][] = $target; return $this; } if (is_null($conditions)) { - $this->params['joins'][] = [$relationName, $alias]; + $this->params['joins'][] = [$target, $alias]; return $this; } - $this->params['joins'][] = [$relationName, $alias, $conditions]; + $this->params['joins'][] = [$target, $alias, $conditions]; return $this; } @@ -205,13 +212,19 @@ trait SelectingBuilderTrait /** * Add LEFT JOIN. * - * @param string $relationName - * A relationName or table. A relationName is in camelCase, a table is in CamelCase. + * @param Join|string $target + * A relation name or table. A relation name should be in camelCase, a table in CamelCase. * @param string|null $alias An alias. * @param WhereItem|array|null $conditions Join conditions. */ - public function leftJoin($relationName, ?string $alias = null, $conditions = null): self + public function leftJoin($target, ?string $alias = null, $conditions = null): self { + if ($target instanceof Join) { + $alias = $alias ?? $target->getAlias(); + $conditions = $conditions ?? $target->getConditions(); + $target = $target->getTarget(); + } + if ($conditions !== null && !is_array($conditions) && !$conditions instanceof WhereItem) { throw new InvalidArgumentException("Conditions must be WhereItem or array."); } @@ -224,8 +237,8 @@ trait SelectingBuilderTrait $this->params['leftJoins'] = []; } - if (is_array($relationName)) { - $joinList = $relationName; + if (is_array($target)) { + $joinList = $target; foreach ($joinList as $item) { $this->params['leftJoins'][] = $item; @@ -234,23 +247,23 @@ trait SelectingBuilderTrait return $this; } - if (is_null($alias) && is_null($conditions) && $this->hasLeftJoinAlias($relationName)) { + if (is_null($alias) && is_null($conditions) && $this->hasLeftJoinAlias($target)) { return $this; } if (is_null($alias) && is_null($conditions)) { - $this->params['leftJoins'][] = $relationName; + $this->params['leftJoins'][] = $target; return $this; } if (is_null($conditions)) { - $this->params['leftJoins'][] = [$relationName, $alias]; + $this->params['leftJoins'][] = [$target, $alias]; return $this; } - $this->params['leftJoins'][] = [$relationName, $alias, $conditions]; + $this->params['leftJoins'][] = [$target, $alias, $conditions]; return $this; } diff --git a/application/Espo/ORM/Query/SelectingTrait.php b/application/Espo/ORM/Query/SelectingTrait.php index 4aa75223c6..8b8a135e90 100644 --- a/application/Espo/ORM/Query/SelectingTrait.php +++ b/application/Espo/ORM/Query/SelectingTrait.php @@ -31,6 +31,7 @@ namespace Espo\ORM\Query; use Espo\ORM\Query\Part\OrderExpression; use Espo\ORM\Query\Part\WhereClause; +use Espo\ORM\Query\Part\Join; use RuntimeException; @@ -76,6 +77,48 @@ trait SelectingTrait return WhereClause::fromRaw($whereClause); } + /** + * Get JOIN items. + * + * @return Join[] + */ + public function getJoins(): array + { + return array_map( + function ($item) { + $conditions = isset($item[2]) ? + WhereClause::fromRaw($item[2]) : + null; + + return Join::create($item[0]) + ->withAlias($item[1] ?? null) + ->withConditions($conditions); + }, + $this->params['joins'] ?? [] + ); + } + + /** + * Get LEFT JOIN items. + * + * @return Join[] + */ + public function getLeftJoins(): array + { + return array_map( + function ($item) { + $conditions = isset($item[2]) ? + WhereClause::fromRaw($item[2]) : + null; + + return Join::create($item[0]) + ->withAlias($item[1] ?? null) + ->withConditions($conditions); + }, + $this->params['leftJoins'] ?? [] + ); + } + private static function validateRawParamsSelecting(array $params): void { } diff --git a/application/Espo/ORM/Repository/RDBRelation.php b/application/Espo/ORM/Repository/RDBRelation.php index a986347802..1d58b72939 100644 --- a/application/Espo/ORM/Repository/RDBRelation.php +++ b/application/Espo/ORM/Repository/RDBRelation.php @@ -36,6 +36,7 @@ use Espo\ORM\{ Query\Select, Query\Part\WhereItem, Query\Part\SelectExpression, + Query\Part\Join, Mapper\RDBMapper, Repository\RDBRelationSelectBuilder as Builder, }; @@ -185,27 +186,27 @@ class RDBRelation /** * Add JOIN. * - * @param string $relationName - * A relationName or table. A relationName is in camelCase, a table is in CamelCase. + * @param Join|string $target + * A relation name or table. A relation name should be in camelCase, a table in CamelCase. * @param string|null $alias An alias. * @param WhereItem|array|null $conditions Join conditions. */ - public function join(string $relationName, ?string $alias = null, $conditions = null): Builder + public function join(string $target, ?string $alias = null, $conditions = null): Builder { - return $this->createSelectBuilder()->join($relationName, $alias, $conditions); + return $this->createSelectBuilder()->join($target, $alias, $conditions); } /** * Add LEFT JOIN. * - * @param string $relationName - * A relationName or table. A relationName is in camelCase, a table is in CamelCase. + * @param Join|string $target + * A relation name or table. A relation name should be in camelCase, a table in CamelCase. * @param string|null $alias An alias. * @param WhereItem|array|null $conditions Join conditions. */ - public function leftJoin(string $relationName, ?string $alias = null, $conditions = null): Builder + public function leftJoin(string $target, ?string $alias = null, $conditions = null): Builder { - return $this->createSelectBuilder()->leftJoin($relationName, $alias, $conditions); + return $this->createSelectBuilder()->leftJoin($target, $alias, $conditions); } /** diff --git a/application/Espo/ORM/Repository/RDBRelationSelectBuilder.php b/application/Espo/ORM/Repository/RDBRelationSelectBuilder.php index aaf09606a3..bc4ba31dae 100644 --- a/application/Espo/ORM/Repository/RDBRelationSelectBuilder.php +++ b/application/Espo/ORM/Repository/RDBRelationSelectBuilder.php @@ -38,6 +38,7 @@ use Espo\ORM\{ Query\SelectBuilder, Query\Part\WhereItem, Query\Part\SelectExpression, + Query\Part\Join, Mapper\Mapper, }; @@ -238,14 +239,14 @@ class RDBRelationSelectBuilder /** * Add JOIN. * - * @param string $relationName - * A relationName or table. A relationName is in camelCase, a table is in CamelCase. + * @param Join|string $target + * A relation name or table. A relation name should be in camelCase, a table in CamelCase. * @param string|null $alias An alias. * @param WhereItem|array|null $conditions Join conditions. */ - public function join($relationName, ?string $alias = null, $conditions = null): self + public function join($target, ?string $alias = null, $conditions = null): self { - $this->builder->join($relationName, $alias, $conditions); + $this->builder->join($target, $alias, $conditions); return $this; } @@ -253,14 +254,14 @@ class RDBRelationSelectBuilder /** * Add LEFT JOIN. * - * @param string $relationName - * A relationName or table. A relationName is in camelCase, a table is in CamelCase. + * @param Join|string $target + * A relation name or table. A relation name should be in camelCase, a table in CamelCase. * @param string|null $alias An alias. * @param WhereItem|array|null $conditions Join conditions. */ - public function leftJoin($relationName, ?string $alias = null, $conditions = null): self + public function leftJoin($target, ?string $alias = null, $conditions = null): self { - $this->builder->leftJoin($relationName, $alias, $conditions); + $this->builder->leftJoin($target, $alias, $conditions); return $this; } diff --git a/application/Espo/ORM/Repository/RDBRepository.php b/application/Espo/ORM/Repository/RDBRepository.php index ad24b2e321..428866c614 100644 --- a/application/Espo/ORM/Repository/RDBRepository.php +++ b/application/Espo/ORM/Repository/RDBRepository.php @@ -39,6 +39,7 @@ use Espo\ORM\{ Query\Select, Query\Part\WhereItem, Query\Part\SelectExpression, + Query\Part\Join, }; use StdClass; @@ -673,27 +674,27 @@ class RDBRepository extends Repository /** * Add JOIN. * - * @param string $relationName - * A relationName or table. A relationName is in camelCase, a table is in CamelCase. + * @param Join|string $target + * A relation name or table. A relation name should be in camelCase, a table in CamelCase. * @param string|null $alias An alias. * @param WhereItem|array|null $conditions Join conditions. */ - public function join($relationName, ?string $alias = null, $conditions = null): RDBSelectBuilder + public function join($target, ?string $alias = null, $conditions = null): RDBSelectBuilder { - return $this->createSelectBuilder()->join($relationName, $alias, $conditions); + return $this->createSelectBuilder()->join($target, $alias, $conditions); } /** * Add LEFT JOIN. * - * @param string $relationName - * A relationName or table. A relationName is in camelCase, a table is in CamelCase. + * @param Join|string $target + * A relation name or table. A relation name should be in camelCase, a table in CamelCase. * @param string|null $alias An alias. * @param WhereItem|array|null $conditions Join conditions. */ - public function leftJoin($relationName, ?string $alias = null, $conditions = null): RDBSelectBuilder + public function leftJoin($target, ?string $alias = null, $conditions = null): RDBSelectBuilder { - return $this->createSelectBuilder()->leftJoin($relationName, $alias, $conditions); + return $this->createSelectBuilder()->leftJoin($target, $alias, $conditions); } /** diff --git a/application/Espo/ORM/Repository/RDBSelectBuilder.php b/application/Espo/ORM/Repository/RDBSelectBuilder.php index 28a6339c2a..aa9754ad41 100644 --- a/application/Espo/ORM/Repository/RDBSelectBuilder.php +++ b/application/Espo/ORM/Repository/RDBSelectBuilder.php @@ -38,6 +38,7 @@ use Espo\ORM\{ Query\SelectBuilder, Query\Part\WhereItem, Query\Part\SelectExpression, + Query\Part\Join, Mapper\Mapper, }; @@ -175,14 +176,14 @@ class RDBSelectBuilder /** * Add JOIN. * - * @param string $relationName - * A relationName or table. A relationName is in camelCase, a table is in CamelCase. + * @param Join|string $target + * A relation name or table. A relation name should be in camelCase, a table in CamelCase. * @param string|null $alias An alias. * @param WhereItem|array|null $conditions Join conditions. */ - public function join($relationName, ?string $alias = null, $conditions = null): self + public function join($target, ?string $alias = null, $conditions = null): self { - $this->builder->join($relationName, $alias, $conditions); + $this->builder->join($target, $alias, $conditions); return $this; } @@ -190,14 +191,14 @@ class RDBSelectBuilder /** * Add LEFT JOIN. * - * @param string $relationName - * A relationName or table. A relationName is in camelCase, a table is in CamelCase. + * @param Join|string $target + * A relation name or table. A relation name should be in camelCase, a table in CamelCase. * @param string|null $alias An alias. * @param WhereItem|array|null $conditions Join conditions. */ - public function leftJoin($relationName, ?string $alias = null, $conditions = null): self + public function leftJoin($target, ?string $alias = null, $conditions = null): self { - $this->builder->leftJoin($relationName, $alias, $conditions); + $this->builder->leftJoin($target, $alias, $conditions); return $this; } diff --git a/tests/unit/Espo/ORM/Query/Parts/JoinTest.php b/tests/unit/Espo/ORM/Query/Parts/JoinTest.php new file mode 100644 index 0000000000..8730f5504b --- /dev/null +++ b/tests/unit/Espo/ORM/Query/Parts/JoinTest.php @@ -0,0 +1,80 @@ +withAlias('testAlias') + ->withConditions($conditions); + + $this->assertEquals('test', $join->getTarget()); + $this->assertEquals('testAlias', $join->getAlias()); + $this->assertEquals($conditions, $join->getConditions()); + + $this->assertTrue($join->isRelation()); + $this->assertFalse($join->isTable()); + } + + public function testCreate2(): void + { + $conditions = Expr::isNull( + Expr::column('test') + ); + + $join = Join::createWithTableTarget('Test') + ->withAlias('testAlias') + ->withConditions($conditions); + + $this->assertEquals('Test', $join->getTarget()); + $this->assertEquals('testAlias', $join->getAlias()); + $this->assertEquals($conditions, $join->getConditions()); + + $this->assertTrue($join->isTable()); + $this->assertFalse($join->isRelation()); + } + + public function testCreate3(): void + { + $join = Join::create('Test', 'testAlias'); + + $this->assertEquals('Test', $join->getTarget()); + $this->assertEquals('testAlias', $join->getAlias()); + } +} diff --git a/tests/unit/Espo/ORM/Query/SelectBuilderTest.php b/tests/unit/Espo/ORM/Query/SelectBuilderTest.php index 00f73e5344..e8f3c95c14 100644 --- a/tests/unit/Espo/ORM/Query/SelectBuilderTest.php +++ b/tests/unit/Espo/ORM/Query/SelectBuilderTest.php @@ -35,6 +35,8 @@ use Espo\ORM\{ Query\Part\Expression as Expr, Query\Part\SelectExpression, Query\Part\OrderExpression, + Query\Part\Join, + Query\Part\WhereClause, }; class SelectBuilderTest extends \PHPUnit\Framework\TestCase @@ -473,7 +475,7 @@ class SelectBuilderTest extends \PHPUnit\Framework\TestCase $this->assertEquals($expected, $raw['whereClause']); } - public function testLeftJoin() + public function testLeftJoin1() { $params = $this->builder ->from('Test') @@ -486,7 +488,50 @@ class SelectBuilderTest extends \PHPUnit\Framework\TestCase $this->assertEquals(['link1', 'link2'], $params['leftJoins']); } - public function testJoin() + public function testLeftJoin2() + { + $query = $this->builder + ->from('Test') + ->leftJoin('link1', 'alias1', ['name' => 'test']) + ->leftJoin('link2', 'alias2') + ->build(); + + $this->assertEquals( + [ + Join::create('link1', 'alias1') + ->withConditions(WhereClause::fromRaw(['name' => 'test'])), + Join::create('link2', 'alias2'), + ], + $query->getLeftJoins() + ); + } + + public function testLeftJoin3() + { + $query = $this->builder + ->from('Test') + ->leftJoin( + Join::create('link1', 'alias1') + ->withConditions( + WhereClause::fromRaw(['name' => 'test']) + ) + ) + ->leftJoin( + Join::create('link2', 'alias2') + ) + ->build(); + + $this->assertEquals( + [ + Join::create('link1', 'alias1') + ->withConditions(WhereClause::fromRaw(['name' => 'test'])), + Join::create('link2', 'alias2'), + ], + $query->getLeftJoins() + ); + } + + public function testJoin1() { $params = $this->builder ->from('Test') @@ -499,6 +544,45 @@ class SelectBuilderTest extends \PHPUnit\Framework\TestCase $this->assertEquals(['link1', 'link2'], $params['joins']); } + public function testJoin2() + { + $query = $this->builder + ->from('Test') + ->join('link1', 'alias1', ['name' => 'test']) + ->join('link2', 'alias2') + ->build(); + + $this->assertEquals( + [ + Join::create('link1', 'alias1') + ->withConditions(WhereClause::fromRaw(['name' => 'test'])), + Join::create('link2', 'alias2'), + ], + $query->getJoins() + ); + } + + public function testJoin3() + { + $query = $this->builder + ->from('Test') + ->join( + Join::create('link1', 'alias1') + ->withConditions(WhereClause::fromRaw(['name' => 'test'])) + ) + ->join(Join::create('link2', 'alias2')) + ->build(); + + $this->assertEquals( + [ + Join::create('link1', 'alias1') + ->withConditions(WhereClause::fromRaw(['name' => 'test'])), + Join::create('link2', 'alias2'), + ], + $query->getJoins() + ); + } + public function testWhereItemUsage1() { $query = $this->builder