TYPO3 REST API

Version

main

Language

en

Authors

Inscript Team

Email

office@inscript.dev

Website

https://www.inscript.team/

License

This extension documentation is published under the CC BY-NC-SA 4.0 (Creative Commons) license

Rendered

Sat, 16 Aug 2025 10:08:45 +0000

The content of this document is related to TYPO3 CMS, a GNU/GPL CMS/Framework available from typo3.org .

This documentation is for the TYPO3 extension t3api.

If you find an error or something is missing, please: Report a Problem.

Table of Contents

Getting started

What does it do

T3api extension provides easy configurable and customizable REST API for your Extbase models. It allows to configure whole API functionality with annotations for classes, properties and methods.

Most of configuration options is based on API Platform to make it easier to use for developers experienced in this awesome framework.

T3api comes with partial support of JSON-LD and Hydra, which allows to build smart frontend applications with auto-discoverability capabilities.

Installation

Run

composer require sourcebroker/t3api
Copied!

Configuration

Route enhancer

Import route enhancer by adding following line on bottom of your site config.yaml .

imports:
  - { resource: "EXT:t3api/Configuration/Routing/config.yaml" }
Copied!

If you do not want to use import you can also manually add new route enhancer of type T3apiResourceEnhancer directly in your site configuration.

routeEnhancers:
  T3api:
    type: T3apiResourceEnhancer
Copied!

Default base path to api requests is: _api. To change it, it is needed to extend route enhancer configuration by basePath property, as in example below:

routeEnhancers:
  T3api:
    type: T3apiResourceEnhancer
    basePath: 'my_custom_api_basepath'
Copied!

Creating API resource

Next step is to make an API resource from our entity. To map Extbase model to API resource it is just needed to add @SourceBroker\T3api\Annotation\ApiResource annotation to our model class.

use SourceBroker\T3api\Annotation\ApiResource;

/**
 * @ApiResource()
 */
class Item extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
{
}
Copied!

Operations

T3api is based on operations. Operation in fact is an API endpoint, which allows to access resources. There are two types of operations: collection and item. As you can guess, collection operation returns multiple items of resource and item operation returns single item (fetched by specified id).

Configuring operations

To configure operation for resource you need to pass collectionOperations or itemOperations parameters into ApiResource annotation as on example below. Don't bother about the key of the operation for now. In our example we use get, but you could use there any other string. The important part is the path. This is the URL path for the endpoint, prefixed by the basePath from route enhancer (see more on Route enhancer).

Path of the item operation needs to contain {id} parameter. This parameter is replaced by uid of the entity when fetching single item.

use SourceBroker\T3api\Annotation\ApiResource;

/**
 * @ApiResource(
 *     collectionOperations={
 *          "get"={
 *              "path"="/items",
 *          },
 *     },
 *     itemOperations={
 *          "get"={
 *              "path"="/items/{id}",
 *          }
 *     },
 * )
 */
class Item extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
{
}
Copied!

Supported operation methods

T3api supports all REST methods

HTTP Method Operation type Example URL Purpose
GET Collection /resource Reading collection of the items
GET Item /resource/{id} Reading single item
POST Collection /resource Creating new item
PATCH Item /resource/{id} Updating the item
PUT Item /resource/{id} Replacing the item
DELETE Item /resource/{id} Deleting the item

Response of GET collection operation

As mentioned in Getting started T3api uses Hydra Core Vocabulary. That's why collection response of GET method is enriched by some additional properties:

  • hydra:member - contains array of matched entities.
  • hydra:totalItems - contains number of all items.
  • hydra:view - contains data useful for pagination (see more on Pagination).
  • hydra:search - contains data useful for filtering (see more on Filtering).

Here is an example of basic response for collection operation. You can open it at following url: https://13.t3api.ddev.site/_api/news/news

{
  "hydra:member": [
    {
      "title": "[EN] Sed ut perspiciatis unde omnis iste natus error sit voluptatem folder A",
      "alternativeTitle": "",
      "teaser": "",
      "datetime": "2020-05-28T19:20:00.000+00:00",
      "author": "",
      "authorEmail": "",
      "categories": [
        {
          "title": "[EN] Category 1A",
          "image": null,
          "uid": 1,
          "@id": "/_api/news/categories/1"
        },
        {
          "title": "[EN] Category 2A",
          "image": null,
          "uid": 2,
          "@id": "/_api/news/categories/2"
        }
      ],
      "type": "0",
      "falMedia": [
        {
          "url": "https://13.t3api.ddev.site/fileadmin/user_upload/test1.jpg",
          "uid": 4,
          "file": {
            "uid": 1,
            "name": "test1.jpg",
            "mimeType": "image/jpeg",
            "size": 42520
          }
        }
      ],
      "internalurl": "",
      "externalurl": "",
      "istopnews": false,
      "tags": [
        {
          "title": "[EN] Tag 1A",
          "uid": 1,
          "@id": "/_api/news/tags/1"
        },
        {
          "title": "[EN] Tag 4A",
          "uid": 4,
          "@id": "/_api/news/tags/4"
        }
      ],
      "singleUri": "https://13.t3api.ddev.site/news/en-sed-ut-perspiciatis-unde-omnis-iste-natus-error-sit-voluptatem-folder-a",
      "imageThumbnail": "https://13.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_ce3d0ad685.jpg",
      "imageLarge": "https://13.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_67bc9e165d.jpg",
      "uid": 1,
      "@id": "/_api/news/news/1"
    },
    {
      "title": "[EN] Natus error sit voluptatem folder A",
      "alternativeTitle": "",
      "teaser": "",
      "datetime": "2020-05-28T19:45:00.000+00:00",
      "author": "",
      "authorEmail": "",
      "categories": [
        {
          "title": "[EN] Category 2A",
          "image": null,
          "uid": 2,
          "@id": "/_api/news/categories/2"
        }
      ],
      "type": "0",
      "falMedia": [
        {
          "url": "https://13.t3api.ddev.site/fileadmin/user_upload/test1.jpg",
          "uid": 2,
          "file": {
            "uid": 1,
            "name": "test1.jpg",
            "mimeType": "image/jpeg",
            "size": 42520
          }
        },
        {
          "url": "https://13.t3api.ddev.site/fileadmin/user_upload/test1.jpg",
          "uid": 3,
          "file": {
            "uid": 1,
            "name": "test1.jpg",
            "mimeType": "image/jpeg",
            "size": 42520
          }
        }
      ],
      "internalurl": "",
      "externalurl": "",
      "istopnews": true,
      "tags": [
        {
          "title": "[EN] Tag 1A",
          "uid": 1,
          "@id": "/_api/news/tags/1"
        },
        {
          "title": "[EN] Tag 2A",
          "uid": 2,
          "@id": "/_api/news/tags/2"
        }
      ],
      "singleUri": "https://13.t3api.ddev.site/news/en-natus-error-sit-voluptatem-folder-a",
      "imageThumbnail": "https://13.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_ce3d0ad685.jpg",
      "imageLarge": "https://13.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_67bc9e165d.jpg",
      "uid": 2,
      "@id": "/_api/news/news/2"
    },
    {
      "title": "[EN] Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur folder A",
      "alternativeTitle": "",
      "teaser": "",
      "datetime": "2020-05-29T20:10:00.000+00:00",
      "author": "",
      "authorEmail": "",
      "categories": [
        {
          "title": "[EN] Category 3A",
          "image": null,
          "uid": 3,
          "@id": "/_api/news/categories/3"
        },
        {
          "title": "[EN] Category 4A",
          "image": null,
          "uid": 4,
          "@id": "/_api/news/categories/4"
        }
      ],
      "type": "0",
      "falMedia": [
        {
          "url": "https://13.t3api.ddev.site/fileadmin/user_upload/test1.jpg",
          "uid": 1,
          "file": {
            "uid": 1,
            "name": "test1.jpg",
            "mimeType": "image/jpeg",
            "size": 42520
          }
        }
      ],
      "internalurl": "",
      "externalurl": "",
      "istopnews": false,
      "tags": [
        {
          "title": "[EN] Tag 4A",
          "uid": 4,
          "@id": "/_api/news/tags/4"
        }
      ],
      "singleUri": "https://13.t3api.ddev.site/news/en-ut-enim-ad-minima-veniam-quis-nostrum-exercitationem-ullam-corporis-suscipit-laboriosam-nisi-ut-aliquid-ex-ea-commodi-consequatur-folder-a",
      "imageThumbnail": "https://13.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_ce3d0ad685.jpg",
      "imageLarge": "https://13.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_67bc9e165d.jpg",
      "uid": 3,
      "@id": "/_api/news/news/3"
    },
    {
      "title": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem folder B",
      "alternativeTitle": "",
      "teaser": "",
      "datetime": "2020-05-28T19:20:00.000+00:00",
      "author": "",
      "authorEmail": "",
      "categories": [],
      "type": "0",
      "falMedia": [
        {
          "url": "https://13.t3api.ddev.site/fileadmin/user_upload/test1.jpg",
          "uid": 8,
          "file": {
            "uid": 1,
            "name": "test1.jpg",
            "mimeType": "image/jpeg",
            "size": 42520
          }
        }
      ],
      "internalurl": "",
      "externalurl": "",
      "istopnews": false,
      "tags": [
        {
          "title": "Tag 1B",
          "uid": 6,
          "@id": "/_api/news/tags/6"
        },
        {
          "title": "Tag 4B",
          "uid": 9,
          "@id": "/_api/news/tags/9"
        }
      ],
      "singleUri": "https://13.t3api.ddev.site/news/sed-ut-perspiciatis-unde-omnis-iste-natus-error-sit-voluptatem-folder-b",
      "imageThumbnail": "https://13.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_ce3d0ad685.jpg",
      "imageLarge": "https://13.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_67bc9e165d.jpg",
      "uid": 5,
      "@id": "/_api/news/news/5"
    },
    {
      "title": "Natus error sit voluptatem folder B",
      "alternativeTitle": "",
      "teaser": "",
      "datetime": "2020-05-28T19:45:00.000+00:00",
      "author": "",
      "authorEmail": "",
      "categories": [],
      "type": "0",
      "falMedia": [
        {
          "url": "https://13.t3api.ddev.site/fileadmin/user_upload/test1.jpg",
          "uid": 6,
          "file": {
            "uid": 1,
            "name": "test1.jpg",
            "mimeType": "image/jpeg",
            "size": 42520
          }
        },
        {
          "url": "https://13.t3api.ddev.site/fileadmin/user_upload/test1.jpg",
          "uid": 7,
          "file": {
            "uid": 1,
            "name": "test1.jpg",
            "mimeType": "image/jpeg",
            "size": 42520
          }
        }
      ],
      "internalurl": "",
      "externalurl": "",
      "istopnews": false,
      "tags": [
        {
          "title": "Tag 1B",
          "uid": 6,
          "@id": "/_api/news/tags/6"
        },
        {
          "title": "Tag 2B",
          "uid": 7,
          "@id": "/_api/news/tags/7"
        }
      ],
      "singleUri": "https://13.t3api.ddev.site/news/natus-error-sit-voluptatem-folder-b",
      "imageThumbnail": "https://13.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_ce3d0ad685.jpg",
      "imageLarge": "https://13.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_67bc9e165d.jpg",
      "uid": 6,
      "@id": "/_api/news/news/6"
    },
    {
      "title": "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur folder B",
      "alternativeTitle": "",
      "teaser": "",
      "datetime": "2020-05-29T20:10:00.000+00:00",
      "author": "",
      "authorEmail": "",
      "categories": [],
      "type": "0",
      "falMedia": [
        {
          "url": "https://13.t3api.ddev.site/fileadmin/user_upload/test1.jpg",
          "uid": 5,
          "file": {
            "uid": 1,
            "name": "test1.jpg",
            "mimeType": "image/jpeg",
            "size": 42520
          }
        }
      ],
      "internalurl": "",
      "externalurl": "",
      "istopnews": false,
      "tags": [
        {
          "title": "Tag 4B",
          "uid": 9,
          "@id": "/_api/news/tags/9"
        }
      ],
      "singleUri": "https://13.t3api.ddev.site/news/ut-enim-ad-minima-veniam-quis-nostrum-exercitationem-ullam-corporis-suscipit-laboriosam-nisi-ut-aliquid-ex-ea-commodi-consequatur-folder-b",
      "imageThumbnail": "https://13.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_ce3d0ad685.jpg",
      "imageLarge": "https://13.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_67bc9e165d.jpg",
      "uid": 7,
      "@id": "/_api/news/news/7"
    }
  ],
  "hydra:totalItems": 6,
  "hydra:view": {
    "hydra:first": "/_api/news/news?page=1",
    "hydra:last": "/_api/news/news?page=1",
    "hydra:pages": [
      "/_api/news/news?page=1"
    ],
    "hydra:page": 1
  },
  "hydra:search": {
    "hydra:template": "/_api/news/news{?order[uid],order[title],order[datetime],istopnews,uid,datetime,pid,search}",
    "hydra:mapping": [
      {
        "variable": "order[uid]",
        "property": "uid"
      },
      {
        "variable": "order[title]",
        "property": "title"
      },
      {
        "variable": "order[datetime]",
        "property": "datetime"
      },
      {
        "variable": "istopnews",
        "property": "istopnews"
      },
      {
        "variable": "uid",
        "property": "uid"
      },
      {
        "variable": "datetime",
        "property": "datetime"
      },
      {
        "variable": "pid",
        "property": "pid"
      },
      {
        "variable": "search",
        "property": "title"
      }
    ]
  }
}
Copied!

