somebdyelse / typo3-content-models
TYPO3 Content Models
Package info
github.com/SomeBdyElse/typo3-content-models
Type:typo3-cms-extension
pkg:composer/somebdyelse/typo3-content-models
Requires
- nette/php-generator: ^4.2.0
- typo3/cms-core: ^v13.4.0 | ^14.0
- typo3/cms-extbase: ^v13.4.0 | ^14.0
- typo3/cms-frontend: ^v13.4.0 | ^14.0
Requires (Dev)
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^11.5 || ^12.0 || ^13.0
- typo3/cms-felogin: ^13.4 || ^14.0
- typo3/cms-fluid-styled-content: ^13.4 || ^14.0
- typo3/cms-form: ^13.4 || ^14.0
- typo3/cms-indexed-search: ^13.4 || ^14.0
- typo3/cms-install: ^13.4 || ^14.0
- typo3/cms-seo: ^13.4 || ^14.0
- typo3/cms-styleguide: ^13.4 || ^14.0
- typo3/coding-standards: ^0.9.0
- typo3/testing-framework: ^9.0
This package is auto-updated.
Last update: 2026-08-31 07:24:40 UTC
README
TYPO3 Content Models generates typed PHP content model classes from TYPO3's TCA schema. They can be used to render frontend content. Instead of passing raw TYPO3\CMS\Core\Domain\Record objects through templates and processors, records can be converted into classes with typed getters, enum-backed select fields, typed relation collections, file collections, links, and date values. This makes sense if you want to pre-process data before passing it to the templates, resolve links and hydrate your own view models.
What it does
Generator
- Generates content model classes for TCA-managed tables and record types.
- Provides eager and lazy DTO-style generators.
- Generates backed enums for valid static
selectSinglefields
Renderer
- Registers generated or custom models through the
#[ContentModel]PHP attribute. - Converts frontend content records to the matching model during rendering.
- Falls back to
GenericContentModelwhen no matching model class exists. - Wraps relations in
LazyContentModelCollection, so related records can be accessed as content models too.
Requirements
- TYPO3
^13.4or^14.0 - Composer mode installation
Installation
composer require somebdyelse/typo3-content-models
Generating models
Create a project configuration file at:
config/system/content_models.yaml
It will extend the default configuration stored at EXT:typo3-content-models/typo3-content-models-demo/cms/packages/content_models/Resources/Private/Configuration/default_configuration.yaml
Example:
$schema: ../../vendor/somebdyelse/typo3-content-models/Resources/Private/Configuration/configuration.schema.yaml targetPhpNamespace: Vendor\Sitepackage\ContentModels targetDirectory: EXT:sitepackage/Classes/ContentModels overrides: generator: SomeBdyElse\Typo3ContentModels\Generation\Generators\LazyDtoGenerator # Whitelist the tables that we would like to have models for generate: false tables: pages: generate: true tt_content: generate: true sys_category: generate: true
targetPhpNamespace defines the root PHP namespace used in the generated class declarations. targetDirectory defines the TYPO3 EXT: path where those PHP files are written. Both values should point to the same logical location in your site package.
Run the generator:
vendor/bin/typo3 content-models:generate_models
The command reads TYPO3's TCA schema, applies the configuration, and writes PHP classes to targetDirectory with namespaces below targetPhpNamespace.
Generated classes must be covered by Symfony service discovery with autoconfiguration enabled so the #[ContentModel] attribute can be turned into the internal content_models.content_model service tag during container compilation. The compiler pass only uses those tagged definitions to build the content model registry and removes the model service definitions afterwards. Content model instances are created through fromRecord(), not shared as Symfony services.
When using the default target inside this content_models extension, this is already handled by the extension's service configuration. If you generate models into a site package or another extension, make sure that target namespace is covered by a service resource with autoconfiguration enabled:
services: Vendor\Sitepackage\: resource: '../Classes/*' autowire: true autoconfigure: true
After generating models, flush/warm TYPO3's caches so the container picks up new or changed classes.
Using generated models
In a PAGEVIEW context, use page-model to convert the current page record to a content model. Use content-models after TYPO3's page-content processor to replace fetched frontend content records with content model instances:
page = PAGE
page {
10 = PAGEVIEW
10 {
paths {
100 = EXT:sitepackage/Resources/Private/PageView/
}
dataProcessing {
10 = page-model
20 = page-content
30 = content-models
}
}
}
pageModel contains the content model for the current page. content contains TYPO3's content areas with their records converted to content models.
Both processors have source and as arguments with defaults:
| Processor | Default source |
Default as |
|---|---|---|
page-model |
page |
pageModel |
content-models |
content |
same as source |
Use source and as if your PAGEVIEW setup uses different variable names:
dataProcessing {
10 = page-model
10.source = page
10.as = currentPage
20 = page-content
20.as = rawContent
30 = content-models
30.source = rawContent
30.as = pageContent
}
You can add your own data processors after dataProcessing.30 to convert the database based content models further to view models.
Configuration
Configuration is loaded from the extension default file and then overlaid by:
config/system/content_models.yaml
Supported top-level options:
| Option | Required | Description |
|---|---|---|
targetPhpNamespace |
yes | Root PHP namespace for generated models. Each table gets a namespace segment below this root. |
targetDirectory |
yes | EXT: path where generated PHP files are written. Each table gets a subdirectory below this path. |
commonCodeGenerator |
no | Class name implementing CommonCodeGeneratorInterface, used for additional code generated after all models. |
overrides |
no | Global, table, type, and field-level generation options. |
Overrides
Overrides are merged from broad to specific:
overridesoverrides.tables.<table>overrides.tables.<table>.types.<type>
Available model override options:
| Option | Description |
|---|---|
generate |
Enables or disables model generation. Defaults to true unless changed by configuration. |
className |
Overrides the generated class name for the current table or type. |
generator |
Generator class implementing ModelGeneratorInterface. |
fields |
Field-specific generation hints. |
Example:
targetPhpNamespace: Vendor\Sitepackage\ContentModel targetDirectory: EXT:sitepackage/Classes/ContentModel overrides: generator: SomeBdyElse\Typo3ContentModels\Generation\Generators\EagerDtoGenerator generate: false tables: pages: generate: true types: 1: className: DefaultPage 3: className: ShortcutPage generate: false tt_content: generate: true generator: SomeBdyElse\Typo3ContentModels\Generation\Generators\LazyDtoGenerator
Relation target types
Relation fields can be narrowed when a relation points to a table with multiple record types. This improves the PHPDoc type of LazyContentModelCollection and runtime validation of related model instances.
overrides: tables: tt_content: types: menu_pages: fields: pages: relationTargetTypes: pages: [1]
Without this hint, the relation generator uses all known target table subtypes. If no model can be resolved for a target, the relation falls back to TYPO3\CMS\Core\Domain\Record.
Built-in generators
EagerDtoGenerator
Creates constructor-promoted public readonly properties. Field values are read from the Record once in fromRecord().
Use this when you want simple immutable data transfer objects and prefer all field conversion to happen immediately.
LazyDtoGenerator
Stores the original Record and creates typed getters. Field values are read when the getter is called.
Use this when you want to avoid resolving every field and relation up front. This is often the better default for records with expensive relation or file fields.
Generated field types
The built-in field generation handles common TYPO3 field types:
| TYPO3 field | Generated PHP type |
|---|---|
| File fields | TYPO3\CMS\Core\Resource\Collection\LazyFileReferenceCollection |
| Link fields | TYPO3\CMS\Core\LinkHandling\TypolinkParameter |
| Date/time fields | ?\DateTimeImmutable |
| Relation fields | LazyContentModelCollection<T> |
Static selectSingle fields with valid scalar items |
PHP backed enum |
| Other static select fields | array |
| Folder, FlexForm, and JSON fields | TYPO3\CMS\Core\Domain\RecordPropertyClosure |
| Fallback fields | Database-derived scalar type where possible, otherwise mixed |
System fields and fields with TCA type none are skipped.
Custom field generation handlers
Custom field generation handlers can be registered by implementing:
SomeBdyElse\Typo3ContentModels\Generation\FieldGeneration\HandlerInterface
Add the AsFieldGenerationHandler attribute to make the handler available during model generation:
use SomeBdyElse\Typo3ContentModels\Generation\FieldGeneration\AsFieldGenerationHandler; use SomeBdyElse\Typo3ContentModels\Generation\FieldGeneration\HandlerInterface; #[AsFieldGenerationHandler( identifier: 'vendor/sitepackage/my-field-handler', before: ['somebdyelse/typo3-content-models/fallback'], )] final readonly class MyFieldHandler implements HandlerInterface { // ... }
Extending model generation
Create a generator that implements:
SomeBdyElse\Typo3ContentModels\Generation\ModelGeneratorInterface
Example:
namespace Vendor\Sitepackage\ContentModelGeneration; use SomeBdyElse\Typo3ContentModels\Generation\GeneratedModel; use SomeBdyElse\Typo3ContentModels\Generation\ModelGeneratorInterface; final readonly class MyModelGenerator implements ModelGeneratorInterface { public function generateModel(string $table, ?string $type): GeneratedModel { // Generate one PHP class for the given table/type and return metadata // about the generated class. throw new \LogicException('Implement model generation for your project.'); } }
Register the class as a service in your extension and make it public, because the generator factory resolves configured generators by class name:
services: Vendor\Sitepackage\ContentModelGeneration\MyModelGenerator: autowire: true public: true
Then select it globally, per table, or per record type:
overrides: generator: Vendor\Sitepackage\ContentModelGeneration\MyModelGenerator tables: tt_content: types: text: generator: Vendor\Sitepackage\ContentModelGeneration\TextContentGenerator
A generator can reuse the extension services used by the built-in generators:
SomeBdyElse\Typo3ContentModels\Generation\Configuration\ConfigurationSomeBdyElse\Typo3ContentModels\Generation\NamingHelperSomeBdyElse\Typo3ContentModels\Generation\FieldGeneration\FieldGeneratorTYPO3\CMS\Core\Schema\TcaSchemaFactory
Generated classes should usually:
- implement
ContentModelInterface - define
#[ContentModel(table: ..., type: ...)] - provide
public static function fromRecord(Record $record): self
The default ContentModelFactory relies on fromRecord() when it converts TYPO3 records to models.
Generating shared code
If you need to generate files after all models are known, implement:
SomeBdyElse\Typo3ContentModels\Generation\CommonCodeGeneratorInterface
Example:
namespace Vendor\Sitepackage\ContentModelGeneration; use SomeBdyElse\Typo3ContentModels\Generation\CommonCodeGeneratorInterface; use SomeBdyElse\Typo3ContentModels\Generation\GeneratedModel; final readonly class MyCommonCodeGenerator implements CommonCodeGeneratorInterface { /** * @param list<GeneratedModel> $generatedModels */ public function generateCommonCode(array $generatedModels = []): void { // Generate registries, union types, helper classes, documentation, etc. } }
Configure it at the top level:
commonCodeGenerator: Vendor\Sitepackage\ContentModelGeneration\MyCommonCodeGenerator
Custom content models without generation
You can also create a model manually and let the runtime registry pick it up:
namespace Vendor\Sitepackage\ContentModel\Content; use SomeBdyElse\Typo3ContentModels\Contract\ContentModel; use SomeBdyElse\Typo3ContentModels\Contract\ContentModelInterface; use TYPO3\CMS\Core\Domain\Record; #[ContentModel(table: 'tt_content', type: 'my_custom_element')] final readonly class MyCustomElement implements ContentModelInterface { public function __construct( public int $uid, public string $headline, ) { } public static function fromRecord(Record $record): self { return new self( uid: $record->get('uid'), headline: (string)$record->get('header'), ); } }
Make sure the class is covered by Symfony service discovery with autoconfiguration enabled, or add the content_models.content_model tag manually. The extension's compiler pass adds every tagged content model class to the runtime registry, then removes the content model service definition so model instances are created through fromRecord() instead of being shared container services.
Development
Run static analysis from the extension directory:
vendor/bin/phpstan analyse -c Build/phpstan/phpstan.neon
When working in the demo project, run the command from the TYPO3 project root or through the configured Docker container so TYPO3 can load the project configuration and TCA.