aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/abstract_controller/caching/fragments.rb
blob: 1fe653cd128e874e2ddde68e88dd5090dc4cd068 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# 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 :fragment_cache_key
          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 suitable for use in reading, writing, or expiring a
      # cached fragment. All keys begin with <tt>views/</tt>,
      # followed by any controller-wide key prefix values, ending
      # with the specified +key+ value. The key is expanded using
      # ActiveSupport::Cache.expand_cache_key.
      def fragment_cache_key(key)
        ActiveSupport::Deprecation.warn(<<-MSG.squish)
          Calling fragment_cache_key directly is deprecated and will be removed in Rails 6.0.
          All fragment accessors now use the combined_fragment_cache_key method that retains the key as an array,
          such that the caching stores can interrogate the parts for cache versions used in
          recyclable cache keys.
        MSG

        head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) }
        tail = key.is_a?(Hash) ? url_for(key).split("://").last : key
        ActiveSupport::Cache.expand_cache_key([*head, *tail], :views)
      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 <tt>:views</tt>,
      # 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
        [ :views, (ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]), *head, *tail ].compact
      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
      #   <tt>pages/45/notes</tt>.
      # * Hash - Treated as an implicit call to +url_for+, like
      #   <tt>{ controller: 'pages', action: 'notes', id: 45}</tt>
      # * Regexp - Will remove any fragment that matches, so
      #   <tt>%r{pages/\d*/notes}</tt> might remove all notes. Make sure you
      #   don't use anchors in the regex (<tt>^</tt> or <tt>$</tt>) because
      #   the actual filename matched looks like
      #   <tt>./cache/filename/path.cache</tt>. 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 <tt>delete_matched</tt>, 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