diff --git a/src/Clauses/OrderByClause.php b/src/Clauses/OrderByClause.php index 2e23176a..a651b1de 100644 --- a/src/Clauses/OrderByClause.php +++ b/src/Clauses/OrderByClause.php @@ -21,8 +21,11 @@ namespace WikibaseSolutions\CypherDSL\Clauses; -use WikibaseSolutions\CypherDSL\Property; +use WikibaseSolutions\CypherDSL\Types\AnyType; +use function array_map; +use WikibaseSolutions\CypherDSL\Order; use WikibaseSolutions\CypherDSL\Traits\EscapeTrait; +use WikibaseSolutions\CypherDSL\Types\PropertyTypes\PropertyType; /** * This class represents an ORDER BY clause. This clause should always be preceded by a RETURN @@ -35,9 +38,9 @@ class OrderByClause extends Clause use EscapeTrait; /** - * @var Property[] The expressions to include in the clause + * @var Order[] The expressions to include in the clause */ - private array $properties = []; + private array $orderings = []; /** * @var bool @@ -47,12 +50,14 @@ class OrderByClause extends Clause /** * Add a property to sort on. * - * @param Property $property The additional property to sort on + * @param PropertyType $property The additional property to sort on. + * @param string|null $order The order of the property to appear. Null is equal to the default in Neo4J. + * * @return OrderByClause */ - public function addProperty(Property $property): self + public function addProperty(PropertyType $property, ?string $order = null): self { - $this->properties[] = $property; + $this->orderings[] = new Order($property, $order); return $this; } @@ -60,34 +65,21 @@ public function addProperty(Property $property): self /** * Returns the properties to order. * - * @return Property[] + * @return AnyType[] */ public function getProperties(): array { - return $this->properties; - } - - /** - * Returns whether the ordering is in descending order. - * - * @return bool - */ - public function isDescending(): bool - { - return $this->descending; + return array_map(static fn (Order $o) => $o->getExpression(), $this->orderings); } /** - * Set to sort in a DESCENDING order. + * Returns the orderings. * - * @param bool $descending - * @return OrderByClause + * @return Order[] */ - public function setDescending(bool $descending = true): self + public function getOrderings(): array { - $this->descending = $descending; - - return $this; + return $this->orderings; } /** @@ -103,9 +95,8 @@ protected function getClause(): string */ protected function getSubject(): string { - $properties = array_map(fn (Property $property): string => $property->toQuery(), $this->properties); - $subject = implode(", ", $properties); + $properties = array_map(static fn ($x) => $x->toQuery(), $this->orderings); - return $this->descending ? sprintf("%s DESCENDING", $subject) : $subject; + return implode(", ", $properties); } } diff --git a/src/Order.php b/src/Order.php new file mode 100644 index 00000000..4c224759 --- /dev/null +++ b/src/Order.php @@ -0,0 +1,100 @@ +expression = $expression; + $this->setOrdering($ordering); + } + + /** + * Returns the expression being ordered. + * + * @return AnyType + */ + public function getExpression(): AnyType + { + return $this->expression; + } + + /** + * @return string|null + */ + public function getOrdering(): ?string + { + return $this->ordering; + } + + public function setOrdering(?string $ordering): self + { + if ($ordering !== null) { + $ordering = strtoupper($ordering); + if (!in_array($ordering, ['ASC', 'DESC', 'ASCENDING', 'DESCENDING'])) { + throw new InvalidArgumentException('Ordering must be null, "ASC", "DESC", "ASCENDING" or "DESCENDING"'); + } + + $this->ordering = $ordering; + } else { + $this->ordering = null; + } + + return $this; + } + + public function toQuery(): string + { + $cypher = $this->getExpression()->toQuery(); + if ($this->ordering) { + $cypher .= ' ' . $this->ordering; + } + + return $cypher; + } +} diff --git a/src/Query.php b/src/Query.php index 8c54d839..f7e8c29d 100644 --- a/src/Query.php +++ b/src/Query.php @@ -475,16 +475,19 @@ public function optionalMatch($patterns): self public function orderBy($properties, bool $descending = false): self { $orderByClause = new OrderByClause(); - $orderByClause->setDescending($descending); if (!is_array($properties)) { $properties = [$properties]; } - foreach ($properties as $property) { + foreach ($properties as $i => $property) { $this->assertClass('property', Property::class, $property); - $orderByClause->addProperty($property); + if ($descending && $i === count($properties) - 1) { + $orderByClause->addProperty($property, 'DESCENDING'); + } else { + $orderByClause->addProperty($property); + } } $this->clauses[] = $orderByClause; diff --git a/tests/Unit/Clauses/OrderByClauseTest.php b/tests/Unit/Clauses/OrderByClauseTest.php index 7d0030f7..e856c095 100644 --- a/tests/Unit/Clauses/OrderByClauseTest.php +++ b/tests/Unit/Clauses/OrderByClauseTest.php @@ -24,6 +24,7 @@ use PHPUnit\Framework\TestCase; use TypeError; use WikibaseSolutions\CypherDSL\Clauses\OrderByClause; +use WikibaseSolutions\CypherDSL\Order; use WikibaseSolutions\CypherDSL\Property; use WikibaseSolutions\CypherDSL\Tests\Unit\TestHelper; use WikibaseSolutions\CypherDSL\Types\AnyType; @@ -41,7 +42,6 @@ public function testEmptyClause(): void $this->assertSame("", $orderBy->toQuery()); $this->assertEquals([], $orderBy->getProperties()); - $this->assertFalse($orderBy->isDescending()); } public function testSingleProperty(): void @@ -52,7 +52,6 @@ public function testSingleProperty(): void $this->assertSame("ORDER BY a.a", $orderBy->toQuery()); $this->assertEquals([$property], $orderBy->getProperties()); - $this->assertFalse($orderBy->isDescending()); } public function testMultipleProperties(): void @@ -66,19 +65,16 @@ public function testMultipleProperties(): void $this->assertSame("ORDER BY a.a, a.b", $orderBy->toQuery()); $this->assertEquals([$propertyA, $propertyB], $orderBy->getProperties()); - $this->assertFalse($orderBy->isDescending()); } public function testSinglePropertyDesc(): void { $orderBy = new OrderByClause(); $property = $this->getQueryConvertableMock(Property::class, "a.a"); - $orderBy->addProperty($property); - $orderBy->setDescending(); + $orderBy->addProperty($property, 'DESCENDING'); $this->assertSame("ORDER BY a.a DESCENDING", $orderBy->toQuery()); $this->assertEquals([$property], $orderBy->getProperties()); - $this->assertTrue($orderBy->isDescending()); } public function testMultiplePropertiesDesc(): void @@ -88,12 +84,24 @@ public function testMultiplePropertiesDesc(): void $propertyB = $this->getQueryConvertableMock(Property::class, "a.b"); $orderBy->addProperty($propertyA); - $orderBy->addProperty($propertyB); - $orderBy->setDescending(); + $orderBy->addProperty($propertyB, 'DESCENDING'); $this->assertSame("ORDER BY a.a, a.b DESCENDING", $orderBy->toQuery()); $this->assertEquals([$propertyA, $propertyB], $orderBy->getProperties()); - $this->assertTrue($orderBy->isDescending()); + } + + public function testMultiplePropertiesMixed(): void + { + $orderBy = new OrderByClause(); + $propertyA = $this->getQueryConvertableMock(Property::class, "a.a"); + $propertyB = $this->getQueryConvertableMock(Property::class, "a.b"); + + $orderBy->addProperty($propertyA, 'ASC'); + $orderBy->addProperty($propertyB, 'DESCENDING'); + + $this->assertSame("ORDER BY a.a ASC, a.b DESCENDING", $orderBy->toQuery()); + $this->assertEquals([$propertyA, $propertyB], $orderBy->getProperties()); + $this->assertEquals([new Order($propertyA, 'asc'), new Order($propertyB, 'descending')], $orderBy->getOrderings()); } /** diff --git a/tests/Unit/OrderTest.php b/tests/Unit/OrderTest.php new file mode 100644 index 00000000..10c3e20e --- /dev/null +++ b/tests/Unit/OrderTest.php @@ -0,0 +1,55 @@ +getQueryConvertableMock(RawExpression::class, 'x')); + + $this->assertEquals('x', $order->toQuery()); + $this->assertNull($order->getOrdering()); + } + + public function testBasicOrderDescending(): void + { + $order = new Order($this->getQueryConvertableMock(RawExpression::class, 'x'), 'desc'); + + $this->assertEquals('x DESC', $order->toQuery()); + $this->assertEquals('DESC', $order->getOrdering()); + } + + public function testBasicOrderAscending(): void + { + $order = new Order($this->getQueryConvertableMock(RawExpression::class, 'x'), 'asc'); + + $this->assertEquals('x ASC', $order->toQuery()); + $this->assertEquals('ASC', $order->getOrdering()); + } + + public function testBasicOrderChange(): void + { + $order = new Order($this->getQueryConvertableMock(RawExpression::class, 'x'), 'asc'); + + $order->setOrdering(null); + + $this->assertEquals('x', $order->toQuery()); + $this->assertNull($order->getOrdering()); + } + + public function testOrderFalse(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectErrorMessage('Ordering must be null, "ASC", "DESC", "ASCENDING" or "DESCENDING"'); + + new Order($this->getQueryConvertableMock(RawExpression::class, 'x'), 'ascc'); + } +}