1
|
|
|
<?php |
2
|
|
|
namespace Agavi\Filter; |
3
|
|
|
|
4
|
|
|
// +---------------------------------------------------------------------------+ |
5
|
|
|
// | This file is part of the Agavi package. | |
6
|
|
|
// | Copyright (c) 2005-2011 the Agavi Project. | |
7
|
|
|
// | Based on the Mojavi3 MVC Framework, Copyright (c) 2003-2005 Sean Kerr. | |
8
|
|
|
// | | |
9
|
|
|
// | For the full copyright and license information, please view the LICENSE | |
10
|
|
|
// | file that was distributed with this source code. You can also view the | |
11
|
|
|
// | LICENSE file online at http://www.agavi.org/LICENSE.txt | |
12
|
|
|
// | vi: set noexpandtab: | |
13
|
|
|
// | Local Variables: | |
14
|
|
|
// | indent-tabs-mode: t | |
15
|
|
|
// | End: | |
16
|
|
|
// +---------------------------------------------------------------------------+ |
17
|
|
|
use Agavi\Config\Config; |
18
|
|
|
use Agavi\Config\ConfigCache; |
19
|
|
|
use Agavi\Dispatcher\ExecutionContainer; |
20
|
|
|
use Agavi\Exception\AgaviException; |
21
|
|
|
use Agavi\Exception\InitializationException; |
22
|
|
|
use Agavi\Exception\UncacheableException; |
23
|
|
|
use Agavi\Exception\ViewException; |
24
|
|
|
use Agavi\Request\RequestDataHolder; |
25
|
|
|
use Agavi\Response\Response; |
26
|
|
|
use Agavi\User\SecurityUser; |
27
|
|
|
use Agavi\Util\ArrayPathDefinition; |
28
|
|
|
use Agavi\Util\Toolkit; |
29
|
|
|
use Agavi\View\View; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* The ExecutionFilter is the last filter registered for each filter chain. |
33
|
|
|
* This filter does all controller and view execution. |
34
|
|
|
* |
35
|
|
|
* @package agavi |
36
|
|
|
* @subpackage filter |
37
|
|
|
* |
38
|
|
|
* @author David Zülke <[email protected]> |
39
|
|
|
* @author Sean Kerr <[email protected]> |
40
|
|
|
* @copyright Authors |
41
|
|
|
* @copyright The Agavi Project |
42
|
|
|
* |
43
|
|
|
* @since 0.9.0 |
44
|
|
|
* |
45
|
|
|
* @version $Id$ |
46
|
|
|
*/ |
47
|
|
|
class ExecutionFilter extends Filter implements ControllerFilterInterface |
48
|
|
|
{ |
49
|
|
|
/* |
50
|
|
|
* The directory inside %core.cache_dir% where cached stuff is stored. |
51
|
|
|
*/ |
52
|
|
|
const CACHE_SUBDIR = 'content'; |
53
|
|
|
|
54
|
|
|
/* |
55
|
|
|
* The name of the file that holds the cached controller data. |
56
|
|
|
* Minuses because these are not allowed in an output type name. |
57
|
|
|
*/ |
58
|
|
|
const CONTROLLER_CACHE_ID = '4-8-15-16-23-42'; |
59
|
|
|
|
60
|
|
|
/* |
61
|
|
|
* Constants for the cache callback event types. |
62
|
|
|
*/ |
63
|
|
|
const CACHE_CALLBACK_CONTROLLER_NOT_CACHED = 0; |
64
|
|
|
const CACHE_CALLBACK_CONTROLLER_CACHE_GONE = 1; |
65
|
|
|
const CACHE_CALLBACK_VIEW_NOT_CACHEABLE = 2; |
66
|
|
|
const CACHE_CALLBACK_VIEW_NOT_CACHED = 3; |
67
|
|
|
const CACHE_CALLBACK_OUTPUT_TYPE_NOT_CACHEABLE = 4; |
68
|
|
|
const CACHE_CALLBACK_VIEW_CACHE_GONE = 5; |
69
|
|
|
const CACHE_CALLBACK_CONTROLLER_CACHE_USELESS = 6; |
70
|
|
|
const CACHE_CALLBACK_VIEW_CACHE_WRITTEN = 7; |
71
|
|
|
const CACHE_CALLBACK_CONTROLLER_CACHE_WRITTEN = 8; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Method that's called when a cacheable Controller/View with a stale cache is |
75
|
|
|
* about to be run. |
76
|
|
|
* Can be used to prevent stampede situations where many requests to an controller |
77
|
|
|
* with an out-of-date cache are run in parallel, slowing down everything. |
78
|
|
|
* For instance, you could set a flag into memcached with the groups of the |
79
|
|
|
* controller that's currently run, and in checkCache check for those and return |
80
|
|
|
* an old, stale cache until the flag is gone. |
81
|
|
|
* |
82
|
|
|
* @param int $eventType The type of the event that occurred. |
83
|
|
|
* See CACHE_CALLBACK_* constants. |
84
|
|
|
* @param array $groups The groups. |
85
|
|
|
* @param array $configThe caching configuration. |
|
|
|
|
86
|
|
|
* @param ExecutionContainer $container The current execution container. |
87
|
|
|
* |
88
|
|
|
* @author David Zülke <[email protected]> |
89
|
|
|
* @since 1.0.0 |
90
|
|
|
*/ |
91
|
|
|
public function startedCacheCreationCallback($eventType, array $groups, array $config, ExecutionContainer $container) |
|
|
|
|
92
|
|
|
{ |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* Method that's called when an Controller/View that was assumed to be cacheable |
97
|
|
|
* turned out not to be (because the View or Output Type isn't). |
98
|
|
|
* |
99
|
|
|
* @see ExecutionFilter::startedCacheCreationCallback() |
100
|
|
|
* |
101
|
|
|
* @param int $eventType The type of the event that occurred. |
102
|
|
|
* See CACHE_CALLBACK_* constants. |
103
|
|
|
* @param array $groups The groups. |
104
|
|
|
* @param array $config The caching configuration. |
105
|
|
|
* @param ExecutionContainer $container The current execution container. |
106
|
|
|
* |
107
|
|
|
* @author David Zülke <[email protected]> |
108
|
|
|
* @since 1.0.0 |
109
|
|
|
*/ |
110
|
|
|
public function abortedCacheCreationCallback($eventType, array $groups, array $config, ExecutionContainer $container) |
|
|
|
|
111
|
|
|
{ |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Method that's called when a cacheable Controller/View with a stale cache has |
116
|
|
|
* finished execution and all caches are written. |
117
|
|
|
* |
118
|
|
|
* @see ExecutionFilter::startedCacheCreationCallback() |
119
|
|
|
* |
120
|
|
|
* @param int $eventType The type of the event that occurred. |
121
|
|
|
* See CACHE_CALLBACK_* constants. |
122
|
|
|
* @param array $groups The groups. |
123
|
|
|
* @param array $config The caching configuration. |
124
|
|
|
* @param ExecutionContainer $container The current execution container. |
125
|
|
|
* |
126
|
|
|
* @author David Zülke <[email protected]> |
127
|
|
|
* @since 1.0.0 |
128
|
|
|
*/ |
129
|
|
|
public function finishedCacheCreationCallback($eventType, array $groups, array $config, ExecutionContainer $container) |
|
|
|
|
130
|
|
|
{ |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* Check if a cache exists and is up-to-date |
135
|
|
|
* |
136
|
|
|
* @param array $groups An array of cache groups |
137
|
|
|
* @param string $lifetime The lifetime of the cache as a strtotime relative string |
138
|
|
|
* without the leading plus sign. |
139
|
|
|
* |
140
|
|
|
* @return bool true, if the cache is up to date, otherwise false |
141
|
|
|
* |
142
|
|
|
* @author David Zülke <[email protected]> |
143
|
|
|
* @since 0.11.0 |
144
|
|
|
*/ |
145
|
|
|
public function checkCache(array $groups, $lifetime = null) |
146
|
|
|
{ |
147
|
|
|
foreach ($groups as &$group) { |
148
|
|
|
$group = base64_encode($group); |
149
|
|
|
} |
150
|
|
|
$filename = Config::get('core.cache_dir') . DIRECTORY_SEPARATOR . self::CACHE_SUBDIR . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $groups) . '.cefcache'; |
151
|
|
|
$isReadable = is_readable($filename); |
152
|
|
|
if ($lifetime === null || !$isReadable) { |
153
|
|
|
return $isReadable; |
154
|
|
|
} else { |
155
|
|
|
$expiry = strtotime('+' . $lifetime, filemtime($filename)); |
156
|
|
|
if ($expiry !== false) { |
157
|
|
|
return $isReadable && ($expiry >= time()); |
158
|
|
|
} else { |
159
|
|
|
return false; |
160
|
|
|
} |
161
|
|
|
} |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* Read the contents of a cache |
166
|
|
|
* |
167
|
|
|
* @param array $groups An array of cache groups |
168
|
|
|
* |
169
|
|
|
* @return array The cache data |
170
|
|
|
* |
171
|
|
|
* @author David Zülke <[email protected]> |
172
|
|
|
* @since 0.11.0 |
173
|
|
|
*/ |
174
|
|
|
public function readCache(array $groups) |
175
|
|
|
{ |
176
|
|
|
foreach ($groups as &$group) { |
177
|
|
|
$group = base64_encode($group); |
178
|
|
|
} |
179
|
|
|
$filename = Config::get('core.cache_dir') . DIRECTORY_SEPARATOR . self::CACHE_SUBDIR . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $groups) . '.cefcache'; |
180
|
|
|
$data = @file_get_contents($filename); |
181
|
|
|
if ($data !== false) { |
182
|
|
|
return unserialize($data); |
183
|
|
|
} else { |
184
|
|
|
throw new AgaviException(sprintf('Failed to read cache file "%s"', $filename)); |
185
|
|
|
} |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* Write cache content |
190
|
|
|
* |
191
|
|
|
* @param array $groups An array of cache groups |
192
|
|
|
* @param array $data The cache data |
193
|
|
|
* @param string $lifetime The lifetime of the cache as a strtotime relative string |
194
|
|
|
* without the leading plus sign. |
195
|
|
|
* |
196
|
|
|
* @return bool The result of the write operation |
197
|
|
|
* |
198
|
|
|
* @author David Zülke <[email protected]> |
199
|
|
|
* @since 0.11.0 |
200
|
|
|
*/ |
201
|
|
|
public function writeCache(array $groups, $data, $lifetime = null) |
|
|
|
|
202
|
|
|
{ |
203
|
|
|
// lifetime is not used in this implementation! |
204
|
|
|
|
205
|
|
|
foreach ($groups as &$group) { |
206
|
|
|
$group = base64_encode($group); |
207
|
|
|
} |
208
|
|
|
@mkdir(Config::get('core.cache_dir') . DIRECTORY_SEPARATOR . self::CACHE_SUBDIR . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, array_slice($groups, 0, -1)), 0777, true); |
|
|
|
|
209
|
|
|
return file_put_contents(Config::get('core.cache_dir') . DIRECTORY_SEPARATOR . self::CACHE_SUBDIR . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $groups) . '.cefcache', serialize($data), LOCK_EX); |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* Flushes the cache for a group |
214
|
|
|
* |
215
|
|
|
* @param array $groups An array of cache groups |
216
|
|
|
* |
217
|
|
|
* @author David Zülke <[email protected]> |
218
|
|
|
* @since 0.11.0 |
219
|
|
|
*/ |
220
|
|
|
public static function clearCache(array $groups = array()) |
221
|
|
|
{ |
222
|
|
|
foreach ($groups as &$group) { |
223
|
|
|
$group = base64_encode($group); |
224
|
|
|
} |
225
|
|
|
$path = self::CACHE_SUBDIR . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $groups); |
226
|
|
|
if (is_file(Config::get('core.cache_dir') . DIRECTORY_SEPARATOR . $path . '.cefcache')) { |
227
|
|
|
Toolkit::clearCache($path . '.cefcache'); |
228
|
|
|
} else { |
229
|
|
|
Toolkit::clearCache($path); |
230
|
|
|
} |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* Builds an array of cache groups using the configuration and a container. |
235
|
|
|
* |
236
|
|
|
* @param array $groups The group array from the configuration. |
237
|
|
|
* @param ExecutionContainer $container The execution container. |
238
|
|
|
* |
239
|
|
|
* @return array An array of groups. |
240
|
|
|
* |
241
|
|
|
* @author David Zülke <[email protected]> |
242
|
|
|
* @since 0.11.0 |
243
|
|
|
*/ |
244
|
|
|
public function determineGroups(array $groups, ExecutionContainer $container) |
245
|
|
|
{ |
246
|
|
|
$retval = array(); |
247
|
|
|
|
248
|
|
|
foreach ($groups as $group) { |
249
|
|
|
$group += array('name' => null, 'source' => null, 'namespace' => null); |
250
|
|
|
$val = $this->getVariable($group['name'], $group['source'], $group['namespace'], $container); |
251
|
|
|
|
252
|
|
|
if (is_object($val) && is_callable(array($val, '__toString'))) { |
253
|
|
|
$val = $val->__toString(); |
254
|
|
|
} elseif (is_object($val)) { |
255
|
|
|
$val = spl_object_hash($val); |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
if ($val === null || $val === false || $val === '') { |
259
|
|
|
$val = '0'; |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
if (!is_scalar($val)) { |
263
|
|
|
throw new UncacheableException('Group value is not a scalar, cannot construct a meaningful string representation.'); |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
$retval[] = $val; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
$retval[] = $container->getModuleName() . '_' . $container->getControllerName(); |
270
|
|
|
|
271
|
|
|
return $retval; |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
/** |
275
|
|
|
* Read a variable from the given source and, optionally, namespace. |
276
|
|
|
* |
277
|
|
|
* @param string $name The variable name. |
278
|
|
|
* @param string $source The optional variable source. |
279
|
|
|
* @param string $namespace The optional namespace in the source. |
280
|
|
|
* @param ExecutionContainer $container The container to use, if necessary. |
281
|
|
|
* |
282
|
|
|
* @return mixed The variable. |
283
|
|
|
* |
284
|
|
|
* @author David Zülke <[email protected]> |
285
|
|
|
* @since 0.11.0 |
286
|
|
|
*/ |
287
|
|
|
public function getVariable($name, $source = 'string', $namespace = null, ExecutionContainer $container = null) |
288
|
|
|
{ |
289
|
|
|
$val = $name; |
290
|
|
|
|
291
|
|
|
switch ($source) { |
292
|
|
|
case 'callback': |
293
|
|
|
$val = $container->getControllerInstance()->$name(); |
|
|
|
|
294
|
|
|
break; |
295
|
|
|
case 'configuration_directive': |
296
|
|
|
$val = Config::get($name); |
297
|
|
|
break; |
298
|
|
|
case 'constant': |
299
|
|
|
$val = constant($name); |
300
|
|
|
break; |
301
|
|
|
case 'container_parameter': |
302
|
|
|
$val = $container->getParameter($name); |
303
|
|
|
break; |
304
|
|
|
case 'global_request_data': |
305
|
|
|
$val = $this->context->getRequest()->getRequestData()->get($namespace ? $namespace : RequestDataHolder::SOURCE_PARAMETERS, $name); |
306
|
|
|
break; |
307
|
|
|
case 'locale': |
308
|
|
|
$val = $this->context->getTranslationManager()->getCurrentLocaleIdentifier(); |
309
|
|
|
break; |
310
|
|
|
case 'request_attribute': |
311
|
|
|
$val = $this->context->getRequest()->getAttribute($name, $namespace); |
312
|
|
|
break; |
313
|
|
|
case 'request_data': |
314
|
|
|
$val = $container->getRequestData()->get($namespace ? $namespace : RequestDataHolder::SOURCE_PARAMETERS, $name); |
315
|
|
|
break; |
316
|
|
|
case 'request_parameter': |
317
|
|
|
$val = $this->context->getRequest()->getRequestData()->getParameter($name); |
318
|
|
|
break; |
319
|
|
|
case 'user_attribute': |
320
|
|
|
$val = $this->context->getUser()->getAttribute($name, $namespace); |
321
|
|
|
break; |
322
|
|
|
case 'user_authenticated': |
323
|
|
|
if (($user = $this->context->getUser()) instanceof SecurityUser) { |
324
|
|
|
$val = $user->isAuthenticated(); |
325
|
|
|
} |
326
|
|
|
break; |
327
|
|
|
case 'user_credential': |
328
|
|
|
if (($user = $this->context->getUser()) instanceof SecurityUser) { |
329
|
|
|
$val = $user->hasCredentials($name); |
330
|
|
|
} |
331
|
|
|
break; |
332
|
|
|
case 'user_parameter': |
333
|
|
|
$val = $this->context->getUser()->getParameter($name); |
334
|
|
|
break; |
335
|
|
|
} |
336
|
|
|
|
337
|
|
|
return $val; |
338
|
|
|
} |
339
|
|
|
|
340
|
|
|
/** |
341
|
|
|
* Execute this filter. |
342
|
|
|
* |
343
|
|
|
* @param FilterChain $filterChain The filter chain. |
344
|
|
|
* @param ExecutionContainer $container The current execution container. |
345
|
|
|
* |
346
|
|
|
* @throws InitializationException If an error occurs during |
347
|
|
|
* View initialization. |
348
|
|
|
* @throws ViewException If an error occurs while |
349
|
|
|
* executing the View. |
350
|
|
|
* |
351
|
|
|
* @author David Zülke <[email protected]> |
352
|
|
|
* @author Felix Gilcher <[email protected]> |
353
|
|
|
* @author Sean Kerr <[email protected]> |
354
|
|
|
* @since 0.9.0 |
355
|
|
|
*/ |
356
|
|
|
public function execute(FilterChain $filterChain, ExecutionContainer $container) |
357
|
|
|
{ |
358
|
|
|
// $lm = $this->context->getLoggerManager(); |
|
|
|
|
359
|
|
|
|
360
|
|
|
// get the context, Dispatcher and validator manager |
361
|
|
|
$dispatcher = $this->context->getDispatcher(); |
362
|
|
|
|
363
|
|
|
// get the current controller information |
364
|
|
|
$controllerName = $container->getControllerName(); |
365
|
|
|
$moduleName = $container->getModuleName(); |
366
|
|
|
|
367
|
|
|
// the controller instance |
368
|
|
|
$controllerInstance = $container->getControllerInstance(); |
369
|
|
|
|
370
|
|
|
$request = $this->context->getRequest(); |
371
|
|
|
|
372
|
|
|
$isCacheable = false; |
373
|
|
|
$cachingDotXml = Toolkit::evaluateModuleDirective( |
374
|
|
|
$moduleName, |
375
|
|
|
'agavi.cache.path', |
376
|
|
|
array( |
377
|
|
|
'moduleName' => $moduleName, |
378
|
|
|
'controllerName' => $controllerName, |
379
|
|
|
) |
380
|
|
|
); |
381
|
|
|
|
382
|
|
|
$config = array(); |
383
|
|
|
|
384
|
|
|
if ($this->getParameter('enable_caching', true) && is_readable($cachingDotXml)) { |
385
|
|
|
// $lm->log('Caching enabled, configuration file found, loading...'); |
|
|
|
|
386
|
|
|
// no _once please! |
387
|
|
|
include(ConfigCache::checkConfig($cachingDotXml, $this->context->getName())); |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
$isControllerCached = false; |
391
|
|
|
|
392
|
|
|
if ($isCacheable) { |
393
|
|
|
try { |
394
|
|
|
$groups = $this->determineGroups($config['groups'], $container); |
395
|
|
|
$controllerGroups = array_merge($groups, array(self::CONTROLLER_CACHE_ID)); |
396
|
|
|
} catch (UncacheableException $e) { |
397
|
|
|
// a group callback threw an exception. that means we're not allowed t cache |
398
|
|
|
$isCacheable = false; |
399
|
|
|
} |
400
|
|
|
if ($isCacheable) { |
401
|
|
|
// this is not wrapped in the try/catch block above as it might throw an exception itself |
402
|
|
|
$isControllerCached = $this->checkCache(array_merge($groups, array(self::CONTROLLER_CACHE_ID)), $config['lifetime']); |
403
|
|
|
|
404
|
|
|
if (!$isControllerCached) { |
405
|
|
|
// cacheable, but controller is not cached. notify our callback so it can prevent the stampede that follows |
406
|
|
|
$this->startedCacheCreationCallback(self::CACHE_CALLBACK_CONTROLLER_NOT_CACHED, $controllerGroups, $config, $container); |
|
|
|
|
407
|
|
|
} |
408
|
|
|
} |
409
|
|
|
} else { |
|
|
|
|
410
|
|
|
// $lm->log('Controller is not cacheable!'); |
|
|
|
|
411
|
|
|
} |
412
|
|
|
|
413
|
|
|
if ($isControllerCached) { |
414
|
|
|
// $lm->log('Controller is cached, loading...'); |
|
|
|
|
415
|
|
|
// cache/dir/4-8-15-16-23-42 contains the controller cache |
416
|
|
|
try { |
417
|
|
|
$controllerCache = $this->readCache($controllerGroups); |
418
|
|
|
// and restore controller attributes |
419
|
|
|
$controllerInstance->setAttributes($controllerCache['controller_attributes']); |
420
|
|
|
} catch (\Exception $e) { |
421
|
|
|
// cacheable, but controller is not cached. notify our callback so it can prevent the stampede that follows |
422
|
|
|
$this->startedCacheCreationCallback(self::CACHE_CALLBACK_CONTROLLER_CACHE_GONE, $controllerGroups, $config, $container); |
423
|
|
|
$isControllerCached = false; |
424
|
|
|
} |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
$isViewCached = false; |
428
|
|
|
$rememberTheView = null; |
429
|
|
|
|
430
|
|
|
while (true) { |
431
|
|
|
if (!$isControllerCached) { |
432
|
|
|
$controllerCache = array(); |
433
|
|
|
|
434
|
|
|
// $lm->log('Controller not cached, executing...'); |
|
|
|
|
435
|
|
|
// execute the Controller and get the View to execute |
436
|
|
|
list($controllerCache['view_module'], $controllerCache['view_name']) = $container->runController(); |
437
|
|
|
|
438
|
|
|
// check if we've just run the controller again after a previous cache read revealed that the view is not cached for this output type and we need to go back to square one due to the lack of controller attribute caching configuration... |
439
|
|
|
// if yes: is the view module/name that we got just now different from what was in the cache? |
440
|
|
|
if (isset($rememberTheView) && $controllerCache != $rememberTheView) { |
441
|
|
|
// yup. clear it! |
442
|
|
|
$ourClass = get_class($this); |
443
|
|
|
$ourClass::clearCache($groups); |
|
|
|
|
444
|
|
|
} |
445
|
|
|
|
446
|
|
|
// check if the returned view is cacheable |
447
|
|
|
if ($isCacheable && is_array($config['views']) && !in_array(array('module' => $controllerCache['view_module'], 'name' => $controllerCache['view_name']), $config['views'], true)) { |
448
|
|
|
$isCacheable = false; |
449
|
|
|
$this->abortedCacheCreationCallback(self::CACHE_CALLBACK_VIEW_NOT_CACHEABLE, $controllerGroups, $config, $container); |
450
|
|
|
|
451
|
|
|
// so that view is not cacheable? okay then: |
452
|
|
|
// check if we've just run the controller again after a previous cache read revealed that the view is not cached for this output type and we need to go back to square one due to the lack of controller attribute caching configuration... |
453
|
|
|
// 'cause then we need to flush all those existing caches - obviously, that data is stale now, as we learned, since we are not allowed to cache anymore for the view that was returned now |
454
|
|
|
if (isset($rememberTheView)) { |
455
|
|
|
// yup. clear it! |
456
|
|
|
$ourClass = get_class($this); |
457
|
|
|
$ourClass::clearCache($groups); |
458
|
|
|
} |
459
|
|
|
// $lm->log('Returned View is not cleared for caching, setting cacheable status to false.'); |
|
|
|
|
460
|
|
|
} else { |
|
|
|
|
461
|
|
|
// $lm->log('Returned View is cleared for caching, proceeding...'); |
|
|
|
|
462
|
|
|
} |
463
|
|
|
|
464
|
|
|
$controllerAttributes = $controllerInstance->getAttributes(); |
465
|
|
|
} |
466
|
|
|
|
467
|
|
|
// clear the response |
468
|
|
|
$response = $container->getResponse(); |
469
|
|
|
$response->clear(); |
470
|
|
|
|
471
|
|
|
// clear any forward set, it's ze view's job |
472
|
|
|
$container->clearNext(); |
473
|
|
|
|
474
|
|
|
if ($controllerCache['view_name'] !== View::NONE) { |
475
|
|
|
$container->setViewModuleName($controllerCache['view_module']); |
|
|
|
|
476
|
|
|
$container->setViewName($controllerCache['view_name']); |
477
|
|
|
|
478
|
|
|
$key = $request->toggleLock(); |
479
|
|
|
try { |
480
|
|
|
$viewInstance = $container->getViewInstance(); |
481
|
|
|
} catch (\Exception $e) { |
482
|
|
|
// we caught an exception... unlock the request and rethrow! |
483
|
|
|
$request->toggleLock($key); |
|
|
|
|
484
|
|
|
throw $e; |
485
|
|
|
} |
486
|
|
|
$request->toggleLock($key); |
|
|
|
|
487
|
|
|
|
488
|
|
|
$outputType = $container->getOutputType()->getName(); |
489
|
|
|
|
490
|
|
|
if ($isCacheable) { |
491
|
|
|
if (isset($config['output_types'][$otConfig = $outputType]) || isset($config['output_types'][$otConfig = '*'])) { |
492
|
|
|
$otConfig = $config['output_types'][$otConfig]; |
493
|
|
|
|
494
|
|
|
$viewGroups = array_merge($groups, array($outputType)); |
495
|
|
|
|
496
|
|
|
if ($isControllerCached) { |
497
|
|
|
$isViewCached = $this->checkCache($viewGroups, $config['lifetime']); |
498
|
|
|
if (!$isViewCached) { |
499
|
|
|
// cacheable, but view is not cached. notify our callback so it can prevent the stampede that follows |
500
|
|
|
$this->startedCacheCreationCallback(self::CACHE_CALLBACK_VIEW_NOT_CACHED, $viewGroups, $config, $container); |
501
|
|
|
} |
502
|
|
|
} |
503
|
|
|
} else { |
504
|
|
|
$this->abortedCacheCreationCallback(self::CACHE_CALLBACK_OUTPUT_TYPE_NOT_CACHEABLE, $controllerGroups, $config, $container); |
505
|
|
|
$isCacheable = false; |
506
|
|
|
} |
507
|
|
|
} |
508
|
|
|
|
509
|
|
|
if ($isViewCached) { |
510
|
|
|
// $lm->log('View is cached, loading...'); |
|
|
|
|
511
|
|
|
try { |
512
|
|
|
$viewCache = $this->readCache($viewGroups); |
|
|
|
|
513
|
|
|
} catch (AgaviException $e) { |
514
|
|
|
$this->startedCacheCreationCallback(self::CACHE_CALLBACK_VIEW_CACHE_GONE, $viewGroups, $config, $container); |
515
|
|
|
$isViewCached = false; |
516
|
|
|
} |
517
|
|
|
} |
518
|
|
|
if (!$isViewCached) { |
519
|
|
|
// view not cached |
520
|
|
|
// has the cache config a list of controller attributes? |
521
|
|
|
if ($isControllerCached && !$config['controller_attributes']) { |
522
|
|
|
// no. that means we must run the controller again! |
523
|
|
|
$isControllerCached = false; |
524
|
|
|
|
525
|
|
|
if ($isCacheable) { |
526
|
|
|
// notify our callback so it can remove the lock that's on the view |
527
|
|
|
// but only if we're still marked as cacheable (if not, then that means the OT is not cacheable, so there wouldn't be a $viewGroups) |
528
|
|
|
$this->abortedCacheCreationCallback(self::CACHE_CALLBACK_CONTROLLER_CACHE_USELESS, $viewGroups, $config, $container); |
529
|
|
|
} |
530
|
|
|
// notify our callback so it can prevent the stampede that follows |
531
|
|
|
$this->startedCacheCreationCallback(self::CACHE_CALLBACK_CONTROLLER_CACHE_USELESS, $controllerGroups, $config, $container); |
532
|
|
|
|
533
|
|
|
// but remember the view info, just in case it differs if we run the controller again now |
534
|
|
|
$rememberTheView = array( |
535
|
|
|
'view_module' => $controllerCache['view_module'], |
536
|
|
|
'view_name' => $controllerCache['view_name'], |
537
|
|
|
); |
538
|
|
|
continue; |
539
|
|
|
} |
540
|
|
|
|
541
|
|
|
$viewCache = array(); |
542
|
|
|
$viewCache['next'] = $this->executeView($container); |
543
|
|
|
} |
544
|
|
|
|
545
|
|
|
if ($viewCache['next'] instanceof ExecutionContainer) { |
546
|
|
|
// $lm->log('Forwarding request, skipping rendering...'); |
|
|
|
|
547
|
|
|
$container->setNext($viewCache['next']); |
|
|
|
|
548
|
|
|
} else { |
549
|
|
|
$output = array(); |
550
|
|
|
$nextOutput = null; |
551
|
|
|
|
552
|
|
|
if ($isViewCached) { |
553
|
|
|
$layers = $viewCache['layers']; |
554
|
|
|
/** @var Response $response */ |
555
|
|
|
$response = $viewCache['response']; |
556
|
|
|
$container->setResponse($response); |
557
|
|
|
|
558
|
|
|
foreach ($viewCache['template_variables'] as $name => $value) { |
559
|
|
|
$viewInstance->setAttribute($name, $value); |
560
|
|
|
} |
561
|
|
|
|
562
|
|
View Code Duplication |
foreach ($viewCache['request_attributes'] as $requestAttribute) { |
|
|
|
|
563
|
|
|
$request->setAttribute($requestAttribute['name'], $requestAttribute['value'], $requestAttribute['namespace']); |
564
|
|
|
} |
565
|
|
|
|
566
|
|
|
foreach ($viewCache['request_attribute_namespaces'] as $ranName => $ranValues) { |
567
|
|
|
$request->setAttributes($ranValues, $ranName); |
568
|
|
|
} |
569
|
|
|
|
570
|
|
|
$nextOutput = $response->getContent(); |
571
|
|
|
} else { |
572
|
|
|
if ($viewCache['next'] !== null) { |
573
|
|
|
// response content was returned from view execute() |
574
|
|
|
$response->setContent($nextOutput = $viewCache['next']); |
575
|
|
|
$viewCache['next'] = null; |
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
$layers = $viewInstance->getLayers(); |
579
|
|
|
|
580
|
|
|
if ($isCacheable) { |
581
|
|
|
$viewCache['template_variables'] = array(); |
582
|
|
|
foreach ($otConfig['template_variables'] as $varName) { |
|
|
|
|
583
|
|
|
$viewCache['template_variables'][$varName] = $viewInstance->getAttribute($varName); |
584
|
|
|
} |
585
|
|
|
|
586
|
|
|
$viewCache['response'] = clone $response; |
587
|
|
|
|
588
|
|
|
$viewCache['layers'] = array(); |
589
|
|
|
|
590
|
|
|
$viewCache['slots'] = array(); |
591
|
|
|
|
592
|
|
|
$lastCacheableLayer = -1; |
593
|
|
|
if (is_array($otConfig['layers'])) { |
594
|
|
|
if (count($otConfig['layers'])) { |
595
|
|
|
for ($i = count($layers)-1; $i >= 0; $i--) { |
596
|
|
|
$layer = $layers[$i]; |
597
|
|
|
$layerName = $layer->getName(); |
598
|
|
|
if (isset($otConfig['layers'][$layerName])) { |
599
|
|
|
if (is_array($otConfig['layers'][$layerName])) { |
600
|
|
|
$lastCacheableLayer = $i - 1; |
601
|
|
|
} else { |
602
|
|
|
$lastCacheableLayer = $i; |
603
|
|
|
} |
604
|
|
|
} |
605
|
|
|
} |
606
|
|
|
} |
607
|
|
|
} else { |
608
|
|
|
$lastCacheableLayer = count($layers) - 1; |
609
|
|
|
} |
610
|
|
|
|
611
|
|
|
for ($i = $lastCacheableLayer + 1; $i < count($layers); $i++) { |
|
|
|
|
612
|
|
|
// $lm->log('Adding non-cacheable layer "' . $layers[$i]->getName() . '" to list'); |
|
|
|
|
613
|
|
|
$viewCache['layers'][] = clone $layers[$i]; |
614
|
|
|
} |
615
|
|
|
} |
616
|
|
|
} |
617
|
|
|
|
618
|
|
|
$attributes =& $viewInstance->getAttributes(); |
619
|
|
|
|
620
|
|
|
// whether or not we should assign the previous' layer's output to the $slots array |
621
|
|
|
$assignInnerToSlots = $this->getParameter('assign_inner_to_slots', false); |
622
|
|
|
|
623
|
|
|
// $lm->log('Starting rendering...'); |
|
|
|
|
624
|
|
|
for ($i = 0; $i < count($layers); $i++) { |
|
|
|
|
625
|
|
|
$layer = $layers[$i]; |
626
|
|
|
$layerName = $layer->getName(); |
627
|
|
|
// $lm->log('Running layer "' . $layerName . '"...'); |
|
|
|
|
628
|
|
|
foreach ($layer->getSlots() as $slotName => $slotContainer) { |
629
|
|
|
if ($isViewCached && isset($viewCache['slots'][$layerName][$slotName])) { |
630
|
|
|
// $lm->log('Loading cached slot "' . $slotName . '"...'); |
|
|
|
|
631
|
|
|
$slotResponse = $viewCache['slots'][$layerName][$slotName]; |
632
|
|
|
} else { |
633
|
|
|
// $lm->log('Running slot "' . $slotName . '"...'); |
|
|
|
|
634
|
|
|
$slotResponse = $slotContainer->execute(); |
635
|
|
|
if ($isCacheable && !$isViewCached && isset($otConfig['layers'][$layerName]) && is_array($otConfig['layers'][$layerName]) && in_array($slotName, $otConfig['layers'][$layerName])) { |
636
|
|
|
// $lm->log('Adding response of slot "' . $slotName . '" to cache...'); |
|
|
|
|
637
|
|
|
$viewCache['slots'][$layerName][$slotName] = $slotResponse; |
638
|
|
|
} |
639
|
|
|
} |
640
|
|
|
// set the presentation data as a template attribute |
641
|
|
|
ArrayPathDefinition::setValue($slotName, $output, $slotResponse->getContent()); |
642
|
|
|
// and merge the other slot's response (this used to be conditional and done only when the content was not null) |
643
|
|
|
// $lm->log('Merging in response from slot "' . $slotName . '"...'); |
|
|
|
|
644
|
|
|
$response->merge($slotResponse); |
645
|
|
|
} |
646
|
|
|
$moreAssigns = array( |
647
|
|
|
'container' => $container, |
648
|
|
|
'inner' => $nextOutput, |
649
|
|
|
'request_data' => $container->getRequestData(), |
650
|
|
|
'response' => $response, |
651
|
|
|
'validation_manager' => $container->getValidationManager(), |
652
|
|
|
'view' => $viewInstance, |
653
|
|
|
); |
654
|
|
|
// lock the request. can't be done outside the loop for the whole run, see #628 |
655
|
|
|
$key = $request->toggleLock(); |
656
|
|
|
try { |
657
|
|
|
$nextOutput = $layer->getRenderer()->render($layer, $attributes, $output, $moreAssigns); |
658
|
|
|
} catch (\Exception $e) { |
659
|
|
|
// we caught an exception... unlock the request and rethrow! |
660
|
|
|
$request->toggleLock($key); |
|
|
|
|
661
|
|
|
throw $e; |
662
|
|
|
} |
663
|
|
|
// and unlock the request again |
664
|
|
|
$request->toggleLock($key); |
|
|
|
|
665
|
|
|
|
666
|
|
|
$response->setContent($nextOutput); |
667
|
|
|
|
668
|
|
|
if ($isCacheable && !$isViewCached && $i === $lastCacheableLayer) { |
|
|
|
|
669
|
|
|
$viewCache['response'] = clone $response; |
670
|
|
|
} |
671
|
|
|
|
672
|
|
|
$output = array(); |
673
|
|
|
if ($assignInnerToSlots) { |
674
|
|
|
$output[$layer->getName()] = $nextOutput; |
675
|
|
|
} |
676
|
|
|
} |
677
|
|
|
} |
678
|
|
|
|
679
|
|
|
if ($isCacheable && !$isViewCached) { |
680
|
|
|
// we're writing the view cache first. this is just in case we get into a situation with really bad timing on the leap of a second |
681
|
|
|
$viewCache['request_attributes'] = array(); |
682
|
|
View Code Duplication |
foreach ($otConfig['request_attributes'] as $requestAttribute) { |
|
|
|
|
683
|
|
|
$viewCache['request_attributes'][] = $requestAttribute + array('value' => $request->getAttribute($requestAttribute['name'], $requestAttribute['namespace'])); |
684
|
|
|
} |
685
|
|
|
$viewCache['request_attribute_namespaces'] = array(); |
686
|
|
|
foreach ($otConfig['request_attribute_namespaces'] as $requestAttributeNamespace) { |
687
|
|
|
$viewCache['request_attribute_namespaces'][$requestAttributeNamespace] = $request->getAttributes($requestAttributeNamespace); |
688
|
|
|
} |
689
|
|
|
|
690
|
|
|
$this->writeCache($viewGroups, $viewCache, $config['lifetime']); |
691
|
|
|
|
692
|
|
|
// notify callback that the execution has finished and caches have been written |
693
|
|
|
$this->finishedCacheCreationCallback(self::CACHE_CALLBACK_VIEW_CACHE_WRITTEN, $viewGroups, $config, $container); |
694
|
|
|
// $lm->log('Writing View cache...'); |
|
|
|
|
695
|
|
|
} |
696
|
|
|
} |
697
|
|
|
|
698
|
|
|
// controller cache writing must occur here, so controllers that return View::NONE also get their cache written |
699
|
|
|
if ($isCacheable && !$isControllerCached) { |
700
|
|
|
$controllerCache['controller_attributes'] = array(); |
701
|
|
|
foreach ($config['controller_attributes'] as $attributeName) { |
702
|
|
|
$controllerCache['controller_attributes'][$attributeName] = $controllerAttributes[$attributeName]; |
|
|
|
|
703
|
|
|
} |
704
|
|
|
|
705
|
|
|
// $lm->log('Writing Controller cache...'); |
|
|
|
|
706
|
|
|
|
707
|
|
|
$this->writeCache($controllerGroups, $controllerCache, $config['lifetime']); |
708
|
|
|
|
709
|
|
|
// notify callback that the execution has finished and caches have been written |
710
|
|
|
$this->finishedCacheCreationCallback(self::CACHE_CALLBACK_CONTROLLER_CACHE_WRITTEN, $controllerGroups, $config, $container); |
711
|
|
|
} |
712
|
|
|
|
713
|
|
|
// we're done here. bai. |
714
|
|
|
break; |
715
|
|
|
} |
716
|
|
|
} |
717
|
|
|
|
718
|
|
|
/** |
719
|
|
|
* Execute the Controller |
720
|
|
|
* |
721
|
|
|
* @param ExecutionContainer $container The current execution container. |
722
|
|
|
* |
723
|
|
|
* @return mixed The processed View information returned by the Controller. |
724
|
|
|
* |
725
|
|
|
* @author David Zülke <[email protected]> |
726
|
|
|
* @author Felix Gilcher <[email protected]> |
727
|
|
|
* @since 0.11.0 |
728
|
|
|
* |
729
|
|
|
* @deprecated since 1.0.0, use ExecutionContainer::runController() |
730
|
|
|
*/ |
731
|
|
|
protected function runController(ExecutionContainer $container) |
732
|
|
|
{ |
733
|
|
|
return $container->runController(); |
734
|
|
|
} |
735
|
|
|
|
736
|
|
|
/** |
737
|
|
|
* execute this containers view instance |
738
|
|
|
* |
739
|
|
|
* @return mixed the view's result |
740
|
|
|
* |
741
|
|
|
* @author David Zülke <[email protected]> |
742
|
|
|
* @author Felix Gilcher <[email protected]> |
743
|
|
|
* @since 1.0.0 |
744
|
|
|
*/ |
745
|
|
|
protected function executeView(ExecutionContainer $container) |
746
|
|
|
{ |
747
|
|
|
$outputType = $container->getOutputType()->getName(); |
748
|
|
|
$request = $this->context->getRequest(); |
749
|
|
|
$viewInstance = $container->getViewInstance(); |
750
|
|
|
|
751
|
|
|
// $lm->log('View is not cached, executing...'); |
|
|
|
|
752
|
|
|
// view initialization completed successfully |
753
|
|
|
$executeMethod = 'execute' . $outputType; |
754
|
|
|
if (!is_callable(array($viewInstance, $executeMethod))) { |
755
|
|
|
$executeMethod = 'execute'; |
756
|
|
|
} |
757
|
|
|
$key = $request->toggleLock(); |
758
|
|
|
try { |
759
|
|
|
$viewResult = $viewInstance->$executeMethod($container->getRequestData()); |
760
|
|
|
} catch (\Exception $e) { |
761
|
|
|
// we caught an exception... unlock the request and rethrow! |
762
|
|
|
$request->toggleLock($key); |
|
|
|
|
763
|
|
|
throw $e; |
764
|
|
|
} |
765
|
|
|
$request->toggleLock($key); |
|
|
|
|
766
|
|
|
return $viewResult; |
767
|
|
|
} |
768
|
|
|
} |
769
|
|
|
|
This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.
Consider the following example. The parameter
$ireland
is not defined by the methodfinale(...)
.The most likely cause is that the parameter was changed, but the annotation was not.