---
title: "Custom content objects"
manual: "Handlebars"
version: "main"
permalink: "https://docs.typo3.org/permalink/cpsit/typo3-handlebars-forms:custom-content-objects@main"
source: "DeveloperCorner/ContentObjects.rst"
rendered: "2026-09-29T09:18:02+00:00"
---

# Custom content objects {#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 {#custom-co-implement}

The abstract base class has one abstract method to implement:

```php
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->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**

```php
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:

```typoscript
myValue = MY_CUSTOM_COBJ
myValue.key = someProperty
```

## stdWrap support {#custom-co-stdwrap}

`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 {#custom-co-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 {#custom-co-recurse}

`$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.

```php
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:

```php
$context->process($subConfig, renderable: $otherRenderable, viewModel: $otherViewModel);
```

## Context stack {#custom-co-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.
