TypoGraph 

Extension Key

typograph

Package name

digicademy/typograph

Version

main

Language

en

Author

Frodo Podschwadek, Digital Academy, Academy of Sciences and Literature | Mainz

License

This document is published under the Creative Commons BY 4.0 license.

Rendered

Fri, 27 Feb 2026 08:02:32 +0000

TYPO3 extension for providing access to TYPO3 database table data via a GraphQL endpoint. Under the bonnet, it uses graphql-php.

You shold be sufficiently familiar with the basic concepts of GraphQL to use this extension. For example, you need to know how to create GraphQL schemas. If you want to learn more about GraphQL or need a refresher, have a look at the GraphQL docs.


Introduction 

Motivation, features and limits of this extension.

Installation 

How to install this extension.

Configuration 

Configuring simple and nested queries.

Pagination 

Using paginated queries.

Example setup 

Set up example tables, schemas and configuration with a CLI command.

Introduction 

TypoGraph provides a middleware for a GraphQL endpoint. This endpoint accepts valid GraphQL requests for related data and creates a GraphQL response for the client.

Performance Characteristics 

TypoGraph implements a DataLoader pattern to prevent N+1 query problems:

  • Batch Loading: All related records are fetched in a single optimised query per relation type
  • Request-Scoped Caching: Each record is loaded only once per GraphQL request
  • Field Selection: Only fields requested in the GraphQL query are fetched from the database
  • Order Preservation: Related records are returned in the same order as stored (respects MM table sorting)

For example, querying 100 research disciplines with related entries for experts in these disciplines from a research information database results in only two database queries: one query for all taxonomies and one query for all unique disciplines referenced by those taxonomies.

Origins 

This extension has been developed by the Digital Academy of the Academy of Sciences and Literature | Mainz while refactoring on the research information system Portal Kleine Fächer, which provides detailed information on minor subjects at German universities and other institutions of higher education. The examples in this documentation still reflect this initial context.

AI Note 

The initial basic extension design has been completely done by humans. However, developers of this extension use Claude Code (Sonnet 4.5, 4.6; Opus 4.6) to streamline routine tasks, upgrades, improve code quality etc. All changes depending on AI (as far as we are aware) are confirmed by a qualified human software developer before merged into the main branch.

Installation 

Install the extension via Composer:

Installation with Composer
composer r digicademy/typograph
Copied!

See also Installing extensions, TYPO3 Getting started.

Once you have configured the TypoGraph extension, you request GraphQL data from your TYPO3 installation via the path /graphql.

Configuration 

TypoGraph is configured via the TYPO3 site configuration file (config/sites/<site-identifier>/config.yaml), under a top-level typograph: key.

  1. schemaFiles: Define the locations of the GraphQL schema files to use (more about schema files below). It makes sense for them to be located somewhere in the Resources/Private folder of your sitepackage but can really be located anywhere in your filesystem as long as TYPO3 can access that location.
  2. tableMapping: The TypoGraph extension fetches data for its responses from your database tables and therefore needs to know which type gets data from which table. This is configured as <type name>: <table name> pairs.

Here is an example for an application that uses the types Taxonomy, Discipline and Expert. For better maintainability, the schema is split into four files, one for the Query schema and one for each type schema (all of this could also be the content of one single file). These are listed in schemaFiles. Note that the TypoGraph resolver will concatenate the content of the schema files in the order they are listed in the configuration.

A GraphQL request will provide one or more type names. The data for these types can come from any TYPO3 database table, even though you will usually want to re-use the tables serving your conventional Extbase domain models. For the TypoGraph resolver to know which table to query for which entity, you need to provide a list of pairs of entity names and table names, as seen below.

config/sites/<site-identifier>/config.yaml
typograph:

  # Schema files
  schemaFiles:
    - 'EXT:sitepackage/Resources/Private/Schemas/Query.graphql'
    - 'EXT:sitepackage/Resources/Private/Schemas/Taxonomy.graphql'
    - 'EXT:sitepackage/Resources/Private/Schemas/Discipline.graphql'
    - 'EXT:sitepackage/Resources/Private/Schemas/Expert.graphql'

  # Types to tables mapping
  tableMapping:
    taxonomies: tx_myextension_domain_model_taxonomies
    disciplines: tx_myextension_domain_model_disciplines
    experts: tx_myextension_domain_model_experts
Copied!

However, most of the time you will want the option to query relations between entries from different tables, or data types. For this to happen, you need to configure the relations between different tables for TypoGraph.

