Custom content objects
If the built-in
HBS_*
content objects do not cover a required value, you
can create your own by extending
\CPSIT\.
Implementing a custom content object
The abstract base class has one abstract method to implement:
abstract protected function resolve(
array $configuration,
ValueResolutionContext $context,
): mixed;
$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->view– its view modelModel $context->form– the active form runtimeRuntime $context->rendering– the Fluid rendering contextContext
Return any value. Strings are returned directly to the TypoScript tree. Non-string
values (arrays, objects) are automatically stored in the
Value under
a placeholder key;
Process 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;
}
}
Use it in TypoScript like any built-in content object:
myValue = MY_CUSTOM_COBJ
myValue.key = someProperty
stdWrap support
std is handled automatically by the base class for every
HBS_*
and custom content object. If
resolve returns a stringable
value,
std is applied directly to it. If the value is non-stringable,
std receives an empty string, but the value is set as current
value so TypoScript conditions inside
std can still read it (e.g.
using
current = 1
). Non-stringable values are wrapped in a
Current object provided by EXT:handlebars: arrays resolve to a
comma-separated list of their scalar values and stringable objects, all other
non-stringable values resolve to an empty string (see
Wrapped current values).
Custom code, such as user functions, can access the original value via the
value
property of the wrapper:
use CPSIT\Typo3Handlebars\DataProcessing\DataSource\CurrentValue;
$currentValue = $contentObjectRenderer->getCurrentVal();
if ($currentValue instanceof CurrentValue) {
$currentValue = $currentValue->value;
}
Important
Since non-stringable objects are provided as wrapped value, the current
value is never
null
for them. Use checks for empty values (e.g.
if or
if.) instead of
checks for
null
values (e.g.
if or
if.).
Registration
The
#
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_) avoids collisions.
Recursing into the TypoScript tree
$context->process re-invokes the
Process 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);
}
To process a different renderable or view model, pass them as named arguments:
$context->process($subConfig, renderable: $otherRenderable, viewModel: $otherViewModel);
Context stack
The
Context is a shared singleton (shared: true in DI). It holds the
stack of
Value objects pushed by
Process as
it walks the TypoScript tree.
Custom content objects receive the current context as a direct argument to
resolve – injecting
Context directly is not necessary and is
considered internal API.