From drupal-workflow
Implements Drupal 10/11 caching with bins, tags, contexts, invalidation strategies, external backends, lazy builders, and CacheableMetadata. For performance optimization and cache configuration.
npx claudepluginhub gkastanis/drupal-workflow --plugin drupal-workflowThis skill uses the workspace's default tool permissions.
Always use cache metadata on render arrays. Never return a render array without `#cache` keys. Check for proper cache tag, cache context, and max-age on every render array you create.
Implements caching strategies using Redis, Memcached, CDN, and invalidation patterns to optimize app performance, reduce database load, and improve response times.
Implements API response caching with Redis, Memcached, cache-aside patterns, TTL, tag-based invalidation, HTTP headers, and stale-while-revalidate for performance optimization.
Assesses caching opportunities and implements multi-layer strategies with Redis/Memcached/CDN, including cache-aside patterns, TTL/event invalidation, and stampede prevention.
Share bugs, ideas, or general feedback.
Always use cache metadata on render arrays. Never return a render array without #cache keys. Check for proper cache tag, cache context, and max-age on every render array you create.
Every render array should include cache metadata:
$build = [
'#theme' => 'my_template',
'#data' => $data,
'#cache' => [
'keys' => ['my_module', 'block', $id],
'contexts' => ['user', 'url.query_args'],
'tags' => ['node:' . $nid, 'node_list'],
'max-age' => 3600,
],
];
| Context | Varies By | Use When |
|---|---|---|
user | Current user ID | Content per user |
user.roles | User roles | Content per role |
user.permissions | Permissions | Access-dependent |
url | Full URL | Page-specific |
url.path | URL path only | Path-dependent |
url.query_args | Query parameters | Filtered content |
languages | Current language | Multilingual |
theme | Active theme | Theme-specific |
session | Session ID | Session-dependent |
// Entity-based tags (auto-invalidated).
'tags' => ['node:42'] // Specific node.
'tags' => ['node_list'] // Any node list.
'tags' => ['taxonomy_term:5'] // Specific term.
'tags' => ['config:system.site'] // Config object.
// Custom tags.
'tags' => ['my_module:feature_x']
Use CacheableMetadata to merge cache metadata from multiple sources. This is cleaner than manually assembling #cache arrays, especially in complex render pipelines.
use Drupal\Core\Cache\CacheableMetadata;
// Build cache metadata from multiple sources.
$cache = new CacheableMetadata();
$cache->addCacheableDependency($node);
$cache->addCacheableDependency($user);
$cache->addCacheContexts(['url.query_args']);
$cache->addCacheTags(['my_module:feature_x']);
$cache->setCacheMaxAge(3600);
// Apply to a render array.
$cache->applyTo($build);
// Merge metadata from an access result into the render array.
$access = $entity->access('view', $account, TRUE);
$cache->addCacheableDependency($access);
Use #lazy_builder for fragments that vary per user inside otherwise cacheable pages. Lazy builders defer rendering until after the page cache is resolved, so the rest of the page can still be cached.
$build['user_greeting'] = [
'#lazy_builder' => [
'my_module.greeting_builder:build', // Service::method
[$user_id], // Arguments (scalars only)
],
'#create_placeholder' => TRUE,
];
The service must implement a build() method returning a render array:
final class GreetingBuilder {
public function build(int $user_id): array {
return ['#markup' => 'Hello, ' . $this->loadUserName($user_id)];
}
}
In services, inject CacheTagsInvalidatorInterface — never use \Drupal::service() calls.
// In a service with injected $cacheInvalidator:
$this->cacheInvalidator->invalidateTags(['node:42', 'my_module:feature_x']);
| Bin | Purpose |
|---|---|
default | General-purpose |
render | Render arrays |
page | Full page cache |
dynamic_page_cache | Authenticated pages |
discovery | Plugin discovery |
data | Data processing |
config | Configuration |
menu | Menu trees |
// settings.php
$settings['redis.connection']['interface'] = 'PhpRedis';
$settings['redis.connection']['host'] = '127.0.0.1';
$settings['cache']['default'] = 'cache.backend.redis';
// Ensure proper cache headers.
$response->headers->set('Cache-Control', 'public, max-age=3600');
$response->headers->set('Surrogate-Control', 'max-age=86400');
#cache).// Bad: N queries.
foreach ($nids as $nid) {
$node = Node::load($nid);
}
// Good: 1 query.
$nodes = Node::loadMultiple($nids);
accessCheck() explicitly.