Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 1.0.x
  • 1.1.x
  • 1.2.x
  • 1.0.0
  • 1.0.1
  • 1.1.0
  • 1.1.1
  • 1.2.0
  • 1.2.1
  • 1.2.2
  • 1.2.3
  • 1.2.4
  • 1.2.5
  • 1.2.6
  • 1.2.7
  • 1.2.8
16 results

Target

Select target project
  • project/jsonapi_menu_items
  • issue/jsonapi_menu_items-3186804
  • issue/jsonapi_menu_items-3171184
  • issue/jsonapi_menu_items-3205065
  • issue/jsonapi_menu_items-3171371
  • issue/jsonapi_menu_items-3211656
  • issue/jsonapi_menu_items-3198306
  • issue/jsonapi_menu_items-3213317
  • issue/jsonapi_menu_items-3216818
  • issue/jsonapi_menu_items-3192576
  • issue/jsonapi_menu_items-3288144
  • issue/jsonapi_menu_items-3322768
  • issue/jsonapi_menu_items-3288143
  • issue/jsonapi_menu_items-3350524
  • issue/jsonapi_menu_items-3276561
  • issue/jsonapi_menu_items-3420066
  • issue/jsonapi_menu_items-3421504
  • issue/jsonapi_menu_items-3270141
  • issue/jsonapi_menu_items-3451149
  • issue/jsonapi_menu_items-3458524
  • issue/jsonapi_menu_items-3464587
  • issue/jsonapi_menu_items-3447727
  • issue/jsonapi_menu_items-3527213
  • issue/jsonapi_menu_items-3529854
  • issue/jsonapi_menu_items-3529865
25 results
Select Git revision
  • 1.0.x
  • 1.1.x
  • 1.2.x
  • 3350524-add-support-for
  • nader.mouldi-1.2.x-patch-24101
  • 1.0.0
  • 1.0.1
  • 1.1.0
  • 1.1.1
  • 1.2.0
  • 1.2.1
  • 1.2.2
  • 1.2.3
  • 1.2.4
