Resource Definition
Resources are defined as PHP files placed in any active extension's
Configuration/ directory. No manual registration is needed —
the extension auto-discovers all *. files from every active package's
Configuration/ directory at boot time and caches the result.
Each file returns a PHP array with the resource configuration.
To modify a resource defined by a third-party package, place an override file
in Configuration/ — see Overriding Third-Party Resource Configs.
General section
The general key defines the basic resource properties:
return [
'general' => [
'table' => 'tx_myext_domain_model_article',
'resourceName' => 'articles',
'resourceType' => 'Article',
// operations defaults to ['list', 'show'] — omit for read-only resources
'itemsPerPage' => 20,
'storagePid' => 1,
],
];
| Key | Description |
|---|---|
table | TYPO3 database table name. |
resource | URL slug used in /_. |
resource | JSON-LD @type value. |
type | Set to 'userinfo' to create a userinfo endpoint. |
operations | Array of enabled operations: list, show, create, update,
delete. Defaults to ['list', 'show'] when omitted. |
items | Default page size for list operations (overrides the global site setting). |
max | Upper limit for items; when set, the requested page size is
clamped to this value. No limit when omitted. |
storage | Page ID for newly created records, or a {$site.
placeholder resolved per site (see Storage pages per site). Also
constrains reads to this page unless read is set. When
omitted, reads are not pid-constrained and writes target page 0. Any
other value fails at boot. |
read | Widens the read-side pid constraint independently of storage.
Accepts an array ([42, 17, 5]) or comma-separated list
('42,17,5') — reads then use pid IN — or the sentinel
'*' to read from all pages regardless of the write target. Entries
may be {$site. placeholders. When omitted, reads fall back
to storage. This is the complete read set: list the write page
explicitly if it should also be readable. |
record | Scopes the resource to record types of the table's TCA ctrl.
field (CType, doktype, …). Accepts an array
(['textmedia', 'header']), a comma-separated list or '*' for
every type in $TCA. In default mode each record then
exposes only its type's columns; the options form
(['include' => …, 'schema' => 'table']) keeps all table columns,
and 'split' => true adds one resource per type (see
One resource per record type). When omitted, the resource serves records of every type with all
columns — see Record types. |
write | Write execution strategy. acting_ (default) — DataHandler runs
under the authenticated user identity, preserving the actor for audit
purposes. system_ — uses a synthetic backend-admin context,
bypassing all TYPO3 ACL boundaries. Only enable for fully trusted,
internal-only APIs. |
group | OpenAPI tag grouping this resource's operations in Swagger UI. A string
('Editorial') or an array with a name and optional
description. Defaults to resource — see
Endpoint grouping (OpenAPI tags). |
Write mode
By default, write operations (create, update, delete) run under the
acting-user context — TYPO3's DataHandler preserves the caller's identity
for audit purposes. Set write to system_ to bypass TYPO3 ACL
checks entirely. Only use this for trusted, back-channel APIs.
'general' => [
'table' => 'tx_myext_domain_model_article',
'resourceName' => 'articles',
'resourceType' => 'Article',
'operations' => ['create', 'update', 'delete'],
'writeMode' => 'system_admin', // default: 'acting_user'
],
Warning
system_ mode bypasses all TYPO3 access-control restrictions on
writes. Never use it for public-facing endpoints.
Storage pages (read vs. write)
storage is the single write target — newly created records land
there. By default it also constrains reads to that one page. To read from
several pages while still writing into one, add read:
'general' => [
'table' => 'tx_myext_domain_model_article',
'resourceName' => 'articles',
'resourceType' => 'Article',
'operations' => ['list', 'show', 'create'],
'storagePid' => 42, // writes go here
'readStoragePids' => [42, 17, 5], // reads span these pages
],
Use the sentinel '*' to read from every page regardless of the write
target:
'storagePid' => 42, // writes go here
'readStoragePids' => '*', // reads ignore pid entirely
read is the complete read set — it does not implicitly
include storage. List the write page explicitly if it should be
readable too.
Storage pages per site
When one resource serves several sites that keep their records on their own
page, point storage and read at a site setting. The
{$…} placeholder is resolved from the settings of the site that receives
the request:
'general' => [
'table' => 'tx_myext_entry',
'resourceName' => 'entries',
'resourceType' => 'Entry',
'storagePid' => '{$myext.storagePid}',
'readStoragePids' => ['{$myext.storagePid}', '{$myext.sharedPid}'],
],
settings:
myext:
storagePid: 12
sharedPid: 3
The resolved pid scopes every operation: list and show read only the
site's pages, update and delete of another site's record answer
404, and create stores the record, its inline-created relations and
its file references on the site's page.
A placeholder fails closed. When the setting is missing, empty, boolean, negative or
not an integer, the request answers 500 (the message names the setting
when tca_ is on) and no query runs without the pid
constraint.
Response caching includes the site and its resolved read pages. Changing a storage-page setting cannot reuse a response from the previous scope, and an invalid setting fails even when a response was previously cached.
Record types
Tables with a TCA ctrl. field — tt_, pages.
— hold records of several types. record limits a resource to some of
them:
'general' => [
'table' => 'tt_content',
'resourceName' => 'content',
'resourceType' => 'Content',
'operations' => ['list', 'show', 'create', 'update'],
'recordTypes' => ['textmedia', 'header'],
],
The scope applies to every operation, not only to list: a record of
another type answers 404 on show, update and delete, exactly
like a record on another page. A filter on the type column cannot do that,
because filters only narrow collections.
On writes:
- A resource with one record type sets the type field on
createitself. A client may omit it; a different value is a422. - A resource with several record types requires the type field on
createand rejects values outside the list with422(INVALID_).RECORD_ TYPE updatecannot move a record to a type outside the list.
The OpenAPI schema describes the type field as an enum of the allowed
types. Unknown types, tables without ctrl. and foreign-field types
('type' => 'local_) fail at boot.
Note
Relations are not scoped: a column referencing records of the table
renders them regardless of record, as it does for
storage.
Recipe: pages
pages. works like CType. Most page types are not content for
an API: sysfolders (254), the recycler (255), spacers (199) and
backend user sections (6). Scope a pages resource to the types a frontend
renders:
'general' => [
'table' => 'pages',
'resourceName' => 'pages',
'resourceType' => 'Page',
'recordTypes' => [1, 3, 4], // standard, link, shortcut
],
Pages of other types answer 404, also on show by uid. Each page
exposes the fields of its type: a link page carries its URL (link, or
url before TYPO3 v14) resolved like any typolink field, and a shortcut
page carries shortcut_ and shortcut as a link to the target page.
Columns per record type
In default mode (no column declares groups), each record exposes only the
columns of its type's form — the fields in the type's showitem, with
palettes unpacked, in the same order. A header content element has no
bodytext or assets, a textmedia element does. System fields that
palettes carry (hidden, starttime, sys_, …) stay hidden
as everywhere else. The type field is always included, even for a type whose
form does not show it.
The type's columns apply as well. A required or max set
for one type is validated only for records of that type, and FlexForm and
field processing use the type's configuration.
- A collection over several types returns each item with its own columns.
- On writes, columns outside the target type's form are ignored — the type being created or switched to, otherwise the record's current type.
- The OpenAPI
Readschema of a multi-type resource is aoneover one schema per type (Of Content,Textmedia Read Content, …) with aHeader Read discriminatoron the type field. A single-type resource'sReadandWriteschemas describe that type. TheWriteschema of a multi-type resource lists the columns of all its types, becausePATCHbodies are partial and cannot be matched against a single type.
Declared columns keep their configuration but count only for the types
whose form contains them. Their TCA-derived validation follows the type's
columns too, while an explicit required or validator of the
same kind still takes precedence. Explicit mode is unaffected: declared columns are
the columns.
To scope a resource without changing its columns, use the options form with
'schema' => 'table':
'recordTypes' => [
'include' => ['textmedia', 'header'], // default: '*'
'schema' => 'table', // default: 'type'
],
One resource per record type
'split' => true expands the resource into one resource per included type,
for clients that work with one type at a time. Each generated resource is the
base config scoped to a single type, so it behaves like a hand-written
single-type resource: create sets the type, other types answer 404.
'general' => [
'table' => 'tt_content',
'resourceName' => 'content',
'resourceType' => 'Content',
'recordTypes' => [
'include' => ['textmedia', 'header'], // default: '*'
'split' => true,
'namePattern' => '{resourceName}-{type}', // default
'keepBase' => true, // default
],
],
'types' => [
'textmedia' => [
'security' => ['create' => AccessRole::FE_USER],
],
],
This registers content- (Content) and
content- (Content) next to content.
namesets the generatedPattern resource; theName resourceis its PascalCase form. A name must contain a letter and may contain letters, digits,Type -and_only, because it is the URL segment.keepdrops the multi-type base resource. Relations to the table that set noBase => false resourcethen link to the first generated resource, which serves only its own type. Keep the base, or set the relation column'sName resource, when other resources link to the table.Name typesholds per-type overrides, merged over the base config of that type's resource only. Nested arrays merge key by key, lists such asoperationsor the groups of an access role are replaced as a whole. An override may setgeneral.andresource Name general.as well, but notresource Type general.: a generated resource serves only its own type.record Types. include typeswithoutsplitfails at boot.- The expansion runs after the
Overrides/pass, so an override file that edits the base config reaches every generated resource. - Without a
group, each generated resource gets its own Swagger UI section, described with the type's label from the TCAitems. - A generated name that is already taken fails at boot.
Warning
Generated resources inherit the base security. Reads are public by
default, so splitting '*' on tt_ publishes one public
endpoint per content type. Restrict security in the base config or in
types, and add the generated names to the tca_
site setting if you use it.
Endpoint grouping (OpenAPI tags)
By default each resource gets its own Swagger UI section, named after its
resource. Set group to place several resources under a shared,
named section:
'general' => [
'table' => 'tx_myext_domain_model_article',
'resourceName' => 'articles',
'resourceType' => 'Article',
'group' => 'Editorial',
],
Use the array form to add a description shown above the section:
'group' => [
'name' => 'Editorial',
'description' => 'Editorial content endpoints',
],
Point several resources at the same name to merge them into one section.
Sections appear in resource-registration order; the first configured
description for a group wins. Omitting group falls back to
resource, so grouping is fully opt-in and backward compatible.
Minimal example (zero-config)
Three keys are all that is needed for a public read-only resource. operations
defaults to ['list', 'show'] and both are PUBLIC by default:
return [
'general' => [
'table' => 'tx_myext_domain_model_article',
'resourceName' => 'articles',
'resourceType' => 'Article',
],
];
To enable write operations, add them explicitly and configure security:
use MaikSchneider\TcaApi\Enum\AccessRole;
return [
'general' => [
'table' => 'tx_myext_domain_model_article',
'resourceName' => 'articles',
'resourceType' => 'Article',
'operations' => ['list', 'show', 'create', 'update', 'delete'],
'storagePid' => 1,
],
'security' => [
'create' => AccessRole::FE_USER,
'update' => AccessRole::FE_USER,
'delete' => AccessRole::BE_ADMIN,
],
];
Full example
use MaikSchneider\TcaApi\Enum\AccessRole;
use MaikSchneider\TcaApi\Filter\ExactFilter;
use MaikSchneider\TcaApi\Filter\MmFilter;
use MaikSchneider\TcaApi\Serializer\FileProcessing\FileProcessor;
use MaikSchneider\TcaApi\Serializer\Processing\TypoLinkProcessor;
return [
'general' => [
'table' => 'tx_myext_domain_model_article',
'resourceName' => 'articles',
'resourceType' => 'Article',
'operations' => ['list', 'show', 'create', 'update', 'delete'],
],
'columns' => [
'title' => [
'type' => 'string',
'groups' => ['list', 'show', 'create', 'update'],
'required' => true,
'validators' => [
['type' => 'maxLength', 'max' => 20],
['type' => 'minLength', 'min' => 3],
['type' => 'regex', 'pattern' => '/^[\w\s]+$/u'],
],
],
'color_id' => ['groups' => ['list', 'show', 'create', 'update']],
'categories' => ['groups' => ['list', 'show', 'create', 'update']],
'profile_photo' => ['groups' => ['list', 'show']],
'downloads' => [
'groups' => ['list', 'show'],
'processor' => FileProcessor::class,
],
'article_url' => [
'groups' => ['list', 'show'],
'processor' => TypoLinkProcessor::class,
],
],
'filters' => [
'title' => ExactFilter::class,
'color_id' => ExactFilter::class,
'categories' => MmFilter::class,
],
'order' => [
'allowed' => ['title', 'uid'],
'default' => ['uid' => 'asc'],
],
'security' => [
'list' => AccessRole::PUBLIC,
'show' => AccessRole::PUBLIC,
'create' => AccessRole::FE_USER,
'update' => AccessRole::FE_USER,
'delete' => AccessRole::BE_ADMIN,
],
];