10

I can access current node context in my block plugin by adding:

 *  context_definitions = {
 *    "node" = @ContextDefinition("entity:node", label = @Translation("Node"), required=FALSE)
 *  }

to its annotation. And then access node object by using:

$context = $this->getContext('node');

/** @var NodeInterface $node */
$node = $context->getContextData()->getValue();

Provided it's available. But let's say I want to create context that is active only if node has specific field.

I'm currently trying to reverse engineer this, because there are literally no resources on context API that I could find, but maybe someone have some working example to share?

1 Answer 1

19

Ok, I figured it out and I'm describing it here. It's really surprising for me that it's not explained anywhere else as of now.

Let's say we have a content type news that have a field field_section storing reference to the term from the sections vocabulary. Now, everytime we are on the node's page, and this node is referencing a section, we want to trigger the context, that will give as a Term object representing our section. Moreover we have a block plugin, that depends on the availability of this section, and should be visible only if section is available on the current page.

We could do this with the node context, but then we would have to add logic checking the content of the field_section of our node, in every context aware plugin that needs this information, and we want to have this simplified to just having context active when section is available on the current page - that's the whole point.

So first off, we have to provide a service that implements Drupal\Core\Plugin\Context\ContextProviderInterface, and tag it with context_provider tag, like that:

  mymodule.section_context:
    class: Drupal\mymodule\ContextProvider\SectionContextProvider
    arguments: ['@context.repository']
    tags:
      - { name: context_provider }

Note that we are injecting @context.repository service, we will be able to extract the node context from it. We are dependent on the node being available in the first place, so we can as well reuse this context. We could also inject here @node.node_route_context service directly and have more explicit dependency, but I felt that it would be more clean to do this trough the repository service. It's up to you.

Now the code of our context provider:

namespace Drupal\mymodule\ContextProvider;

use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextInterface;
use Drupal\Core\Plugin\Context\ContextProviderInterface;
use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\node\NodeInterface;

/**
 * Class SectionContextProvider
 */
class SectionContextProvider implements ContextProviderInterface {

  use StringTranslationTrait;

  /**
   * @var ContextRepositoryInterface
   */
  private $contextRepository;

  /**
   * @param ContextRepositoryInterface $contextRepository
   */
  public function __construct(ContextRepositoryInterface $contextRepository) {
    $this->contextRepository = $contextRepository;
  }

  /**
   * {@inheritdoc}
   */
  public function getRuntimeContexts(array $unqualified_context_ids) {

    $nodeContextId = '@node.node_route_context:node';

    /** @var ContextInterface $nodeContext */
    $nodeContext = $this->contextRepository->getRuntimeContexts([$nodeContextId])[$nodeContextId];
    $value = NULL;

    /** @var NodeInterface $node */
    $node = $nodeContext->getContextData()->getValue();
    if ($node && $node->hasField('field_section')) {
      $value = $node->get('field_section')->entity;
    }

    // We are reusing cache contexts.
    $cacheContexts = $nodeContext->getCacheContexts();
    $cacheability = new CacheableMetadata();
    $cacheability->setCacheContexts($cacheContexts);

    $contextDefinition = EntityContextDefinition::create('taxonomy_term');

    $context = new Context($contextDefinition, $value);
    $context->addCacheableDependency($cacheability);
    $result['mymodule_section'] = $context;

    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function getAvailableContexts() {
    $context = EntityContext::fromEntityTypeId('taxonomy_term', $this->t('Mywebsite Section from Node'));
    return ['mymodule_section' => $context];
  }

}

Basically will are getting the current node from the node context, and then extract section data if it's available.

But that's not all.

We want to make our plugin block dependent on the context, so we are are adding to its annotation:

 *  context_definitions = {
 *    "mymodule_section" = @ContextDefinition("entity:taxonomy_term:sections", label = @Translation("Mywebsite Section"))
 *  }

But it still won't work, and that's the tricky part. We also have to configure the context mapping of our plugin block. We can do this by providing it as a part of default configuration like that:

  /**
   * @return array
   */
  public function defaultConfiguration() {
    return [
      'context_mapping' => [
        'mymodule_section' => '@mymodule.section_context:mymodule_section',
      ],
    ] + parent::defaultConfiguration();
  }

And ONLY THEN it will work. The block wont be rendered if context is not available, and if it is, we can access the term referenced by node's field_section field by:

$sectionTerm = $this->getContext('mymodule_section')->getContextData()->getValue();

That's way too complicated for this to not have any documentation, but I hope this description will help someone.

And if anyone know, where on the drupal.org website I could write up a documentation for that, please let me know, we have to improve that stuff.

2
  • Thanks for the write up. I share you're feeling that there is way to few documentation for this. I think drupal.org/docs/8/api/plugin-api/plugin-contexts would be a good place to add this as a new section or subpage (not sure how to add one though.
    – berliner
    Mar 18, 2020 at 9:18
  • Thank you! Something very much like field_section is also my use case.
    – lolcode
    May 19, 2020 at 11:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.