# frozen_string_literal: true
module ActionView
module Helpers # :nodoc:
# = Action View Cache \Helpers
module CacheHelper
class UncacheableFragmentError < StandardError; end
# This helper exposes a method for caching fragments of a view
# rather than an entire action or page. This technique is useful
# caching pieces like menus, lists of new topics, static HTML
# fragments, and so on. This method takes a block that contains
# the content you wish to cache.
#
# The best way to use this is by doing recyclable key-based cache expiration
# on top of a cache store like Memcached or Redis that'll automatically
# kick out old entries.
#
# When using this method, you list the cache dependency as the name of the cache, like so:
#
# <% cache project do %>
# All the topics on this project
# <%= render project.topics %>
# <% end %>
#
# This approach will assume that when a new topic is added, you'll touch
# the project. The cache key generated from this call will be something like:
#
# views/template/action:7a1156131a6928cb0026877f8b749ac9/projects/123
# ^template path ^template tree digest ^class ^id
#
# This cache key is stable, but it's combined with a cache version derived from the project
# record. When the project updated_at is touched, the #cache_version changes, even
# if the key stays stable. This means that unlike a traditional key-based cache expiration
# approach, you won't be generating cache trash, unused keys, simply because the dependent
# record is updated.
#
# If your template cache depends on multiple sources (try to avoid this to keep things simple),
# you can name all these dependencies as part of an array:
#
# <% cache [ project, current_user ] do %>
# All the topics on this project
# <%= render project.topics %>
# <% end %>
#
# This will include both records as part of the cache key and updating either of them will
# expire the cache.
#
# ==== \Template digest
#
# The template digest that's added to the cache key is computed by taking an MD5 of the
# contents of the entire template file. This ensures that your caches will automatically
# expire when you change the template file.
#
# Note that the MD5 is taken of the entire template file, not just what's within the
# cache do/end call. So it's possible that changing something outside of that call will
# still expire the cache.
#
# Additionally, the digestor will automatically look through your template file for
# explicit and implicit dependencies, and include those as part of the digest.
#
# The digestor can be bypassed by passing skip_digest: true as an option to the cache call:
#
# <% cache project, skip_digest: true do %>
# All the topics on this project
# <%= render project.topics %>
# <% end %>
#
# ==== Implicit dependencies
#
# Most template dependencies can be derived from calls to render in the template itself.
# Here are some examples of render calls that Cache Digests knows how to decode:
#
# render partial: "comments/comment", collection: commentable.comments
# render "comments/comments"
# render 'comments/comments'
# render('comments/comments')
#
# render "header" translates to render("comments/header")
#
# render(@topic) translates to render("topics/topic")
# render(topics) translates to render("topics/topic")
# render(message.topics) translates to render("topics/topic")
#
# It's not possible to derive all render calls like that, though.
# Here are a few examples of things that can't be derived:
#
# render group_of_attachments
# render @project.documents.where(published: true).order('created_at')
#
# You will have to rewrite those to the explicit form:
#
# render partial: 'attachments/attachment', collection: group_of_attachments
# render partial: 'documents/document', collection: @project.documents.where(published: true).order('created_at')
#
# === Explicit dependencies
#
# Sometimes you'll have template dependencies that can't be derived at all. This is typically
# the case when you have template rendering that happens in helpers. Here's an example:
#
# <%= render_sortable_todolists @project.todolists %>
#
# You'll need to use a special comment format to call those out:
#
# <%# Template Dependency: todolists/todolist %>
# <%= render_sortable_todolists @project.todolists %>
#
# In some cases, like a single table inheritance setup, you might have
# a bunch of explicit dependencies. Instead of writing every template out,
# you can use a wildcard to match any template in a directory:
#
# <%# Template Dependency: events/* %>
# <%= render_categorizable_events @person.events %>
#
# This marks every template in the directory as a dependency. To find those
# templates, the wildcard path must be absolutely defined from app/views or paths
# otherwise added with +prepend_view_path+ or +append_view_path+.
# This way the wildcard for app/views/recordings/events would be recordings/events/* etc.
#
# The pattern used to match explicit dependencies is /# Template Dependency: (\S+)/,
# so it's important that you type it out just so.
# You can only declare one template dependency per line.
#
# === External dependencies
#
# If you use a helper method, for example, inside a cached block and
# you then update that helper, you'll have to bump the cache as well.
# It doesn't really matter how you do it, but the MD5 of the template file
# must change. One recommendation is to simply be explicit in a comment, like:
#
# <%# Helper Dependency Updated: May 6, 2012 at 6pm %>
# <%= some_helper_method(person) %>
#
# Now all you have to do is change that timestamp when the helper method changes.
#
# === Collection Caching
#
# When rendering a collection of objects that each use the same partial, a :cached
# option can be passed.
#
# For collections rendered such:
#
# <%= render partial: 'projects/project', collection: @projects, cached: true %>
#
# The cached: true will make Action View's rendering read several templates
# from cache at once instead of one call per template.
#
# Templates in the collection not already cached are written to cache.
#
# Works great alongside individual template fragment caching.
# For instance if the template the collection renders is cached like:
#
# # projects/_project.html.erb
# <% cache project do %>
# <%# ... %>
# <% end %>
#
# Any collection renders will find those cached templates when attempting
# to read multiple templates at once.
#
# If your collection cache depends on multiple sources (try to avoid this to keep things simple),
# you can name all these dependencies as part of a block that returns an array:
#
# <%= render partial: 'projects/project', collection: @projects, cached: -> project { [ project, current_user ] } %>
#
# This will include both records as part of the cache key and updating either of them will
# expire the cache.
def cache(name = {}, options = {}, &block)
if controller.respond_to?(:perform_caching) && controller.perform_caching
CachingRegistry.track_caching do
name_options = options.slice(:skip_digest)
safe_concat(fragment_for(cache_fragment_name(name, **name_options), options, &block))
end
else
yield
end
nil
end
# Returns whether the current view fragment is within a +cache+ block.
#
# Useful when certain fragments aren't cacheable:
#
# <% cache project do %>
# <% raise StandardError, "Caching private data!" if caching? %>
# <% end %>
def caching?
CachingRegistry.caching?
end
# Raises +UncacheableFragmentError+ when called from within a +cache+ block.
#
# Useful to denote helper methods that can't participate in fragment caching:
#
# def project_name_with_time(project)
# uncacheable!
# "#{project.name} - #{Time.now}"
# end
#
# # Which will then raise if used within a +cache+ block:
# <% cache project do %>
# <%= project_name_with_time(project) %>
# <% end %>
def uncacheable!
raise UncacheableFragmentError, "can't be fragment cached" if caching?
end
# Cache fragments of a view if +condition+ is true
#
# <% cache_if admin?, project do %>
# All the topics on this project
# <%= render project.topics %>
# <% end %>
def cache_if(condition, name = {}, options = {}, &block)
if condition
cache(name, options, &block)
else
yield
end
nil
end
# Cache fragments of a view unless +condition+ is true
#
# <% cache_unless admin?, project do %>
# All the topics on this project
# <%= render project.topics %>
# <% end %>
def cache_unless(condition, name = {}, options = {}, &block)
cache_if !condition, name, options, &block
end
# This helper returns the name of a cache key for a given fragment cache
# call. By supplying skip_digest: true to cache, the digestion of cache
# fragments can be manually bypassed. This is useful when cache fragments
# cannot be manually expired unless you know the exact key which is the
# case when using memcached.
def cache_fragment_name(name = {}, skip_digest: nil, digest_path: nil)
if skip_digest
name
else
fragment_name_with_digest(name, digest_path)
end
end
def digest_path_from_template(template) # :nodoc:
digest = Digestor.digest(name: template.virtual_path, format: template.format, finder: lookup_context, dependencies: view_cache_dependencies)
if digest.present?
"#{template.virtual_path}:#{digest}"
else
template.virtual_path
end
end
private
def fragment_name_with_digest(name, digest_path)
name = controller.url_for(name).split("://").last if name.is_a?(Hash)
if @current_template&.virtual_path || digest_path
digest_path ||= digest_path_from_template(@current_template)
[ digest_path, name ]
else
name
end
end
def fragment_for(name = {}, options = nil, &block)
if content = read_fragment_for(name, options)
@view_renderer.cache_hits[@current_template&.virtual_path] = :hit if defined?(@view_renderer)
content
else
@view_renderer.cache_hits[@current_template&.virtual_path] = :miss if defined?(@view_renderer)
write_fragment_for(name, options, &block)
end
end
def read_fragment_for(name, options)
controller.read_fragment(name, options)
end
def write_fragment_for(name, options, &block)
fragment = output_buffer.capture(&block)
controller.write_fragment(name, fragment, options)
end
module CachingRegistry # :nodoc:
extend self
def caching?
ActiveSupport::IsolatedExecutionState[:action_view_caching] ||= false
end
def track_caching
caching_was = ActiveSupport::IsolatedExecutionState[:action_view_caching]
ActiveSupport::IsolatedExecutionState[:action_view_caching] = true
yield
ensure
ActiveSupport::IsolatedExecutionState[:action_view_caching] = caching_was
end
end
end
end
end