7

I have custom module with some routes which return JSON, for instance terms of a vocabulary.

routing.yml

mymodule.terms:
  path: '/api/terms/{vid}'
  defaults:
    _controller: '\Drupal\mymodule\Controller\MyModuleController::getTerms'
    _title: 'Terms'
  requirements:
    _permission: 'access content'
    vid: '^(foobar|barfoo)$'

Controller:

public function getTerms($vid) {
  $response = $this->jsonifyVocabulary($vid);
  return $response;
}

private function jsonifyVocabulary($vid) {
  $request = \Drupal::request();
  $lang = $request->query->get('lang', 'en');
  $output = [];
  $taxonomyManager = \Drupal::entityManager()->getStorage('taxonomy_term');
  $terms = $taxonomyManager->loadTree($vid, 0, 1, false);
  $response = CacheableJsonResponse::create($output);

  debug(1);

  foreach ($terms as $term) {
    $term = Term::load($term->tid);
    if ($term->hasTranslation($lang) && $lang !== $term->language()) {
      $term = $term->getTranslation($lang);
    }
    $entry = [
      'tid' => (int) $term->id(),
      'label' => $term->getName(),
    ];

    // $response->addCacheableDependency($term);
    $output[$vid][] = $entry;
  }

  $response->setData($output);

  $cacheMeta = (new CacheableMetadata())->addCacheContexts(['url.query_args:lang']);
  $response->addCacheableDependency($cacheMeta);

  return $response;
}

Thanks to this answer I know how to actually add the URL params as a cache context, but the

debug(1);

always gets called, I guess I just somehow have to tell Drupal that this JSON response's cache should relate to that route...

I don't use the core's REST module on purpose since I find it hard to configure to output exactly what I need.

3
  • Hard to say what's wrong, can you post the headers, specifically anything cache related? You at least also need a taxonomy_term_list cache tag, so the cache is invalidated when the terms change. Or a more specific custom cache tag that you invalidate only when terms of your vocabularies are saved
    – Berdir
    Commented Mar 24, 2017 at 22:31
  • I dont even know if the cached output gets loaded in the first place. I guess it creates the cached output, but never "gets" it. Do I really have to load explicit cache items from the db?
    – Alex
    Commented Mar 27, 2017 at 7:26
  • 2
    No, you should not have to do anything except adding the cache contexts. Again, start with adding the requested headers and information then we can start to identify the problem. Also, for starters, instead of debug(), use a different mechanism, e.g. write to a file, because using debug() starts a sesson and results in not caching your output.
    – Berdir
    Commented Mar 27, 2017 at 13:06

3 Answers 3

14
+25

This is all you need to set up caching for a response:

  $response = CacheableJsonResponse::create($output);
  return $response;

If you use a response that implements the CacheableResponseInterface it will be cached indefinitely.

Cache tags

When you add a list of taxonomy terms you have to add these tags:

  $list_tags = $this->entityTypeManager()->getDefinition('taxonomy_term')->getListCacheTags();
  $cache_metadata->addCacheTags($list_tags);

  foreach ($terms as $term) {
    $cache_metadata->addCacheableDependency($term);
  }

This will invalidated the cache entry when the terms are modified in the database.

Cache contexts

When you use a query parameter add this context:

  $lang = $request->query->get('lang', 'en');
  $cache_metadata->addCacheContexts(['url.query_args:lang']);

This will vary the cache by the query arguments.

Cacheable metadata in a response

To put all this together create at the beginning of the controller an empty CacheableMetadata object:

  $cache_metadata = new CacheableMetadata();

While building the content collect the cache metadata for all dependencies you use as described in the two examples before and then add the metadata to the response at the end of the controller:

