Pimcore Tips-N-Tricks - Chapter 1: Adding a Custom Menu Item

Marco Guiducci
10 min readNov 4, 2023

--

Photo by Florian Olivo on Unsplash

During my last five years working, I was mostly focused on developing custom solutions for my customers with Pimcore.

As any developer may confirm, although you are using the same technology and patterns for different projects, every customer is different and, consequently, every project is unique.

Even if the previous sentence is true, at the same time we can affirm that, usually, we are facing a situation for which we need to re-implement some piece of logic that we already have coded before.

This is exactly my everyday story; interface customization, some magic button that “can do something”, import/export facilities, and so on… all things that let me say:

After the fifth time that I needed to open an old project (after having spent minutes thinking about “which” project I implemented a feature before) to retrieve some pieces of code, I said to myself: “OK, I must share that with someone.”

So here we are, with my first chapter about Pimcore “Tips-N-Tricks”; in this chapter, we will discuss how to create a simple custom menu to add to the main toolbar.

Before starting, I want to say that I’ve not created something brand-new; this article, and the following ones, are just “putting together” information due to documentation lack and reverse engineering needed to produce concrete features, with the hope that this will save time for someone like me.

If I caught your curiosity, I hope you will also have a look at my published book about Pimcore written in collaboration with Daniele Fontani and Francesco Minà.

What’s on the pot?

As the title of this article said, the main topic is Pimcore; so, you will forgive me if I suppose that everyone interested in this article already knows what Pimcore is and how it works, so I will not spend words on that.

Having said that, I have prepared a self-contained repository that contains the Pimcore installation and all the logic we will discuss in the following chapters.

So, the road map of the article will be the following:

  • How to install the repository
  • Creating the context for the menu
  • How to implement the JavaScript stuff
  • How to serve the menu from the backend

Install and Configure Pimcore

As previously mentioned, in the references at the bottom of the article you will find a repository with the source code. The repository is simply based on the dockerized Pimcore Skeleton project.

To install and configure the repository, just follow the instructions on the Readme file, so just open a shell and type:

docker-compose up -d
docker-compose exec php bash restore.sh

This script automatically restores vendor packages, installs assets, and performs a cache warm-up.
This also provides an already configured database with some data.

Credentials for Pimcore admin are `pimcore/pimcore`.

Presenting the Context

Let’s start by giving a concrete context for what we are going to realize; in a recent project, a customer positioned in the manufacturing industry asked me to create a menu in which he could manage the conversion between “squared millimeters” and “AWG” for some section attributes.

If you don’t know what AWG is don’t be afraid, as it was my first reaction:

I don’t want to bother you with details, but if you are curious AWG stands for “American wire gauge”, an alternative measurement system to express cables and wire section.

Now that we have the context, let’s start coding some code!

Mixing the Potion Ingredients

Now that we know what’s in the pot, it’s to prepare the ingredients and mix them.

Create a Custom Table on the Database

The quickest way to create a custom table on the Pimcore database is obviously to create it in the database administrator; but, in concrete scenarios in which you have development, test, and production environments, you will have to repeat the operation manually.

To avoid the need to repeat actions manually, you can generate a Migration; in the Pimcore documentation, you will find the commands to generate and execute the migration and other useful configurations to automatically run migrations on your project.

Once you have generated a new migration, you can implement the “up” function; let’s see the code snippet:

final class Version20231030132309 extends AbstractMigration
{
private $tablesToInstall = [
'mm2_to_awg_conversion' =>
"CREATE TABLE IF NOT EXISTS `mm2_to_awg_conversion` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`mm2` decimal(10,2) DEFAULT NULL,
`awg` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"
];

public function up(Schema $schema): void
{
foreach ($this->tablesToInstall as $name => $statement) {
if ($schema->hasTable($name)) {
continue;
}

$this->addSql($statement);
}

}
}

In the snippet above, you can see how easy adding a new table to the schema is; in my experience, I usually create an array variable having table names as keys and the creation statement as value; this is useful also for creating installers.

JavaScript event to render the menu

If your goal is the creation of a custom menu, the first thing that you need to do is to add the menu item to the toolbar and render it on the main panel.

OK, maybe it could be a bit obvious…

In the new Pimcore 11 version, adding a custom item to the menu was made simpler than in the previous versions; as reported in Pimcore documentation, you only need to implement an event listener for the “preMenuBuild” UI event and add a new item.

Let’s start creating a “startup.js” file inside the “public” folder if you want to add your menu directly in the project, or inside the “Resource” folder if you’re doing it inside a bundle.

pimcore.registerNS("pimcore.plugin.CustomMenu");

