# frozen_string_literal: true module AbstractController module Caching # 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) # :nodoc: ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", instrument_payload(key)) { yield } end end end end