Extending Widgets
Widgets are the main building blocks of perspectives. Adding a custom widget type requires three backend components: a schema, a repository, and a hydrator.
Working example: The studio-example-bundle contains a complete
example_iframecustom widget covering all steps below, including frontend integration.
Registering a Widget Type
Register your widget type in the pimcore_studio_backend configuration. Load this config
via your bundle extension's prepend() method (since it targets another bundle's config key):
# config/pimcore/studio_backend.yaml
pimcore_studio_backend:
widget_types:
- my_custom_widget_type
// src/DependencyInjection/MyBundleExtension.php
public function prepend(ContainerBuilder $container): void
{
if ($container->hasExtension('pimcore_studio_backend')) {
$loader = new YamlFileLoader(
$container,
new FileLocator(__DIR__ . '/../../config')
);
$loader->load('pimcore/studio_backend.yaml');
}
}
Widget Config Schema
Create a class extending Pimcore\Bundle\StudioBackendBundle\Perspective\Schema\WidgetConfig
to define the data model for your widget. The base class provides id, name, widgetType,
and icon properties. Add any custom properties your widget needs.
<?php
declare(strict_types=1);
namespace App\Perspective\Schema;
use OpenApi\Attributes\Property;
use OpenApi\Attributes\Schema;
use Pimcore\Bundle\StudioBackendBundle\Perspective\Schema\WidgetConfig;
use Pimcore\Bundle\StudioBackendBundle\Response\ElementIcon;
#[Schema(
title: 'Custom Widget',
required: [
'customField',
'createdAt',
'isWriteable',
],
type: 'object'
)]
final class MyCustomWidget extends WidgetConfig
{
public function __construct(
string $id,
string $name,
?ElementIcon $icon = null,
#[Property(description: 'Custom Field', type: 'string', example: 'Some Value')]
private readonly string $customField = 'Some Value',
#[Property(description: 'Created At timestamp', type: 'int', example: 1738917191)]
private readonly ?int $createdAt = null,
#[Property(description: 'Is Writeable', type: 'bool', example: false)]
private readonly bool $isWriteable = false,
) {
parent::__construct($id, $name, 'my_custom_widget_type', $icon);
}
public function getCustomField(): string
{
return $this->customField;
}
public function getCreatedAt(): ?int
{
return $this->createdAt;
}
public function isWriteable(): bool
{
return $this->isWriteable;
}
}
Widget Config Repository
Create a class implementing WidgetConfigRepositoryInterface to handle CRUD operations for
your widget configurations. The getSupportedWidgetType() return value must match the type
string registered in the YAML config above.
Key Methods
| Method | Purpose |
|---|---|
getSupportedWidgetType() | Returns the widget type string |
isWidgetTypeOnlyWrapper() | true if this repo wraps an existing config source |
createConfiguration() | Creates a new widget config, returns the ID |
updateConfiguration() | Updates an existing widget config |
getConfiguration() | Returns a single config as array |
listConfigurations() | Returns all configs as arrays |
deleteConfiguration() | Deletes a widget config |
Configuration Storage
The repository uses LocationAwareConfigRepository for persistent storage. Three backends
are available:
| Type | Best for | Description |
|---|---|---|
settings-store | Third-party bundles | Pimcore SettingsStore (database). Supports CRUD without container recompilation. Recommended for custom widgets. |
symfony-config | Core / first-party | YAML files loaded into the container at compile time. Used by built-in element_tree widgets. Requires hooking into the bundle extension's load() to read configs from disk. |
disabled | Read-only | No writes allowed. |
The symfony-config backend relies on configs being loaded into the Symfony container at boot
time via LocationAwareConfigRepository::loadSymfonyConfigFiles(). Newly created configs are
only visible after a cache clear. For third-party bundles, use settings-store instead. It
reads directly from the database so new configs are available immediately.
The storage location for element_tree_widgets (the built-in type) can be configured:
pimcore_studio_backend:
config_location:
element_tree_widgets:
write_target:
type: 'symfony-config'
options:
directory: '/var/www/html/var/config/element_tree_widgets'
For details on storage location configuration, see Configuration Storage Locations.
Wrapper Repositories
In some cases a custom configuration already includes all necessary properties and the data
can be used in the widget directly. Set isWidgetTypeOnlyWrapper() to return true to
indicate the repository only loads existing configs. In this case, createConfiguration(),
updateConfiguration(), and deleteConfiguration() should throw NotWriteableException.
Example: Wrapper Repository
<?php
declare(strict_types=1);
namespace App\Perspective\Widget\Repository;
use App\Custom\Configuration\Repository\ConfigurationRepositoryInterface;
use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException;
use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotWriteableException;
use Pimcore\Bundle\StudioBackendBundle\Perspective\Repository\WidgetConfigRepositoryInterface;
final readonly class CustomWidgetConfigRepository implements WidgetConfigRepositoryInterface
{
public function __construct(
private ConfigurationRepositoryInterface $configurationRepository,
) {
}
public function getSupportedWidgetType(): string
{
return 'my_custom_widget_type';
}
public function isWidgetTypeOnlyWrapper(): bool
{
return true;
}
/**
* @throws NotWriteableException
*/
public function createConfiguration(array $widgetData): string
{
throw new NotWriteableException(
'myType',
'Creating new my_custom_widget_type via widgets is not supported.'
);
}
/**
* @throws NotWriteableException
*/
public function updateConfiguration(array $widgetData): void
{
throw new NotWriteableException(
'myType',
'Updating my_custom_widget_type via widgets is not supported.'
);
}
/**
* @throws NotFoundException|NotWriteableException
*/
public function getConfiguration(string $widgetId): array
{
$data = $this->configurationRepository->get($widgetId);
if (!is_array($data) || $data[0] === null) {
return [];
}
return $data;
}
/**
* @throws NotFoundException|NotWriteableException
*/
public function listConfigurations(): array
{
// Each item must include at least id, name, and icon
return $this->configurationRepository->listConfigurations();
}
/**
* @throws NotWriteableException
*/
public function deleteConfiguration(string $widgetId): void
{
throw new NotWriteableException(
'myType',
'Deleting my_custom_widget_type via widgets is not supported.'
);
}
}
For a full CRUD implementation using settings-store, see
IframeWidgetConfigRepository.php
in the example bundle. For the built-in reference using symfony-config, see
ElementTreeWidgetConfigRepository in studio-backend-bundle.
Widget Config Hydrator
Create a class implementing WidgetConfigHydratorInterface. The hydrator transforms raw
config arrays (from the repository) into your typed schema object. The
getSupportedWidgetType() return value must match the repository and YAML config.
<?php
declare(strict_types=1);
namespace App\Perspective\Widget\Hydrator;
use App\Perspective\Schema\MyCustomWidget;
use Pimcore\Bundle\StudioBackendBundle\Perspective\Schema\WidgetConfig\WidgetConfigHydratorInterface;
final readonly class CustomWidgetConfigHydrator implements WidgetConfigHydratorInterface
{
public function getSupportedWidgetType(): string
{
return 'my_custom_widget_type';
}
public function hydrate(array $widgetData): MyCustomWidget
{
return new MyCustomWidget(
$widgetData['id'],
$widgetData['name'],
$widgetData['icon'],
$widgetData['customField'],
$widgetData['createdAt'],
$widgetData['isWriteable']
);
}
}
The hydrator must return a class extending
Pimcore\Bundle\StudioBackendBundle\Perspective\Schema\WidgetConfig.
Service Registration
Register both services with their respective DI tags:
services:
App\Perspective\Widget\Repository\CustomWidgetConfigRepository:
tags:
- { name: pimcore.studio_backend.widget_repository }
App\Perspective\Widget\Hydrator\CustomWidgetConfigHydrator:
tags:
- { name: pimcore.studio_backend.widget_hydrator }
The repository and hydrator are automatically discovered by the Studio Backend via their tags.
Restrictions
- Widget configuration name must be 3-80 characters. Only alphanumeric characters, underscores, and spaces.
- Widget configuration ID is auto-generated by Pimcore Studio (UUID format).
- Every widget configuration must include at least
id,name, andiconproperties. - When updating a widget, all configuration properties must be present in the request body.
Element Tree Context Menu Permissions
Add custom context menu permissions for element tree widgets by implementing an event
subscriber that injects the ContextPermissionsServiceInterface service.
1. Define Your Subscriber Service
services:
_defaults:
autowire: true
autoconfigure: true
public: false
App\EventSubscriber\ElementTreeContextPermissionsSubscriber: ~
2. (Optional) Conditionally Add the Subscriber
If Studio Backend is not a required dependency, load the service definition only when the bundle is active:
public function prepend(ContainerBuilder $container): void
{
if ($container->hasExtension('pimcore_studio_backend')) {
$loader = new YamlFileLoader(
$container,
new FileLocator(__DIR__ . '/../../config')
);
$loader->load('pimcore_studio_backend.yaml');
}
}
3. Implement the Subscriber
<?php
declare(strict_types=1);
namespace App\EventSubscriber;
use Pimcore\Bundle\StudioBackendBundle\Element\Model\ContextPermissionData;
use Pimcore\Bundle\StudioBackendBundle\Element\Service\Permissions\ContextPermissionsServiceInterface;
use Pimcore\Bundle\StudioBackendBundle\Util\Constant\ElementTypes;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\KernelEvents;
final readonly class ElementTreeContextPermissionsSubscriber implements EventSubscriberInterface
{
public function __construct(
private ContextPermissionsServiceInterface $permissionsService,
) {
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::CONTROLLER => 'addDocumentContextPermissions',
];
}
public function addDocumentContextPermissions(): void
{
$this->permissionsService->add(
new ContextPermissionData('addMyBundleContextAction', ElementTypes::TYPE_DOCUMENT, false)
);
}
}
Example Widget Configuration (YAML)
pimcore_studio_backend:
element_tree_widgets:
1efe7ac9_a03a_6334_9e48_13f662882599:
id: 1efe7ac9_a03a_6334_9e48_13f662882599
name: 'My Object Tree Widget'
element_type: 'data-object' # data-object, asset or document types are supported
icon:
type: path
value: 'path/to/config-icon.svg'
root_folder: '/path/to/root/folder'
show_root: true
classes: ['CAR']
pql: null # PQL query to filter the tree items
page_size: 50 # define custom page size for your tree
context_permissions:
add: true
addFolder: true
changeChildrenSortBy: true
copy: true
cut: true
delete: true
lock: true
lockAndPropagate: true
paste: true
publish: true
reload: true
rename: true
searchAndMove: true
unlock: true
unlockAndPropagate: true
unpublish: true
Frontend Integration
To render your widget type in perspectives, register a matching React component in the
Studio UI's WidgetRegistry. See the
Widget Manager plugin example
for frontend registration details, and the
Adding Object Datatypes
guide for an example of the full cross-layer pattern.