Adding tabs to Sulu’s Admin UI


It is recommended to read Extend Admin UI beforehand to get a better understandig of how Sulu admin classes work.

This tutorial will walk you through the process of adding an extra tab to the administration interface.

This could be useful in many different situations, like e.g. adding an extra tab for social media information to pages or contacts.

In this example we’ll be adding a “Socials” tab to the page form of Sulu.


Create the form for this view

You have to create the form that is rendered in this view. Therefore create a form at config/forms/page_socials.xml.


Note how we use slashes in the names of the properties, this returns the values in the given hierarchy.

<?xml version="1.0" ?>
<form xmlns=""

        <section name="twitter">
                <title lang="en">Twitter</title>
                <property name="ext/social/twitter_title" type="text_line">
                        <title lang="en">Twitter title</title>
                <property name="ext/social/twitter_description" type="text_line">
                        <title lang="en">Twitter description</title>
                <property name="ext/social/twitter_image" type="single_media_selection">
                        <title lang="en">Twitter image</title>
                        <param name="types" value="image"/>
                        <param name="formats" type="collection">
                            <param name="og_image" />

Register the view in your admin class

We’ll create a class src/Admin/SocialAdmin which extends Sulu\Bundle\AdminBundle\Admin.

In the Admin we need to implement configureViews. For our example we need the webspace_manager in combination with security_checker to check if the logged in user has permission to edit pages.

Note the setParent call adding the view as a child to PageAdmin::EDIT_FORM_VIEW. This will result in a new tab in the edit form for pages.

The setFormKey takes a string reference, which should be the same as the key tag in the above form XML.

class SocialAdmin extends Admin
    * @var ViewBuilderFactoryInterface
    private $viewBuilderFactory;

    * @var WebspaceManagerInterface
    private $webspaceManager;

    * @var SecurityCheckerInterface
    private $securityChecker;

    public function __construct(
        ViewBuilderFactoryInterface $viewBuilderFactory,
        WebspaceManagerInterface $webspaceManager,
        SecurityCheckerInterface $securityChecker
    ) {
        $this->viewBuilderFactory = $viewBuilderFactory;
        $this->webspaceManager = $webspaceManager;
        $this->securityChecker = $securityChecker;

    public function configureViews(ViewCollection $viewCollection): void
        $formToolbarActionsWithoutType = [
            new ToolbarAction('sulu_admin.save_with_publishing'),

        $routerAttributesToFormRequest = ['parentId', 'webspace'];
        $routerAttributesToFormMetdata = ['webspace'];

        $previewCondition = 'nodeType == 1';

        if ($this->hasSomeWebspacePermission()) {
                    ->createPreviewFormViewBuilder('sulu_page.page_edit_form.socials', '/socials')

    private function hasSomeWebspacePermission(): bool
        foreach ($this->webspaceManager->getWebspaceCollection()->getWebspaces() as $webspace) {
            $hasWebspacePermission = $this->securityChecker->hasPermission(
                PageAdmin::SECURITY_CONTEXT_PREFIX . $webspace->getKey(),

            if ($hasWebspacePermission) {
                return true;

        return false;

We can register this class as a service and give it a sulu.admin tag, then it will be picked up by Sulu.

    class: App\Admin\SocialAdmin
        - '@Sulu\Bundle\AdminBundle\Admin\View\ViewBuilderFactoryInterface'
        - '@sulu_core.webspace.webspace_manager'
        - '@sulu_security.security_checker'
        - { name: 'sulu.admin'}
        - { name: 'sulu.context', context: 'admin' }

When you debug the container right now your should see your own Admin class show up.

$ php bin/console debug:container --tag=sulu.admin

    Service ID               Class name
    sulu_contact.admin       Sulu\Bundle\ContactBundle\Admin\ContactAdmin
    sulu_preview.admin       Sulu\Bundle\PreviewBundle\Admin\PreviewAdmin
    sulu_custom_urls.admin   Sulu\Bundle\CustomUrlBundle\Admin\CustomUrlAdmin
    sulu_website.admin       Sulu\Bundle\WebsiteBundle\Admin\WebsiteAdmin
    sulu_tag.admin           Sulu\Bundle\TagBundle\Admin\TagAdmin
    sulu_page.admin          Sulu\Bundle\PageBundle\Admin\PageAdmin
    sulu_snippet.admin       Sulu\Bundle\SnippetBundle\Admin\SnippetAdmin
    sulu_category.admin      Sulu\Bundle\CategoryBundle\Admin\CategoryAdmin
    sulu_security.admin      Sulu\Bundle\SecurityBundle\Admin\SecurityAdmin
    sulu_media.admin         Sulu\Bundle\MediaBundle\Admin\MediaAdmin
    sulu_search.admin        Sulu\Bundle\SearchBundle\Admin\SearchAdmin
    app.social_admin         App\Admin\SocialAdmin

You should now see the tab in the administration interface, but the data of the form is not saved yet.

Persist the data of the form

In the final step, we need to persist the data of the added tab. In the case of the pages, we can utilize the existing pages API by registering a new StructureExtension. In other cases, we would need to implement our own API endpoint for the tab as shown in the Extend Admin UI chapter.


class SocialStructureExtension extends AbstractExtension implements ExportExtensionInterface
    * name of structure extension.
    const SOCIAL_EXTENSION_NAME = 'social';

    protected $properties = [

    protected $name = self::SOCIAL_EXTENSION_NAME;

    protected $additionalPrefix = 'social';

    public function save(NodeInterface $node, $data, $webspaceKey, $languageCode)
        $this->setLanguageCode($languageCode, 'i18n', null);

        $data = $this->encodeImages($data);

        $this->saveProperty($node, $data, 'twitter_title');
        $this->saveProperty($node, $data, 'twitter_description');
        $this->saveProperty($node, $data, 'twitter_image');

    public function load(NodeInterface $node, $webspaceKey, $languageCode)
        $twitterImageNode = $this->loadProperty($node, 'twitter_image');
        $twitterImage = null;
        if ($twitterImageNode) {
            $twitterImage = json_decode($twitterImageNode, true);

        return [
            'twitter_title' => $this->loadProperty($node, 'twitter_title'),
            'twitter_description' => $this->loadProperty($node, 'twitter_description'),
            'twitter_image' => $twitterImage,

    public function export($properties, $format = null)
        $data = [];
        foreach ($properties as $key => $property) {
            $value = $property;
            if (\is_bool($value)) {
                $value = (int) $value;

            $data[$key] = [
                'name' => $key,
                'value' => $value,
                'type' => '',

        return $data;

    public function import(NodeInterface $node, $data, $webspaceKey, $languageCode, $format)
        $this->setLanguageCode($languageCode, 'i18n', null);

        $this->save($node, $data, $webspaceKey, $languageCode);

    public function getImportPropertyNames()
        return $this->properties;

    private function encodeImages(array $data)
        if ($data['twitter_image']) {
            $data['twitter_image'] = json_encode($data['twitter_image']);

        return $data;

This class needs to be registered as a service with the tag sulu.structure.extension.

    class: App\Structure\SocialStructureExtension
    tags: { name: 'sulu.structure.extension' }