$response->addCacheableDependency($cache_metadata);
5
  • but I wonder why each time although the cache should still be valid, by debug() gets called. to me this means that not the cached output is used but new queries being made... i will try out whole of your code though and reply afterwards. thanks so far
    – Alex
    Commented Mar 28, 2017 at 7:53
  • Do you have development settings enabled?
    – 4uk4
    Commented Mar 28, 2017 at 8:01
  • yes I have them enabled
    – Alex
    Commented Mar 28, 2017 at 8:07
  • 1
    For testing the caching behavior you have to disable the development settings.
    – 4uk4
    Commented Mar 28, 2017 at 10:00
  • Oh thank @4k4 you save me
    – Kevin
    Commented Apr 29, 2020 at 20:45
1

..I guess I just somehow have to tell Drupal that this json response's cache should relate to that route...

Looking at the "Cache contexts" documentation under "Debugging":

"All of the above is helpful information when debugging something that is being cached. But, there's one more thing: let's say something is being cached with the cache keys ['foo', 'bar'] and the cache contexts ['languages:language_interface', 'user.permissions', 'route']. Then the corresponding cache item will be cached in a particular cache bin with a CID (cache ID) of:

foo:bar:[languages:language_interface]=en:[user.permissions]=A_QUITE_LONG_HASH:[route]=myroute.ROUTE_PARAMS_HASH"

It looks like if you're defining the cache contexts in the response, it should cache in the cache bin that has a CID for that route & cache context.

I dont use the core's rest module on purpose since I find it hard to configure to output exactly what I need.

Drupal core REST includes a "Taxonomy vocabulary" REST resource that takes in the vocabulary ID as an argument: enter image description here

By default, it returns the vocabulary's information like:

{
  "uuid": "46fb0c72-4570-4c7f-b90d-faa69f2c5e7a",
  "langcode": "en",
  "status": true,
  "dependencies": [],
  "name": "Vocabulary Name",
  "vid": "vocabulary_vid",
  "description": "Some vocabulary description.",
  "hierarchy": 0,
  "weight": 0
}

However, to get the terms for a particular vocabulary to show up in the JSON response, you can create a custom normalizer that can alter the output of this REST resource to your liking.

See:

1
  • thanks a lot so far, but it does not quite answer my question. Does it mean I have to get my cache id, check whether there is a valid cache of my response and return that or else create a new one? How does that look? I dont find any code example. I'm not just outputting plain terms and nodes, also relations. Also, there are so many fields and properties I dont want to be output and I find hard to configure in the rest way hence my own api
    – Alex
    Commented Mar 27, 2017 at 7:24
1

Not sure if you have already found an answer for your question. I have spent literally half a day trying to find out what is wrong... Well - the answer is very simple: make sure you have the Internal Dynamic Page Cache module enabled. (This simplest way: drush en -y dynamic_page_cache)

When the module is disabled than NOTHING is being taken from cache. The module itself has an EventSubscriber, triggered on route match (see dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php):

/**
 * Sets a response in case of a Dynamic Page Cache hit.
 *
 * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
 *   The event to process.
 */
public function onRouteMatch(GetResponseEvent $event) {
  // Don't cache the response if the Dynamic Page Cache request policies are
  // not met. Store the result in a static keyed by current request, so that
  // onResponse() does not have to redo the request policy check.
  $request = $event->getRequest();
  $request_policy_result = $this->requestPolicy->check($request);
  $this->requestPolicyResults[$request] = $request_policy_result;
  if ($request_policy_result === RequestPolicyInterface::DENY) {
    return;
  }

  // Sets the response for the current route, if cached.
  $cached = $this->renderCache->get($this->dynamicPageCacheRedirectRenderArray);
  if ($cached) {
    $response = $this->renderArrayToResponse($cached);
    $response->headers->set(self::HEADER, 'HIT');
    $event->setResponse($response);
  }
}

which actually takes care of the whole cache reading and returning a cached json response. Without the module the whole CachedJsonResponse is useless.

NOTE: I would like to give full credit for this answer to Monika From the Forests near Rzeszów. It was her dedication and commitment to finding this problem, that has given me this splendid opportunity to work on the challenge. Thank you. God bless you.

Your Answer

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

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