9.4 KiB
Headless Integration Guide for TYPO3 13 Custom Plugins
This guide explains how to integrate a custom TYPO3 Extbase plugin with the TYPO3 Headless extension to provide JSON API output.
The Challenge
When integrating custom plugins with TYPO3 Headless, you face two main obstacles:
-
Lost Context: Headless creates new ContentObjectRenderer contexts when rendering JSON fields, causing the content element context to be lost. Standard methods like
$conf['parentObj']or$GLOBALS['TSFE']->cObj->datawill only contain page data, not the content element. -
Extbase Limitations: Extbase repositories don't work in UserFunc context because the full Extbase framework isn't bootstrapped during headless JSON rendering.
The Solution
Step 1: Override Headless TypoScript in Site Set
Create or modify your site set's setup.typoscript file to add custom fields to the headless JSON output:
File: packages/yourext/Configuration/Sets/YourSetName/setup.typoscript
# Override headless rendering for list plugins
tt_content.list.fields.content.fields.yourCustomField = USER
tt_content.list.fields.content.fields.yourCustomField {
userFunc = Vendor\YourExt\UserFunc\YourJsonRenderer->render
}
Why in site set? Site sets load AFTER headless TypoScript, allowing you to override the default configuration.
Step 2: Create a UserFunc Renderer
Create a UserFunc class that will render your plugin data as JSON:
File: packages/yourext/Classes/UserFunc/YourJsonRenderer.php
<?php
declare(strict_types=1);
namespace Vendor\YourExt\UserFunc;
use Doctrine\DBAL\ParameterType;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
class YourJsonRenderer
{
public function render(string $content, array $conf): string
{
// Step 1: Query tt_content to find the plugin configuration
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
->getQueryBuilderForTable('tt_content');
$contentElements = $queryBuilder
->select('*')
->from('tt_content')
->where(
$queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)),
$queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter('yourext_pluginname', ParameterType::STRING)),
$queryBuilder->expr()->eq('deleted', 0),
$queryBuilder->expr()->eq('hidden', 0)
)
->executeQuery()
->fetchAllAssociative();
if (empty($contentElements)) {
return json_encode([]);
}
$contentElement = $contentElements[0];
// Step 2: Parse FlexForm settings
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? '');
$settings = $flexFormData['settings'] ?? [];
// Step 3: Use direct database queries (NOT Extbase repositories)
$dataQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
->getQueryBuilderForTable('tx_yourext_domain_model_yourmodel');
$records = $dataQueryBuilder
->select('*')
->from('tx_yourext_domain_model_yourmodel')
->where(
$dataQueryBuilder->expr()->eq('deleted', 0),
$dataQueryBuilder->expr()->eq('hidden', 0)
// Add your filters based on $settings
)
->executeQuery()
->fetchAllAssociative();
// Step 4: Serialize to JSON
$data = [];
foreach ($records as $record) {
$data[] = [
'uid' => (int)$record['uid'],
'title' => $record['title'] ?? '',
// Add more fields as needed
];
}
return json_encode($data);
}
}
Step 3: Using FlexForm Settings
You can access any FlexForm field from your plugin configuration:
// Parse FlexForm
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? '');
$settings = $flexFormData['settings'] ?? [];
// Access specific settings
$categoryUids = array_filter(
array_map('intval', explode(',', (string)($settings['categories'] ?? '')))
);
$debugMode = (bool)($settings['debug'] ?? false);
// Use settings in your query
if (!empty($categoryUids)) {
// Add filtering based on categories
}
Conditional Debug Output
You can add debug information based on a FlexForm checkbox:
// At the end of your render method:
if ($debugMode) {
return json_encode([
'products' => $data,
'debug' => [
'pageId' => $pageId,
'categoryUids' => $categoryUids,
'recordCount' => count($data),
'settings' => $settings // Show all settings for troubleshooting
]
]);
}
// Return just the data array when debug is off
return json_encode($data);
This allows you to toggle debug information on/off directly in the backend without code changes.
Step 4: Important Notes
Use Doctrine ParameterType (TYPO3 13)
In TYPO3 13, always use Doctrine\DBAL\ParameterType instead of \PDO::PARAM_*:
use Doctrine\DBAL\ParameterType;
// Correct for TYPO3 13:
$queryBuilder->createNamedParameter($value, ParameterType::INTEGER)
$queryBuilder->createNamedParameter($value, ParameterType::STRING)
// Wrong (deprecated):
$queryBuilder->createNamedParameter($value, \PDO::PARAM_INT)
Why Direct Database Queries?
Don't use:
// This won't work in UserFunc context
$repository = GeneralUtility::makeInstance(YourRepository::class);
$records = $repository->findAll(); // Returns empty!
Use instead:
// Direct database query
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
->getQueryBuilderForTable('tx_yourext_domain_model_yourmodel');
$records = $queryBuilder->select('*')->from('tx_yourext_domain_model_yourmodel')->...
Step 5: Clear Cache
After making changes, always clear the TYPO3 cache:
./vendor/bin/typo3 cache:flush
Testing
Test your JSON output using curl and jq:
# Check if your field appears
curl -s "https://yourdomain.com/your-page" | jq '.content.colPos0[] | select(.type=="your_plugin") | .content'
# Check specific field
curl -s "https://yourdomain.com/your-page" | jq '.content.colPos0[] | select(.type=="your_plugin") | .content.yourCustomField'
# With debug mode enabled in backend, you'll see:
curl -s "https://yourdomain.com/your-page" | jq '.content.colPos0[] | select(.type=="your_plugin") | .content.yourCustomField.debug'
Example Output
Without debug mode:
[
{
"uid": 1,
"title": "Product Name",
"slug": "product-name"
}
]
With debug mode enabled (checkbox in backend):
{
"products": [
{
"uid": 1,
"title": "Product Name",
"slug": "product-name"
}
],
"debug": {
"pageId": 5,
"categoryUids": [8],
"productCount": 1,
"settings": {
"debug": "1",
"categories": "8",
"layout": "0"
}
}
}
Example: Real-World Implementation
See Classes/UserFunc/ProductListJsonRenderer.php for a complete working example that:
- Queries tt_content for plugin configuration
- Parses FlexForm settings (including debug mode, categories)
- Queries products with visibility filters using direct database queries
- Provides conditional debug output based on FlexForm settings
- Serializes product data to JSON with backward compatibility
Common Issues
Empty Results
- Problem: UserFunc returns empty array
- Solution: Check that you're using direct database queries, not Extbase repositories
Parameter Type Errors
- Problem:
createNamedParameter(): Argument #2 ($type) must be of type Doctrine\DBAL\ParameterType - Solution: Use
ParameterType::INTEGERinstead of\PDO::PARAM_INT
Content Element Not Found
- Problem: tt_content query returns empty
- Solution: Verify the
list_typevalue matches your plugin signature exactly
Wrong Context Data
- Problem: Trying to access content element via
$GLOBALS['TSFE']->cObj->data - Solution: This only contains page data. Query tt_content directly instead
Architecture Summary
1. Browser requests JSON page
↓
2. Headless extension renders page as JSON
↓
3. Encounters tt_content.list (your plugin)
↓
4. Executes your TypoScript override
↓
5. Calls your UserFunc
↓
6. UserFunc queries tt_content for plugin config
↓
7. UserFunc queries database directly for records
↓
8. Returns JSON array
↓
9. Headless includes it in final JSON output
Conclusion
The key insight is that headless rendering requires a different approach than normal Extbase plugins:
- Use site sets for TypoScript overrides
- Use UserFuncs for custom rendering
- Use direct database queries instead of repositories
- Query tt_content to recover plugin configuration
This approach provides full control over JSON output while working within the constraints of the headless rendering context.