Item operation response

In response of all item operations only object is received. Response does not contain any additional attributes because they are useless in single item operation context.

{
  "@id": "/_api/news/news/1",
  "uid": 1,
  "title": "Lorem ipsum dolor sit amet enim",
  "teaser": "Pellentesque facilisis. Nulla imperdiet sit amet magna.",
  "datetime": "2019-08-30T07:30:00+00:00",
  "bodytext": "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>",
}
Copied!

Customizing returned properties

By default all properties of entities are returned. That may not be expected behavior because of performance and security reasons. You can easily manage properties returned from any endpoint using serialization context groups.

Main endpoint

There is one special build-in endpoint which is not determined by @ApiResource annotation - Main endpoint. It contains list all available collection operations. It is useful for creating Postman requests collections or for frontend applications which avoids to use hardcoded path for the endpoints. Main endpoint is available under base path (default https://example.com/_api/).

Example response on the main endpoint for th3 t3api demo is available at: https://13.t3api.ddev.site/_api and looks like:

{
  "resources": {
    "SourceBroker\\T3apinews\\Domain\\Model\\File": "/_api/news/files",
    "SourceBroker\\T3apinews\\Domain\\Model\\Tag": "/_api/news/tags",
    "SourceBroker\\T3apinews\\Domain\\Model\\Category": "/_api/news/categories",
    "SourceBroker\\T3apinews\\Domain\\Model\\News": "/_api/news/news"
  }
}
Copied!

Class used as a response in main endpoint may be overwritten in ext_localconf.php:

$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['mainEndpointResponseClass'] = \Vendor\Ext\MyCustomMainEndpoint::class;
Copied!

To disable main endpoint it is just needed to set mainEndpointResponseClass to null. It will result in 404 error response.

$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['mainEndpointResponseClass'] = null;
Copied!

Customizing operation handler

Sometimes predefined operations may not be enough to handle all use cases. In fact build-in endpoints supports CRUD on Extbase entities. If you would like to create something more complex then you need to utilize custom operation handlers.

As name suggests, "operation handlers" determines how specific request is processed. T3api dispatcher checks if there is handler matching current request and executes it.

To register new operation handler it is needed to add new item to t3api configuration array, as on example below:

ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['operationHandlers'][\Vendor\Extension\OperationHandler\MyCustomOperationHandler::class] = 100;
Copied!

Name of the element of array is the name of the class and the value is the priority. Priority is needed because there may be multiple handlers which supports current request. Higher priority means that handler wins and will be executed. Only one handler will be executed for every request. Most of build-in handlers have negative priorities so it is suggested to use priority higher than 0 in custom handlers.

Every handler has to implement interface \SourceBroker\T3api\OperationHandler\OperationHandlerInterface. This interface forces handler class to contain two methods:

  • supports (static) - Basing on input arguments method takes a decision if current class can handle the operation and return boolean value (true - supports; false - does not support). Accepts arguments:
    • $operation (\SourceBroker\T3api\Domain\Model\OperationInterface).
    • $request (\Symfony\Component\HttpFoundation\Request).
  • handle - Contains code responsible for handling operation. Executed only when supports returns true. Accepts arguments:
    • $operation (\SourceBroker\T3api\Domain\Model\OperationInterface).
    • $request (\Symfony\Component\HttpFoundation\Request).
    • $route (array) - Array with route parameters (e.g. $route['id'] inside single items operations which uses {id} in URL).
    • &$response (\Psr\Http\Message\ResponseInterface) - reference to response.

It is useful and advised (but not required) to extend existing abstract classes when creating custom handlers:

  • \SourceBroker\T3api\OperationHandler\AbstractCollectionOperationHandler when creating custom collection operation handler.
  • \SourceBroker\T3api\OperationHandler\AbstractItemOperationHandler when creating custom item operation handler.
  • \SourceBroker\T3api\OperationHandler\AbstractOperationHandler or at least common abstract operation handler which contain bunch of useful things which may be needed in your custom handler (like deserialization process, injecting services or getting appropriate repository for current operation).

Even better to extend any of already existing operations if that is possible:

  • \SourceBroker\T3api\OperationHandler\CollectionGetOperationHandler
  • \SourceBroker\T3api\OperationHandler\CollectionPostOperationHandler
  • \SourceBroker\T3api\OperationHandler\FileUploadOperationHandler
  • \SourceBroker\T3api\OperationHandler\ItemDeleteOperationHandler
  • \SourceBroker\T3api\OperationHandler\ItemGetOperationHandler
  • \SourceBroker\T3api\OperationHandler\ItemPatchOperationHandler
  • \SourceBroker\T3api\OperationHandler\ItemPutOperationHandler

Example

Example of declaration and usage of custom operation handler can be found in current user endpoint use case.

Filtering

Filters gives possibility to customize SQL query used to receive results in GET collection operation. Information about filters available for operation are returned inside hydra:search section in response body. To register filter for resource it is needed to add appropriate annotation:

/**
 * @T3api\ApiResource(
 *     collectionOperations={
 *          "get"={
 *              "method"="GET",
 *              "path"="/news/news",
 *              "normalizationContext"={
 *                  "groups"={"api_get_collection_t3apinews_news"}
 *              },
 *          },
 *          "post"={
 *              "method"="POST",
 *              "path"="/news/news",
 *              "normalizationContext"={
 *                  "groups"={"api_post_item_t3apinews_news"}
 *              },
 *          },
 *     },
 *     itemOperations={
 *          "get"={
 *              "path"="/news/news/{id}",
 *              "normalizationContext"={
 *                  "groups"={"api_get_item_t3apinews_news"}
 *              },
 *          },
 *          "patch"={
 *              "method"="PATCH",
 *              "path"="/news/news/{id}",
 *              "normalizationContext"={
 *                  "groups"={"api_patch_item_t3apinews_news"}
 *              },
 *          },
 *          "put"={
 *              "method"="PUT",
 *              "path"="/news/news/{id}",
 *              "normalizationContext"={
 *                  "groups"={"api_put_item_t3apinews_news"}
 *              },
 *          },
 *          "delete"={
 *              "method"="DELETE",
 *              "path"="/news/news/{id}",
 *          },
 *     },
 *     attributes={
 *          "pagination_client_enabled"=true,
 *          "pagination_items_per_page"=20,
 *          "maximum_items_per_page"=100,
 *          "pagination_client_items_per_page"=true,
 *          "persistence"={
 *              "storagePid"="3,6",
 *              "recursive"=1
 *          }
 *     }
 * )
 *
 * @T3api\ApiFilter(
 *     OrderFilter::class,
 *     properties={"uid","title","datetime"}
 * )
 *
 * @T3api\ApiFilter(
 *     BooleanFilter::class,
 *     properties={"istopnews"}
 * )
 *
 * @T3api\ApiFilter(
 *     UidFilter::class,
 *     properties={"uid"}
 * )
 *
 * @T3api\ApiFilter(
 *     RangeFilter::class,
 *     properties={
 *       "datetime": "datetime"
 *     }
 * )
 *
 * @T3api\ApiFilter(
 *     NumericFilter::class,
 *     properties={"pid"}
 * )
 *
 * @T3api\ApiFilter(
 *     SearchFilter::class,
 *     properties={
 *          "title": "partial",
 *          "alternativeTitle": "partial",
 *          "bodytext": "partial",
 *          "tags.title": "partial",
 *     },
 *     arguments={
 *          "parameterName": "search",
 *     }
 * )
 */
class News extends \GeorgRinger\News\Domain\Model\News
{
}
Copied!

It is possible to configure filters only for whole resource. That means filters are available for all GET collection operations within this resource. It is not possible (yet) to configure filters only for specific operations. There are few build-in filters. See the next pages.

BooleanFilter

Should be used to filter items by boolean fields.

Syntax: ?property=<true|false|1|0>

use SourceBroker\T3api\Annotation as T3api;
use SourceBroker\T3api\Filter\SearchFilter;

/**
 * @T3api\ApiResource (
 *     collectionOperations={
 *          "get"={
 *              "method"="GET",
 *              "path"="/news/news",
 *          },
 *     },
 * )
 *
 * @T3api\ApiFilter(
 *     BooleanFilter::class,
 *     properties={"istopnews"}
 * )
 */
class News extends \GeorgRinger\News\Domain\Model\News
{
}
Copied!

ContainFilter

@todo - write docs

DistanceFilter

Distance filter allows to filter map points points by radius. Map points kept in the database needs to contain latitude and longitude to use this filter.

Configuration for distance filter looks a little bit different than for other build-in filter. Because distance filter is not based on single field it should not contain properties definition. Instead of that it is needed to specify which model properties contain latitude and longitude in arguments. Moreover, as properties is not defined, parameterName is required. Beside default values in arguments, distance filter accepts also:

  • latProperty (string) - Name of the property which holds latitude
  • lngProperty (string) - name of the property which holds longitude
  • unit (ENUM: "mi", "km"; default "km") - Unit of the radius
  • radius (float/int; default 100) - Radius to filter in; if allowClientRadius is set to true, then used as default value.
  • allowClientRadius (bool; default false) - Set to true allow to change the radius from GET parameter.
use SourceBroker\T3api\Filter\DistanceFilter;

/**
 * @T3api\ApiFilter(
 *     DistanceFilter::class,
 *     arguments={
 *          "parameterName"="position",
 *          "latProperty"="gpsLatitude",
 *          "lngProperty"="gpsLongitude",
 *          "radius"="100",
 *          "unit"="km",
 *     }
 * )
 */
class Item extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
{
}
Copied!

NumericFilter

Should be used to filter items by numeric fields.

Syntax: ?property=<int|decimal...> or ?property[]=<int|decimal...>&property[]=<int|decimal...>.

use SourceBroker\T3api\Annotation as T3api;
use SourceBroker\T3api\Filter\NumericFilter;

/**
 * @T3api\ApiResource (
 *     collectionOperations={
 *          "get"={
 *              "path"="/users",
 *          },
 *     },
 * )
 * @T3api\ApiFilter(
 *     NumericFilter::class,
 *     properties={"address.number", "height"},
 * )
 */
class User extends \TYPO3\CMS\Extbase\Domain\Model\FrontendUser
{
}
Copied!

OrderFilter

Allows to change default ordering of collection responses.

Syntax: ?order[property]=<asc|desc>

It is possible to order by single field (query string ?order[title]=asc) or by multiple of them (query string ?order[title]=asc&order[datetime]=desc).

It may happen that conflict of names will occur if order is also the name of property with enabled another filter. Solution in such cases would be a change of parameter name used by OrderFilter. It can be done using argument orderParameterName, as on example below:

use SourceBroker\T3api\Annotation as T3api;
use SourceBroker\T3api\Filter\OrderFilter;

/**
 * @T3api\ApiResource (
 *     collectionOperations={
 *          "get"={
 *              "path"="/news/news",
 *          },
 *     },
 * )
 *
 * @T3api\ApiFilter(
 *     OrderFilter::class,
 *     properties={"title","datetime"}
 *     arguments={"orderParameterName": "myOrderParameterName"},
 * )
 */
 class News extends \GeorgRinger\News\Domain\Model\News
{
}
Copied!

RangeFilter

Allows to filter by a value lower than (or equal), greater than (or equal) and between two values.

Syntax: ?property[<lt|gt|lte|gte|between>]=value

RangeFilter supports two different strategies:

  • int (alternatively number or integer) - default strategy if not specified. Values passed in filter is casted to integer.
  • datetime - allows to filter results by date time range (value passed in filter is casted to DateTime object before passed to Extbase query).
use SourceBroker\T3api\Annotation as T3api;
use SourceBroker\T3api\Filter\RangeFilter;

/**
 * @T3api\ApiResource (
 *     collectionOperations={
 *          "get"={
 *              "path"="/news/news",
 *          },
 *     },
 * )
 *
 * @T3api\ApiFilter(
 *     RangeFilter::class,
 *     properties={
 *          "datetime": "datetime",
 *          "uid": "int",
 *     },
 * )
 */
 class News extends \GeorgRinger\News\Domain\Model\News
{
}
Copied!

SearchFilter

Should be used to filter items by text fields. SearchFilter supports 3 strategies:

  • exact - Filters items which matches exactly search term (WHERE property = "value" in MySQL language). This is strategy used by default if no other is configured.
use SourceBroker\T3api\Annotation as T3api;
use SourceBroker\T3api\Filter\SearchFilter;

/**
 * @T3api\ApiResource (
 *     collectionOperations={
 *          "get"={
 *              "path"="/users",
 *          },
 *     },
 * )
 *
 * @T3api\ApiFilter(
 *     SearchFilter::class,
 *     properties={"firstName", "middleName", "lastName"}
 * )
 */
class User extends \TYPO3\CMS\Extbase\Domain\Model\FrontendUser
{
}
Copied!
  • partial - Filters items which matches partially search term (WHERE property LIKE "%value%" in MySQL language).
use SourceBroker\T3api\Annotation as T3api;
use SourceBroker\T3api\Filter\SearchFilter;

/**
 * @T3api\ApiResource (
 *     collectionOperations={
 *          "get"={
 *              "path"="/users",
 *          },
 *     },
 * )
 *
 * @T3api\ApiFilter(
 *     SearchFilter::class,
 *     properties={
 *          "firstName": "partial",
 *          "middleName": "partial",
 *          "lastName": "partial",
 *          "address.street": "partial",
 *     },
 * )
 */
class User extends \TYPO3\CMS\Extbase\Domain\Model\FrontendUser
{
}
Copied!
  • matchAgainst - Filters items which matches search term using full text search (MATCH(property) AGAINST ("value" IN NATURAL LANGUAGE MODE) in MySQL language). In it possible to extend query with WITH QUERY EXPANSION by adding withQueryExpansion in arguments (Read more about query expansion)
use SourceBroker\T3api\Annotation as T3api;
use SourceBroker\T3api\Filter\SearchFilter;

/**
 * @T3api\ApiResource (
 *     collectionOperations={
 *          "get"={
 *              "path"="/users",
 *          },
 *     },
 * )
 *
 * @T3api\ApiFilter(
 *     SearchFilter::class,
 *     properties={
 *          "firstName": "matchAgainst",
 *          "middleName": "matchAgainst",
 *          "lastName": "matchAgainst",
 *          "address.street": "matchAgainst",
 *     },
 *     arguments={
 *          "withQueryExpansion": true,
 *     },
 * )
 */
class User extends \TYPO3\CMS\Extbase\Domain\Model\FrontendUser
{
}
Copied!

UidFilter

Should be used to filter items by uid property. In fact it only extends NumericFilter but beside adding new condition into SQL's WHERE clause it also modifies Extbase's Query Settings to make it possible to find translated records by passing default language record uid value. Modification in Query Settings are exactly the same as modifications which are applied when \TYPO3\CMS\Extbase\Persistence\Repository::findByUid is called.

An example - if default record uid is 8 and uid for translated record is 10 and you are doing request for translation (see how multilanguage works in t3api) then you could add a param uid with value 8 (?uid=8 or ?uid[]=8). If you would use NumericFilter in same example, then it would be needed to set uid param to 10 (otherwise record would not be included in response).

Syntax: ?property=<int> or ?property[]=<int>&property[]=<int>.

use SourceBroker\T3api\Annotation as T3api;
use SourceBroker\T3api\Filter\UidFilter;

/**
 * @T3api\ApiResource (
 *     collectionOperations={
 *          "get"={
 *              "path"="/news/news",
 *          },
 *     },
 * )
 *
 * @T3api\ApiFilter(
 *     UidFilter::class,
 *     properties={"uid"},
 * )
 */
 class News extends \GeorgRinger\News\Domain\Model\News
 {
 }
Copied!

Custom filters

It is super easy to create custom filters which will match your specific requirements. Custom filters has to implement interface \SourceBroker\T3api\Filter\FilterInterface and contain one public method filterProperty. Method filterProperty accepts 4 arguments:

  • $property (string) - Name of the property to filter by.
  • $values (mixed) - Values passed in request.
  • $query (TYPO3\CMS\Extbase\Persistence\QueryInterface) - Instance of Extbase's query.
  • $apiFilter (SourceBroker\T3api\Domain\Model\ApiFilter) - Instance of T3api's filter.

Example implementation of custom filter may looks as follows:

declare(strict_types=1);
namespace Vendor\Extension\Filter;

use SourceBroker\T3api\Domain\Model\ApiFilter;
use SourceBroker\T3api\Filter\FilterInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;

class MyCustomFilter implements FilterInterface
{
    public function filterProperty(string $property, $values, QueryInterface $query, ApiFilter $apiFilter): ?ConstraintInterface
    {
        return $query->equals($property, $values);
    }
}
Copied!

It may be useful, but not required, to extend class \SourceBroker\T3api\Filter\AbstractFilter which will give you bunch of methods inside your filter. An example of such method may be addJoinsForNestedProperty which may be really useful to handle more complex filters, especially when you need to implement something which is out of reach Extbase's query builder. Check code inside \SourceBroker\T3api\Filter\ContainFilter and \SourceBroker\T3api\Filter\SearchFilter for example usages.

declare(strict_types=1);
namespace Vendor\Extension\Filter;

use SourceBroker\T3api\Domain\Model\ApiFilter;
use SourceBroker\T3api\Filter\AbstractFilter;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;

class MyCustomFilter extends AbstractFilter
{
    public function filterProperty(string $property, $values, QueryInterface $query, ApiFilter $apiFilter): ?ConstraintInterface
    {
        // ...
    }
}
Copied!

To use your new custom filter is it just needed to pass it as first parameter into @ApiFilter annotation:

use SourceBroker\T3api\Annotation as T3api;
use Vendor\Extension\Filter\MyCustomFilter;

/**
 * @T3api\ApiFilter(
 *     MyCustomFilter::class,
 *     properties={"username"},
 * )
 */
class User extends \TYPO3\CMS\Extbase\Domain\Model\FrontendUser
{
}
Copied!

SQL "IN" operator

When using query params ?property=<value> only items which match exactly such condition are returned. But it is possible to pass multiple values. If you would like to receive all items which property matches value1 or value2 then you can send property as an array in query string: ?property[]=<value1>&property[]=<value2>. From build-in filters NumericFilter and SearchFilter are the filters which support IN operator.

SQL "OR" conjunction

By default AND conjunction is used between all applied filters but there is a ways to change conjunction to OR. Frontend applications often needs single input field which searches multiple fields. To create such filter it is needed to set same parameterName for multiple fields. Example code below means that request to URL /users?search=john will return records where any of the fields (firstName, middleName, lastName or address.street) matches searched text. If we would not determine parameterName in filter arguments this configuration would work as separate filters for every property.

use SourceBroker\T3api\Annotation as T3api;
use SourceBroker\T3api\Filter\SearchFilter;

/**
 * @T3api\ApiResource (
 *     collectionOperations={
 *          "get"={
 *              "path"="/users",
 *          },
 *     },
 * )
 *
 * @T3api\ApiFilter(
 *     SearchFilter::class,
 *     properties={
 *          "firstName": "partial",
 *          "middleName": "partial",
 *          "lastName": "partial",
 *          "address.street": "partial",
 *     },
 *     arguments={
 *          "parameterName": "search",
 *     }
 * )
 */
class User extends \TYPO3\CMS\Extbase\Domain\Model\FrontendUser
{
}
Copied!

Pagination

@todo - write docs

Global configuration

@todo - write docs

Resource specific configuration

@todo - write docs

Server side

@todo - write docs

Client side

@todo - write docs

Security

  • @todo - write docs: general usage of expression language inside of "security" annotation
  • @todo - write docs: general usage of expression language inside of "security_post_denormalize" annotation
  • @todo - write docs: example usage of frontend user
  • @todo - write docs: example usage of frontend user group
  • @todo - write docs: example usage of backend user
  • @todo - write docs: example usage of backend user group
  • @todo - write docs: example usage of object in items operations (+ information that object doesn't exist in collection operation)
  • @todo - write docs: information that most of the conditions from typoscript can be used (https://docs.typo3 .org/m/typo3/reference-typoscript/master/en-us/Conditions/Index.html#description; tree is an example object which can not be used)

Serialization

@todo - write docs

Context groups

@todo - write docs

Customization

@todo - write docs: describe how to customize serialization context using \SourceBroker\T3api\Serializer\ContextBuilder\ContextBuilderInterface::SIGNAL_CUSTOMIZE_SERIALIZER_CONTEXT_ATTRIBUTES (e.g. how to conditionally include fields if current user belongs to specified group)

Handlers

@todo - write docs

FileReference

@todo - write docs

Image

@todo - write docs

RecordUri

@todo - write docs

Custom handlers

@todo - write docs

Subscribers

A subscriber is a class that listens to one or more events during the serialization or deserialization process. These events can include pre-serialization, post-serialization, pre-deserialization, and post-deserialization. Subscribers are used to customize the serialization/deserialization process. For example, you might use a subscriber to change the serialized representation of certain types of objects, or to perform some custom logic before an object is serialized.

Here is a list of build in T3API subscribers.

AbstractEntitySubscriber

This subscriber listens to the POST_SERIALIZE and PRE_DESERIALIZE events. In the case of the POST_SERIALIZE event, it adds additional properties (defined in $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['forceEntityProperties'] ) and IRI to the serialized object if the object is an instance of AbstractDomainObject. In the case of the PRE_DESERIALIZE event, it changes the type to a custom one to enable data handling with a serializer handler.

CurrentFeUserSubscriber

This subscriber listens to the PRE_SERIALIZE and PRE_DESERIALIZE events. In the case of the PRE_SERIALIZE event, it changes the type to a custom one if the type is equal to CurrentFeUserHandler::TYPE. In the case of the PRE_DESERIALIZE event, it adds the FE user identifier to the data. This identifier is later used by CurrentFeUserHandler to get FeUser object. It allows to securely attach info about FeUser to incoming data. Look for more info at at current user endpoint use case.

FileReferenceSubscriber

This subscriber listens to the PRE_SERIALIZE and PRE_DESERIALIZE events. In both cases, it changes the type to a custom one to enable data handling with a serializer handler if the type is a subclass of AbstractFileFolder.

GenerateMetadataSubscriber

This subscriber listens to the PRE_SERIALIZE and PRE_DESERIALIZE events. In both cases, it generates yml serializer metadata and cache it in var/cache/code/t3api. Data from var/cache/code/t3api is later used to serialize/deserialize by JSM serializer.

ResourceTypeSubscriber

This subscriber listens to the POST_SERIALIZE event. It adds the resource type (@type) to the serialized object if the object is an instance of AbstractDomainObject. Examples of types: SourceBroker\\T3apinews\\Tag, SourceBroker\\T3apinews\\News. Adding @type is not activated by default as it can take a lot of space in the response. To activate it add in your local extension :file:`ext_localconf.php.

$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['serializerSubscribers'][] = \SourceBroker\T3api\Serializer\Subscriber\ResourceTypeSubscriber::class;
Copied!

ThrowableSubscriber

This subscriber listens to the POST_SERIALIZE event. It adds a description and debug to the serialized object if the object is an instance of Throwable.

Yaml metadata

@todo - write docs: how to use serializerMetadataDirs

Exceptions

TYPO3 may encounter issues with FileReference or File objects, such as when a file is missing, inaccessible, or if the relation is broken. These issues can interrupt the serialization process and a jsonified error will be returned. To address this, we've introduced a configuration option that allows for the graceful handling of specific exceptions during the serialization process. This configuration is defined on a per-class basis, meaning that different classes can have different sets of exceptions that are handled gracefully.

Once configured, these exceptions will not interrupt the serialization process for the respective class. Instead, they will be handled appropriately, allowing the serialization process to continue uninterrupted. This ensures that a problem with a single object does not prevent the successful serialization of other objects.

Setting responsible for that is:

$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['serializer']['exclusionForExceptionsInAccessorStrategyGetValue']
Copied!

Example value:

$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['serializer']['exclusionForExceptionsInAccessorStrategyGetValue'] = [
    \SourceBroker\T3apinews\Domain\Model\FileReference::class => [
        \TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException::class,
    ],
];
Copied!

Asterix as "all exceptions" is also supported: Example:

$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['serializer']['exclusionForExceptionsInAccessorStrategyGetValue'] = [
    \SourceBroker\T3apinews\Domain\Model\FileReference::class => ['*'],
];
Copied!

Integration

@todo - write docs

Integration with other extensions

@todo - write docs @todo - write docs: configure serializer for classes which can not be override Yaml metadata

News extension - Example integration

@todo - write docs t3apinews

Inline output

@todo - write docs: If you would like to include your JSON directly in TYPO3 HTML output (e.g. to omit waiting for initial request to API) you can use xxxViewHelper as follows:

<html xmlns="http://www.w3.org/1999/xhtml" lang="en"
  xmlns:t3api="http://typo3.org/ns/SourceBroker/T3api/ViewHelpers"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  data-namespace-typo3-fluid="true">

    <script type="application/json">
        <t3api:xxx />
    </script>

</html>
Copied!

Customization

Api Resource Path

By default t3api will search for API Resource classes in Classes/Domain/Model/*.php of currently loaded extensions. This behaviour is defined in LoadedExtensionsDomainModelApiResourcePathProvider and registered in ext_localconf.php like this:

$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['apiResourcePathProviders'] = [
      \SourceBroker\T3api\Provider\ApiResourcePath\LoadedExtensionsDomainModelApiResourcePathProvider::class,
  ];
Copied!

The same way you can add your own providers for additional patches.

Expression Language

Symfony expression language is widely used in TYPO3 core. T3api also uses it in two places:

T3api utilizes TYPO3 core expression language feature. So, if you would like to extend expression language used in t3api core, you need to register new provider in the same way as you do it e.g. for TS. Just notice that appropriate context has to be specified as array key (in this case it's t3api). Code below shows how to register MyCustomProvider in t3api context.

typo3conf/ext/my_ext/Configuration/ExpressionLanguage.php
return [
     't3api' => [
         \Vendor\MyExt\ExpressionLanguage\MyCustomProvider::class,
     ],
];
Copied!

After registration of custom provider follow TYPO3 documentation how to add additional variables and additional functions into expression language. Now you can use your custom variables and functions inside t3api security check or serialization and deserialization expressions.

Collection response schema

@todo - write docs

Events

After deserialization

An event that is dispatched after request payload is deserialized to objects. May be useful when it is needed e.g. to change datetime property to current or assign current TYPO3 user as an author. This event gives access to following data:

  • operation - instance of \SourceBroker\T3api\HydraCollectionResponseomain\Model\OperationInterface
  • object - object deserialized from payload

Example event registration in Services.yaml:

V\Site\EventListener\AfterDeserializeOperationEventListener:
  tags:
    - name: 'event.listener'
Copied!

Example implementation:

<?php

namespace V\Site\EventListener;

use SourceBroker\T3api\Event\AfterDeserializeOperationEvent;

class AfterDeserializeOperationEventListener
{
    public function __invoke(AfterDeserializeOperationEvent $event): void
    {
        $operation = $event->getOperation();
        $object => $event->getObject();
        ...
        $event->setObject($object);
    }
}
Copied!

After processing operation

An event that is dispatched after an operation is processed and before objects are passed to the serializer. This event gives access to following data:

  • operation - instance of \SourceBroker\T3api\Domain\Model\OperationInterface
  • result - instance of resource class (entity) or, in collection GET request, instance of \SourceBroker\T3api\Response\AbstractCollectionResponse (\SourceBroker\T3api\Response\HydraCollectionResponse by default if not customized)

Example event registration in Services.yaml:

V\Site\EventListener\AfterProcessOperationEventListener:
  tags:
    - name: 'event.listener'
Copied!

Example implementation:

<?php

namespace V\Site\EventListener;

use SourceBroker\T3api\Event\AfterProcessOperationEvent;

class AfterProcessOperationEventListener
{
    public function __invoke(AfterProcessOperationEvent $event): void
    {
        $operation = $event->getOperation();
        $result = $event->getResult();
    }
}
Copied!

After create context for operation

Event emitted during building serialization context. Useful to modify serialization context attributes (e.g. add groups which can conditionally include some properties in API response). This event gives access to following data:

  • operation - instance of \SourceBroker\T3api\Domain\Model\OperationInterface
  • request - instance of Symfony\Component\HttpFoundation\Request
  • context - instance of JMS\Serializer\SerializationContext

Example event registration in Services.yaml:

V\Site\EventListener\AfterCreateContextForOperationEventListener:
  tags:
    - name: 'event.listener'
Copied!

Example implementation:

<?php

namespace V\Site\EventListener;

use SourceBroker\T3api\Event\AfterCreateContextForOperationEvent;

class AfterCreateContextForOperationEventListener
{
    public function __invoke(AfterCreateContextForOperationEvent $event): void
    {
        $operation = $event->getOperation();
        $request = $event->getRequest();
        $context = $event->getContext();
    }
}
Copied!

Before grant access

\SourceBroker\T3api\Security\FilterAccessChecker and \SourceBroker\T3api\Security\OperationAccessChecker are services used to decide if filter or operation are allowed for current request (check security documentation for more information).

There are 3 events dispatched before evaluation of security expressions.

Before grant operation access

Event dispatched before grant operation access. This event gives access to following data:

  • operation - instance of \SourceBroker\T3api\Domain\Model\OperationInterface
  • expressionLanguageVariables - array of variables passed to expression language (empty array)

Example event registration in Services.yaml:

V\Site\EventListener\BeforeOperationAccessGrantedEventListener:
  tags:
    - name: 'event.listener'
Copied!

Example implementation:

<?php

namespace V\Site\EventListener;

use SourceBroker\T3api\Event\BeforeOperationAccessGrantedEvent;

class BeforeOperationAccessGrantedEventListener
{
    public function __invoke(BeforeOperationAccessGrantedEvent $event): void
    {
        $operation = $event->getOperation();
        $expressionLanguageVariables = $event->getExpressionLanguageVariables();
        ...
    }
}
Copied!

Before grant post denormalize operation access

Event dispatched before grant post denormalize operation access. This event gives access to following data:

  • operation - instance of \SourceBroker\T3api\Domain\Model\OperationInterface
  • expressionLanguageVariables - array of variables passed to expression language (contains denormalized object)

Example event registration in Services.yaml:

V\Site\EventListener\BeforeOperationAccessGrantedPostDenormalizeEventListener:
  tags:
    - name: 'event.listener'
Copied!

Example implementation:

<?php

namespace V\Site\EventListener;

use SourceBroker\T3api\Event\BeforeOperationAccessGrantedPostDenormalizeEvent;

class BeforeOperationAccessGrantedPostDenormalizeEventListener
{
    public function __invoke(BeforeOperationAccessGrantedPostDenormalizeEvent $event): void
    {
        $operation = $event->getOperation();
        $expressionLanguageVariables = $event->getExpressionLanguageVariables();
        ...
    }
}
Copied!

Before grant filter access

Event dispatched before grant filter access. This event gives access to following data:

  • filter - instance of \SourceBroker\T3api\Domain\Model\ApiFilter
  • expressionLanguageVariables - array of variables passed to expression language (empty array)

Example event registration in Services.yaml:

V\Site\EventListener\BeforeFilterAccessGrantedEventListener:
  tags:
    - name: 'event.listener'
Copied!

Example implementation:

<?php

namespace V\Site\EventListener;

use SourceBroker\T3api\Event\BeforeFilterAccessGrantedEvent;

class BeforeFilterAccessGrantedEventListener
{
    public function __invoke(BeforeFilterAccessGrantedEvent $event): void
    {
        $filter = $event->getFilter();
        $expressionLanguageVariables = $event->getExpressionLanguageVariables();
        ...
    }
}
Copied!

Multilanguage

Yes, t3api supports multilanguage applications!

T3api offers two ways you can request multilanguage data:

  • standard language prefix
  • language header

Standard prefix

First is a standard way, just to prefix your request with language base as defined in your site's config.yaml.

  • https://13.t3api.ddev.site/_api/news/news will return news in default language.
  • https://13.t3api.ddev.site/de/_api/news/news will return news in de language.

Language header

Second way it to use always the same default lang url and add request header X-Locale with value set to identifier of expected language. Identifier means languageId value from your site's config.yaml.

  • https://13.t3api.ddev.site/_api/news/news with header X-Locale: 0 or no header at all will return news in default language.
  • https://13.t3api.ddev.site/_api/news/news with header X-Locale: 1 will return news in de language.

Here is an config.yaml for the above examples:

languages:
  -
    title: English
    enabled: true
    languageId: '0'
    base: /
    typo3Language: default
    locale: en_US.UTF-8
    iso-639-1: en
    navigationTitle: ''
    hreflang: en-US
    direction: ltr
    flag: en-us-gb
  -
    title: German
    enabled: true
    languageId: '1'
    base: /de/
    typo3Language: de
    locale: de_DE.UTF-8
    iso-639-1: de
    navigationTitle: ''
    hreflang: de-DE
    direction: ltr
    fallbackType: strict
    flag: de
Copied!

It is possible to customize name of the language header inside ext_localconf.php:

$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['languageHeader'] = 'My-Header-Name';
Copied!

Handling File Upload

Creating file resource

To create uploadable resource it is needed to create POST endpoint for resource which class extends \TYPO3\CMS\Extbase\Domain\Model\File.

declare(strict_types=1);
namespace Vendor\Users\Domain\Model;

use SourceBroker\T3api\Annotation as T3api;

/**
 * @T3api\ApiResource (
 *     collectionOperations={
 *          "post"={
 *              "path"="/files",
 *              "method"="POST",
 *          },
 *     }
 * )
 */
class File extends \TYPO3\CMS\Extbase\Domain\Model\File
{
}
Copied!

There is plenty configuration options which allows you to customize upload endpoint for your needs.

  • folder - destination folder (default: 1:/user_upload/ which means files will be uploaded into user_upload directory of file storage ID 1).
  • allowedFileExtensions - Array of allowed file extensions (default:$GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']).
  • conflictMode - Value of enumeration \TYPO3\CMS\Core\Resource\DuplicationBehavior (default: \TYPO3\CMS\Core\Resource\DuplicationBehavior::RENAME which means that new file name will be changed if same file already exists).
  • filenameMask - Allows to change the name of the uploaded file (default: [filename]; see how to customize name of uploaded file).
  • filenameHashAlgorithm - (default: md5; see how to customize name of uploaded file).
  • contentHashAlgorithm - (default: md5; see how to customize name of uploaded file).
declare(strict_types=1);
namespace Vendor\Users\Domain\Model;

use SourceBroker\T3api\Annotation as T3api;

/**
 * @T3api\ApiResource (
 *     collectionOperations={
 *          "post"={
 *              "path"="/files",
 *              "method"="POST",
 *          },
 *     },
 *     attributes={
 *          "upload"={
 *              "folder"="1:/user_upload/",
 *              "allowedFileExtensions"={"jpg", "jpeg", "png"},
 *              "conflictMode"=DuplicationBehavior::RENAME,
 *          }
 *     }
 * )
 */
class File extends \TYPO3\CMS\Extbase\Domain\Model\File
{
}
Copied!

Configuring TCA

It may be needed to adjust TCA configuration to correctly fill sys_file_reference columns. Correct TCA configuration contains at least 3 elements inside foreign_match_fields array - fieldname, tablenames and table_local but extension builder by default creates only fieldname (at least in current version).

$GLOBALS['TCA']['tx_users_domain_model_user']['columns']['photo']['config']['foreign_match_fields']['fieldname'] = 'photo';
$GLOBALS['TCA']['tx_users_domain_model_user']['columns']['photo']['config']['foreign_match_fields']['tablenames'] = 'tx_users_domain_model_user';
$GLOBALS['TCA']['tx_users_domain_model_user']['columns']['photo']['config']['foreign_match_fields']['table_local'] = 'sys_file';
Copied!

Appropriate TCA configuration for uploadable field may look like code below. Mind that \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig adds element fieldname by default so it is needed only to take care of tablenames and table_local.

'photo' => [
    'exclude' => true,
    'label' => 'LLL:EXT:users/Resources/Private/Language/locallang_db.xlf:tx_users_domain_model_user.photo',
    'config' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig(
        'photo',
        [
            'foreign_match_fields' => [
                'tablenames' => 'tx_users_domain_model_user',
                'table_local' => 'sys_file',
            ],
            'appearance' => [
                'createNewRelationLinkTitle' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:images.addFileReference'
            ],
            'foreign_types' => [
                '0' => [
                    'showitem' => '
                    --palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
                    --palette--;;filePalette'
                ],
                \TYPO3\CMS\Core\Resource\File::FILETYPE_TEXT => [
                    'showitem' => '
                    --palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
                    --palette--;;filePalette'
                ],
                \TYPO3\CMS\Core\Resource\File::FILETYPE_IMAGE => [
                    'showitem' => '
                    --palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
                    --palette--;;filePalette'
                ],
                \TYPO3\CMS\Core\Resource\File::FILETYPE_AUDIO => [
                    'showitem' => '
                    --palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
                    --palette--;;filePalette'
                ],
                \TYPO3\CMS\Core\Resource\File::FILETYPE_VIDEO => [
                    'showitem' => '
                    --palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
                    --palette--;;filePalette'
                ],
                \TYPO3\CMS\Core\Resource\File::FILETYPE_APPLICATION => [
                    'showitem' => '
                    --palette--;LLL:EXT:lang/locallang_tca.xlf:sys_file_reference.imageoverlayPalette;imageoverlayPalette,
                    --palette--;;filePalette'
                ]
            ],
            'maxitems' => 1
        ],
        $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']
    ),
],
Copied!

File upload request

@todo - write docs

@todo - write docs: request with multiple files (ObjectStorage with FileReference)

File upload response

@todo - write docs

Save reference to new file

{
   "photo": {
      "uidLocal": 15,
   }
}
Copied!

If you would like to save any other data inside file reference it is needed to extend TYPO3\CMS\Extbase\Domain\Model\FileReference class.

{
   "falMedia": [
      {
         "uidLocal": 15,
         "showinpreview": 1
      },
      {
         "uidLocal": 16,
         "showinpreview": 0
      }
   ]
}
Copied!

@todo - write docs information about handling custom class of file reference (which extends standard extbase FileReference)

@todo - write docs

Removing single file reference

To remove existing file reference it is needed to send value 0. Because of extbase and JMS serializer limitations sending `NULL` will not remove existing file reference. "Extbase limitation" means that existing file references are not removed when persisting empty value instead of file reference object (column for property in entity is cleared but file reference is kept). "JMS serializer limitations" means that JMS does not allow to apply custom subscribers and handlers when NULL is sent.

declare(strict_types=1);
namespace Vendor\User\Domain\Model;

use SourceBroker\T3api\Annotation as T3api;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;

/**
 * @T3api\ApiResource (
 *     itemOperations={
 *          "patch"={
 *              "path"="/users/{id}",
 *              "method"="PATCH",
 *          }
 *     },
 * )
 */

class User extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
{
    /**
     * @var \TYPO3\CMS\Extbase\Domain\Model\FileReference
     */
    protected $avatar = null;

    public function getAvatar(): ?FileReference
    {
        return $this->avatar;
    }

    public function setAvatar(?FileReference $avatar): void
    {
        $this->avatar = $avatar;
    }
}
Copied!

To remove file from model definition above we need to send a JSON payload as follows to PATCH /users/X endpoint to remove image.

{
   "avatar": 0
}
Copied!

Removing collection file reference

To remove collection file reference it is needed to send array with new elements. If array is empty - all elements will be removed.

declare(strict_types=1);
namespace Vendor\News\Domain\Model;

use SourceBroker\T3api\Annotation as T3api;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

/**
 * @T3api\ApiResource (
 *     itemOperations={
 *          "patch"={
 *              "path"="/news/{id}",
 *              "method"="PATCH",
 *          },
 *     }
 * )
 */
class News extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
{
    /**
     * @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\TYPO3\CMS\Extbase\Domain\Model\FileReference>
     */
    protected $falMedia;

    public function __construct()
    {
        $this->falMedia = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage();
    }

    /**
     * @return \TYPO3\CMS\Extbase\Persistence\ObjectStorage
     */
    public function getFalMedia(): ObjectStorage
    {
        return $this->falMedia;
    }

    /**
     * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage $falMedia
     */
    public function setFalMedia(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $falMedia): void
    {
        $this->falMedia = $falMedia;
    }

    /***
     * @param \TYPO3\CMS\Extbase\Domain\Model\FileReference $falMedia
     */
    public function addFalMedia(\TYPO3\CMS\Extbase\Domain\Model\FileReference $falMedia): void
    {
        $this->falMedia->attach($falMedia);
    }
}
Copied!

To remove files from model definition above we need to send a JSON payload as follows to PATCH /news/X endpoint to remove image.

{
   "falMedia": []
}
Copied!

Customizing name of uploaded file

Keeping name of the file uploaded by client sometimes may not be wanted - as developers we need to protect website against some joke or vulgar URLs which does not return 404 errors. In such cases very useful will be processing of the name of uploaded file. It is possible to achieve that using configuration option filenameMask.

declare(strict_types=1);
namespace Vendor\Users\Domain\Model;

use SourceBroker\T3api\Annotation as T3api;

/**
 * @T3api\ApiResource (
 *     collectionOperations={
 *          "post"={
 *              "path"="/files",
 *              "method"="POST",
 *          },
 *     },
 *     attributes={
 *          "upload"={
 *              "folder"="1:/user_upload/",
 *              "allowedFileExtensions"={"jpg", "jpeg", "png"},
 *              "conflictMode"=DuplicationBehavior::RENAME,
 *              "filenameMask"="static-prefix-[filenameHash][extensionWithDot]",
 *          }
 *     }
 * )
 */
class File extends \TYPO3\CMS\Extbase\Domain\Model\File
{
}
Copied!

filenameMask supports few "magic" strings:

  • [filename] - File name without extension.
  • [extension] - Extension.
  • [extensionWithDot] - Extension prefixed by dot.
  • [contentHash] - Hash generated from file content.
  • [filenameHash] - Hash generated from file name.

It is possible to customize hash algorithm used to generate contentHash and filenameHash strings. By default md5 is used, but inside contentHashAlgorithm and filenameHashAlgorithm settings you can easily change it to any hash method supported by PHP hash method.

declare(strict_types=1);
namespace Vendor\Users\Domain\Model;

use SourceBroker\T3api\Annotation as T3api;

/**
 * @T3api\ApiResource (
 *     collectionOperations={
 *          "post"={
 *              "path"="/files",
 *              "method"="POST",
 *          },
 *     },
 *     attributes={
 *          "upload"={
 *              "folder"="1:/user_upload/",
 *              "allowedFileExtensions"={"jpg", "jpeg", "png"},
 *              "conflictMode"=DuplicationBehavior::RENAME,
 *              "filenameMask"="static-prefix-[filenameHash]-[contentHash][extensionWithDot]",
 *              "contentHashAlgorithm"="sha1",
 *              "filenameHashAlgorithm"="sha1",
 *          }
 *     }
 * )
 */
class File extends \TYPO3\CMS\Extbase\Domain\Model\File
{
}
Copied!

Handling Cascade Persistence

Create relation to existing entity

It is super easy to create relation to already existing entity. To do so you just need to pass uid of the related object as a value of property. It works in exactly same way for One-To-One and Many-To-One relations for whom target entity contains information about one related entity. As on example below - we want to assign User to Department assuming that User can belongs to only one Department.

{
   "department": 12
}
Copied!

In similar way it works also for One-To-Many and Many-To-Many relations for whom target entity contains a collection of related entities. In such case to create relation it is needed to pass array of identifiers. In example below we create/update news and assign it to categories with specified identifiers.

{
   "categories": [
      10,
      12,
      13
   ]
}
Copied!

Current user endpoint

Getting current user is a common issue in JSON API frameworks. It is not possible to use any build-in CRUD endpoint to fetch current user data because we need to make a request to get single item operation which requires identifier in the URL. But we do not know the identifier of current user before request is done.

T3api resolves that issue using custom operation handlers. Firstly let's register an endpoint as we do for any CRUD operation.

typo3conf/ext/users/Classes/Domain/Model/User.php
declare(strict_types=1);

namespace Vendor\Users\Domain\Model;

use SourceBroker\T3api\Annotation as T3api;

/**
 * @T3api\ApiResource (
 *     itemOperations={
 *          "get_current"={
 *              "path"="/users/current",
 *          },
 *     }
 * )
 */
class User extends \TYPO3\CMS\Extbase\Domain\Model\FrontendUser
{
}
Copied!

Now let's register our operation handler inside ext_localconf.php and create a handler class. Here you can read more about implementing your own operation handlers.

typo3conf/ext/users/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['operationHandlers'][\Vendor\Users\OperationHandler\GetCurrentUserOperationHandler::class] = 500;
Copied!
typo3conf/ext/users/Classes/OperationHandler/GetCurrentUserOperationHandler.php
declare(strict_types=1);

namespace Vendor\Users\OperationHandler;

use Vendor\Users\Domain\Model\User;
use Psr\Http\Message\ResponseInterface;
use SourceBroker\T3api\Domain\Model\OperationInterface;
use SourceBroker\T3api\OperationHandler\ItemGetOperationHandler;
use Symfony\Component\HttpFoundation\Request;
use TYPO3\CMS\Extbase\DomainObject\AbstractDomainObject;

class GetCurrentUserOperationHandler extends ItemGetOperationHandler
{
    public static function supports(OperationInterface $operation, Request $request): bool
    {
        return $operation->getApiResource()->getEntity() === User::class && $operation->getKey() === 'get_current';
    }

    public function handle(
        OperationInterface $operation,
        Request $request,
        array $route,
        ?ResponseInterface &$response
    ): AbstractDomainObject {
        if (empty($GLOBALS['TSFE']->fe_user->user['uid'])) {
            throw new \RuntimeException('Unknown current user ID. Are you logged in?', 1592570206680);
        }

        $route['id'] = $GLOBALS['TSFE']->fe_user->user['uid'];

        return parent::handle($operation, $request, $route, $response);
    }
}
Copied!

And that's it! Request to URL /users/current will return current user data depending on properties, getters and serialization configuration for Vendor\Users\Domain\Model\User. Current user will be authorized in exactly the same way as it is authorized on normal request to TYPO3 page (using cookie by default). If request is done by not logged in user exception Unknown current user ID. Are you logged in? will be thrown.

Current user assignment

If your API allows users to interact by making some push requests and persist data it is very likely you want to assign some records to current frontend user.

Let's say that our frontend users can vote in the poll and we want to persist user whom voted to know the answer and protect against double voting. It means we want to assign voting user to Vote.user property. We assume that voting user is the one who is doing the request to API.

To achieve the same effect you could just send current user UID inside payload of the request (like {"user": 50}) but there is one huge difference in these approaches - if value comes from request payload you need to ensure it is not faked.

It is much easier just to add @T3api\Serializer\Type\CurrentFeUser annotation to property where current user should be assigned to. It is done already on API side so you do not need to protect it in any special way.

Annotation @T3api\Serializer\Type\CurrentFeUser accepts one parameter which is the name of the class which represents frontend user (in our example it is: Vendor\Poll\Domain\Model\User).

typo3conf/ext/poll/Classes/Domain/Model/Vote.php
declare(strict_types=1);

namespace Vendor\Poll\Domain\Model;

use SourceBroker\T3api\Annotation as T3api;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

/**
 * @T3api\ApiResource (
 *     collectionOperations={
 *          "post"={
 *              "method"="POST",
 *              "path"="/votes",
 *              "security"="frontend.user.isLoggedIn",
 *          }
 *     }
 * )
 */
class Vote extends AbstractEntity
{
   // ...

   /**
    * @var User
    * @T3api\Serializer\Type\CurrentFeUser(User::class)
    */
   protected $user;

   // ...
}
Copied!

Cross-Origin Resource Sharing (CORS)

If you are facing issues while requesting API from the browser and errors in the console looks similar to the one below, you need to setup CORS policy for your API.

We are not going here to explain what the CORS is. There is plenty of websites explaining it and official specification which you should definitely check before configuring CORS for your website. In documentation below you will find only explanation how to configure CORS in t3api.

CORS configuration in t3api is based and (almost) fully compatible with well known Symfony bundle nelmio/cors-bundle.

In code below there is a list of all supported configuration options and their default values. If you would like to change these values to custom ones, you should use ext_localconf.php file of your extension (typo3conf/ext/my_custom_ext/ext_localconf.php).

$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['cors']['allowCredentials'] = false;
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['cors']['allowOrigin'] = [];
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['cors']['allowHeaders'] = [];
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['cors']['allowMethods'] = [];
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['cors']['exposeHeaders'] = [];
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['cors']['maxAge'] = 0;
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['cors']['originRegex'] = false;
Copied!
  • allowCredentials (boolean) - when set to true adds response header Access-Control-Allow-Credentials with value true.
  • allowOrigin (array or string) - can be set to * to accept any value. Can be an array with string values of allowed origins (e.g. ['http://www.example.com', 'https://www.example.com']) or regular expressions when originRegex is set to true (e.g. ['http://.*\.example\.com', 'https://.*\.example\.com']).
  • allowHeaders (array or string) - can be set to * to accept any value or an array of strings with allowed headers (e.g. ['Content-Type']).
  • allowMethods (array) - array of strings with HTTP methods (e.g. ['GET', 'POST', 'PUT']).
  • exposeHeaders (array) - controls the value of Access-Control-Expose-Headers.
  • maxAge (int) - controls the value of Access-Control-Max-Age.
  • originRegex (boolean) - indicates if values from allowOrigin should be treated as regular expression.

Changelog

3.0.0

  • [!!!] Changes signal slots into PSR-14 events [issue]
  • Protect against "&cHash empty" error when cacheHash.enforceValidation is set to true [issue]
  • Add testing instance for TYPO3 12, remove testing instance for TYPO3 10. Change PHP to 8.1 for testing instances.
  • [!!!] Drop TYPO3 10, TYPO3 11 on dependencies. Update dependencies to TYPO3 12.
  • Move changing language request set in header X-Locale to earlier stage, before "typo3/cms-frontend/tsfe". Add support for setting language of api request by standard language prefix instead of header X-Locale. [commit]
  • Prevent FileReferenceHandler and ImageHandler from throwing error on problems with processing, missing file etc. [commit]
  • Add crop support for ImageHandler [commit]
  • Add essential meta information to the OpenAPI spec [commit]
  • Do not convert empty string to absolute url [commit]
  • Refactor link generation. Replace getTypoLink_URL with linkFactory. [commit]
  • Add support for graceful exception handling for serialization process in TYPO3 [commit]
  • Add optional @type attribute for resource. As this is rarely used, and can influence size of response, it must be activated by adding a subscriber to serializerSubscribers array. [commit]

2.0.3

  • Extend dependency for symfony/cache to prevent t3api from downgrading symfony/cache in TYPO3 11 environments [commit]

2.0.2

  • PHP 8.1 fix warning about undefined array key "cacheCmd" [commit]
  • Add PHP 8.1 to CI matrix. [commit]
  • Update local installer [commit]

2.0.1

  • Changes signal slot parameters structure to indexed array instead of associative for slots afterProcessOperation and afterDeserializeOperation [commit]
  • Excludes property from serialization to avoid an error on serialize process [commit]

2.0.0

  • Move from travis to github actions [commit]
  • Make php-cs-fixer config form TYPO3/coding-standards. Update .editorconfig from TYPO3/coding-standards [commit]
  • Refactor ddev for test instances. [commit]
  • Adds support for TYPO3 v11 [issue]
  • Makes it possible to include custom paths as API resources [issue]
  • Adds support for API resources inside subdirectories of Classes/Domain/Model/ [issue]
  • [!!!] Drops support for TYPO3 < 10.4
  • [!!!] Changes annotation ReadOnly to ReadOnlyProperty [commit 1; commit 2]
  • [!!!] Changes signal slot \SourceBroker\T3api\Serializer\ContextBuilder\ContextBuilderInterface::SIGNAL_CUSTOMIZE_SERIALIZER_CONTEXT_ATTRIBUTES parameters structure to indexed array instead of associative [commit]

1.2.3

  • Handle edge cases for forcing absolute url. [issue; aeb7081]
  • Handle case when file is not available on system file. [issue; 2765ccf; 11be8dd; 53af4ab]
  • Handle edge cases for combining url and host in UrlService::forceAbsoluteUrl [099df00]

1.2.2

  • Fixes composer dependencies conflict regarding doctrine/cache [pull request; discussion]
  • Unifies expression language context for all usages [84cb085; 85c2455]
  • Differentiates cache for API resource according to site's and API base path [f54843c]
  • Adds possibility to set different serializer context attributes for serialization and deserialization [bbf104d]

1.2.0

  • Implements support for CORS configuration [pull request] [doc]
  • Implements request processors and moves language handling into processor [2f9a75e]
  • Base development version on DDEV
  • Implements easy and safe way to assign current frontend user to records [56a38d7] [doc]
  • Adds support for RTE on serialization [3c09f5]
  • Adds support for password hashing on serialization [78dcbef]
  • Fixes current site resolver
  • Extends backend module by site selector [3389936]

Common Issues

"&cHash empty" issue

Request parameters could not be validated (&cHash empty)

If you are receiving such TYPO3's error when trying to access endpoints potential reasons could be:

  • t3api version lower than 2.1.
  • TYPO3 global configuration or one of the installed extensions is resetting cacheHash.excludedParameters value. Check if t3apiResource is inside collection of excluded parameters defined in $GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters']. Such value is added by t3api core so you should use merge excludedParameters instead of override.

Introduction

During development of t3api, you will be dealing with two different TYPO3 installations, which serve different purposes.

Unit and functional testing installation

  • It is a minimal TYPO3 installation, which is used for running unit and functional tests.
  • Files are under directory .Build
  • You can install it manually with ddev composer i
  • It is installed automatically while using command ddev ci [T3_VERSION|all] [PHP_VERSION] [lowest]
  • There can be only one TYPO3 version installed at one time.

Integration and manual testing installation

  • This installation is a full TYPO3 accessible under https://[TYPO3_VERSION].t3api.ddev.site and it is used for testing REST API endpoints using Postman tests. It is also used for manual testing.
  • Files are under directory /.test/[TYPO3_VERSION]
  • You can install it manually using command ddev install [T3_VERSION|all]
  • It is installed automatically while using command ddev ci [T3_VERSION|all] [PHP_VERSION] [lowest]
  • There can be multiple TYPO3 versions integrations installations at one time each under different url.

Commands list

Below is a list of commands that you can use in the development process of ext:t3api.

ddev cache-flush

This command will run typo3 flush:cache for all active TYPO3 integration testing instances.

ddev ci [T3_VERSION|all] [PHP_VERSION] [lowest]

If called without any arguments, this command will run multiple commands for testing and linting.

  • ddev composer ci which will run:

    • ci:composer:normalize
    • ci:yaml:lint
    • ci:json:lint
    • ci:php:lint
    • ci:php:cs-fixer
    • ci:php:stan
    • ci:tests:unit
    • ci:tests:functional
    • ci:tests:postman
  • ddev docs ci which will check if docs can be rendered without problems.

If called with arguments like ddev ci [T3_VERSION] [PHP_VERSION] [lowest] this command will restart ddev, set required PHP, install required version of TYPO3 with optional composer option --prefer-lowest and then run ddev ci on it.

When the argument is only all, then this command will run matrix tests for all supported TYPO3, PHP, COMPOSER. The same command is run for each TYPO3, PHP, COMPOSER combination in matrix tests on github actions.

Examples:

ddev ci
ddev ci 12 8.3 lowest
ddev ci all
Copied!

You can use this command to fast switch with development to whatever TYPO3/PHP version you like because after ci tests it does not return to previous TYPO3/PHP.

ddev data [export|import] [T3_VERSION]

This command will export or import database/files of specific testing instance into folder .ddev/test/impexp/. It uses the TYPO3 core extension impexp. These exported files are later used by command ddev install [T3_VERSION]. When you do new features or write postman tests this is very likely that you will need to do changes to database/files and commit this state to git. This command is just for that reason.

All TYPO3 testing instances are using the same exported files. This means that there is no much difference if you make ddev data export 13 or ddev data export 12. Important is only that you do export from the testing instance you actually modified.

ddev docs [watch|build|ci]

build
will build docs into the folder Documentation-GENERATED-temp You can browse it for example on PHPStorm "open in browser" option.
watch
this command will run hot reload for documentation.
ci
this command will test if docs are able to render correctly. Used in ddev ci [T3_VERSION|all] [PHP_VERSION] [lowest] command.

ddev fix

This command will run all possible automate fixes. For now it makes PHP CS Fixer changes and composer normalisation.

ddev install [T3_VERSION|all]

This command will install specific (or all) integration testing instances of TYPO3 in folder /.test/. List of supported TYPO3 versions is defined in file .ddev/docker-compose.web.yaml in variable TYPO3_VERSIONS. Integration testing instances are available under url https://[T3_VERSION].t3api.ddev.site. You can also open https://t3api.ddev.site to see list of all supported testing instances for given t3api version.

Example:

ddev install
ddev install 12
ddev install all
Copied!

ddev next [major|minor|patch]

This command will help to tag next release of extension.

It does several checks. Amongst other it:

  1. Checks if you are on default branch.
  2. Checks if you are are up to date with remote default branch.
  3. Checks if you do not have any not pushed changes to remote.
  4. Checks validity of last tag.
  5. Increase version in few files:

    • /ext_emconf.php
    • /Documentation/guides.xml

After all checks it outputs a command you need to run to push changes and tag to git.

Example output:

git add Documentation/guides.xml ext_emconf.php && git commit -m 'Tag version 3.1.0' && git tag -a '3.1.0' -m 'Version 3.1.0' -s && git push origin main --tags
Copied!

Bugs Fixing

  1. Fork

    Make your fork of t3api on github https://github.com/sourcebroker/t3api

  2. Clone

    Clone your forked repo locally.

  3. First run of ddev

    Go inside cloned repo and run: ddev restart

  4. Install and make init test

    Inside cloned repo run: ddev ci 12

    This will install project locally (for TYPO3 12) and make tests to check if your installed version is working well before you start to modify it.

  5. Branch

    Create branch in your repo.

  6. Fix bug

    When you modify code of t3api then a symlinked version at .test/[T3_VERSION]/src/t3api is also modified and you can access your modified version of t3api at fully working TYPO3 https://[T3_VERSION].t3api.ddev.site

    Open https://t3api.ddev.site to get overview on user/password to backend.

    Look at .test/[T3_VERSION]/src/. Except t3api you have there two extensions that can be helpful for testing.

    1. First extension is site, which is regular TYPO3 local mods extension.
    2. Second extension is t3apinews, which is extension that expose extension news models and is supposed to have only mods for news. To test it open:

    Sometimes you may want to flush cache for the TYPO3 located at .test/[T3_VERSION]/. Of course you can do ddev exec "cd .test/[T3_VERSION] && ./vendor/bin/typo3 cache:flush". But you can also use special ddev command ddev cache-flush

  7. Documentation

    Run ddev docs to run documentation in watch mode. Browser should open automatically. You can modify files at Documentation and watch in real time how the docs will looks like. Look at https://docs.typo3.org/m/typo3/docs-how-to-document/main/en-us/Index.html for info about formatting possibilities.

  8. Automated fixes and tests

    If you think you are ready with your bug then:

    • run automated fixes: ddev fix
    • run automated test on current TYPO3: ddev ci

    If all is ok then you can run full matrix test as it will be done on github.

    • ddev ci all
  9. Commit, push and make PR

    https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request

Sitemap