pimcore.plugin.CustomMenu = Class.create({
initialize: function () {
document.addEventListener(pimcore.events.preMenuBuild, this.preMenuBuild.bind(this));
},

preMenuBuild: function (e) {
// the event contains the existing menu
let menu = e.detail.menu;

// the property name is used as id with the prefix pimcore_menu_ in the html markup e.g. pimcore_menu_mybundle
menu.custommenu = {
label: t('Custom Settings'), // set your label here, will be shown as tooltip
iconCls: 'pimcore_nav_icon_properties', // set full icon name here
priority: 42, // define the position where you menu should be shown. Core menu items will leave a gap of 10 custom main menu items
items: [], //if your main menu has subitems please see Adding Custom Submenus To ExistingNavigation Items
shadow: false,
handler: this.openCustomMenu, // defining a handler will override the standard "showSubMenu" functionality, use in combination with "noSubmenus"
noSubmenus: true, // if there are no submenus set to true otherwise menu won't show up
cls: "pimcore_navigation_flyout", // use pimcore_navigation_flyout if you have subitems
};
},

openCustomMenu: function(element) {
try {
pimcore.globalmanager.get("plugin_pimcore_custommenu").activate();
} catch (e) {
pimcore.globalmanager.add("plugin_pimcore_custommenu", new pimcore.plugin.CustomMenu.settings());
}
}
});

var CustomMenuPlugin = new pimcore.plugin.CustomMenu();

By now, nothing more and nothing less than what is shown in the documentation. This script just adds a new icon in the main menu, but it doesn’t do anything else; how to effectively render a menu to do something concrete?

The documentation is lacking at this point, and this is the main reason that pushed me to write this kind of article: avoid to other developers spending hours of reverse engineering to find out how others did things.

So, let’s see how to effectively render the menu; for practicing reasons, I have created a second script named “settings.js” (but you can choose any name) to put the logic in. Let’s see some crucial steps:

pimcore.registerNS("pimcore.plugin.CustomMenu.settings");

pimcore.plugin.CustomMenu.settings = Class.create({

activate: function () {
var tabPanel = Ext.getCmp('pimcore_panel_tabs');

if(tabPanel.items.keys.indexOf("custommenu_settings") > -1){
tabPanel.setActiveItem('custommenu_settings');
}else{
throw "need to reload";
}
},

initialize: function () {
this.getData();
},

getData: function () {
Ext.Ajax.request({
url: '/admin/custom-menu/get-custommenu-settings-attributes',
success: function (response) {
this.data = Ext.decode(response.responseText).data;
this.getTabPanel();
}.bind(this)
});
},

getTabPanel: function () {
//Stay Tuned...
}
});

As you can see in the snippet above, the first function is the “activate” one; this function checks if the menu is already opened in a panel in the main section, otherwise, it requires to reload it. Please note that this function is called by the previous script.

The “initialize” function is the constructor one; it just calls the function that calls the controller to get the data to be shown (we will see it later). After the data are retrieved, the “getPanelData” function is called to show them.

getTabPanel: function () {
if (!this.panel) {
var self = this;

this.panel = Ext.create('Ext.panel.Panel', {
id: 'custommenu_settings',
title: t('Custom Settings'),
iconCls: 'pimcore_icon_system',
border: false,
layout: 'fit',
closable: true
});

var tabPanel = Ext.getCmp('pimcore_panel_tabs');
tabPanel.add(this.panel);
tabPanel.setActiveItem('custommenu_settings');

var mm2AwgModel = Ext.create('Ext.data.Model', {
fields: [
{name: 'mm2', type: 'string'},
{name: 'awg', type: 'string'},
]
});

var mm2AwgValues = new Ext.create('Ext.data.Store', {
id: 'storeId',
model: mm2AwgModel,
data : this.data.mm2AwgValues
});

var mm2AwgGrid = Ext.create('Ext.grid.Panel', {
store: mm2AwgValues,
id: 'mm2AwgGrid',
actions: {
delete:{
iconCls: 'pimcore_icon_delete',
tooltip: "Delete",
handler: function (obj, row, rowIndex) {
self.deleteRow(rowIndex);
}.bind(self, this)
}
},
columns: [
{
text: 'Mm2',
dataIndex: 'mm2',
xtype: 'gridcolumn',
editor: {
xtype: 'numberfield'
}
},
{
text: 'Awg',
dataIndex: 'awg',
xtype: 'gridcolumn',
editor: {
xtype: 'textfield'
}
},
{
width: 70,
sortable: false,
menuDisabled: true,
xtype: 'actioncolumn',
items: ['@delete']
}
],
height: 800,
width: 500,
selModel: 'cellmodel',
plugins: {
ptype: 'cellediting',
clicksToEdit: 1
},
renderTo: Ext.getBody()
});

var mm2AwgPanel = Ext.create('Ext.form.Panel', {
title: t('MM2-AWG Conversion'),
autoScroll: true,
forceLayout: true,
defaults: {
forceLayout: true,
listeners: {
render: function (el) {
me.checkForInheritance(el);
}
}
},
fieldDefaults: {
labelWidth: 250
},
items: []
});

var button = Ext.create('Ext.Button', {
text: 'Create Row',
renderTo: Ext.getBody(),
handler: function (obj) {
self.createRow(this);
grid.reconfigure(store)
}.bind(self, this)
});

mm2AwgPanel.items.add(mm2AwgGrid);
mm2AwgPanel.items.add(button);

this.layout = Ext.create('Ext.tab.Panel', {
bodyStyle: 'padding:20px 5px 20px 5px;',
border: true,
autoScroll: true,
forceLayout: true,
defaults: {
forceLayout: true
},
fieldDefaults: {
labelWidth: 500
},
buttons: [
{
text: t('save'),
handler: this.save.bind(this),
iconCls: 'pimcore_icon_apply'
}
]
});


self.mm2AwgGrid = mm2AwgGrid;
this.layout.add(mm2AwgPanel);

this.panel.add(this.layout);
this.layout.setActiveItem(0);

pimcore.layout.refresh();
}

return this.panel;
}

