class LinksetController

Same name and namespace in other branches
  1. 11.x core/modules/system/src/Controller/LinksetController.php \Drupal\system\Controller\LinksetController

Linkset controller.

Provides a menu endpoint.

@internal This class's API is internal and it is not intended for extension.

Hierarchy

Expanded class hierarchy of LinksetController

File

core/modules/system/src/Controller/LinksetController.php, line 24

Namespace

Drupal\system\Controller
View source
final class LinksetController extends ControllerBase {
  
  /**
   * Linkset constructor.
   *
   * @param \Drupal\Core\Menu\MenuLinkTreeInterface $menuTree
   *   The menu tree loader service. This is used to load a menu's link
   *   elements so that they can be serialized into a linkset response.
   */
  public function __construct(protected readonly MenuLinkTreeInterface $menuTree) {
  }
  
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static($container->get('menu.link_tree'));
  }
  
  /**
   * Serve linkset requests.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   An HTTP request.
   * @param \Drupal\system\MenuInterface $menu
   *   A menu for which to produce a linkset.
   *
   * @return \Drupal\Core\Cache\CacheableJsonResponse
   *   A linkset response.
   */
  public function process(Request $request, MenuInterface $menu) {
    // Load the given menu's tree of elements.
    $tree = $this->loadMenuTree($menu);
    // Get the incoming request URI and parse it so the linkset can use a
    // relative URL for the linkset anchor.
    [
      'path' => $path,
      'query' => $query,
    ] = parse_url($request->getUri()) + [
      'query' => FALSE,
    ];
    // Construct a relative URL.
    $anchor = $path . (!empty($query) ? '?' . $query : '');
    $cacheability = CacheableMetadata::createFromObject($menu);
    // Encode the menu tree as links in the application/linkset+json media type
    // and add the machine name of the menu to which they belong.
    $menu_id = $menu->id();
    $links = $this->toLinkTargetObjects($tree, $cacheability);
    foreach ($links as $rel => $target_objects) {
      $links[$rel] = array_map(function (array $target) use ($menu_id) {
        // According to the Linkset specification, this member must be an array
        // since the "machine-name" target attribute is non-standard.
        // See https://tools.ietf.org/html/draft-ietf-httpapi-linkset-08#section-4.2.4.3
        return $target + [
          'machine-name' => [
            $menu_id,
          ],
        ];
      }, $target_objects);
    }
    $linkset = !empty($tree) ? [
      [
        'anchor' => $anchor,
      ] + $links,
    ] : [];
    $data = [
      'linkset' => $linkset,
    ];
    // Set the response content-type header.
    $headers = [
      'content-type' => 'application/linkset+json',
    ];
    $response = new CacheableJsonResponse($data, 200, $headers);
    // Attach cacheability metadata to the response.
    $response->addCacheableDependency($cacheability);
    return $response;
  }
  
  /**
   * Encode a menu tree as link items and capture any cacheability metadata.
   *
   * This method recursively traverses the given menu tree to produce a flat
   * array of link items encoded according the application/linkset+json
   * media type.
   *
   * To preserve hierarchical information, the target attribute contains a
   * `hierarchy` member. Its value is an array containing the position of a link
   * within a particular sub-tree prepended by the positions of its ancestors,
   * and can be used to reconstruct a hierarchical data structure.
   *
   * The reason that a `hierarchy` member is used instead of a `parent` or
   * `children` member is because it is more compact, more suited to the linkset
   * media type, and because it simplifies many menu operations. Specifically:
   *
   * 1. Creating a `parent` member would require each link to have an `id`
   *    in order to have something referenceable by the `parent` member. Reusing
   *    the link plugin IDs would not be viable because it would leak
   *    information about which modules are installed on the site. Therefore,
   *    this ID would have to be invented and would probably end up looking a
   *    lot like the `hierarchy` value. Finally, link IDs would encourage
   *    clients to hardcode the ID instead of using link relation types
   *    appropriately.
   * 2. The linkset media type is not itself hierarchical. This means that
   *    `children` is infeasible without inventing our own Drupal-specific media
   *    type.
   * 3. The `hierarchy` member can be used to efficiently perform tree
   *    operations that would otherwise be more complicated to implement. For
   *    example, by comparing the first X amount of hierarchy levels, you can
   *    find any subtree without writing recursive logic or complicated loops.
   *    Visit the URL below for more examples.
   *
   * The structure of a `hierarchy` value is defined below.
   *
   * A link which is a child of another link will always be prefixed by the
   * exact value of their parent's hierarchy member. For example, if a link /bar
   * is a child of a link /foo and /foo has a hierarchy member with the value
   * ["1"], then the link /bar might have a hierarchy member with the value
   * ["1", "0"]. The link /foo can be said to have depth 1, while the link
   * /bar can be said to have depth 2.
   *
   * Links which have the same parent (or no parent) have their relative order
   * preserved in the final component of the hierarchy value.
   *
   * According to the Linkset specification, each value in the hierarchy array
   * must be a string. See https://tools.ietf.org/html/draft-ietf-httpapi-linkset-08#section-4.2.4.3
   *
   * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
   *   A tree of menu elements.
   * @param \Drupal\Core\Cache\RefinableCacheableDependencyInterface $cacheability
   *   An object to capture any cacheability metadata.
   * @param array $hierarchy_ancestors
   *   (Internal use only) The hierarchy value of the parent element
   *   if $tree is a subtree. Do not pass this value.
   *
   * @return array
   *   An array which can be JSON-encoded to represent the given link tree.
   *
   * @see https://www.drupal.org/project/decoupled_menus/issues/3204132#comment-14439385
   */
  protected function toLinkTargetObjects(array $tree, RefinableCacheableDependencyInterface $cacheability, $hierarchy_ancestors = []) : array {
    $links = [];
    // Calling array_values() discards any key names so that $index will be
    // numerical.
    foreach (array_values($tree) as $index => $element) {
      // Extract and preserve the access cacheability metadata.
      $element_access = $element->access;
      assert($element_access instanceof AccessResultInterface);
      $cacheability->addCacheableDependency($element_access);
      // If an element is not accessible, it should not be encoded. Its
      // cacheability should be preserved regardless, which is why that is done
      // outside of this conditional.
      if ($element_access->isAllowed()) {
        // Get and generate the URL of the link's target. This can create
        // cacheability metadata also.
        $url = $element->link
          ->getUrlObject();
        $generated_url = $url->toString(TRUE);
        $cacheability = $cacheability->addCacheableDependency($generated_url);
        // Take the hierarchy value for the current element and append it
        // to the link element parent's hierarchy value. See this method's
        // docblock for more context on why this value is the way it is.
        $hierarchy = $hierarchy_ancestors;
        array_push($hierarchy, strval($index));
        $link_options = $element->link
          ->getOptions();
        $link_attributes = $link_options['attributes'] ?? [];
        $link_rel = $link_attributes['rel'] ?? 'item';
        // Encode the link.
        $link = [
          'href' => $generated_url->getGeneratedUrl(),
          // @todo should this use the "title*" key if it is internationalized?
          // Follow up issue:
          // https://www.drupal.org/project/decoupled_menus/issues/3280735
'title' => $element->link
            ->getTitle(),
          'hierarchy' => $hierarchy,
        ];
        $this->processCustomLinkAttributes($link, $link_attributes);
        $links[$link_rel][] = $link;
        // Recurse into the element's subtree.
        if (!empty($element->subtree)) {
          // Recursion!
          $links = array_merge_recursive($links, $this->toLinkTargetObjects($element->subtree, $cacheability, $hierarchy));
        }
      }
    }
    return $links;
  }
  
  /**
   * Process custom link parameters.
   *
   * Since the values for attributes are dynamic and we can't
   * guarantee that they adhere to the linkset specification,
   * we do some custom processing as follows,
   * 1. Transform all of them into an array if
   *    they are not already an array.
   * 2. Transform all non-string values into strings
   *    (e.g. ["42"] instead of [42])
   * 3. Ignore (for now) any keys that are already specified.
   *    Namely: hreflang, media, type, title, and title*.
   * 4. Ensure that custom names do not contain an
   *    asterisk and ignore them if they do.
   * 5. These attributes require special handling. For instance,
   *    these parameters must be strings instead of an array of strings.
   *
   * NOTE: Values which are not object/array are cast to string.
   *
   * @param array $link
   *   Link structure.
   * @param array $attributes
   *   Attributes available for the link.
   */
  private function processCustomLinkAttributes(array &$link, array $attributes = []) {
    $attribute_keys_to_ignore = [
      'hreflang',
      'media',
      'type',
      'title',
      'title*',
    ];
    foreach ($attributes as $key => $value) {
      if (in_array($key, $attribute_keys_to_ignore, TRUE)) {
        continue;
      }
      // Skip the attribute key if it has an asterisk (*).
      if (str_contains($key, '*')) {
        continue;
      }
      // Skip the value if it is an object.
      if (is_object($value)) {
        continue;
      }
      // See https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-linkset-03#section-4.2.4.3
      // Values for custom attributes must follow these rules,
      // - Values MUST be array.
      // - Each item in the array MUST be a string.
      if (is_array($value)) {
        $link[$key] = [];
        foreach ($value as $val) {
          if (is_object($val) || is_array($val)) {
            continue;
          }
          $link[$key][] = (string) $val;
        }
      }
      else {
        $link[$key] = [
          (string) $value,
        ];
      }
    }
  }
  
  /**
   * Loads a menu tree.
   *
   * @param \Drupal\system\MenuInterface $menu
   *   A menu for which a tree should be loaded.
   *
   * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
   *   A menu link tree.
   */
  protected function loadMenuTree(MenuInterface $menu) : array {
    $parameters = new MenuTreeParameters();
    $parameters->onlyEnabledLinks();
    $parameters->setMinDepth(0);
    $tree = $this->menuTree
      ->load($menu->id(), $parameters);
    $manipulators = [
      [
        'callable' => 'menu.default_tree_manipulators:checkAccess',
      ],
      [
        'callable' => 'menu.default_tree_manipulators:generateIndexAndSort',
      ],
    ];
    return $this->menuTree
      ->transform($tree, $manipulators);
  }

}

