Skip to main content

Sulu Headless exposing form config

This blog post might be outdated!
This blog post was published more than one year ago and might be outdated!
· 3 min read
Stephan Hochdörfer

In one of our recent projects, we are using Sulu CMS with the Sulu Headless bundle and a React frontend. Additionally, we are also using the Sulu Forms bundle for form handling.

The Sulu Headless bundle out-of-the-box supports a lot of different content types. However, Sulu Forms are not yet supported by the Sulu Headless bundle. Luckily, the Sulu bundles in general are open for extension and this is what we did.

All we had to do is to implement an instance of the ContentTypeResolverInterface and tag that service with the sulu_headless.content_type_resolver tag. This is how a simple approach for return form data looks like:

use Sulu\Bundle\FormBundle\Form\BuilderInterface;
use Sulu\Bundle\HeadlessBundle\Content\ContentTypeResolver\ContentTypeResolverInterface;
use Sulu\Bundle\HeadlessBundle\Content\ContentView;
use Sulu\Component\Content\Compat\PropertyInterface;
use Sulu\Component\Content\Compat\Structure\PageBridge;
use Sulu\Component\Content\Compat\Structure\StructureBridge;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Validator\Exception\MissingOptionsException;

class FormSelectionResolver implements ContentTypeResolverInterface
{
/**
* @var BuilderInterface
*/
private BuilderInterface $formBuilder;

public function __construct(BuilderInterface $formBuilder)
{
$this->formBuilder = $formBuilder;
}

public static function getContentType(): string
{
return 'single_form_selection';
}

/**
* @inheritDoc
*/
public function resolve($data, PropertyInterface $property, string $locale, array $attributes = []): ContentView
{
$return = new ContentView([], ['default' => []]);
$id = (int) $property->getValue();

if (!$id) {
return $return;
}

if (!isset($property->getParams()['resourceKey'])) {
throw new MissingOptionsException('SuluFormBundle: The parameter "resourceKey" is missing!', []);
}

/** @var string $resourceKey */
$resourceKey = $property->getParams()['resourceKey']->getValue();

/** @var PageBridge $structure */
$structure = $property->getStructure();

$form = $this->formBuilder->build(
$id,
$resourceKey,
(string) $structure->getUuid(),
$structure->getLanguageCode(),
$property->getName()
);

if (!$form) {
$form = $this->loadShadowForm($property, $id, $resourceKey);
if (!$form) {
return $return;
}
}

$formData = [];
foreach ($form->all() as $child) {
$propertyPath = $child->getConfig()->getName();
if ($child->getConfig()->getOption('property_path') !== null) {
$propertyPath = $child->getConfig()->getOption('property_path');
// get rid of "data[]" from property_path
$propertyPath = substr($propertyPath, strpos($propertyPath, '[') + 1, -1);
}

$formData[$child->getConfig()->getName()] = [
'label' => $child->getConfig()->getOption('label'),
'attr' => $child->getConfig()->getOption('attr'),
'required' => $child->getConfig()->getOption('required'),
'property_path' => $propertyPath,
'data' => $child->getConfig()->getData()
];
}

return new ContentView([], [$form->getName() => $formData]);
}

private function loadShadowForm(PropertyInterface $property, int $id, string $type): ?FormInterface
{
$structure = $property->getStructure();

if (!$structure instanceof StructureBridge) {
return null;
}

if (!$structure->getIsShadow()) {
return null;
}

return $this->formBuilder->build(
$id,
$type,
(string) $structure->getUuid(),
$structure->getShadowBaseLanguage(),
$property->getName()
);
}
}

In the resolve() method of the ContentTypeResolverInterface we create a form instance based on the available parameters and in case that is not possible, build the form for the shadow language.

Once the form is built, we iterate over the form children and extract their names and parts of their configuration. For now, this gives us enough information to render the fields and send the expected form contents back to the server.