actionpack/lib/abstract_controller/caching/fragments.rb
# frozen_string_literal: true
# :markup: markdown
module AbstractController
module Caching
# # Abstract Controller Caching Fragments
#
# Fragment caching is used for caching various blocks within views without
# caching the entire action as a whole. This is useful when certain elements of
# an action change frequently or depend on complicated state while other parts
# rarely change or can be shared amongst multiple parties. The caching is done
# using the `cache` helper available in the Action View. See
# ActionView::Helpers::CacheHelper for more information.
#
# While it's strongly recommended that you use key-based cache expiration (see
# links in CacheHelper for more information), it is also possible to manually
# expire caches. For example:
#
# expire_fragment('name_of_cache')
module Fragments
extend ActiveSupport::Concern
included do
if respond_to?(:class_attribute)
class_attribute :fragment_cache_keys
else
mattr_writer :fragment_cache_keys
end
self.fragment_cache_keys = []
if respond_to?(:helper_method)
helper_method :combined_fragment_cache_key
end
end
module ClassMethods
# Allows you to specify controller-wide key prefixes for cache fragments. Pass
# either a constant `value`, or a block which computes a value each time a cache
# key is generated.
#
# For example, you may want to prefix all fragment cache keys with a global
# version identifier, so you can easily invalidate all caches.
#
# class ApplicationController
# fragment_cache_key "v1"
# end
#
# When it's time to invalidate all fragments, simply change the string constant.
# Or, progressively roll out the cache invalidation using a computed value:
#
# class ApplicationController
# fragment_cache_key do
# @account.id.odd? ? "v1" : "v2"
# end
# end
def fragment_cache_key(value = nil, &key)
self.fragment_cache_keys += [key || -> { value }]
end
end
# Given a key (as described in `expire_fragment`), returns a key array suitable
# for use in reading, writing, or expiring a cached fragment. All keys begin
# with `:views`, followed by `ENV["RAILS_CACHE_ID"]` or
# `ENV["RAILS_APP_VERSION"]` if set, followed by any controller-wide key prefix
# values, ending with the specified `key` value.
def combined_fragment_cache_key(key)
head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) }
tail = key.is_a?(Hash) ? url_for(key).split("://").last : key
cache_key = [:views, ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"], head, tail]
cache_key.flatten!(1)
cache_key.compact!
cache_key
end
# Writes `content` to the location signified by `key` (see `expire_fragment` for
# acceptable formats).
def write_fragment(key, content, options = nil)
return content unless cache_configured?
key = combined_fragment_cache_key(key)
instrument_fragment_cache :write_fragment, key do
content = content.to_str
cache_store.write(key, content, options)
end
content
end
# Reads a cached fragment from the location signified by `key` (see
# `expire_fragment` for acceptable formats).
def read_fragment(key, options = nil)
return unless cache_configured?
key = combined_fragment_cache_key(key)
instrument_fragment_cache :read_fragment, key do
result = cache_store.read(key, options)
result.respond_to?(:html_safe) ? result.html_safe : result
end
end
# Check if a cached fragment from the location signified by `key` exists (see
# `expire_fragment` for acceptable formats).
def fragment_exist?(key, options = nil)
return unless cache_configured?
key = combined_fragment_cache_key(key)
instrument_fragment_cache :exist_fragment?, key do
cache_store.exist?(key, options)
end
end
# Removes fragments from the cache.
#
# `key` can take one of three forms:
#
# * String - This would normally take the form of a path, like
# `pages/45/notes`.
# * Hash - Treated as an implicit call to `url_for`, like `{ controller:
# 'pages', action: 'notes', id: 45}`
# * Regexp - Will remove any fragment that matches, so `%r{pages/\d*/notes}`
# might remove all notes. Make sure you don't use anchors in the regex (`^`
# or `$`) because the actual filename matched looks like
# `./cache/filename/path.cache`. Note: Regexp expiration is only supported
# on caches that can iterate over all keys (unlike memcached).
#
#
# `options` is passed through to the cache store's `delete` method (or
# `delete_matched`, for Regexp keys).
def expire_fragment(key, options = nil)
return unless cache_configured?
key = combined_fragment_cache_key(key) unless key.is_a?(Regexp)
instrument_fragment_cache :expire_fragment, key do
if key.is_a?(Regexp)
cache_store.delete_matched(key, options)
else
cache_store.delete(key, options)
end
end
end
def instrument_fragment_cache(name, key, &block) # :nodoc:
ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", instrument_payload(key), &block)
end
end
end
end