Custom content objects 

If the built-in HBS_* content objects do not cover a required value, you can create your own by extending \CPSIT\Typo3HandlebarsForms\ContentObject\AbstractHandlebarsFormsContentObject.

Implementing a custom content object 

The abstract base class has one abstract method to implement:

abstract protected function resolve(
    array $configuration,
    ValueResolutionContext $context,
): mixed;
Copied!

$configuration is the TypoScript sub-tree below your content object's key. $context exposes the current state:

  • $context->renderable – the EXT:form renderable being processed
  • $context->viewModel – its view model
  • $context->formRuntime – the active form runtime
  • $context->renderingContext – the Fluid rendering context

Return any value. Strings are returned directly to the TypoScript tree. Non-string values (arrays, objects) are automatically stored in the ValueCollector under a placeholder key; ProcessFormProcessor replaces the placeholder with the real value after the full tree has been resolved.

Example

use CPSIT\Typo3HandlebarsForms\ContentObject;
use Symfony\Component\DependencyInjection;

#[DependencyInjection\Attribute\AutoconfigureTag(
    'frontend.contentobject',
    ['identifier' => 'MY_CUSTOM_COBJ'],
)]
final class MyCustomContentObject extends ContentObject\AbstractHandlebarsFormsContentObject
{
    protected function resolve(
        array $configuration,
        ContentObject\Context\ValueResolutionContext $context,
    ): mixed {
        return $context->renderable->getProperties()[$configuration['key'] ?? ''] ?? null;
    }
}
Copied!

Use it in TypoScript like any built-in content object:

myValue = MY_CUSTOM_COBJ
myValue.key = someProperty
Copied!

stdWrap support 

stdWrap is handled automatically by the base class for every HBS_* and custom content object. If resolve() returns a stringable value, stdWrap is applied directly to it. If the value is non-stringable, stdWrap receives an empty string but the original value is set as the currentValue so TypoScript conditions inside stdWrap can still read it.

Registration 

The #[AutoconfigureTag('frontend.contentobject', ['identifier' => '...'])] attribute is sufficient for registration when the class lives in a directory that is autowired. No explicit YAML service definition is needed.

The identifier must be globally unique across all loaded extensions. Prefixing it with your vendor name (e.g. MYVENDOR_FOO) avoids collisions.

Recursing into the TypoScript tree 

$context->process() re-invokes the ProcessFormProcessor pipeline with an arbitrary TypoScript configuration array. Its primary purpose is to let your content object re-process the current (or a different) renderable under a different configuration block — for example, to apply a sub-section of your own configuration, or to switch the active renderable or view model entirely before the pipeline runs.

protected function resolve(
    array $configuration,
    ContentObject\Context\ValueResolutionContext $context,
): mixed {
    $subConfiguration = $configuration['items.'] ?? [];

    if (!is_array($subConfiguration)) {
        return null;
    }

    return $context->process($subConfiguration);
}
Copied!

To process a different renderable or view model, pass them as named arguments:

$context->process($subConfig, renderable: $otherRenderable, viewModel: $otherViewModel);
Copied!

Context stack 

The ContextStack is a shared singleton (shared: true in DI). It holds the stack of ValueResolutionContext objects pushed by ProcessFormProcessor as it walks the TypoScript tree.

Custom content objects receive the current context as a direct argument to resolve() – injecting ContextStack directly is not necessary and is considered internal API.