Let’s resume the steps made by the function:

  • First of all, the menu main panel tab is created.
  • Then, the data storage is defined, and a grid is created to show them, adding an extra column to delete a row.
  • A sub-panel is created to show the grid; in this way, you can handle multiple sub-tabs in one single menu.
  • In the end, all is mixed adding also a button to create a new row.

In the article repository, you will also find the specific function to create and delete rows, and the function that calls the controller to save the data, which I can omit in this article as they are not central to the topic.

Last but not least: please note that static JavaScript files are not automatically loaded anymore since the Pimcore X release; to let Pimcore load your scripts, it’s crucial to implement a specific event listener.

services:
App\EventListener\PimcoreAdminListener:
tags:
- { name: kernel.event_listener, event: pimcore.bundle_manager.paths.css, method: addCSSFiles }
- { name: kernel.event_listener, event: pimcore.bundle_manager.paths.js, method: addJSFiles }
class PimcoreAdminListener
{
public function addJSFiles(PathsEvent $event)
{
$event->setPaths(
array_merge(
$event->getPaths(),
[
'/static/js/startup.js',
'/static/js/menu/settings.js',
]
)
);
}
}

Create the Admin Controller

As mentioned in the previous section, we need a controller to retrieve and save the data. The implementation is quite easy, we just need to extend the standard Pimcore AdminAbstractController.

/**
* @Route("/admin/custom-menu")
*/
class CustomMenuController extends AdminAbstractController
{
/**
* @Route("/get-custommenu-settings-attributes", name="get-custommenu-settings-attributes", methods={"GET"})
*
* @return array
*/
public function getSettingsAttributesAction(Request $request)
{
$mm2AwgValues = [];

$db = Db::get();
$mm2AwgConversionValues = $db->fetchAllAssociative('SELECT * FROM mm2_to_awg_conversion');
foreach ($mm2AwgConversionValues as $value) {
$row = [
'mm2' => $value["mm2"],
'awg' => $value["awg"],
'id' => $value["id"]
];

$mm2AwgValues[] = $row;
}

$response = [
"mm2AwgValues" => $mm2AwgValues,
];

return $this->adminJson(["data" => $response]);
}

/**
* @Route("/save-custommenu-settings-attributes", methods={"POST"})
*/
public function saveSettingsAttributesAction(Request $request)
{
$mm2AwgValues = $request->get("mm2AwgValues");
$mm2AwgSettings = json_decode($mm2AwgValues,true);

$response = array('success' => true);

$db = Db::get();

foreach ($mm2AwgSettings as $mm2AwgRow) {
$mm2 = $mm2AwgRow["mm2"];
$awg = $mm2AwgRow["awg"];
$id = $mm2AwgRow["id"];

$query = "INSERT INTO mm2_to_awg_conversion (id,mm2, awg) VALUES($id, '$mm2', '$awg') ON DUPLICATE KEY UPDATE id=$id, mm2 = '$mm2', awg = '$awg'";
$db->executeQuery($query);
}

return $this->adminJson($response);
}
}

A Practical Usage

In the article repository, you will find a practical usage of the implemented menu; to be more specific, you will find an example class that has:

  • A select attribute served by an OptionProvider for squared millimeters values
  • A CalculatedValue attribute that shows the converted AWG value

You just need to install the repo and discover how all this works.

What to Take Home

In this article, we have seen how to implement a custom menu in Pimcore. Starting from the migration to add context, we then have seen the steps to render the menu on the frontend and how to serve it through a controller.

Although this is not a revolutionary implementation, it response to a very real case scenario that developers are facing in their projects.

Pimcore documentation is great, but it’s often too generic and usually covers only at most 90% of the needs that developers have to face in everyday work; the remaining percentage can be filled only through several hours of reverse engineering and search in community talks (in which, to be honest, it’s hard to find what you need).

In the next chapter, I will talk about how to automatically render and download a PDF to realize a product technical sheet.

I hope that sharing knowledge through simple scenarios will help the community grow.

References

--

--