xref: /dokuwiki/inc/Cache/Cache.php (revision 72c2bae8d6b05c60c9a71e50d60489ff63353f57)
1<?php
2
3namespace dokuwiki\Cache;
4
5/**
6 * Generic handling of caching
7 */
8class Cache
9{
10    public $key = '';          // primary identifier for this item
11    public $ext = '';          // file ext for cache data, secondary identifier for this item
12    public $cache = '';        // cache file name
13    public $depends = array(); // array containing cache dependency information,
14    //   used by makeDefaultCacheDecision to determine cache validity
15
16    public $_event = '';       // event to be triggered during useCache
17    public $_time;
18    public $_nocache = false;  // if set to true, cache will not be used or stored
19
20    /**
21     * @param string $key primary identifier
22     * @param string $ext file extension
23     */
24    public function __construct($key, $ext)
25    {
26        $this->key = $key;
27        $this->ext = $ext;
28        $this->cache = getCacheName($key, $ext);
29    }
30
31    /**
32     * public method to determine whether the cache can be used
33     *
34     * to assist in centralisation of event triggering and calculation of cache statistics,
35     * don't override this function override makeDefaultCacheDecision()
36     *
37     * @param  array $depends array of cache dependencies, support dependecies:
38     *                            'age'   => max age of the cache in seconds
39     *                            'files' => cache must be younger than mtime of each file
40     *                                       (nb. dependency passes if file doesn't exist)
41     *
42     * @return bool    true if cache can be used, false otherwise
43     */
44    public function useCache($depends = array())
45    {
46        $this->depends = $depends;
47        $this->addDependencies();
48
49        if ($this->_event) {
50            return $this->stats(trigger_event($this->_event, $this, array($this, 'makeDefaultCacheDecision')));
51        } else {
52            return $this->stats($this->makeDefaultCacheDecision());
53        }
54    }
55
56    /**
57     * private method containing cache use decision logic
58     *
59     * this function processes the following keys in the depends array
60     *   purge - force a purge on any non empty value
61     *   age   - expire cache if older than age (seconds)
62     *   files - expire cache if any file in this array was updated more recently than the cache
63     *
64     * Note that this function needs to be public as it is used as callback for the event handler
65     *
66     * can be overridden
67     *
68     * @internal This method may only be called by the event handler! Call \dokuwiki\Cache\Cache::useCache instead!
69     *
70     * @return bool               see useCache()
71     */
72    public function makeDefaultCacheDecision()
73    {
74
75        if ($this->_nocache) {
76            return false;
77        }                              // caching turned off
78        if (!empty($this->depends['purge'])) {
79            return false;
80        }              // purge requested?
81        if (!($this->_time = @filemtime($this->cache))) {
82            return false;
83        }   // cache exists?
84
85        // cache too old?
86        if (!empty($this->depends['age']) && ((time() - $this->_time) > $this->depends['age'])) {
87            return false;
88        }
89
90        if (!empty($this->depends['files'])) {
91            foreach ($this->depends['files'] as $file) {
92                if ($this->_time <= @filemtime($file)) {
93                    return false;
94                }         // cache older than files it depends on?
95            }
96        }
97
98        return true;
99    }
100
101    /**
102     * add dependencies to the depends array
103     *
104     * this method should only add dependencies,
105     * it should not remove any existing dependencies and
106     * it should only overwrite a dependency when the new value is more stringent than the old
107     */
108    protected function addDependencies()
109    {
110        global $INPUT;
111        if ($INPUT->has('purge')) {
112            $this->depends['purge'] = true;
113        }   // purge requested
114    }
115
116    /**
117     * retrieve the cached data
118     *
119     * @param   bool $clean true to clean line endings, false to leave line endings alone
120     * @return  string          cache contents
121     */
122    public function retrieveCache($clean = true)
123    {
124        return io_readFile($this->cache, $clean);
125    }
126
127    /**
128     * cache $data
129     *
130     * @param   string $data the data to be cached
131     * @return  bool           true on success, false otherwise
132     */
133    public function storeCache($data)
134    {
135        if ($this->_nocache) {
136            return false;
137        }
138
139        return io_savefile($this->cache, $data);
140    }
141
142    /**
143     * remove any cached data associated with this cache instance
144     */
145    public function removeCache()
146    {
147        @unlink($this->cache);
148    }
149
150    /**
151     * Record cache hits statistics.
152     * (Only when debugging allowed, to reduce overhead.)
153     *
154     * @param    bool $success result of this cache use attempt
155     * @return   bool              pass-thru $success value
156     */
157    protected function stats($success)
158    {
159        global $conf;
160        static $stats = null;
161        static $file;
162
163        if (!$conf['allowdebug']) {
164            return $success;
165        }
166
167        if (is_null($stats)) {
168            $file = $conf['cachedir'] . '/cache_stats.txt';
169            $lines = explode("\n", io_readFile($file));
170
171            foreach ($lines as $line) {
172                $i = strpos($line, ',');
173                $stats[substr($line, 0, $i)] = $line;
174            }
175        }
176
177        if (isset($stats[$this->ext])) {
178            list($ext, $count, $hits) = explode(',', $stats[$this->ext]);
179        } else {
180            $ext = $this->ext;
181            $count = 0;
182            $hits = 0;
183        }
184
185        $count++;
186        if ($success) {
187            $hits++;
188        }
189        $stats[$this->ext] = "$ext,$count,$hits";
190
191        io_saveFile($file, join("\n", $stats));
192
193        return $success;
194    }
195}
196