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:

  1. A ComparatorInterface implementation injected via dependency injection. This defines how strings are compared (e.g. locale-aware collation).
  2. 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:

Schema with sortBy argument
type Query {
  experts(sortBy: String): [Expert]
  expertsConnection(sortBy: String, first: Int, after: String): ExpertConnection
}
Copied!

The sortBy value should be the camelCase GraphQL field name (e.g. familyName). TypoGraph converts it to the snake_case database column name automatically.

Query with sort field
{
  experts(sortBy: "familyName") {
    familyName
    givenName
  }
}
Copied!

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:

EXT:typograph/Classes/Comparator/ComparatorInterface.php
namespace Digicademy\TypoGraph\Comparator;

interface ComparatorInterface
{
    public function compare(string $a, string $b): int;
}
Copied!

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:

EXT:my_sitepackage/Classes/Comparator/LocalizedComparator.php
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);
    }
}
Copied!

Then register your implementation as the ComparatorInterface service in your extension's Services.yaml:

EXT:my_sitepackage/Configuration/Services.yaml
services:
  Digicademy\TypoGraph\Comparator\ComparatorInterface:
    class: Vendor\MySitepackage\Comparator\LocalizedComparator
Copied!

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:

Schema
type Query {
  experts(sortBy: String): [Expert]
  expertsConnection(sortBy: String, first: Int, after: String): ExpertConnection
}

type Expert {
  familyName: String
  givenName: String
}
Copied!

A plain list query sorted by family name:

Query sorted by familyName
{
  experts(sortBy: "familyName") {
    familyName
    givenName
  }
}
Copied!

The same type sorted by a different field:

Query sorted by givenName
{
  experts(sortBy: "givenName") {
    familyName
    givenName
  }
}
Copied!

A connection query sorts records within each page:

Paginated query with sorting
{
  expertsConnection(sortBy: "familyName", first: 10) {
    edges {
      node {
        familyName
        givenName
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}
Copied!