root/trunk/src/filter/AgaviExecutionFilter.class.php

Revision 2612, 24.8 KB (checked in by david, 4 months ago)

Changed default for assigning of "inner" content to $slots template array to disabled, closes #794 and refs #793

  • Property keywords set to Id
  • Property svn:keywords set to Id
Line 
1<?php
2
3// +---------------------------------------------------------------------------+
4// | This file is part of the Agavi package.                                   |
5// | Copyright (c) 2005-2008 the Agavi Project.                                |
6// | Based on the Mojavi3 MVC Framework, Copyright (c) 2003-2005 Sean Kerr.    |
7// |                                                                           |
8// | For the full copyright and license information, please view the LICENSE   |
9// | file that was distributed with this source code. You can also view the    |
10// | LICENSE file online at http://www.agavi.org/LICENSE.txt                   |
11// |   vi: set noexpandtab:                                                    |
12// |   Local Variables:                                                        |
13// |   indent-tabs-mode: t                                                     |
14// |   End:                                                                    |
15// +---------------------------------------------------------------------------+
16
17/**
18 * AgaviExecutionFilter is the last filter registered for each filter chain.
19 * This filter does all action and view execution.
20 *
21 * @package    agavi
22 * @subpackage filter
23 *
24 * @author     David Zülke <dz@bitxtender.com>
25 * @author     Sean Kerr <skerr@mojavi.org>
26 * @copyright  Authors
27 * @copyright  The Agavi Project
28 *
29 * @since      0.9.0
30 *
31 * @version    $Id$
32 */
33class AgaviExecutionFilter extends AgaviFilter implements AgaviIActionFilter
34{
35  /*
36   * The directory inside %core.cache_dir% where cached stuff is stored.
37   */
38  const CACHE_SUBDIR = 'content';
39
40  /*
41   * The name of the file that holds the cached action data.
42   * Minuses because these are not allowed in an output type name.
43   */
44  const ACTION_CACHE_ID = '4-8-15-16-23-42';
45
46  /**
47   * Check if a cache exists and is up-to-date
48   *
49   * @param      array  An array of cache groups
50   * @param      string The lifetime of the cache as a strtotime relative string
51   *                    without the leading plus sign.
52   *
53   * @return     bool true, if the cache is up to date, otherwise false
54   *
55   * @author     David Zülke <dz@bitxtender.com>
56   * @since      0.11.0
57   */
58  public function checkCache(array $groups, $lifetime = null)
59  {
60    foreach($groups as &$group) {
61      $group = base64_encode($group);
62    }
63    $filename = AgaviConfig::get('core.cache_dir') . DIRECTORY_SEPARATOR . self::CACHE_SUBDIR . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $groups) . '.cefcache';
64    $isReadable = is_readable($filename);
65    if($lifetime === null || !$isReadable) {
66      return $isReadable;
67    } else {
68      $expiry = strtotime('+' . $lifetime, filemtime($filename));
69      if($expiry !== false) {
70        return $isReadable && ($expiry >= time());
71      } else {
72        return false;
73      }
74    }
75  }
76
77  /**
78   * Read the contents of a cache
79   *
80   * @param      array An array of cache groups
81   *
82   * @return     array The cache data
83   *
84   * @author     David Zülke <dz@bitxtender.com>
85   * @since      0.11.0
86   */
87  public function readCache(array $groups)
88  {
89    foreach($groups as &$group) {
90      $group = base64_encode($group);
91    }
92    $filename = AgaviConfig::get('core.cache_dir') . DIRECTORY_SEPARATOR . self::CACHE_SUBDIR . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $groups) . '.cefcache';
93    $data = @file_get_contents($filename);
94    if($data !== false) {
95      return unserialize($data);
96    } else {
97      throw new AgaviException(sprintf('Failed to read cache file "%s"', $filename));
98    }
99  }
100
101  /**
102   * Write cache content
103   *
104   * @param      array  An array of cache groups
105   * @param      array  The cache data
106   * @param      string The lifetime of the cache as a strtotime relative string
107   *                    without the leading plus sign.
108   *
109   * @return     bool The result of the write operation
110   *
111   * @author     David Zülke <dz@bitxtender.com>
112   * @since      0.11.0
113   */
114  public function writeCache(array $groups, $data, $lifetime = null)
115  {
116    // lifetime is not used in this implementation!
117   
118    foreach($groups as &$group) {
119      $group = base64_encode($group);
120    }
121    @mkdir(AgaviConfig::get('core.cache_dir') . DIRECTORY_SEPARATOR  . self::CACHE_SUBDIR . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR , array_slice($groups, 0, -1)), 0777, true);
122    return file_put_contents(AgaviConfig::get('core.cache_dir') . DIRECTORY_SEPARATOR . self::CACHE_SUBDIR . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $groups) . '.cefcache', serialize($data), LOCK_EX);
123  }
124
125  /**
126   * Flushes the cache for a group
127   *
128   * @param      array An array of cache groups
129   *
130   * @author     David Zülke <dz@bitxtender.com>
131   * @since      0.11.0
132   */
133  public static function clearCache(array $groups = array())
134  {
135    foreach($groups as &$group) {
136      $group = base64_encode($group);
137    }
138    $path = self::CACHE_SUBDIR . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $groups);
139    if(is_file(AgaviConfig::get('core.cache_dir') . DIRECTORY_SEPARATOR . $path . '.cefcache')) {
140      AgaviToolkit::clearCache($path . '.cefcache');
141    } else {
142      AgaviToolkit::clearCache($path);
143    }
144  }
145
146  /**
147   * Builds an array of cache groups using the configuration and a container.
148   *
149   * @param      array                   The group array from the configuration.
150   * @param      AgaviExecutionContainer The execution container.
151   *
152   * @return     array An array of groups.
153   *
154   * @author     David Zülke <dz@bitxtender.com>
155   * @since      0.11.0
156   */
157  public function determineGroups(array $groups, AgaviExecutionContainer $container)
158  {
159    $retval = array();
160
161    foreach($groups as $group) {
162      $group += array('name' => null, 'source' => null, 'namespace' => null);
163      $val = $this->getVariable($group['name'], $group['source'], $group['namespace'], $container);
164      if($val === null) {
165        $val = "0";
166      } elseif(is_object($val) && is_callable(array($val, '__toString'))) {
167        $val = $val->__toString();
168      } elseif(is_object($val) && function_exists('spl_object_hash')) {
169        $val = spl_object_hash($val);
170      }
171      $retval[] = $val;
172    }
173
174    $retval[] = $container->getModuleName() . '_' . $container->getActionName();
175
176    return $retval;
177  }
178
179  /**
180   * Read a variable from the given source and, optionally, namespace.
181   *
182   * @param      string The variable name.
183   * @param      string The optional variable source.
184   * @param      string The optional namespace in the source.
185   * @param      AgaviExecutionContainer The container to use, if necessary.
186   *
187   * @return     mixed The variable.
188   *
189   * @author     David Zülke <dz@bitxtender.com>
190   * @since      0.11.0
191   */
192  public function getVariable($name, $source = 'string', $namespace = null, AgaviExecutionContainer $container = null)
193  {
194    $val = $name;
195   
196    switch($source) {
197      case 'constant':
198        $val = constant($name);
199        break;
200      case 'container_parameter':
201        $val = $container->getParameter($name);
202        break;
203      case 'global_request_data':
204        $val = $this->context->getRequest()->getRequestData()->get($namespace ? $namespace : AgaviRequestDataHolder::SOURCE_PARAMETERS, $name);
205        break;
206      case 'locale':
207        $val = $this->context->getTranslationManager()->getCurrentLocaleIdentifier();
208        break;
209      case 'request_attribute':
210        $val = $this->context->getRequest()->getAttribute($name, $namespace);
211        break;
212      case 'request_data':
213        $val = $container->getRequestData()->get($namespace ? $namespace : AgaviRequestDataHolder::SOURCE_PARAMETERS, $name);
214        break;
215      case 'request_parameter':
216        $val = $this->context->getRequest()->getRequestData()->getParameter($name);
217        break;
218      case 'user_attribute':
219        $val = $this->context->getUser()->getAttribute($name, $namespace);
220        break;
221      case 'user_authenticated':
222        if(($user = $this->context->getUser()) instanceof AgaviISecurityUser) {
223          $val = $user->isAuthenticated();
224        }
225        break;
226      case 'user_credential':
227        if(($user = $this->context->getUser()) instanceof AgaviISecurityUser) {
228          $val = $user->hasCredentials($name);
229        }
230        break;
231      case 'user_parameter':
232        $val = $this->context->getUser()->getParameter($name);
233        break;
234    }
235   
236    return $val;
237  }
238
239  /**
240   * Execute this filter.
241   *
242   * @param      AgaviFilterChain        The filter chain.
243   * @param      AgaviExecutionContainer The current execution container.
244   *
245   * @throws     <b>AgaviInitializationException</b> If an error occurs during
246   *                                                 View initialization.
247   * @throws     <b>AgaviViewException</b>           If an error occurs while
248   *                                                 executing the View.
249   *
250   * @author     David Zülke <dz@bitxtender.com>
251   * @author     Sean Kerr <skerr@mojavi.org>
252   * @since      0.9.0
253   */
254  public function execute(AgaviFilterChain $filterChain, AgaviExecutionContainer $container)
255  {
256    // $lm = $this->context->getLoggerManager();
257
258    // get the context, controller and validator manager
259    $controller = $this->context->getController();
260
261    // get the current action information
262    $actionName = $container->getActionName();
263    $moduleName = $container->getModuleName();
264   
265    // the action instance
266    $actionInstance = $container->getActionInstance();
267
268    $request = $this->context->getRequest();
269
270    $isCacheable = false;
271    if($this->getParameter('enable_caching', true) && is_readable($cachingDotXml = AgaviConfig::get('core.module_dir') . '/' . $moduleName . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . $actionName . '.xml')) {
272      // $lm->log('Caching enabled, configuration file found, loading...');
273      // no _once please!
274      include(AgaviConfigCache::checkConfig($cachingDotXml, $this->context->getName()));
275    }
276
277    $isActionCached = false;
278
279    if($isCacheable) {
280      $groups = $this->determineGroups($config["groups"], $container);
281      $isActionCached = $this->checkCache(array_merge($groups, array(self::ACTION_CACHE_ID)), $config['lifetime']);
282    } else {
283      // $lm->log('Action is not cacheable!');
284    }
285
286    if($isActionCached) {
287      // $lm->log('Action is cached, loading...');
288      // cache/dir/4-8-15-16-23-42 contains the action cache
289      try {
290        $actionCache = $this->readCache(array_merge($groups, array(self::ACTION_CACHE_ID)));
291        // and restore action attributes
292        $actionInstance->setAttributes($actionCache['action_attributes']);
293      } catch(AgaviException $e) {
294        $isActionCached = false;
295      }
296    }
297   
298    $isViewCached = false;
299    $rememberTheView = null;
300   
301    while(true) {
302      if(!$isActionCached) {
303        $actionCache = array();
304     
305        // $lm->log('Action not cached, executing...');
306        // execute the Action and get the View to execute
307        list($actionCache['view_module'], $actionCache['view_name']) = $this->runAction($container);
308       
309        // check if we've just run the action 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 action attribute caching configuration...
310        // if yes: is the view module/name that we got just now different from what was in the cache?
311        if(isset($rememberTheView) && $actionCache != $rememberTheView) {
312          // yup. clear it!
313          $ourClass = get_class($this);
314          call_user_func(array($ourClass, 'clearCache'), $groups);
315        }
316       
317        // check if the returned view is cacheable
318        if($isCacheable && is_array($config['views']) && !in_array(array('module' => $actionCache['view_module'], 'name' => $actionCache['view_name']), $config['views'], true)) {
319          $isCacheable = false;
320         
321          // so that view is not cacheable? okay then:
322          // check if we've just run the action 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 action attribute caching configuration...
323          // '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
324          if(isset($rememberTheView)) {
325            // yup. clear it!
326            $ourClass = get_class($this);
327            call_user_func(array($ourClass, 'clearCache'), $groups);
328          }
329          // $lm->log('Returned View is not cleared for caching, setting cacheable status to false.');
330        } else {
331          // $lm->log('Returned View is cleared for caching, proceeding...');
332        }
333
334        $actionAttributes = $actionInstance->getAttributes();
335      }
336
337      // clear the response
338      $response = $container->getResponse();
339      $response->clear();
340
341      // clear any forward set, it's ze view's job
342      $container->clearNext();
343
344      if($actionCache['view_name'] !== AgaviView::NONE) {
345
346        $container->setViewModuleName($actionCache['view_module']);
347        $container->setViewName($actionCache['view_name']);
348
349        $key = $request->toggleLock();
350        try {
351          // get the view instance
352          $viewInstance = $controller->createViewInstance($actionCache['view_module'], $actionCache['view_name']);
353          // initialize the view
354          $viewInstance->initialize($container);
355        } catch(Exception $e) {
356          // we caught an exception... unlock the request and rethrow!
357          $request->toggleLock($key);
358          throw $e;
359        }
360        $request->toggleLock($key);
361
362        // Set the View Instance in the container
363        $container->setViewInstance($viewInstance);
364     
365        $outputType = $container->getOutputType()->getName();
366
367        if($isCacheable) {
368          if(isset($config['output_types'][$otConfig = $outputType]) || isset($config['output_types'][$otConfig = '*'])) {
369            $otConfig = $config['output_types'][$otConfig];
370
371            if($isActionCached) {
372              $isViewCached = $this->checkCache(array_merge($groups, array($outputType)), $config['lifetime']);
373            }
374          } else {
375            $isCacheable = false;
376          }
377        }
378
379        if($isViewCached) {
380          // $lm->log('View is cached, loading...');
381          try {
382            $viewCache = $this->readCache(array_merge($groups, array($outputType)));
383          } catch(AgaviException $e) {
384            $isViewCached = false;
385          }
386        }
387        if(!$isViewCached) {
388          // view not cached
389          // but the action  might
390          if($isActionCached && !$config['action_attributes']) {
391            // has the cache config a list of action attributes?
392            // no. that means we must run the action again!
393            $isActionCached = false;
394            // but remember the view info, just in case it differs if we run the action again now
395            $rememberTheView = array(
396              'view_module' => $actionCache['view_module'],
397              'view_name' => $actionCache['view_name'],
398            );
399            continue;
400          }
401       
402          $viewCache = array();
403
404          // $lm->log('View is not cached, executing...');
405          // view initialization completed successfully
406          $executeMethod = 'execute' . $outputType;
407          if(!method_exists($viewInstance, $executeMethod)) {
408            $executeMethod = 'execute';
409          }
410          $key = $request->toggleLock();
411          try {
412            $viewCache['next'] = $viewInstance->$executeMethod($container->getRequestData());
413          } catch(Exception $e) {
414            // we caught an exception... unlock the request and rethrow!
415            $request->toggleLock($key);
416            throw $e;
417          }
418          $request->toggleLock($key);
419        }
420
421        if($viewCache['next'] instanceof AgaviExecutionContainer) {
422          // $lm->log('Forwarding request, skipping rendering...');
423          $container->setNext($viewCache['next']);
424        } else {
425          $output = array();
426          $nextOutput = null;
427       
428          if($isViewCached) {
429            $layers = $viewCache['layers'];
430            $response = $viewCache['response'];
431            $container->setResponse($response);
432
433            foreach($viewCache['template_variables'] as $name => $value) {
434              $viewInstance->setAttribute($name, $value);
435            }
436
437            foreach($viewCache['request_attributes'] as $requestAttribute) {
438              $request->setAttribute($requestAttribute['name'], $requestAttribute['value'], $requestAttribute['namespace']);
439            }
440         
441            foreach($viewCache['request_attribute_namespaces'] as $ranName => $ranValues) {
442              $request->setAttributes($ranValues, $ranName);
443            }
444
445            $nextOutput = $response->getContent();
446          } else {
447            if($viewCache['next'] !== null) {
448              // response content was returned from view execute()
449              $response->setContent($nextOutput = $viewCache['next']);
450              $viewCache['next'] = null;
451            }
452
453            $layers = $viewInstance->getLayers();
454
455            if($isCacheable) {
456              $viewCache['template_variables'] = array();
457              foreach($otConfig['template_variables'] as $varName) {
458                $viewCache['template_variables'][$varName] = $viewInstance->getAttribute($varName);
459              }
460
461              $viewCache['response'] = clone $response;
462
463              $viewCache['layers'] = array();
464
465              $viewCache['slots'] = array();
466
467              $lastCacheableLayer = -1;
468              if(is_array($otConfig['layers'])) {
469                if(count($otConfig['layers'])) {
470                  for($i = count($layers)-1; $i >= 0; $i--) {
471                    $layer = $layers[$i];
472                    $layerName = $layer->getName();
473                    if(isset($otConfig['layers'][$layerName])) {
474                      if(is_array($otConfig['layers'][$layerName])) {
475                        $lastCacheableLayer = $i - 1;
476                      } else {
477                        $lastCacheableLayer = $i;
478                      }
479                    }
480                  }
481                }
482              } else {
483                $lastCacheableLayer = count($layers) - 1;
484              }
485
486              for($i = $lastCacheableLayer + 1; $i < count($layers); $i++) {
487                // $lm->log('Adding non-cacheable layer "' . $layers[$i]->getName() . '" to list');
488                $viewCache['layers'][] = clone $layers[$i];
489              }
490            }
491          }
492
493          $attributes =& $viewInstance->getAttributes();
494
495          // whether or not we should assign the previous' layer's output to the $slots array
496          $assignInnerToSlots = $this->getParameter('assign_inner_to_slots', false);
497         
498          // $lm->log('Starting rendering...');
499          for($i = 0; $i < count($layers); $i++) {
500            $layer = $layers[$i];
501            $layerName = $layer->getName();
502            // $lm->log('Running layer "' . $layerName . '"...');
503            foreach($layer->getSlots() as $slotName => $slotContainer) {
504              if($isViewCached && isset($viewCache['slots'][$layerName][$slotName])) {
505                // $lm->log('Loading cached slot "' . $slotName . '"...');
506                $slotResponse = $viewCache['slots'][$layerName][$slotName];
507              } else {
508                // $lm->log('Running slot "' . $slotName . '"...');
509                $slotResponse = $slotContainer->execute();
510                if($isCacheable && !$isViewCached && isset($otConfig['layers'][$layerName]) && is_array($otConfig['layers'][$layerName]) && in_array($slotName, $otConfig['layers'][$layerName])) {
511                  // $lm->log('Adding response of slot "' . $slotName . '" to cache...');
512                  $viewCache['slots'][$layerName][$slotName] = $slotResponse;
513                }
514              }
515              // set the presentation data as a template attribute
516              if(($output[$slotName] = $slotResponse->getContent()) !== null) {
517                // $lm->log('Merging in response from slot "' . $slotName . '"...');
518                // the slot really output something
519                // let our response grab the stuff it needs from the slot's response
520                $response->merge($slotResponse);
521              }
522            }
523            $moreAssigns = array(
524              'container' => $container,
525              'inner' => $nextOutput,
526              'request_data' => $container->getRequestData(),
527              'validation_manager' => $container->getValidationManager(),
528              'view' => $viewInstance,
529            );
530            // lock the request. can't be done outside the loop for the whole run, see #628
531            $key = $request->toggleLock();
532            try {
533              $nextOutput = $layer->getRenderer()->render($layer, $attributes, $output, $moreAssigns);
534            } catch(Exception $e) {
535              // we caught an exception... unlock the request and rethrow!
536              $request->toggleLock($key);
537              throw $e;
538            }
539            // and unlock the request again
540            $request->toggleLock($key);
541
542            $response->setContent($nextOutput);
543
544            if($isCacheable && !$isViewCached && $i === $lastCacheableLayer) {
545              $viewCache['response'] = clone $response;
546            }
547
548            $output = array();
549            if($assignInnerToSlots) {
550              $output[$layer->getName()] = $nextOutput;
551            }
552          }
553        }
554
555        if($isCacheable && !$isViewCached) {
556          // 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
557          $viewCache['request_attributes'] = array();
558          foreach($otConfig['request_attributes'] as $requestAttribute) {
559            $viewCache['request_attributes'][] = $requestAttribute + array('value' => $request->getAttribute($requestAttribute['name'], $requestAttribute['namespace']));
560          }
561          $viewCache['request_attribute_namespaces'] = array();
562          foreach($otConfig['request_attribute_namespaces'] as $requestAttributeNamespace) {
563            $viewCache['request_attribute_namespaces'][$requestAttributeNamespace] = $request->getAttributes($requestAttributeNamespace);
564          }
565
566          $this->writeCache(array_merge($groups, array($outputType)), $viewCache, $config['lifetime']);
567
568          // $lm->log('Writing View cache...');
569          $isViewCached = true;
570        }
571      }
572   
573      // action cache writing must occur here, so actions that return AgaviView::NONE also get their cache written
574      if($isCacheable && !$isActionCached) {
575        $actionCache['action_attributes'] = array();
576        foreach($config['action_attributes'] as $attributeName) {
577          $actionCache['action_attributes'][$attributeName] = $actionAttributes[$attributeName];
578        }
579
580        // $lm->log('Writing Action cache...');
581
582        $this->writeCache(array_merge($groups, array(self::ACTION_CACHE_ID)), $actionCache, $config[<