14 results
Show changes
Commits on Source (8)
Showing
with 357 additions and 118 deletions
################
# DrupalCI GitLabCI template
#
# Gitlab-ci.yml to replicate DrupalCI testing for Contrib
#
# With thanks to:
# * The GitLab Acceleration Initiative participants
# * DrupalSpoons
################
################
# Guidelines
#
# This template is designed to give any Contrib maintainer everything they need to test, without requiring modification. It is also designed to keep up to date with Core Development automatically through the use of include files that can be centrally maintained.
#
# However, you can modify this template if you have additional needs for your project.
################
################
# Includes
#
# Additional configuration can be provided through includes.
# One advantage of include files is that if they are updated upstream, the changes affect all pipelines using that include.
#
# Includes can be overridden by re-declaring anything provided in an include, here in gitlab-ci.yml
# https://docs.gitlab.com/ee/ci/yaml/includes.html#override-included-configuration-values
################
include:
################
# DrupalCI includes:
# As long as you include this, any future includes added by the Drupal Association will be accessible to your pipelines automatically.
# View these include files at https://git.drupalcode.org/project/gitlab_templates/
################
- project: $_GITLAB_TEMPLATES_REPO
ref: $_GITLAB_TEMPLATES_REF
file:
- '/includes/include.drupalci.main.yml'
- '/includes/include.drupalci.variables.yml'
- '/includes/include.drupalci.workflows.yml'
################
# Pipeline configuration variables
#
# These are the variables provided to the Run Pipeline form that a user may want to override.
#
# Docs at https://git.drupalcode.org/project/gitlab_templates/-/blob/1.0.x/includes/include.drupalci.variables.yml
################
variables:
_PHPUNIT_CONCURRENT: 1
OPT_IN_TEST_PREVIOUS_MINOR: 1
OPT_IN_TEST_NEXT_MINOR: 1
OPT_IN_TEST_NEXT_MAJOR: 1
# Use PHP 8.1 for Drupal Core 9.5.x
CORE_PREVIOUS_PHP_MIN: 8.1
_CSPELL_WORDS: 'pathmenu, pathuser'
composer (next major):
variables:
_LENIENT_ALLOW_LIST: 'jsonapi_hypermedia, menu_item_extras'
cspell:
allow_failure: false
eslint:
allow_failure: false
phpcs:
allow_failure: false
phpstan:
allow_failure: false
phpstan (next major):
allow_failure: true
stylelint:
allow_failure: false
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
- Supports `menu_link_content` and [menu_link_config](https://www.drupal.org/project/menu_link_config) menu items. - Supports `menu_link_content` and [menu_link_config](https://www.drupal.org/project/menu_link_config) menu items.
- Supports filtering by depth, parents and custom query conditions. - Supports filtering by depth, parents and custom query conditions.
- Support for [JSON:API Hypermedia](https://www.drupal.org/project/jsonapi_hypermedia) based links in `/jsonapi` root document. - Support for [JSON:API Hypermedia](https://www.drupal.org/project/jsonapi_hypermedia) based links in `/jsonapi` root document.
- Support for fields added to menu links via [Menu Item Extras](https://www.drupal.org/project/menu_item_extras).
## Filters ## Filters
......
...@@ -10,10 +10,12 @@ ...@@ -10,10 +10,12 @@
"homepage": "https://www.drupal.org/project/jsonapi_menu_items", "homepage": "https://www.drupal.org/project/jsonapi_menu_items",
"minimum-stability": "dev", "minimum-stability": "dev",
"require": { "require": {
"drupal/core": "^9 || ^10", "drupal/jsonapi_resources": "^1.0",
"drupal/jsonapi_resources": "^1.0" "drupal/core": "^9.5 || ^10 || ^11"
}, },
"require-dev": { "require-dev": {
"drupal/jsonapi_hypermedia": "^1.6" "drupal/jsonapi_hypermedia": "^1.6",
"drupal/menu_link_config": "^1.0",
"drupal/menu_item_extras": "^3.0"
} }
} }
name: 'JSON:API Menu items' name: 'JSON:API Menu items'
description: Adds a JSON API resource for menu items. description: Adds a JSON API resource for menu items.
type: module type: module
core_version_requirement: ^9 || ^10 core_version_requirement: ^9.5 || ^10 || ^11
dependencies: dependencies:
- drupal:menu_link_content - drupal:menu_link_content
- jsonapi_resources:jsonapi_resources - jsonapi_resources:jsonapi_resources
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
/** /**
* Install new submodule for hypermedia integration. * Install new submodule for hypermedia integration.
*/ */
function jsonapi_menu_items_update_8001() { function jsonapi_menu_items_update_8001(): void {
if (\Drupal::moduleHandler()->moduleExists('jsonapi_hypermedia')) { if (\Drupal::moduleHandler()->moduleExists('jsonapi_hypermedia')) {
\Drupal::service('module_installer')->install(['jsonapi_menu_items_hypermedia']); \Drupal::service('module_installer')->install(['jsonapi_menu_items_hypermedia']);
} }
......
menu_items: menu_items:
description: "The link's target points to a resource that provides menu items." description: "The link's target points to a resource that provides menu items."
notes: "This link relation type extends the `related` link relation type. It inherits all of the same semantics while adding new semantics of its own." notes: 'This link relation type extends the `related` link relation type. It inherits all of the same semantics while adding new semantics of its own.'
route_callbacks: jsonapi_menu_items.menu:
- '\Drupal\jsonapi_menu_items\Routing\Routes::routes' path: '/%jsonapi%/menu_items/{menu}'
defaults:
_jsonapi_resource: '\Drupal\jsonapi_menu_items\Resource\MenuItemsResource'
options:
parameters:
menu:
type: entity:menu
requirements:
_access: 'TRUE'
name: 'JSON:API Menu items Hypermedia' name: 'JSON:API Menu items Hypermedia'
description: Integrates jsonapi_menu_items and jsonapi_hypermedia. description: Integrates jsonapi_menu_items and jsonapi_hypermedia.
type: module type: module
core_version_requirement: ^9 || ^10 core_version_requirement: ^9.5 || ^10 || ^11
dependencies: dependencies:
- jsonapi_menu_items:jsonapi_menu_items - jsonapi_menu_items:jsonapi_menu_items
- jsonapi_hypermedia:jsonapi_hypermedia - jsonapi_hypermedia:jsonapi_hypermedia
...@@ -14,10 +14,8 @@ class MenuItemsLinkProviderDeriver extends DeriverBase implements ContainerDeriv ...@@ -14,10 +14,8 @@ class MenuItemsLinkProviderDeriver extends DeriverBase implements ContainerDeriv
/** /**
* The menu storage. * The menu storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/ */
protected $menuStorage; protected EntityStorageInterface $menuStorage;
/** /**
* Constructs new MenuItemsLinkProvider. * Constructs new MenuItemsLinkProvider.
......
...@@ -4,14 +4,24 @@ namespace Drupal\jsonapi_menu_items\Resource; ...@@ -4,14 +4,24 @@ namespace Drupal\jsonapi_menu_items\Resource;
use Drupal\Core\Access\AccessResultInterface; use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\GeneratedUrl; use Drupal\Core\GeneratedUrl;
use Drupal\Core\Menu\MenuLinkTreeInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\jsonapi\JsonApiResource\LinkCollection; use Drupal\jsonapi\JsonApiResource\LinkCollection;
use Drupal\jsonapi\JsonApiResource\ResourceObject; use Drupal\jsonapi\JsonApiResource\ResourceObject;
use Drupal\jsonapi\JsonApiResource\ResourceObjectData; use Drupal\jsonapi\JsonApiResource\ResourceObjectData;
use Drupal\jsonapi\ResourceResponse; use Drupal\jsonapi\ResourceResponse;
use Drupal\jsonapi_resources\Resource\ResourceBase; use Drupal\jsonapi_resources\Resource\ResourceBase;
use Drupal\Core\Menu\MenuTreeParameters; use Drupal\menu_link_content\Plugin\Menu\MenuLinkContent;
use Drupal\system\MenuInterface; use Drupal\system\MenuInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route; use Symfony\Component\Routing\Route;
...@@ -20,14 +30,74 @@ use Symfony\Component\Routing\Route; ...@@ -20,14 +30,74 @@ use Symfony\Component\Routing\Route;
* *
* @internal * @internal
*/ */
final class MenuItemsResource extends ResourceBase { final class MenuItemsResource extends ResourceBase implements ContainerInjectionInterface {
/** /**
* A list of menu items. * A list of menu items.
* *
* @var array * @var array
*/ */
protected $menuItems = []; protected array $menuItems = [];
/**
* The menu tree.
*/
private MenuLinkTreeInterface $menuLinkTree;
/**
* The entity type manager service.
*/
private EntityTypeManagerInterface $entityTypeManager;
/**
* The entity field manager service.
*/
private EntityFieldManagerInterface $entityFieldManager;
/**
* The cache backend.
*/
private CacheBackendInterface $cache;
/**
* The entity repository.
*/
private EntityRepositoryInterface $entityRepository;
/**
* Construct a new MenuItemsResource object.
*
* @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_link_tree
* The menu link tree service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager interface.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
*/
public function __construct(MenuLinkTreeInterface $menu_link_tree, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, CacheBackendInterface $cache, EntityRepositoryInterface $entity_repository) {
$this->menuLinkTree = $menu_link_tree;
$this->entityTypeManager = $entity_type_manager;
$this->entityFieldManager = $entity_field_manager;
$this->cache = $cache;
$this->entityRepository = $entity_repository;
}
/**
* {@inheritDoc}
*/
public static function create(ContainerInterface $container) {
return new self(
$container->get('menu.link_tree'),
$container->get('entity_type.manager'),
$container->get('entity_field.manager'),
$container->get('cache.discovery'),
$container->get('entity.repository')
);
}
/** /**
* Process the resource request. * Process the resource request.
...@@ -54,12 +124,13 @@ final class MenuItemsResource extends ResourceBase { ...@@ -54,12 +124,13 @@ final class MenuItemsResource extends ResourceBase {
} }
$parameters->onlyEnabledLinks(); $parameters->onlyEnabledLinks();
$menu_tree = \Drupal::menuTree(); $tree = $this->menuLinkTree->load($menu->id(), $parameters);
$tree = $menu_tree->load($menu->id(), $parameters);
if (empty($tree)) { if (empty($tree)) {
$response = $this->createJsonapiResponse(new ResourceObjectData([]), $request, 200, []); $response = $this->createJsonapiResponse(new ResourceObjectData([]), $request, 200, []);
$response->addCacheableDependency($cacheability); if ($response instanceof CacheableResponseInterface) {
$response->addCacheableDependency($cacheability);
}
return $response; return $response;
} }
...@@ -69,13 +140,15 @@ final class MenuItemsResource extends ResourceBase { ...@@ -69,13 +140,15 @@ final class MenuItemsResource extends ResourceBase {
// Use the default sorting of menu links. // Use the default sorting of menu links.
['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'], ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
]; ];
$tree = $menu_tree->transform($tree, $manipulators); $tree = $this->menuLinkTree->transform($tree, $manipulators);
$this->getMenuItems($tree, $this->menuItems, $cacheability); $this->getMenuItems($tree, $this->menuItems, $cacheability, $menu);
$data = new ResourceObjectData($this->menuItems); $data = new ResourceObjectData($this->menuItems);
$response = $this->createJsonapiResponse($data, $request, 200, [] /* , $pagination_links */); $response = $this->createJsonapiResponse($data, $request, 200, [] /* , $pagination_links */);
$response->addCacheableDependency($cacheability); if ($response instanceof CacheableResponseInterface) {
$response->addCacheableDependency($cacheability);
}
return $response; return $response;
} }
...@@ -84,14 +157,48 @@ final class MenuItemsResource extends ResourceBase { ...@@ -84,14 +157,48 @@ final class MenuItemsResource extends ResourceBase {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getRouteResourceTypes(Route $route, string $route_name): array { public function getRouteResourceTypes(Route $route, string $route_name): array {
$resource_types = []; $map_id = "route_resource_types.resource_type.$route_name";
$cached = $this->cache->get($map_id);
if ($cached) {
return $cached->data;
}
$possible_resource_types['menu_link_content'] = ['menu_link_content'];
// If menu_link_config is enabled, gather those menu links as well.
if ($this->entityTypeManager->hasDefinition('menu_link_config')) {
$possible_resource_types['menu_link_config'] = ['menu_link_config'];
}
foreach (['menu_link_config', 'menu_link_content'] as $type) { $menu_link_content_definition = $this->entityTypeManager->getDefinition('menu_link_content');
$resource_type = $this->resourceTypeRepository->get($type, $type); $menu_link_content_bundle_entity_type = $menu_link_content_definition->get('bundle_entity_type');
if ($resource_type) { if ($this->entityTypeManager->hasDefinition($menu_link_content_bundle_entity_type)) {
$resource_types[] = $resource_type; $bundles = $this->entityTypeManager
->getStorage($menu_link_content_bundle_entity_type)
->getQuery()
->accessCheck(FALSE)
->execute();
$possible_resource_types['menu_link_content'] = $bundles;
}
// Now that we've got a list of resource types we care about, go get the
// resource type for each entity type and bundle.
$resource_types = [];
foreach ($possible_resource_types as $entity_type => $bundles) {
foreach ($bundles as $bundle) {
$resource_type = $this->resourceTypeRepository->get($entity_type, $bundle);
if (!is_null($resource_type)) {
$resource_types[] = $resource_type;
}
} }
} }
$this->cache->set($map_id, $resource_types, CacheBackendInterface::CACHE_PERMANENT, [
'jsonapi_resource_types',
'entity_field_info',
'entity_bundles',
'entity_types',
]);
return $resource_types; return $resource_types;
} }
...@@ -106,7 +213,7 @@ final class MenuItemsResource extends ResourceBase { ...@@ -106,7 +213,7 @@ final class MenuItemsResource extends ResourceBase {
* @return \Drupal\Core\Menu\MenuTreeParameters * @return \Drupal\Core\Menu\MenuTreeParameters
* The Menu Tree Parameters object. * The Menu Tree Parameters object.
*/ */
protected function applyFiltersToParams(Request $request, MenuTreeParameters $parameters) { protected function applyFiltersToParams(Request $request, MenuTreeParameters $parameters): MenuTreeParameters {
$filter = $request->query->all('filter'); $filter = $request->query->all('filter');
if (!empty($filter['min_depth'])) { if (!empty($filter['min_depth'])) {
...@@ -142,39 +249,42 @@ final class MenuItemsResource extends ResourceBase { ...@@ -142,39 +249,42 @@ final class MenuItemsResource extends ResourceBase {
/** /**
* Generate the menu items. * Generate the menu items.
* *
* @param array $tree * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* The menu tree. * The menu tree.
* @param array $items * @param array $items
* The already created items. * The already created items.
* @param \Drupal\Core\Cache\CacheableMetadata $cache * @param \Drupal\Core\Cache\CacheableMetadata $cache
* The cacheable metadata. * The cacheable metadata.
* @param \Drupal\system\MenuInterface $menu
* The menu that the links belong to.
*/ */
protected function getMenuItems(array $tree, array &$items, CacheableMetadata $cache) { protected function getMenuItems(array $tree, array &$items, CacheableMetadata &$cache, MenuInterface $menu): void {
$menu_link_content_storage = $this->entityTypeManager->getStorage('menu_link_content');
foreach ($tree as $menu_link) { foreach ($tree as $menu_link) {
if ($menu_link->access !== NULL && !$menu_link->access instanceof AccessResultInterface) { if ($menu_link->access !== NULL && !$menu_link->access instanceof AccessResultInterface) {
throw new \DomainException('MenuLinkTreeElement::access must be either NULL or an AccessResultInterface object.'); throw new \DomainException('MenuLinkTreeElement::access must be either NULL or an AccessResultInterface object.');
} }
if ($menu_link->access instanceof AccessResultInterface) { if ($menu_link->access instanceof AccessResultInterface) {
$cache->merge(CacheableMetadata::createFromObject($menu_link->access)); $cache = $cache->merge(CacheableMetadata::createFromObject($menu_link->access));
} }
// Only return accessible links. // Only return accessible links.
if ($menu_link->access instanceof AccessResultInterface && !$menu_link->access->isAllowed()) { if ($menu_link->access instanceof AccessResultInterface && !$menu_link->access->isAllowed()) {
continue; continue;
} }
$id = $menu_link->link->getPluginId();
[$plugin] = explode(':', $id);
switch ($plugin) {
case 'menu_link_content':
case 'menu_link_config':
$resource_type = $this->resourceTypeRepository->get($plugin, $plugin);
break;
default: $id = $menu_link->link->getPluginId();
// @todo Use a custom resource type? [$plugin] = explode(':', $id, 2);
if ($plugin === 'menu_link_config') {
$resource_type = $this->resourceTypeRepository->get('menu_link_config', 'menu_link_config');
}
else {
$resource_type = $this->resourceTypeRepository->get('menu_link_content', $menu_link->link->getMenuName());
if ($resource_type === NULL) {
$resource_type = $this->resourceTypeRepository->get('menu_link_content', 'menu_link_content'); $resource_type = $this->resourceTypeRepository->get('menu_link_content', 'menu_link_content');
}
} }
$url = $menu_link->link->getUrlObject()->toString(TRUE); $url = $menu_link->link->getUrlObject()->toString(TRUE);
...@@ -198,15 +308,40 @@ final class MenuItemsResource extends ResourceBase { ...@@ -198,15 +308,40 @@ final class MenuItemsResource extends ResourceBase {
'url' => $url->getGeneratedUrl(), 'url' => $url->getGeneratedUrl(),
'weight' => (int) $menu_link->link->getWeight(), 'weight' => (int) $menu_link->link->getWeight(),
]; ];
$language = NULL;
if ($menu_link->link instanceof MenuLinkContent) {
// @todo once minimum supported Drupal core version is 10.2, use
// \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent::getEntity.
// $link = $menu_link->link->getEntity();
$entity_id = $menu_link->link->getMetaData()['entity_id'] ?? NULL;
if ($entity_id !== NULL) {
$link = $menu_link_content_storage->load($entity_id);
if ($link !== NULL) {
$link = $this->entityRepository->getTranslationFromContext($link);
$language = $link->language();
$langcode_key = $link->getEntityType()->getKey('langcode');
$field_definitions = $this->entityFieldManager->getFieldDefinitions($link->getEntityTypeId(), $link->bundle());
foreach ($field_definitions as $field_name => $field_definition) {
if ($field_definition instanceof BaseFieldDefinition && $field_name !== $langcode_key && $field_definition->getProvider() === 'menu_link_content') {
continue;
}
$fields[$field_name] = $link->{$field_name};
}
}
}
}
$links = new LinkCollection([]); $links = new LinkCollection([]);
$resource_object_cacheability = new CacheableMetadata(); $resource_object_cacheability = new CacheableMetadata();
$resource_object_cacheability->addCacheableDependency($menu_link->access); $resource_object_cacheability->addCacheableDependency($menu_link->access);
$resource_object_cacheability->addCacheableDependency($cache); $resource_object_cacheability->addCacheableDependency($cache);
$items[$id] = new ResourceObject($resource_object_cacheability, $resource_type, $id, NULL, $fields, $links); $items[$id] = new ResourceObject($resource_object_cacheability, $resource_type, $id, NULL, $fields, $links, $language);
if ($menu_link->subtree) { if ($menu_link->subtree) {
$this->getMenuItems($menu_link->subtree, $items, $cache); $this->getMenuItems($menu_link->subtree, $items, $cache, $menu);
} }
} }
} }
......
<?php
namespace Drupal\jsonapi_menu_items\Routing;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\jsonapi_menu_items\Resource\MenuItemsResource;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Defines dynamic routes.
*
* Each Menu will result in a jsonapi resource at:
* /{jsonapi_namespace}/menu_items/{menu_id}
*/
class Routes implements ContainerInjectionInterface {
const RESOURCE_NAME = MenuItemsResource::class;
const JSONAPI_RESOURCE_KEY = '_jsonapi_resource';
/**
* {@inheritdoc}
*/
public function __construct() {}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static();
}
/**
* {@inheritdoc}
*/
public function routes() {
$routes = new RouteCollection();
$route = new Route('/%jsonapi%/menu_items/{menu}');
$route->addDefaults([
static::JSONAPI_RESOURCE_KEY => static::RESOURCE_NAME,
]);
$route->setOption('parameters', [
'menu' => [
'type' => 'entity:menu',
],
]);
$routes->add('jsonapi_menu_items.menu', $route);
$routes->addRequirements(['_access' => 'TRUE']);
return $routes;
}
}
...@@ -41,7 +41,8 @@ ...@@ -41,7 +41,8 @@
}, },
"title": "%title", "title": "%title",
"url": "%base_pathmenu_callback_title", "url": "%base_pathmenu_callback_title",
"weight": 0 "weight": 0,
"langcode": "%langcode"
} }
}, },
{ {
......
...@@ -41,7 +41,8 @@ ...@@ -41,7 +41,8 @@
}, },
"title": "%title", "title": "%title",
"url": "%base_pathmenu_callback_title", "url": "%base_pathmenu_callback_title",
"weight": 0 "weight": 0,
"langcode": "%langcode"
} }
}, },
{ {
......
...@@ -41,7 +41,8 @@ ...@@ -41,7 +41,8 @@
}, },
"title": "%title", "title": "%title",
"url": "%base_pathmenu_callback_title", "url": "%base_pathmenu_callback_title",
"weight": 0 "weight": 0,
"langcode": "%langcode"
} }
} }
] ]
......
...@@ -20,7 +20,8 @@ ...@@ -20,7 +20,8 @@
}, },
"title": "%title", "title": "%title",
"url": "%base_pathmenu_callback_title", "url": "%base_pathmenu_callback_title",
"weight": 0 "weight": 0,
"langcode": "%langcode"
} }
}, },
{ {
......
id: jsonapi-menu-items-test2
label: JSON:API menu items test menu 2
description: 'Test menu 2'
langcode: en
locked: true
# eslint-disable yml/no-empty-document
name: JSON:API Menu items test name: JSON:API Menu items test
description: 'Test functionality for JSON:API Menu items' description: 'Test functionality for JSON:API Menu items'
core_version_requirement: ^9.0 || ^10.0
type: module type: module
package: Testing
dependencies: dependencies:
- drupal:menu_link_content - drupal:menu_link_content
- jsonapi_menu_items:jsonapi_menu_items - jsonapi_menu_items:jsonapi_menu_items
...@@ -22,3 +22,10 @@ jsonapi_menu_test.user.logout.disabled: ...@@ -22,3 +22,10 @@ jsonapi_menu_test.user.logout.disabled:
route_name: user.logout route_name: user.logout
enabled: 0 enabled: 0
parent: jsonapi_menu_test.open parent: jsonapi_menu_test.open
# To test access cacheability.
jsonapi_menu_test2.user.access:
title: 'Logout'
menu_name: jsonapi-menu-items-test2
description: 'Logout.'
route_name: user.logout
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
namespace Drupal\Tests\jsonapi_menu_items\Functional; namespace Drupal\Tests\jsonapi_menu_items\Functional;
use Drupal\Component\Serialization\Json; use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\DeprecationHelper;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url; use Drupal\Core\Url;
use Drupal\menu_link_content\Entity\MenuLinkContent; use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\Tests\BrowserTestBase; use Drupal\Tests\BrowserTestBase;
...@@ -19,10 +21,8 @@ class JsonapiMenuItemsTest extends BrowserTestBase { ...@@ -19,10 +21,8 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
/** /**
* The account to use for authentication. * The account to use for authentication.
*
* @var null|\Drupal\Core\Session\AccountInterface
*/ */
protected $account; protected ?AccountInterface $account;
/** /**
* {@inheritdoc} * {@inheritdoc}
...@@ -55,10 +55,9 @@ class JsonapiMenuItemsTest extends BrowserTestBase { ...@@ -55,10 +55,9 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
* @param string $expected_cache_context * @param string $expected_cache_context
* The expected cache context. * The expected cache context.
*/ */
protected function assertCacheContext(array $headers, $expected_cache_context) { protected function assertCacheContext(array $headers, string $expected_cache_context): void {
$cache_contexts = explode(' ', $headers['X-Drupal-Cache-Contexts'][0]); $cache_contexts = explode(' ', $headers['X-Drupal-Cache-Contexts'][0]);
$this $this->assertContains($expected_cache_context, $cache_contexts, "'$expected_cache_context' is present in the X-Drupal-Cache-Contexts header.");
->assertTrue(in_array($expected_cache_context, $cache_contexts), "'" . $expected_cache_context . "' is present in the X-Drupal-Cache-Contexts header.");
} }
/** /**
...@@ -75,7 +74,7 @@ class JsonapiMenuItemsTest extends BrowserTestBase { ...@@ -75,7 +74,7 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
// There are 5 items in this menu - 4 from // There are 5 items in this menu - 4 from
// jsonapi_menu_items_test.links.menu.yml and the content item created // jsonapi_menu_items_test.links.menu.yml and the content item created
// above. One of the four in that file is disabled and should be filtered // above. One of the four in that file is disabled and should be filtered
// out, another is not accesible to the current users. This leaves a total // out, another is not accessible to the current users. This leaves a total
// of 3 items in the response. // of 3 items in the response.
$this->assertCount(3, $content['data']); $this->assertCount(3, $content['data']);
...@@ -83,6 +82,7 @@ class JsonapiMenuItemsTest extends BrowserTestBase { ...@@ -83,6 +82,7 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
'%uuid' => $content_link->uuid(), '%uuid' => $content_link->uuid(),
'%title' => $link_title, '%title' => $link_title,
'%base_path' => Url::fromRoute('<front>')->toString(), '%base_path' => Url::fromRoute('<front>')->toString(),
'%langcode' => 'en',
])); ]));
$this->assertEquals($expected_items['data'], $content['data']); $this->assertEquals($expected_items['data'], $content['data']);
...@@ -112,7 +112,7 @@ class JsonapiMenuItemsTest extends BrowserTestBase { ...@@ -112,7 +112,7 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
$this->drupalLogin($this->account); $this->drupalLogin($this->account);
$link_title = $this->randomMachineName(); $link_title = $this->randomMachineName();
$content_link = $this->createMenuLink($link_title, 'jsonapi_menu_test.user.login'); $this->createMenuLink($link_title, 'jsonapi_menu_test.user.login');
$url = Url::fromRoute('jsonapi_menu_items.menu', [ $url = Url::fromRoute('jsonapi_menu_items.menu', [
'menu' => 'jsonapi-menu-items-test', 'menu' => 'jsonapi-menu-items-test',
...@@ -123,7 +123,11 @@ class JsonapiMenuItemsTest extends BrowserTestBase { ...@@ -123,7 +123,11 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
[$content, $headers] = $this->getJsonApiMenuItemsResponse($url); [$content, $headers] = $this->getJsonApiMenuItemsResponse($url);
self::assertCount(0, $content['data']); self::assertCount(0, $content['data']);
self::assertCacheContext($headers, 'url.query_args:filter'); DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '10.4',
fn() => self::assertCacheContext($headers, 'url.query_args'),
fn() => self::assertCacheContext($headers, 'url.query_args:filter')
);
} }
/** /**
...@@ -144,17 +148,34 @@ class JsonapiMenuItemsTest extends BrowserTestBase { ...@@ -144,17 +148,34 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
[$content, $headers] = $this->getJsonApiMenuItemsResponse($url); [$content, $headers] = $this->getJsonApiMenuItemsResponse($url);
self::assertCount(2, $content['data']); self::assertCount(2, $content['data']);
self::assertCacheContext($headers, 'url.query_args:filter'); DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '10.4',
fn() => self::assertCacheContext($headers, 'url.query_args'),
fn() => self::assertCacheContext($headers, 'url.query_args:filter')
);
$expected_items = Json::decode(strtr(file_get_contents(dirname(__DIR__, 2) . '/fixtures/parents-expected-items.json'), [ $expected_items = Json::decode(strtr(file_get_contents(dirname(__DIR__, 2) . '/fixtures/parents-expected-items.json'), [
'%uuid' => $content_link->uuid(), '%uuid' => $content_link->uuid(),
'%title' => $link_title, '%title' => $link_title,
'%base_path' => Url::fromRoute('<front>')->toString(), '%base_path' => Url::fromRoute('<front>')->toString(),
'%langcode' => 'en',
])); ]));
$content = $this->cleanUrlForTest($content);
self::assertEquals($expected_items['data'], $content['data']); self::assertEquals($expected_items['data'], $content['data']);
} }
/**
* Clear the token from the URL.
*/
public function cleanUrlForTest(array $content): array {
// Remove token from URL since it varies per session, using a default
// token value would result in test failures.
$content['data'] = array_map(fn (array $value) => ['attributes' => ['url' => parse_url($value['attributes']['url'] ?? '', \PHP_URL_PATH)] + $value['attributes']] + $value, $content['data']);
return $content;
}
/** /**
* Tests the JSON:API Menu Items resource with the 'parent' filter. * Tests the JSON:API Menu Items resource with the 'parent' filter.
*/ */
...@@ -170,12 +191,17 @@ class JsonapiMenuItemsTest extends BrowserTestBase { ...@@ -170,12 +191,17 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
[$content, $headers] = $this->getJsonApiMenuItemsResponse($url); [$content, $headers] = $this->getJsonApiMenuItemsResponse($url);
self::assertCount(1, $content['data']); self::assertCount(1, $content['data']);
self::assertCacheContext($headers, 'url.query_args:filter'); DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '10.4',
fn() => self::assertCacheContext($headers, 'url.query_args'),
fn() => self::assertCacheContext($headers, 'url.query_args:filter')
);
$expected_items = Json::decode(strtr(file_get_contents(dirname(__DIR__, 2) . '/fixtures/parent-expected-items.json'), [ $expected_items = Json::decode(strtr(file_get_contents(dirname(__DIR__, 2) . '/fixtures/parent-expected-items.json'), [
'%base_path' => Url::fromRoute('<front>')->toString(), '%base_path' => Url::fromRoute('<front>')->toString(),
])); ]));
$content = $this->cleanUrlForTest($content);
self::assertEquals($expected_items['data'], $content['data']); self::assertEquals($expected_items['data'], $content['data']);
} }
...@@ -197,14 +223,20 @@ class JsonapiMenuItemsTest extends BrowserTestBase { ...@@ -197,14 +223,20 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
[$content, $headers] = $this->getJsonApiMenuItemsResponse($url); [$content, $headers] = $this->getJsonApiMenuItemsResponse($url);
self::assertCount(2, $content['data']); self::assertCount(2, $content['data']);
self::assertCacheContext($headers, 'url.query_args:filter'); DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '10.4',
fn() => self::assertCacheContext($headers, 'url.query_args'),
fn() => self::assertCacheContext($headers, 'url.query_args:filter')
);
$expected_items = Json::decode(strtr(file_get_contents(dirname(__DIR__, 2) . '/fixtures/min-depth-expected-items.json'), [ $expected_items = Json::decode(strtr(file_get_contents(dirname(__DIR__, 2) . '/fixtures/min-depth-expected-items.json'), [
'%uuid' => $content_link->uuid(), '%uuid' => $content_link->uuid(),
'%title' => $link_title, '%title' => $link_title,
'%base_path' => Url::fromRoute('<front>')->toString(), '%base_path' => Url::fromRoute('<front>')->toString(),
'%langcode' => 'en',
])); ]));
$content = $this->cleanUrlForTest($content);
self::assertEquals($expected_items['data'], $content['data']); self::assertEquals($expected_items['data'], $content['data']);
$url = Url::fromRoute('jsonapi_menu_items.menu', [ $url = Url::fromRoute('jsonapi_menu_items.menu', [
...@@ -234,12 +266,16 @@ class JsonapiMenuItemsTest extends BrowserTestBase { ...@@ -234,12 +266,16 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
[$content, $headers] = $this->getJsonApiMenuItemsResponse($url); [$content, $headers] = $this->getJsonApiMenuItemsResponse($url);
self::assertCount(3, $content['data']); self::assertCount(3, $content['data']);
self::assertCacheContext($headers, 'url.query_args:filter'); DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '10.4',
fn() => self::assertCacheContext($headers, 'url.query_args'),
fn() => self::assertCacheContext($headers, 'url.query_args:filter')
);
$expected_items = Json::decode(strtr(file_get_contents(dirname(__DIR__, 2) . '/fixtures/max-depth-expected-items.json'), [ $expected_items = Json::decode(strtr(file_get_contents(dirname(__DIR__, 2) . '/fixtures/max-depth-expected-items.json'), [
'%uuid' => $content_link->uuid(), '%uuid' => $content_link->uuid(),
'%title' => $link_title, '%title' => $link_title,
'%base_path' => Url::fromRoute('<front>')->toString(), '%base_path' => Url::fromRoute('<front>')->toString(),
'%langcode' => 'en',
])); ]));
self::assertEquals($expected_items['data'], $content['data']); self::assertEquals($expected_items['data'], $content['data']);
...@@ -273,7 +309,10 @@ class JsonapiMenuItemsTest extends BrowserTestBase { ...@@ -273,7 +309,10 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
[$content, $headers] = $this->getJsonApiMenuItemsResponse($url); [$content, $headers] = $this->getJsonApiMenuItemsResponse($url);
self::assertCount(2, $content['data']); self::assertCount(2, $content['data']);
self::assertCacheContext($headers, 'url.query_args:filter'); DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '10.4',
fn() => self::assertCacheContext($headers, 'url.query_args'),
fn() => self::assertCacheContext($headers, 'url.query_args:filter')
);
$expected_items = Json::decode(strtr(file_get_contents(dirname(__DIR__, 2) . '/fixtures/conditions-expected-items.json'), [ $expected_items = Json::decode(strtr(file_get_contents(dirname(__DIR__, 2) . '/fixtures/conditions-expected-items.json'), [
'%base_path' => Url::fromRoute('<front>')->toString(), '%base_path' => Url::fromRoute('<front>')->toString(),
...@@ -282,6 +321,26 @@ class JsonapiMenuItemsTest extends BrowserTestBase { ...@@ -282,6 +321,26 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
self::assertEquals($expected_items['data'], $content['data']); self::assertEquals($expected_items['data'], $content['data']);
} }
/**
* Tests the JSON:API Menu Items resource.
*/
public function testJsonapiMenuItemsResourceCacheabilityBubbling() {
$url = Url::fromRoute('jsonapi_menu_items.menu', [
'menu' => 'jsonapi-menu-items-test2',
]);
[$content, $headers] = $this->getJsonApiMenuItemsResponse($url);
// There are 0 items in this menu because the anonymous user does not have
// access to logout.
$this->assertCount(0, $content['data']);
$this->assertCacheContext($headers, 'user.roles:authenticated');
$this->drupalLogin($this->account);
[$content, $headers] = $this->getJsonApiMenuItemsResponse($url);
// There is 1 item in this menu because a user does have access to logout.
$this->assertCount(1, $content['data']);
$this->assertCacheContext($headers, 'user.roles:authenticated');
}
/** /**
* Create menu link. * Create menu link.
* *
...@@ -290,10 +349,10 @@ class JsonapiMenuItemsTest extends BrowserTestBase { ...@@ -290,10 +349,10 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
* @param string $parent * @param string $parent
* The menu link parent id. * The menu link parent id.
* *
* @return Drupal\menu_link_content\Entity\MenuLinkContent * @return \Drupal\menu_link_content\Entity\MenuLinkContent
* The menu link. * The menu link.
*/ */
protected function createMenuLink(string $title, string $parent) { protected function createMenuLink(string $title, string $parent): MenuLinkContent {
$content_link = MenuLinkContent::create([ $content_link = MenuLinkContent::create([
'link' => ['uri' => 'route:menu_test.menu_callback_title'], 'link' => ['uri' => 'route:menu_test.menu_callback_title'],
'langcode' => 'en', 'langcode' => 'en',
...@@ -317,7 +376,7 @@ class JsonapiMenuItemsTest extends BrowserTestBase { ...@@ -317,7 +376,7 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
* @return array * @return array
* The response document and headers. * The response document and headers.
*/ */
protected function getJsonApiMenuItemsResponse(Url $url) { protected function getJsonApiMenuItemsResponse(Url $url): array {
$request_options = []; $request_options = [];
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json'; $request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
......