Configuring Relations 

When your GraphQL types reference other types (e.g., a Taxonomy has multiple Discipline objects), you need to configure how TypoGraph should resolve these relations from your database.

Why Relations Need Configuration 

Unlike scalar fields (strings, integers), object-type fields require the TypoGraph extension to:

  1. Identify which database column stores the relation reference
  2. Know which table to query for the related records
  3. Understand the storage format (single UID, comma-separated UIDs, or MM table)

Without explicit configuration, TypoGraph will log a warning and return null for unconfigured relations.

Relation Configuration Structure 

Relations are configured under the typograph.relations key in the site configuration, nested as <TypeName>: <fieldName>: <config>:

config/sites/<site-identifier>/config.yaml
typograph:
  relations:
    TypeName:
      fieldName:
        sourceField: database_column_name
        targetType: RelatedTypeName
        storageType: uid  # uid | commaSeparated | mmTable | foreignKey

        # Additional fields for mmTable storage type:
        mmTable: tx_some_mm_table
        mmSourceField: uid_local
        mmTargetField: uid_foreign
        mmSortingField: sorting

        # Additional fields for foreignKey storage type:
        foreignKeyField: column_in_target_table
Copied!

Configuration Fields 

Field Required Default Description
sourceField No Field name in snake_case Database column in the source table containing the relation reference (not used for foreignKey type)
targetType Yes <none> GraphQL type name of the related entity (must exist in tableMapping)
storageType No uid How the relation is stored: uid, commaSeparated, mmTable, or foreignKey
mmTable For mmTable only <none> Name of the MM (many-to-many) intermediary table
mmSourceField For mmTable only uid_local Column in MM table referencing source record
mmTargetField For mmTable only uid_foreign Column in MM table referencing target record
mmSortingField For mmTable only sorting Column in MM table for sorting order
foreignKeyField For foreignKey only <none> Column in target table that references the source record UID

Storage Types 

The following variants are available for the storageType field.

1. Single UID (uid) 

Use when a database column contains a single UID reference.

Database structure
tx_taxonomy table:
  uid: 1
  name: "Computer Science"
  main_discipline: 42  ← Single UID
Copied!
GraphQL schema
type Taxonomy {
  name: String
  mainDiscipline: Discipline
}
Copied!
config/sites/<site-identifier>/config.yaml
typograph:
  relations:
    Taxonomy:
      mainDiscipline:
        sourceField: main_discipline
        targetType: Discipline
        storageType: uid
Copied!

2. Comma-Separated UIDs (commaSeparated) 

Use when a database column contains multiple UIDs as a comma-separated string.

Database structure
tx_taxonomy table:
  uid: 1
  name: "Computer Science"
  disciplines: "12,45,78"  ← Comma-separated UIDs
Copied!
GraphQL schema
type Taxonomy {
  name: String
  disciplines: [Discipline]
}
Copied!
config/sites/<site-identifier>/config.yaml
typograph:
  relations:
    Taxonomy:
      disciplines:
        sourceField: disciplines
        targetType: Discipline
        storageType: commaSeparated
Copied!

3. MM Table (mmTable) 

Use for many-to-many relations stored via an intermediary MM table.

Database structure
tx_expert table:
  uid: 5
  name: "Dr. Smith"

tx_expert_discipline_mm table:
  uid_local: 5      ← References expert
  uid_foreign: 12   ← References discipline
  sorting: 1

tx_discipline table:
  uid: 12
  name: "Physics"
Copied!
GraphQL schema
type Expert {
  name: String
  disciplines: [Discipline]
}
Copied!
config/sites/<site-identifier>/config.yaml
typograph:
  relations:
    Expert:
      disciplines:
        targetType: Discipline
        storageType: mmTable
        mmTable: tx_expert_discipline_mm
        mmSourceField: uid_local
        mmTargetField: uid_foreign
        mmSortingField: sorting
Copied!

4. Foreign Key / Inverse Relation (foreignKey) 

Use when the target table has a foreign key column pointing back to the source record. This handles 'sloppy MM' scenarios where multiple target records can reference the same source record, potentially with duplicate data. While this ideally should not happen, sometimes you may have to work with legacy databases containing denormalized data, inverse relations where child records point to the parent, or just with cases where somebody couldn't be bothered to set up proper MM tables (every software project has at least one former team member like this).

Database structure
tx_taxonomy table:
  uid: 5
  name: "Applied Sciences"

