Initial Commit
This commit is contained in:
1 parent
4f495177e4
commit
b6d2142214
166 files changed
+13952
-178
No files matched your search
@@ -0,0 +1,307 @@
|
||||
# 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.
|
||||
Reference in new issue
Block a user