# 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 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.