tx_discipline table:
  uid: 1
  name: "Physics"
  discipline_taxonomy: 5  ← Foreign key pointing to taxonomy

tx_discipline table:
  uid: 2
  name: "Physics"          ← Same name, different record
  discipline_taxonomy: 5  ← Same taxonomy reference

tx_discipline table:
  uid: 3
  name: "Chemistry"
  discipline_taxonomy: 5  ← Another discipline for same taxonomy
Copied!
GraphQL schema
type Taxonomy {
  name: String
  disciplines: [Discipline]
}

type Discipline {
  name: String
}
Copied!
config/sites/<site-identifier>/config.yaml
typograph:
  relations:
    Taxonomy:
      disciplines:
        targetType: Discipline
        storageType: foreignKey
        foreignKeyField: discipline_taxonomy
Copied!

How it works: In this case, TypoGraph queries the target table (tx_discipline) with a WHERE discipline_taxonomy = <source-uid> query and returns all matching records (including duplicates).

Complete Configuration Example 

This is a complete example with multiple relation types:

config/sites/<site-identifier>/config.yaml
typograph:

  # Schema files
  schemaFiles:
    - 'EXT:pkf_website/Resources/Private/Schemas/Query.graphql'
    - 'EXT:pkf_website/Resources/Private/Schemas/Taxonomy.graphql'
    - 'EXT:pkf_website/Resources/Private/Schemas/Discipline.graphql'
    - 'EXT:pkf_website/Resources/Private/Schemas/Expert.graphql'

  # Root elements to tables mapping
  tableMapping:
    disciplines: tx_dmdb_domain_model_discipline
    experts: tx_academy_domain_model_persons
    taxonomies: tx_dmdb_domain_model_discipline_taxonomy

  # Relation configuration
  relations:

    Taxonomy:
      # Single UID relation
      mainDiscipline:
        sourceField: main_discipline
        targetType: Discipline
        storageType: uid

      # Comma-separated UIDs relation
      disciplines:
        sourceField: disciplines
        targetType: Discipline
        storageType: commaSeparated

      # Foreign key relation (inverse/legacy MM)
      relatedDisciplines:
        targetType: Discipline
        storageType: foreignKey
        foreignKeyField: discipline_taxonomy

    Expert:
      # MM table relation
      disciplines:
        targetType: Discipline
        storageType: mmTable
        mmTable: tx_academy_persons_discipline_mm
        mmSourceField: uid_local
        mmTargetField: uid_foreign
        mmSortingField: sorting
Copied!

GraphQL Query Example 

With the configuration above, you can now query nested relations:

GraphQL query
{
  experts {
    familyName
    givenName
    disciplines {
      name
    }
  }
}
Copied!

Pagination 

TypoGraph supports cursor-based pagination following the GraphQL Cursor Connections Specification.

How It Works 

When a root query field returns a Connection type (a type whose name ends with Connection), TypoGraph automatically applies cursor-based pagination. If the return type is a plain list (e.g. [Discipline]), the resolver behaves as before and no pagination is applied.

Cursors are opaque, base64-encoded strings. Clients should treat them as opaque tokens and never parse or construct them manually.

Schema Setup 

To enable pagination for a type, you need to define three types in your GraphQL schema and include the shared Pagination.graphql schema file provided by the TypoGraph extension.

1. Include the shared PageInfo type 

The Pagination.graphql file ships with TypoGraph and defines:

PageInfo schema
type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}
Copied!

Add it as the first entry in schemaFiles in the site configuration:

config/sites/<site-identifier>/config.yaml
typograph:
  schemaFiles:
    - 'EXT:typograph/Resources/Private/Schemas/Pagination.graphql'
    - 'EXT:sitepackage/Resources/Private/Schemas/Query.graphql'
    # ...
Copied!

2. Define Connection and Edge types for your entity 

Example pagination schema for the Expert type
type Expert {
  familyName: String
  givenName: String
}

