Drupal 8 — Creating custom menu API

Deeps
4 min readMar 9, 2021

Recently, I worked on creating custom API in order to get different data structure from default.

Here is what I did.

Create a plugin file called MainNavigation.php in your module <module_name>/src/Plugin/rest/resource/MainNavigation.php

Add namespace, use statement and Plugin annotation defination.

namespace Drupal\<module name>\Plugin\rest\resource;

use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Drupal\system\MenuInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Provides a resource to get view modes by entity and bundle.
*
*
@RestResource(
* id = "navigation_menu",
* label =
@Translation("Navigation menu"),
* serialization_class = "Drupal\system\Entity\Menu",
* uri_paths = {
* "canonical" = "/api/menu/{menu}"
* }
* )
*/

Simplify data show it strips data I do not need.

/**
* Simply menu to show the data that needs in API.
*
*
@param $menu_links
*
*
@return array
* An array of simplified menu items.
*/
private function simplifyMenu(array $menu_links) {
$menus = [];
foreach ($menu_links as $key => $menu_link) {
$menus[$key]['id'] = $menu_link->uuid();
$menus[$key]['title'] = $menu_link->getTitle();
$menus[$key]['parent'] = str_replace('menu_link_content:', '', $menu_link->getParentId());
$menus[$key]['url'] = $menu_link->link->first()->url;

$changed = $menu_link->changed->first()->value;
$menus[$key]['changed'] = date('Y-m-d\\TH:i:sP', $changed);
}

return $menus;
}

Make a API nested structure so child item is nested within parentent.

/**
* Transform menu to tree structure.
*
*
@param $menus
*
*
@return array
* Nested menu structure.
*/
private function treeTransformation(array $menus) {
$nodes = [];
$tree = [];
foreach ($menus as &$node) {
$node['childNodes'] = [];
$id = $node['id'];
$parent_id = $node['parent'];

$nodes[$id] =& $node;
if (array_key_exists($parent_id, $nodes)) {
$nodes[$parent_id]['childNodes'][] =& $node;
} else {
$tree[] =& $node;
}
}

return $tree;
}

And add get method

/**
* {
@inheritdoc}
*/
public function get(MenuInterface $menu) {
$result = \Drupal::entityQuery('menu_link_content')->condition('menu_name', $menu->id())->execute();

// menu items empty check
if (empty($result)) {
$response = new ResourceResponse([
'message' => t('No menu items defined in @menu', array('@menu' => $menu->id())),
'menu_items' => []
]);
return $response;
}
// loads menu items
$menu_links = MenuLinkContent::loadMultiple($result);

// simply menu items to get id, title, parent id, link and changed
$menus = $this->simplifyMenu($menu_links);

// transform menu items to tree structure
$tree = $this->treeTransformation($menus);

$response = new ResourceResponse(['menu_items' => $tree]);
// In order to generate fresh result every time (without clearing
// the cache), you need to invalidate the cache.
$response->addCacheableDependency($tree);

return $response;
}

And this is what I get in the API when I go to /api/menu/<menu name>.

{
"menu_items": [
{
"id": "31858dc9-1f06-4dad-ba9d-9cf916213841",
"title": "Parent Menu 1",
"parent": "",
"url": "/parent-menu-1",
"changed": "2021-03-09T12:46:40+00:00",
"childNodes": [
{
"id": "32ca8da2-2bde-4c6c-a0b3-56387bb00e84",
"title": "Child menu 1",
"parent": "31858dc9-1f06-4dad-ba9d-9cf916213841",
"url": "/child-menu-1",
"changed": "2021-03-09T12:46:40+00:00",
"childNodes": []
},
.......
]
},
{
"id": "6670df30-e405-4b22-b599-50f9e6b84654",
"title": "Parent Menu 2",
"parent": "",
"url": "/parent-menu-2",
"changed": "2021-03-09T12:46:40+00:00",
"childNodes": [
{
"id": "2fcfcbc3-618d-4f6a-84dc-e9fdaea7587b",
"title": "Child menu 2",
"parent": "6670df30-e405-4b22-b599-50f9e6b84654",
"url": "/child-menu-2",
"changed": "2021-03-09T12:46:40+00:00",
"childNodes": []
},

Full script

<?php

namespace Drupal\<module name>\Plugin\rest\resource;

use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Drupal\system\MenuInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Provides a resource to get view modes by entity and bundle.
*
*
@RestResource(
* id = "navigation_menu",
* label =
@Translation("Navigation menu"),
* serialization_class = "Drupal\system\Entity\Menu",
* uri_paths = {
* "canonical" = "/api/menu/{menu}"
* }
* )
*/

class MainNavigation extends ResourceBase {

/**
* {
@inheritdoc}
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
array $serializer_formats,
LoggerInterface $logger) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
}

/**
* {
@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->getParameter('serializer.formats'),
$container->get('logger.factory')->get('rest')
);
}

/**
* {
@inheritdoc}
*/
public function get(MenuInterface $menu) {
$result = \Drupal::entityQuery('menu_link_content')->condition('menu_name', $menu->id())->execute();

// menu items empty check
if (empty($result)) {
$response = new ResourceResponse([
'message' => t('No menu items defined in @menu', array('@menu' => $menu->id())),
'menu_items' => []
]);
return $response;
}
// loads menu items
$menu_links = MenuLinkContent::loadMultiple($result);

// simply menu items to get id, title, parent id, link and changed
$menus = $this->simplifyMenu($menu_links);

// transform menu items to tree structure
$tree = $this->treeTransformation($menus);

$response = new ResourceResponse(['menu_items' => $tree]);
// In order to generate fresh result every time (without clearing
// the cache), you need to invalidate the cache.
$response->addCacheableDependency($tree);

return $response;
}

/**
* Simply menu to show the data that needs in API.
*
*
@param $menu_links
*
*
@return array
* An array of simplified menu items.
*/
private function simplifyMenu(array $menu_links) {
$menus = [];
foreach ($menu_links as $key => $menu_link) {
$menus[$key]['id'] = $menu_link->uuid();
$menus[$key]['title'] = $menu_link->getTitle();
$menus[$key]['parent'] = str_replace('menu_link_content:', '', $menu_link->getParentId());
$menus[$key]['url'] = $menu_link->link->first()->url;

$changed = $menu_link->changed->first()->value;
$menus[$key]['changed'] = date('Y-m-d\\TH:i:sP', $changed);
}

return $menus;
}

/**
* Transform menu to tree structure.
*
*
@param $menus
*
*
@return array
* Nested menu structure.
*/
private function treeTransformation(array $menus) {
$nodes = [];
$tree = [];
foreach ($menus as &$node) {
$node['childNodes'] = [];
$id = $node['id'];
$parent_id = $node['parent'];

$nodes[$id] =& $node;
if (array_key_exists($parent_id, $nodes)) {
$nodes[$parent_id]['childNodes'][] =& $node;
} else {
$tree[] =& $node;
}
}

return $tree;
}

/**
* {
@inheritdoc}
*/
protected function getBaseRoute($canonical_path, $method) {
$route = parent::getBaseRoute($canonical_path, $method);

$parameters = $route->getOption('parameters') ?: [];
$parameters['menu']['type'] = 'entity:menu';
$route->setOption('parameters', $parameters);

return $route;
}

}

--

--