308 lines
9.4 KiB
Markdown
308 lines
9.4 KiB
Markdown
# 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:
|
|
|
|
1. **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->data` will only contain page data, not the content element.
|
|
|
|
2. **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`
|
|
|
|
```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
|
|
<?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:
|
|
|
|
```php
|
|
// 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:
|
|
|
|
```php
|
|
// 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_*`:
|
|
|
|
```php
|
|
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**:
|
|
```php
|
|
// This won't work in UserFunc context
|
|
$repository = GeneralUtility::makeInstance(YourRepository::class);
|
|
$records = $repository->findAll(); // Returns empty!
|
|
```
|
|
|
|
**Use instead**:
|
|
```php
|
|
// 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:
|
|
|
|
```bash
|
|
./vendor/bin/typo3 cache:flush
|
|
```
|
|
|
|
## Testing
|
|
|
|
Test your JSON output using curl and jq:
|
|
|
|
```bash
|
|
# 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**:
|
|
```json
|
|
[
|
|
{
|
|
"uid": 1,
|
|
"title": "Product Name",
|
|
"slug": "product-name"
|
|
}
|
|
]
|
|
```
|
|
|
|
**With debug mode enabled** (checkbox in backend):
|
|
```json
|
|
{
|
|
"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::INTEGER` instead of `\PDO::PARAM_INT`
|
|
|
|
### Content Element Not Found
|
|
- **Problem**: tt_content query returns empty
|
|
- **Solution**: Verify the `list_type` value 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.
|