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.