Sorting
TypoGraph supports custom sorting of query results via the
ComparatorInterface. This allows external packages to inject
locale-aware or domain-specific sorting logic without modifying
TypoGraph itself.
How It Works
Sorting is based on two components:
- A ComparatorInterface implementation injected via dependency injection. This defines how strings are compared (e.g. locale-aware collation).
- A sortBy argument on the GraphQL query that specifies which field to sort by.
When both are present, TypoGraph applies the comparator to sort result sets. When either is absent, results are returned in their default order (UID for connection queries, database default for plain lists).
Sorting Behaviour
Sorting behaviour differs depending on the query type:
- Plain list queries (e.g.
[Expert]) - The full result set is sorted by the comparator before being returned.
- Connection queries (e.g.
ExpertConnection) - Cursor-based pagination relies on UID ordering for stable pagination across pages. The comparator sorts records within the current page only after the database query and pagination slicing. This means the overall page boundaries are determined by UID order, but the records within each page are reordered by the comparator.
Using the sortBy Argument
Clients specify which field to sort by using the sortBy argument on a
query field. To enable this, add sortBy: String to the relevant query
fields in your GraphQL schema:
type Query {
experts(sortBy: String): [Expert]
expertsConnection(sortBy: String, first: Int, after: String): ExpertConnection
}
The sortBy value should be the camelCase GraphQL field name (e.g.
familyName). TypoGraph converts it to the snake_case database column
name automatically.
{
experts(sortBy: "familyName") {
familyName
givenName
}
}
When sortBy is omitted, no comparator-based sorting is applied and
results are returned in their default database order.
Nonexistent fields: If the sortBy value refers to a field that does
not exist in the result records, sorting is silently skipped and the original
order is preserved.
Implementing a Custom Comparator
TypoGraph ships a ComparatorInterface with a single method:
namespace Digicademy\TypoGraph\Comparator;
interface ComparatorInterface
{
public function compare(string $a, string $b): int;
}
The method follows PHP's standard comparison contract: return a negative
integer if $a < $b, zero if equal, or a positive integer if $a > $b.
To provide a custom comparator, create a class implementing this interface in your sitepackage or extension:
namespace Vendor\MySitepackage\Comparator;
use Digicademy\TypoGraph\Comparator\ComparatorInterface;
class LocalizedComparator implements ComparatorInterface
{
private static array $collators = [];
public function compare(string $a, string $b): int
{
if (!isset(self::$collators['de_DE'])) {
self::$collators['de_DE'] = \Collator::create('de_DE');
}
return self::$collators['de_DE']->compare($a, $b);
}
}
Then register your implementation as the ComparatorInterface service in
your extension's Services.yaml:
services:
Digicademy\TypoGraph\Comparator\ComparatorInterface:
class: Vendor\MySitepackage\Comparator\LocalizedComparator
TypoGraph's ResolverService accepts the comparator as an optional
constructor dependency. When no implementation is registered, the parameter
defaults to null and no custom sorting is applied.
Complete Example
Given this schema and configuration:
type Query {
experts(sortBy: String): [Expert]
expertsConnection(sortBy: String, first: Int, after: String): ExpertConnection
}
type Expert {
familyName: String
givenName: String
}
A plain list query sorted by family name:
{
experts(sortBy: "familyName") {
familyName
givenName
}
}
The same type sorted by a different field:
{
experts(sortBy: "givenName") {
familyName
givenName
}
}
A connection query sorts records within each page:
{
expertsConnection(sortBy: "familyName", first: 10) {
edges {
node {
familyName
givenName
}
}
pageInfo {
hasNextPage
endCursor
}
}
}