Verified Commit d05c9c0f authored by Andrei Mateescu's avatar Andrei Mateescu
Browse files

task: #2571679 Replace views_add_contextual_links() in views.module with service

By: narendrar
By: claudiu.cristea
By: quietone
By: nicxvan
By: utkarsh_33
By: smustgrave
By: godotislate
By: berdir
By: amateescu
parent 7ff3e039
Loading
Loading
Loading
Loading
Loading
+150 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\views;

use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

/**
 * Views contextual links helper service.
 */
class ContextualLinksHelper {

  public function __construct(
    protected ModuleHandlerInterface $moduleHandler,
    protected EntityTypeManagerInterface $entityTypeManager,
    #[Autowire(service: 'plugin.manager.views.display')]
    protected PluginManagerInterface $displayManager,
  ) {}

  /**
   * Adds view's display contextual links to a renderable array.
   *
   * Checks the view's display plugin for any contextual links defined for the
   * requested location and attaches them if found.
   *
   * Each display plugin controls which contextual links it provides and where
   * they appear, using the 'contextual_links' and 'contextual_links_locations'
   * properties in its attribute.
   *
   * This function attaches two properties to the passed-in array:
   *
   * - #contextual_links: The standard contextual links for the display.
   * - #views_contextual_links_info: A metadata array keyed by module name
   *   (matching the keys in #contextual_links). Each entry holds three values:
   *   'location', 'view_name', and 'view_display_id', reflecting the arguments
   *   passed to this function. This metadata is useful when you need to
   *   inspect or alter the renderable array later in the page request, such as
   *   inside alter hooks.
   *
   * @param array $renderElement
   *   The renderable array to which contextual links will be added.
   * @param string $location
   *   The location in which the calling function intends to render the view
   *   and its contextual links. The core system supports three options for
   *   this parameter:
   *   - block: Used when rendering a block which contains a view. This
   *     retrieves any contextual links intended to be attached to the block
   *     itself.
   *   - page: Used when rendering the main content of a page which contains
   *     a view. This retrieves any contextual links intended to be attached to
   *     the page itself (for example, links which are displayed directly next
   *     to the page title).
   *   - view: Used when rendering the view itself, in any context. This
   *     retrieves any contextual links intended to be attached directly to the
   *     view.
   *   Example: If you are rendering a view and its contextual links in another
   *   location, you can pass in a different value for the $location parameter.
   *   However, you will also need to set 'contextual_links_locations' in your
   *   plugin annotation to indicate which view displays support having their
   *   contextual links rendered in the location you have defined.
   * @param string $displayId
   *   The ID of the view display whose contextual links will be added.
   * @param array|null $viewElement
   *   (optional) The render array of the view. Defaults to $renderElement. It
   *   should contain the following properties:
   *   - #view_id: The ID of the view.
   *   - #view_display_show_admin_links: A boolean indicating whether the admin
   *     links should be shown.
   *   - #view_display_plugin_id: The plugin ID of the display.
   *
   * @see \Drupal\views\Plugin\Block\ViewsBlock::addContextualLinks()
   * @see \Drupal\views\Hook\ViewsThemeHooks::preprocessViewsView()
   */
  public function addLinks(array &$renderElement, string $location, string $displayId, ?array $viewElement = NULL): void {
    assert(in_array($location, ['block', 'page', 'view'], TRUE));

    if (!isset($viewElement)) {
      $viewElement = $renderElement;
    }

    // Exit if the Contextual Links module is not enabled or if the view is
    // configured to hide its administrative links.
    if (!$this->moduleHandler->moduleExists('contextual') || !$viewElement['#view_display_show_admin_links']) {
      return;
    }

    $viewId = $viewElement['#view_id'];
    $displayPluginId = $viewElement['#view_display_plugin_id'];

    $plugin = $this->displayManager->getDefinition($displayPluginId);
    // If contextual_links_locations are not set, provide a sane default. Filter
    // empty items because, to avoid displaying any contextual links at all, a
    // display plugin can still set 'contextual_links_locations' to, e.g., {""}.
    $plugin['contextual_links_locations'] = array_filter($plugin['contextual_links_locations'] ?? ['view']);

    // On exposed_forms blocks contextual links should always be visible.
    $plugin['contextual_links_locations'][] = 'exposed_filter';
    $hasLinks = !empty($plugin['contextual links']) && !empty($plugin['contextual_links_locations']);

    // Also, do not do anything if the display plugin has not defined any
    // contextual links that are intended to be displayed in the requested
    // location.
    if (!$hasLinks || !in_array($location, $plugin['contextual_links_locations'])) {
      return;
    }

    $viewStorage = $this->entityTypeManager->getStorage('view')->load($viewId);

    foreach ($plugin['contextual links'] as $group => $link) {
      $args = [];
      $valid = TRUE;
      if (!empty($link['route_parameters_names'])) {
        foreach ($link['route_parameters_names'] as $parameterName => $property) {
          // If the plugin is trying to create an invalid contextual link (for
          // example, "path/to/{$view->storage->property}", where
          // $view->storage->{property} does not exist), we cannot construct the
          // link, so we skip it.
          if (!property_exists($viewStorage, $property)) {
            $valid = FALSE;
            break;
          }
          else {
            $args[$parameterName] = $viewStorage->get($property);
          }
        }
      }

      if (!$valid) {
        continue;
      }

      // Link is valid. Attach information about it to the renderable array.
      $renderElement['#views_contextual_links'] = TRUE;
      $renderElement['#contextual_links'][$group] = [
        'route_parameters' => $args,
        'metadata' => [
          'location' => $location,
          'name' => $viewId,
          'display_id' => $displayId,
        ],
      ];
      $renderElement['#cache']['contexts'][] = 'user.permissions';
    }
  }

}
+4 −3
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@