type ExpertConnection {
  edges: [ExpertEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type ExpertEdge {
  cursor: String!
  node: Expert!
}
Copied!

3. Define both a plain-list field and a Connection field in the Query type 

The recommended convention is to expose two root fields per entity: a plain-list field for unpaginated access and a *Connection field for paginated access. This preserves backwards compatibility for clients that do not need pagination.

Query schema for plain and paginated Expert type
type Query {
  experts(familyName: String): [Expert]
  expertsConnection(familyName: String, first: Int, after: String): ExpertConnection
}
Copied!

The first and after arguments on the Connection field are recognised as pagination arguments and are not turned into WHERE conditions. All other arguments (like familyName) continue to work as filters on both fields.

Both field names must be present in tableMapping.

Pagination Configuration 

Configure default and maximum page sizes in the site configuration:

config/sites/<site-identifier>/config.yaml
typograph:
  pagination:
    defaultLimit: 20
    maxLimit: 100
Copied!
Setting Default Description
defaultLimit 20 Page size when first is not provided
maxLimit 100 Upper bound for first; requests above this are clamped

Querying with Pagination 

Using the pagination arguments first and after, or last and before allows for fine-grained pagination that can be combined the the usual filter arguments.

First page
{
  expertsConnection(first: 10) {
    edges {
      cursor
      node {
        familyName
        givenName
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
    totalCount
  }
}
Copied!
Next page
{
  expertsConnection(first: 10, after: "Y3Vyc29yOjQy") {
    edges {
      cursor
      node {
        familyName
        givenName
      }
    }
    pageInfo {
      hasNextPage
      hasPreviousPage
      endCursor
    }
  }
}
Copied!
Combining filters with pagination
{
  expertsConnection(familyName: "Smith", first: 5) {
    edges {
      node {
        familyName
        givenName
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
    totalCount
  }
}
Copied!

Response Structure 

A paginated response has this shape
{
  "data": {
    "expertsConnection": {
      "edges": [
        {
          "cursor": "Y3Vyc29yOjE=",
          "node": {
            "familyName": "Smith",
            "givenName": "Jane"
          }
        }
      ],
      "pageInfo": {
        "hasNextPage": true,
        "hasPreviousPage": false,
        "startCursor": "Y3Vyc29yOjE=",
        "endCursor": "Y3Vyc29yOjE="
      },
      "totalCount": 42
    }
  }
}
Copied!
Field Description
edges Array of edge objects, each containing a cursor and a node (the actual entity)
pageInfo.hasNextPage true if more records exist after this page
pageInfo.hasPreviousPage true if an after cursor was provided (i.e. this is not the first page)
pageInfo.startCursor Cursor of the first edge in this page (null if empty)
pageInfo.endCursor Cursor of the last edge in this page (null if empty). Pass this as the after argument to fetch the next page.
totalCount Total number of matching records across all pages. Only queried from the database when this field is actually requested in the GraphQL selection.

Error Handling 

TypoGraph logs helpful warnings and errors when relations are misconfigured:

  • Missing configuration: "Relation Taxonomy.disciplines is not configured in the site configuration"
  • Missing targetType: "Relation Taxonomy.disciplines is missing targetType configuration"
  • Unmapped target: "Target type Discipline is not mapped to a table in tableMapping"
  • Missing MM table: "Relation Expert.disciplines with storageType=mmTable is missing mmTable configuration"
  • Missing foreign key field: "Relation Taxonomy.disciplines with storageType=foreignKey is missing foreignKeyField configuration"

Check your TYPO3 logs if relations return null or empty arrays unexpectedly.

Example Setup 

For testing the TypoGraph extension in your system without hooking up any of your actual data, you can set up some example tables and seed them with a number of related entries:

Example setup command
vendor/bin/typo3 typograph:example
Copied!

The command accepts two parameters:

  • --site: If you install the extension in a multisite instance, you can specify for which site the GraphQL endpoint is configures. Defaults to main if omitted.
  • --no-seed: If you only want the example schemas and tables rolled out but fill them with data yourself, you can skip the entry seeding with this parameter.

The command creates the following tables and (unless you skip the seeding part) fills them with a handful of entries:

  • tx_typograph_example_taxonomies
  • tx_typograph_example_disciplines
  • tx_typograph_example_experts
  • tx_typograph_example_experts_disciplines_mm

It will also create the following schema files withing the TypoGraph package folder:

  • Resources\Private\Schemas\ExampleQuery.graphql
  • Resources\Private\Schemas\ExampleTypes.graphql

Finally, the command also adds the necessary configuration entries in config/sites/<site-name>/config.yaml.

Once the commmand has finished, you need to manually clear the TYPO3 and PHP caches via the TYPO3 Backend Maintenance Tool. This is recommended because due to your setup (e.g., if you are working with PHP-FPM), OPcache for worker threads will not be cleared unless done via the web interface (see, e.g., this discussion on TYPO3 cache flushing via the command line).

Now you can query the GraphQL endpoint available at the path /graphql for the example data. You can also run the Codeception API tests from tests\Api against the endpoint, if needed.