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

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
23 results
Select Git revision
Show changes
Commits on Source (7)
Showing
with 679 additions and 50 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'
......@@ -11,6 +11,7 @@
- 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.
- 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
......
......@@ -10,7 +10,12 @@
"homepage": "https://www.drupal.org/project/jsonapi_menu_items",
"minimum-stability": "dev",
"require": {
"drupal/jsonapi_resources": "^1.0",
"drupal/core": "^9.5 || ^10 || ^11"
},
"require-dev": {
"drupal/jsonapi_hypermedia": "^1.6",
"drupal/jsonapi_resources": "^1.0"
"drupal/menu_link_config": "^1.0",
"drupal/menu_item_extras": "^3.0"
}
}
name: 'JSON:API Menu items'
description: Adds a JSON API resource for menu items.
type: module
core_version_requirement: ^8.8 || ^9 || ^10
core_version_requirement: ^9.5 || ^10 || ^11
dependencies:
- drupal:menu_link_content
- jsonapi_hypermedia:jsonapi_hypermedia
- jsonapi_resources:jsonapi_resources
<?php
/**
* @file
* Install, update and uninstall functions for the module.
*/
/**
* Install new submodule for hypermedia integration.
*/
function jsonapi_menu_items_update_8001() {
if (\Drupal::moduleHandler()->moduleExists('jsonapi_hypermedia')) {
\Drupal::service('module_installer')->install(['jsonapi_menu_items_hypermedia']);
}
}
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.'
name: 'JSON:API Menu items Hypermedia'
description: Integrates jsonapi_menu_items and jsonapi_hypermedia.
type: module
core_version_requirement: ^9.5 || ^10 || ^11
dependencies:
- jsonapi_menu_items:jsonapi_menu_items
- jsonapi_hypermedia:jsonapi_hypermedia
<?php
namespace Drupal\jsonapi_menu_items\Plugin\Derivative;
namespace Drupal\jsonapi_menu_items_hypermedia\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityStorageInterface;
......
<?php
namespace Drupal\jsonapi_menu_items\Plugin\jsonapi_hypermedia\LinkProvider;
namespace Drupal\jsonapi_menu_items_hypermedia\Plugin\jsonapi_hypermedia\LinkProvider;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\CacheableMetadata;
......@@ -16,7 +16,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*
* @JsonapiHypermediaLinkProvider(
* id = "jsonapi_menu_items.top_level.menu_items",
* deriver = "Drupal\jsonapi_menu_items\Plugin\Derivative\MenuItemsLinkProviderDeriver",
* deriver = "Drupal\jsonapi_menu_items_hypermedia\Plugin\Derivative\MenuItemsLinkProviderDeriver",
* link_relation_type = "menu_items",
* )
*/
......
<?php
namespace Drupal\Tests\jsonapi_menu_items\Functional;
namespace Drupal\Tests\jsonapi_menu_items_hypermedia\Functional;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Url;
......@@ -12,7 +12,7 @@ use GuzzleHttp\RequestOptions;
/**
* Tests JSON:API Hypermedia integration.
*
* @group jsonapi_menu_items
* @group jsonapi_menu_items_hypermedia
* @requires jsonapi_hypermedia
*/
final class HypermediaIntegrationTest extends BrowserTestBase {
......@@ -31,12 +31,13 @@ final class HypermediaIntegrationTest extends BrowserTestBase {
protected static $modules = [
'jsonapi_hypermedia',
'jsonapi_menu_items',
'jsonapi_menu_items_hypermedia',
];
/**
* Tests the `menu_items` links.
*/
public function testMenuItemsLinks() {
public function testMenuItemsLinks(): void {
$url = Url::fromRoute('jsonapi.resource_list');
$request_options = [];
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
......
......@@ -4,14 +4,24 @@ namespace Drupal\jsonapi_menu_items\Resource;
use Drupal\Core\Access\AccessResultInterface;
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\Menu\MenuLinkTreeInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\jsonapi\JsonApiResource\LinkCollection;
use Drupal\jsonapi\JsonApiResource\ResourceObject;
use Drupal\jsonapi\JsonApiResource\ResourceObjectData;
use Drupal\jsonapi\ResourceResponse;
use Drupal\jsonapi_resources\Resource\ResourceBase;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\menu_link_content\Plugin\Menu\MenuLinkContent;
use Drupal\system\MenuInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
......@@ -20,7 +30,7 @@ use Symfony\Component\Routing\Route;
*
* @internal
*/
final class MenuItemsResource extends ResourceBase {
final class MenuItemsResource extends ResourceBase implements ContainerInjectionInterface {
/**
* A list of menu items.
......@@ -29,6 +39,76 @@ final class MenuItemsResource extends ResourceBase {
*/
protected $menuItems = [];
/**
* The menu tree.
*
* @var \Drupal\system\MenuInterface
*/
private $menuLinkTree;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
private $entityTypeManager;
/**
* The entity field manager service.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
private $entityFieldManager;
/**
* The cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
private $cache;
/**
* The entity repository.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
private $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.
*
......@@ -54,12 +134,13 @@ final class MenuItemsResource extends ResourceBase {
}
$parameters->onlyEnabledLinks();
$menu_tree = \Drupal::menuTree();
$tree = $menu_tree->load($menu->id(), $parameters);
$tree = $this->menuLinkTree->load($menu->id(), $parameters);
if (empty($tree)) {
$response = $this->createJsonapiResponse(new ResourceObjectData([]), $request, 200, []);
$response->addCacheableDependency($cacheability);
if ($response instanceof CacheableResponseInterface) {
$response->addCacheableDependency($cacheability);
}
return $response;
}
......@@ -69,13 +150,15 @@ final class MenuItemsResource extends ResourceBase {
// Use the default sorting of menu links.
['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);
$response = $this->createJsonapiResponse($data, $request, 200, [] /* , $pagination_links */);
$response->addCacheableDependency($cacheability);
if ($response instanceof CacheableResponseInterface) {
$response->addCacheableDependency($cacheability);
}
return $response;
}
......@@ -84,14 +167,48 @@ final class MenuItemsResource extends ResourceBase {
* {@inheritdoc}
*/
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'];
}
$menu_link_content_definition = $this->entityTypeManager->getDefinition('menu_link_content');
$menu_link_content_bundle_entity_type = $menu_link_content_definition->get('bundle_entity_type');
if ($this->entityTypeManager->hasDefinition($menu_link_content_bundle_entity_type)) {
$bundles = $this->entityTypeManager
->getStorage($menu_link_content_bundle_entity_type)
->getQuery()
->accessCheck(FALSE)
->execute();
$possible_resource_types['menu_link_content'] = $bundles;
}
foreach (['menu_link_config', 'menu_link_content'] as $type) {
$resource_type = $this->resourceTypeRepository->get($type, $type);
if ($resource_type) {
$resource_types[] = $resource_type;
// 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;
}
......@@ -107,7 +224,7 @@ final class MenuItemsResource extends ResourceBase {
* The Menu Tree Parameters object.
*/
protected function applyFiltersToParams(Request $request, MenuTreeParameters $parameters) {
$filter = $request->query->get('filter');
$filter = $request->query->all('filter');
if (!empty($filter['min_depth'])) {
$parameters->setMinDepth((int) $filter['min_depth']);
......@@ -142,39 +259,42 @@ final class MenuItemsResource extends ResourceBase {
/**
* Generate the menu items.
*
* @param array $tree
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* The menu tree.
* @param array $items
* The already created items.
* @param \Drupal\Core\Cache\CacheableMetadata $cache
* 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) {
$menu_link_content_storage = $this->entityTypeManager->getStorage('menu_link_content');
foreach ($tree as $menu_link) {
if ($menu_link->access !== NULL && !$menu_link->access instanceof AccessResultInterface) {
throw new \DomainException('MenuLinkTreeElement::access must be either NULL or an AccessResultInterface object.');
}
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.
if ($menu_link->access instanceof AccessResultInterface && !$menu_link->access->isAllowed()) {
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:
// @todo Use a custom resource type?
$id = $menu_link->link->getPluginId();
[$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');
}
}
$url = $menu_link->link->getUrlObject()->toString(TRUE);
......@@ -198,6 +318,28 @@ final class MenuItemsResource extends ResourceBase {
'url' => $url->getGeneratedUrl(),
'weight' => (int) $menu_link->link->getWeight(),
];
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);
$field_definitions = $this->entityFieldManager->getFieldDefinitions($link->getEntityTypeId(), $link->bundle());
foreach ($field_definitions as $field_name => $field_definition) {
if ($field_definition instanceof BaseFieldDefinition && $field_definition->getProvider() === 'menu_link_content') {
continue;
}
$fields[$field_name] = $link->{$field_name};
}
}
}
}
$links = new LinkCollection([]);
$resource_object_cacheability = new CacheableMetadata();
......@@ -206,7 +348,7 @@ final class MenuItemsResource extends ResourceBase {
$items[$id] = new ResourceObject($resource_object_cacheability, $resource_type, $id, NULL, $fields, $links);
if ($menu_link->subtree) {
$this->getMenuItems($menu_link->subtree, $items, $cache);
$this->getMenuItems($menu_link->subtree, $items, $cache, $menu);
}
}
}
......
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
description: 'Test functionality for JSON:API Menu items'
core_version_requirement: ^8.8 || ^9.0 || ^10
type: module
package: Testing
dependencies:
- drupal:menu_link_content
- jsonapi_menu_items:jsonapi_menu_items
......@@ -22,3 +22,10 @@ jsonapi_menu_test.user.logout.disabled:
route_name: user.logout
enabled: 0
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,7 @@
namespace Drupal\Tests\jsonapi_menu_items\Functional;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\DeprecationHelper;
use Drupal\Core\Url;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\Tests\BrowserTestBase;
......@@ -57,8 +58,7 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
*/
protected function assertCacheContext(array $headers, $expected_cache_context) {
$cache_contexts = explode(' ', $headers['X-Drupal-Cache-Contexts'][0]);
$this
->assertTrue(in_array($expected_cache_context, $cache_contexts), "'" . $expected_cache_context . "' is present in the X-Drupal-Cache-Contexts header.");
$this->assertContains($expected_cache_context, $cache_contexts, "'$expected_cache_context' is present in the X-Drupal-Cache-Contexts header.");
}
/**
......@@ -75,7 +75,7 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
// There are 5 items in this menu - 4 from
// 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
// 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.
$this->assertCount(3, $content['data']);
......@@ -112,18 +112,22 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
$this->drupalLogin($this->account);
$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', [
'menu' => 'jsonapi-menu-items-test',
'filter' => [
'parent' => "fake-item",
'parents' => "fake_item",
],
]);
[$content, $headers] = $this->getJsonApiMenuItemsResponse($url);
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,7 +148,10 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
[$content, $headers] = $this->getJsonApiMenuItemsResponse($url);
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'), [
'%uuid' => $content_link->uuid(),
......@@ -152,9 +159,22 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
'%base_path' => Url::fromRoute('<front>')->toString(),
]));
$content = $this->cleanUrlForTest($content);
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.
*/
......@@ -170,12 +190,17 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
[$content, $headers] = $this->getJsonApiMenuItemsResponse($url);
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'), [
'%base_path' => Url::fromRoute('<front>')->toString(),
]));
$content = $this->cleanUrlForTest($content);
self::assertEquals($expected_items['data'], $content['data']);
}
......@@ -197,7 +222,10 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
[$content, $headers] = $this->getJsonApiMenuItemsResponse($url);
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'), [
'%uuid' => $content_link->uuid(),
......@@ -205,6 +233,8 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
'%base_path' => Url::fromRoute('<front>')->toString(),
]));
$content = $this->cleanUrlForTest($content);
self::assertEquals($expected_items['data'], $content['data']);
$url = Url::fromRoute('jsonapi_menu_items.menu', [
......@@ -234,7 +264,10 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
[$content, $headers] = $this->getJsonApiMenuItemsResponse($url);
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'), [
'%uuid' => $content_link->uuid(),
......@@ -273,7 +306,10 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
[$content, $headers] = $this->getJsonApiMenuItemsResponse($url);
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'), [
'%base_path' => Url::fromRoute('<front>')->toString(),
......@@ -282,6 +318,26 @@ class JsonapiMenuItemsTest extends BrowserTestBase {
self::assertEquals($expected_items['data'], $content['data']);
}
/**
* Tests the JSON:API Menu Items resource.
*/
public function testJsonapiMenuItemsResourceCacheabilltyBubbling() {
$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.
*
......
<?php
declare(strict_types=1);
namespace Drupal\Tests\jsonapi_menu_items\Kernel;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel;
use Drupal\jsonapi\JsonApiResource\ResourceObject;
use Drupal\jsonapi\Normalizer\Value\CacheableNormalization;
use Drupal\jsonapi\ResourceType\ResourceType;
use Drupal\jsonapi_menu_items\Resource\MenuItemsResource;
use Drupal\KernelTests\KernelTestBase;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\system\Entity\Menu;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
/**
* Tests MenuItemsResource.
*
* @group jsonapi_menu_items
* @coversDefaultClass \Drupal\jsonapi_menu_items\Resource\MenuItemsResource
*/
final class MenuItemsResourceTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'user',
'system',
'file',
'link',
'serialization',
'jsonapi',
'jsonapi_resources',
'menu_link_content',
'jsonapi_menu_items',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('menu_link_content');
}
/**
* Tests getRouteResourceTypes.
*
* @param string[] $extra_modules
* Any additional modules to install.
* @param string[] $expected_resource_types
* The expected resource types.
*
* @dataProvider dataGetRouteResourceTypes
*
* @covers ::getRouteResourceTypes
*/
public function testGetRouteResourceTypes(array $extra_modules, array $expected_resource_types): void {
if (count($extra_modules) > 0) {
$this->container->get('module_installer')->install($extra_modules);
}
Menu::create([
'id' => 'menu-test',
'label' => 'Test menu',
'description' => 'Description text',
])->save();
$this->container->get('entity_type.bundle.info')->clearCachedBundles();
$sut = $this->getSut();
$resource_types = $sut->getRouteResourceTypes(new Route('/'), 'foo');
$resource_type_ids = array_map(
static fn (ResourceType $type) => $type->getTypeName(),
$resource_types
);
self::assertEquals($resource_type_ids, $expected_resource_types);
}
/**
* Data for testGetRouteResourceTypes.
*
* @return array[]
* The test cases.
*/
public static function dataGetRouteResourceTypes(): array {
return [
'menu_link_content only' => [
[],
['menu_link_content--menu_link_content'],
],
'menu_link_config' => [
['menu_link_config'],
['menu_link_content--menu_link_content', 'menu_link_config--menu_link_config'],
],
'menu_item_extras' => [
['menu_item_extras'],
['menu_link_content--menu-test'],
],
'menu_link_config and menu_item_extras' => [
['menu_link_config', 'menu_item_extras'],
['menu_link_content--menu-test', 'menu_link_config--menu_link_config'],
],
];
}
/**
* Tests process.
*
* @dataProvider dataProcess
* @covers ::process
*/
public function testProcess(array $extra_modules, array $expected_resource_objects): void {
$this->container->get('module_installer')->install(
array_merge(['menu_test', 'jsonapi_menu_items_test'], $extra_modules)
);
MenuLinkContent::create([
'uuid' => '5d0a9864-d151-4e8f-9f72-b573446ba1d6',
'id' => 'llama',
'title' => 'Llama Gabilondo',
'description' => 'Llama Gabilondo',
'link' => 'https://nl.wikipedia.org/wiki/Llama',
'weight' => 0,
'menu_name' => 'jsonapi-menu-items-test',
])->save();
$this->container->get('entity_type.bundle.info')->clearCachedBundles();
$sut = $this->getSut();
$request = Request::create('/jsonapi/menu_items/jsonapi-menu-items-test');
$menu = Menu::load('jsonapi-menu-items-test');
self::assertNotNull($menu);
$response = $sut->process($request, $menu);
$top_level = $response->getResponseData();
self::assertInstanceOf(JsonApiDocumentTopLevel::class, $top_level);
$resource_objects = array_map(
static fn (ResourceObject $object) => [
'resource_type' => $object->getTypeName(),
'id' => $object->getId(),
],
$top_level->getData()->toArray()
);
self::assertEquals($expected_resource_objects, $resource_objects);
}
/**
* Data for testProcess.
*
* @return array[]
* The test data.
*/
public static function dataProcess(): array {
return [
'menu_link_content' => [
[],
[
[
'resource_type' => 'menu_link_content--menu_link_content',
'id' => 'jsonapi_menu_test.open',
],
[
'resource_type' => 'menu_link_content--menu_link_content',
'id' => 'menu_link_content:5d0a9864-d151-4e8f-9f72-b573446ba1d6',
],
[
'resource_type' => 'menu_link_content--menu_link_content',
'id' => 'jsonapi_menu_test.user.login',
],
],
],
'menu_item_extras' => [
['menu_item_extras'],
[
[
'resource_type' => 'menu_link_content--jsonapi-menu-items-test',
'id' => 'jsonapi_menu_test.open',
],
[
'resource_type' => 'menu_link_content--jsonapi-menu-items-test',
'id' => 'menu_link_content:5d0a9864-d151-4e8f-9f72-b573446ba1d6',
],
[
'resource_type' => 'menu_link_content--jsonapi-menu-items-test',
'id' => 'jsonapi_menu_test.user.login',
],
],
],
];
}
/**
* Tests with menu_item_extras and fields added to the menu.
*/
public function testMenuItemExtrasFields(): void {
$this->container->get('module_installer')->install([
'menu_test',
'jsonapi_menu_items_test',
'menu_item_extras',
]);
$this->container->get('entity_type.bundle.info')->clearCachedBundles();
FieldStorageConfig::create([
'field_name' => 'test_field',
'type' => 'string',
'entity_type' => 'menu_link_content',
'cardinality' => 1,
])->save();
FieldConfig::create([
'entity_type' => 'menu_link_content',
'field_name' => 'test_field',
'bundle' => 'jsonapi-menu-items-test',
'label' => 'Test field',
])->save();
MenuLinkContent::create([
'uuid' => '5d0a9864-d151-4e8f-9f72-b573446ba1d6',
'id' => 'llama',
'title' => 'Llama Gabilondo',
'description' => 'Llama Gabilondo',
'link' => 'https://nl.wikipedia.org/wiki/Llama',
'weight' => 0,
'menu_name' => 'jsonapi-menu-items-test',
'test_field' => 'foo bar baz',
'view_mode' => 'default',
])->save();
$sut = $this->getSut();
$request = Request::create('/jsonapi/menu_items/jsonapi-menu-items-test');
$menu = Menu::load('jsonapi-menu-items-test');
self::assertNotNull($menu);
$response = $sut->process($request, $menu);
$normalized = $this->container->get('jsonapi.serializer')->normalize(
$response->getResponseData(),
'api_json',
[
'account' => NULL,
'sparse_fieldset' => NULL,
]
);
self::assertInstanceOf(CacheableNormalization::class, $normalized);
$data = $normalized->getNormalization();
self::assertEquals([
[
'type' => 'menu_link_content--jsonapi-menu-items-test',
'id' => 'jsonapi_menu_test.open',
'attributes' => [
'description' => 'Home.',
'enabled' => TRUE,
'expanded' => FALSE,
'menu_name' => 'jsonapi-menu-items-test',
'meta' => [],
'options' => [],
'parent' => '',
'provider' => 'jsonapi_menu_items_test',
'route' => [
'name' => 'menu_test.menu_name_test',
'parameters' => [],
],
'title' => 'Home',
'url' => '/menu_name_test',
'weight' => -10,
],
],
[
'type' => 'menu_link_content--jsonapi-menu-items-test',
'id' => 'menu_link_content:5d0a9864-d151-4e8f-9f72-b573446ba1d6',
'attributes' => [
'description' => 'Llama Gabilondo',
'enabled' => TRUE,
'expanded' => FALSE,
'menu_name' => 'jsonapi-menu-items-test',
'meta' => [
'entity_id' => '1',
],
'options' => [
'external' => TRUE,
],
'parent' => '',
'provider' => 'menu_link_content',
'route' => [
'name' => '',
'parameters' => [],
],
'title' => 'Llama Gabilondo',
'url' => 'https://nl.wikipedia.org/wiki/Llama',
'weight' => 0,
'test_field' => 'foo bar baz',
'view_mode' => 'default',
],
],
[
'type' => 'menu_link_content--jsonapi-menu-items-test',
'id' => 'jsonapi_menu_test.user.login',
'attributes' => [
'description' => 'Login.',
'enabled' => TRUE,
'expanded' => FALSE,
'menu_name' => 'jsonapi-menu-items-test',
'meta' => [],
'options' => [],
'parent' => '',
'provider' => 'jsonapi_menu_items_test',
'route' => [
'name' => 'user.login',
'parameters' => [],
],
'title' => 'Login',
'url' => '/user/login',
'weight' => 0,
],
],
], $data['data']);
}
/**
* Gets the subject under test.
*
* @return \Drupal\jsonapi_menu_items\Resource\MenuItemsResource
* The subject under test.
*/
private function getSut(): MenuItemsResource {
$sut = MenuItemsResource::create($this->container);
$sut->setResourceTypeRepository($this->container->get('jsonapi.resource_type.repository'));
$sut->setResourceResponseFactory($this->container->get('jsonapi_resources.resource_response_factory'));
return $sut;
}
}