-
Notifications
You must be signed in to change notification settings - Fork 0
/
CacheNesting.module
353 lines (300 loc) · 12.1 KB
/
CacheNesting.module
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
<?php
/**
* CacheNesting (0.0.1)
* This module manages caches and dependencies of nested caches.
*
* @author Jan Kirchner
*
* ProcessWire 2.x
* Copyright (C) 2011 by Ryan Cramer
* Licensed under GNU/GPL v2, see LICENSE.TXT
*
* http://www.processwire.com
* http://www.ryancramer.com
*
*/
namespace ProcessWire;
class CacheNesting extends WireData implements Module
{
/**
* PW module info method
*/
public static function getModuleInfo(){
return array(
'title' => "CacheNesting",
'version' => "0.0.2",
'summary' => "A Processwire module that manages nested caches and their dependencies. It enables you to cache multiple parts of a page with different expiration which also can be nested in larger caches.",
'author' => "JohnK",
'href' => "https://github.com/janKir/CacheNesting",
'icon' => "sitemap",
'autoload' => true,
'singular' => true,
'requires' => "ProcessWire>=2.6"
);
}
public function ready() {
if ($this->page->template != "admin") {
$this->addHookBefore("Page::render", $this, "hookInit");
$this->addHookAfter("Page::render", $this, "hookFinish");
}
}
/**
* cache prefix
*/
private static $cachePrefix = "cnd";
/**
* list of page's cache dependencies
*/
protected static $dependencyList = array();
/**
* associative array to store the expiration value of every dependency cache
*/
protected static $cacheAvailable = array();
/**
* stores the $cache->getInfo array
*/
protected static $cacheAvailableVerbose = array();
/**
* associative array to store the status of every dependency cache
* key: cache name
* value: true if cache is available, otherwise false
*/
protected static $cacheStatus = array();
/**
* current page's cache name
*/
protected static $cachePage = "unknownPage";
protected function hookInit($event) {
$page = $event->object;
$prefix = self::$cachePrefix;
$title = $this->sanitizer->pageName($page->title);
$cacheName = "{$prefix}__{$page->id}-{$title}";
self::initCacheStatus($cacheName);
}
protected function hookFinish($event) {
self::saveCacheStatus();
}
/**
* returns the current cache page name
*/
public static function getCachePage() {
return self::$cachePage;
}
/**
* sets the current cache page name
*/
public static function setCachePage($cachePage) {
if (is_string($cachePage)) {
self::$cachePage = $cachePage;
}
}
/**
* initializes dependency tree for the current page
* to be called before all caching.
*/
public static function initCacheStatus($pageName) {
if (count(self::$dependencyList) > 0) {
throw new WireException("Cannot initialize cache dependencies, because the dependency List is not empty.");
}
if (!is_string($pageName)){
throw new WireException("Argument pageName was expected to be a string, but given type ". gettype($pageName));
}
self::$cachePage = $pageName;
$cache = wire()->cache;
$cache->maintenance(); // remove expired caches
$cache->preloadFor($pageName);
// load dependency tree from cache
$cachedDeps = $cache->getFor($pageName, "cacheDependencyTree");
if (is_array($cachedDeps)) {
self::$dependencyList = $cachedDeps;
}
// load infos about cache
self::$cacheAvailableVerbose = $cache->getInfo(false);
// create an associative array with cache names as keys and expirations as values
self::$cacheAvailable = array_combine(
array_map(function($ele) { return $ele["name"];}, self::$cacheAvailableVerbose),
array_map(
function($ele) {
$res = strtotime($ele["expires"]);
return $res ? $res - time() : $ele["expires"];
},
self::$cacheAvailableVerbose)
);
// keep only elements for the current page's cache
self::$cacheAvailable = array_filter(
self::$cacheAvailable,
function($k) { return substr($k, 0, strlen(self::$cachePage)) == self::$cachePage; },
ARRAY_FILTER_USE_KEY);
// keep only elements which are not expired by now
// NOTE: this is somehow neccessary because sometimes even expired caches are returned?!
self::$cacheAvailable = array_filter(
self::$cacheAvailable,
function($v) { return is_string($v) || $v < -60000000 || $v > 0; });
self::traverseCacheDeps();
}
/**
* traverses the dependecy tree and checks every dependency if it is available
* returns true if all dependencies are available, otherwise false
*/
protected static function traverseCacheDeps($tree = null) {
$cache = wire()->cache;
$full_traverse = false;
// if no tree given, use whole dependency list and traverse fully
if (!is_array($tree)){
$tree = self::$dependencyList;
$full_traverse = true;
}
// iterate the dependency tree and check which dependencies are available
$all_available = true;
foreach ($tree as $depName => $depValue) {
// skip "this" values
if ($depName === "this")
continue;
// skip entries which are already checked as non-available
if (array_key_exists($depName, self::$cacheStatus) && self::$cacheStatus[$depName] === false) {
$all_available = false;
// skip here if no full traverse is neccessary
if (!$full_traverse)
return false;
continue;
}
// only use cache if node is availabale and ALL children, too.
$this_available = true;
if (array_key_exists($depName, self::$cacheAvailable)) {
if (is_array($depValue)){
// recursive call to traverse all child dependencies
if (!self::traverseCacheDeps($depValue)) {
$this_available = false;
}
}
}
// this dependency is not available
else {
$this_available = false;
}
// add dependency and status to $cacheStatus
self::$cacheStatus[$depName] = $this_available;
// remove from dependency tree if not available to avoid checking same dependency again
if (!$this_available) {
unset(self::$dependencyList[$depName]);
$all_available = false;
// and delete straight from cache db
if (!$cache->delete($depName))
wire()->log->warning("failed deleting cache: $depName");
}
// skip if false and no full traverse needed
if (!$all_available && !$full_traverse) {
return false;
}
}
// return true if all dependencies are available, otherwise false
return $all_available;
}
/**
* returns cache for specified cache name or null if expired or not available
*/
public static function getCache($cacheName) {
if (!is_string($cacheName)){
throw new WireException("Argument cacheName was expected to be a string, but given type ". gettype($cacheName));
}
$cacheFullName = self::$cachePage . "__" . $cacheName;
if (array_key_exists($cacheFullName, self::$cacheStatus) && self::$cacheStatus[$cacheFullName] === true){
$cache = wire()->cache;
return $cache->getFor(self::$cachePage, $cacheName);
}
return null;
}
/**
* returns cache for specified cache name
* or if cache is not available, executes cache function to create and save cache
*/
public static function getCacheOrCreate($cacheName, $cacheFunction, $cacheExpire = null, $dependencies = array()) {
$cacheData = self::getCache($cacheName);
if (!$cacheData) {
$cacheData = $cacheFunction();
self::createCache($cacheName, $cacheData, $cacheExpire, $dependencies);
}
return $cacheData;
}
/**
* saves the cache dependency tree to cache
* to be called when all caching is done and output is created
*/
public static function saveCacheStatus() {
// create cache
$cache = wire()->cache;
$success = $cache->saveFor(self::$cachePage, "cacheDependencyTree", self::$dependencyList, WireCache::expireNever);
if (!$success)
wire()->log->error("The CacheNestedDependency module failed saving the cache dependency tree!");
}
/**
* creates cache for a given input, only if not specified before
* same arguments as ::createCache
*/
public static function createCacheOnce($cacheName, $cacheData, $cacheExpire = null, $dependencies = array()) {
$cacheFullName = self::$cachePage . "__" . $cacheName;
if (!array_key_exists($cacheFullName, self::$dependencyList)) {
self::createCache($cacheName, $cacheData, $cacheExpire, $dependencies);
}
}
/**
* creates cache for a given input data
* param $cacheName: name of the cached input
* param $cacheData: data to be cached
* param $cacheExpire: expiration value, possible values specified by WireCache or null
* e.g. timestamp, seconds, Page object, Page ids, WireCache constants
* param $dependencies: array of cache names this cache depends on
* thus, if any dependency expires, this cache will, too.
*/
public static function createCache($cacheName, $cacheData, $cacheExpire = null, $dependencies = array()) {
if (!is_string($cacheName)){
throw new WireException("Argument cacheName was expected to be a string, but given type ". gettype($cacheName));
}
$cacheFullName = self::$cachePage . "__" . $cacheName;
// check input type of $dependencies
if (!is_array($dependencies)) {
if (is_string($dependencies))
$dependencies = array ( $dependencies );
else
$dependencies = array();
}
// remove existing entry
if (array_key_exists($cacheFullName, self::$dependencyList)) {
unset(self::$dependencyList[$cacheFullName]);
}
// if this cache is a non-leaf node
if (count($dependencies) > 0) {
// create dependencies array
$thisDependencies = array();
// if this cache has expiration, add a "this" entry to dependencies
if ($cacheExpire) {
$thisDependencies["this"] = $cacheExpire;
}
// add all specified dependencies
foreach ($dependencies as $dependency) {
$dependency = self::$cachePage . "__" . $dependency;
// check if dependencies are available
if (!array_key_exists($dependency, CacheNesting::$dependencyList)){
throw new WireException("CacheNesting::createCache() : trying to reference a non-available dependency: $dependency. Dependencies must be cached before their dependent.");
}
// create nested dependency list
$thisDependencies[$dependency] = CacheNesting::$dependencyList[$dependency];
}
}
// if this cache is a leaf node
else {
$thisDependencies = $cacheExpire;
}
// add dependencies to list
CacheNesting::$dependencyList[$cacheFullName] = $thisDependencies;
// create cache
$cache = wire()->cache;
$cache->saveFor(self::$cachePage, $cacheName, $cacheData, $cacheExpire);
}
/**
* returns the current page's dependency list
*/
public static function getDep() {
return CacheNesting::$dependencyList;
}
}