use Drupal\Core\Render\Attribute\RenderElement;
use Drupal\Core\Render\Element\RenderElementBase;
use Drupal\views\ContextualLinksHelper;
use Drupal\views\Exception\ViewRenderElementException;
use Drupal\views\Views;

@@ -84,12 +85,12 @@ public static function preRenderViewElement($element) {
        }

        if (empty($view->display_handler->getPluginDefinition()['returns_response'])) {
          // views_add_contextual_links() needs the following information in
          // order to be attached to the view.
          // \Drupal\views\ContextualLinksHelper::addLinks() needs the following
          // information to be attached to the view.
          $element['#view_id'] = $view->storage->id();
          $element['#view_display_show_admin_links'] = $view->getShowAdminLinks();
          $element['#view_display_plugin_id'] = $view->display_handler->getPluginId();
          views_add_contextual_links($element, 'view', $view->current_display);
          \Drupal::service(ContextualLinksHelper::class)->addLinks($element, 'view', $view->current_display);
        }
      }
      if (empty($view->display_handler->getPluginDefinition()['returns_response'])) {
+15 −19
Original line number Diff line number Diff line
@@ -8,6 +8,7 @@
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\views\ContextualLinksHelper;
use Drupal\views\ViewExecutableFactory;
use Drupal\Core\Entity\EntityStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -47,22 +48,11 @@ abstract class ViewsBlockBase extends BlockBase implements ContainerFactoryPlugi
  protected $user;

  /**
   * Constructs a \Drupal\views\Plugin\Block\ViewsBlockBase object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin ID for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\views\ViewExecutableFactory $executable_factory
   *   The view executable factory.
   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
   *   The views storage.
   * @param \Drupal\Core\Session\AccountInterface $user
   *   The current user.
   * The Views contextual links service.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, ViewExecutableFactory $executable_factory, EntityStorageInterface $storage, AccountInterface $user) {
  protected ContextualLinksHelper $contextualLinks;

  public function __construct(array $configuration, $plugin_id, $plugin_definition, ViewExecutableFactory $executable_factory, EntityStorageInterface $storage, AccountInterface $user, ?ContextualLinksHelper $contextual_links = NULL) {
    $this->pluginId = $plugin_id;
    $delta = $this->getDerivativeId();
    [$name, $this->displayID] = explode('-', $delta, 2);
@@ -71,6 +61,11 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
    $this->view = $executable_factory->get($view);
    $this->displaySet = $this->view->setDisplay($this->displayID);
    $this->user = $user;
    if (!$contextual_links) {
      @trigger_error('Calling ' . __METHOD__ . '() without the $contextual_links argument is deprecated in drupal:11.4.0 and will be required in drupal:13.0.0. See https://www.drupal.org/node/3382344', E_USER_DEPRECATED);
      $contextual_links = \Drupal::service(ContextualLinksHelper::class);
    }
    $this->contextualLinks = $contextual_links;

    parent::__construct($configuration, $plugin_id, $plugin_definition);
  }
@@ -83,7 +78,8 @@ public static function create(ContainerInterface $container, array $configuratio
      $configuration, $plugin_id, $plugin_definition,
      $container->get('views.executable'),
      $container->get('entity_type.manager')->getStorage('view'),
      $container->get('current_user')
      $container->get('current_user'),
      $container->get(ContextualLinksHelper::class)
    );
  }

@@ -242,12 +238,12 @@ protected function addContextualLinks(&$output, $block_type = 'block') {
        $output = ['#markup' => $output];
      }

      // views_add_contextual_links() needs the following information in
      // order to be attached to the view.
      // \Drupal\views\ContextualLinksHelper::addLinks() needs the following
      // information to be attached to the view.
      $output['#view_id'] = $this->view->storage->id();
      $output['#view_display_show_admin_links'] = $this->view->getShowAdminLinks();
      $output['#view_display_plugin_id'] = $this->view->display_handler->getPluginId();
      views_add_contextual_links($output, $block_type, $this->displayID);
      $this->contextualLinks->addLinks($output, $block_type, $this->displayID);
    }
  }

+11 −2
Original line number Diff line number Diff line
@@ -3,7 +3,10 @@
namespace Drupal\views\Routing;

use Drupal\Component\Utility\Xss;
use Drupal\Core\DependencyInjection\AutowireTrait;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\views\ContextualLinksHelper;
use Drupal\views\Plugin\views\display\Page;
use Drupal\views\Render\ViewsRenderPipelineMarkup;
use Drupal\views\Views;
@@ -11,7 +14,13 @@
/**
 * Defines a page controller to execute and render a view.
 */
class ViewPageController {
class ViewPageController implements ContainerInjectionInterface {

  use AutowireTrait;

  public function __construct(
    protected readonly ContextualLinksHelper $contextualLinks,
  ) {}

  /**
   * Handler a response for a given view and display.
@@ -60,7 +69,7 @@ public function handle($view_id, $display_id, RouteMatchInterface $route_match)
      $build = $class::buildBasicRenderable($view_id, $display_id, $args, $route);
      Page::setPageRenderArray($build);

      views_add_contextual_links($build, 'page', $display_id, $build);
      $this->contextualLinks->addLinks($build, 'page', $display_id, $build);

      return $build;
    }
+12 −18
Original line number Diff line number Diff line
@@ -10,10 +10,12 @@
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Plugin\Context\ContextInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\views\ContextualLinksHelper;
use Drupal\views\Plugin\Block\ViewsBlock;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\Stub;

/**
 * Tests Drupal\views\Plugin\block\ViewsBlock.
@@ -64,6 +66,11 @@ class ViewsBlockTest extends UnitTestCase {
   */
  protected $displayHandler;

  /**
   * The Views contextual links service.
   */
  protected ContextualLinksHelper|Stub $contextualLinks;

  /**
   * {@inheritdoc}
   */
@@ -149,6 +156,7 @@ protected function setUp(): void {
      ->with('test_view')
      ->willReturn($this->view);
    $this->account = $this->createMock('Drupal\Core\Session\AccountInterface');
    $this->contextualLinks = $this->createStub(ContextualLinksHelper::class);
  }

  /**
@@ -176,7 +184,7 @@ public function testBuild(): void {
    $definition = [];

    $definition['provider'] = 'views';
    $plugin = new ViewsBlock($config, $block_id, $definition, $this->executableFactory, $this->storage, $this->account);
    $plugin = new ViewsBlock($config, $block_id, $definition, $this->executableFactory, $this->storage, $this->account, $this->contextualLinks);

    $this->assertEquals($build, $plugin->build());
  }
@@ -228,7 +236,7 @@ public function testCacheableMetadata(int $blockCacheMaxAge, int $viewCacheMaxAg
    $definition = [
      'provider' => 'views',
    ];
    $plugin = new ViewsBlock($config, $block_id, $definition, $this->executableFactory, $this->storage, $this->account);
    $plugin = new ViewsBlock($config, $block_id, $definition, $this->executableFactory, $this->storage, $this->account, $this->contextualLinks);
    $plugin->setContext('context_name', $blockContext);

    // Assertions.
@@ -273,7 +281,7 @@ public function testBuildEmpty(): void {
    $definition = [];

    $definition['provider'] = 'views';
    $plugin = new ViewsBlock($config, $block_id, $definition, $this->executableFactory, $this->storage, $this->account);
    $plugin = new ViewsBlock($config, $block_id, $definition, $this->executableFactory, $this->storage, $this->account, $this->contextualLinks);

    $this->assertEquals(array_intersect_key($build, ['#cache' => TRUE]), $plugin->build());
  }
@@ -295,23 +303,9 @@ public function testBuildFailed(): void {
    $definition = [];

    $definition['provider'] = 'views';
    $plugin = new ViewsBlock($config, $block_id, $definition, $this->executableFactory, $this->storage, $this->account);
    $plugin = new ViewsBlock($config, $block_id, $definition, $this->executableFactory, $this->storage, $this->account, $this->contextualLinks);

    $this->assertEquals([], $plugin->build());
  }

}

// @todo https://www.drupal.org/node/2571679 replace
//   views_add_contextual_links().
namespace Drupal\views\Plugin\Block;

if (!function_exists('views_add_contextual_links')) {

  /**
   * Define method views_add_contextual_links for this test.
   */
  function views_add_contextual_links(&$render_element, $location, $display_id, ?array $view_element = NULL): void {
  }

}
Loading