Members

Title Sort descending Modifiers Object type Summary Overriden Title Overrides
ControllerBase::$configFactory protected property The configuration factory.
ControllerBase::$currentUser protected property The current user service. 2
ControllerBase::$entityFormBuilder protected property The entity form builder.
ControllerBase::$entityTypeManager protected property The entity type manager.
ControllerBase::$formBuilder protected property The form builder. 1
ControllerBase::$keyValue protected property The key-value storage. 1
ControllerBase::$languageManager protected property The language manager. 1
ControllerBase::$moduleHandler protected property The module handler. 1
ControllerBase::$stateService protected property The state service.
ControllerBase::cache protected function Returns the requested cache bin.
ControllerBase::config protected function Retrieves a configuration object.
ControllerBase::container private function Returns the service container.
ControllerBase::currentUser protected function Returns the current user. 2
ControllerBase::entityFormBuilder protected function Retrieves the entity form builder.
ControllerBase::entityTypeManager protected function Retrieves the entity type manager.
ControllerBase::formBuilder protected function Returns the form builder service. 1
ControllerBase::keyValue protected function Returns a key/value storage collection. 1
ControllerBase::languageManager protected function Returns the language manager service. 1
ControllerBase::moduleHandler protected function Returns the module handler. 1
ControllerBase::redirect protected function Returns a redirect response object for the specified route.
ControllerBase::state protected function Returns the state storage service.
LinksetController::create public static function Instantiates a new instance of the implementing class using autowiring. Overrides AutowireTrait::create
LinksetController::loadMenuTree protected function Loads a menu tree.
LinksetController::process public function Serve linkset requests.
LinksetController::processCustomLinkAttributes private function Process custom link parameters.
LinksetController::toLinkTargetObjects protected function Encode a menu tree as link items and capture any cacheability metadata.
LinksetController::__construct public function Linkset constructor.
LoggerChannelTrait::$loggerFactory protected property The logger channel factory service.
LoggerChannelTrait::getLogger protected function Gets the logger for a specific channel.
LoggerChannelTrait::setLoggerFactory public function Injects the logger channel factory.
MessengerTrait::$messenger protected property The messenger. 16
MessengerTrait::messenger public function Gets the messenger. 16
MessengerTrait::setMessenger public function Sets the messenger.
RedirectDestinationTrait::$redirectDestination protected property The redirect destination service. 2
RedirectDestinationTrait::getDestinationArray protected function Prepares a 'destination' URL query parameter for use with \Drupal\Core\Url.
RedirectDestinationTrait::getRedirectDestination protected function Returns the redirect destination service.
RedirectDestinationTrait::setRedirectDestination public function Sets the redirect destination service.
StringTranslationTrait::$stringTranslation protected property The string translation service. 3
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.