1: <?php
2: /**
3: * @category Horde
4: * @package View
5: */
6:
7: /**
8: * Abstract base class for Horde_View to get private constructs out of
9: * template scope.
10: *
11: * @category Horde
12: * @package View
13: */
14: abstract class Horde_View_Base
15: {
16: /**
17: * @var string
18: */
19: public static $defaultFormBuilder = 'Horde_View_Helper_Form_Builder';
20:
21: /**
22: * Path stack for templates.
23: *
24: * @var array
25: */
26: private $_templatePath = array('./');
27:
28: /**
29: * Template to execute.
30: *
31: * Stored in a private variable to keep it out of the public view scope.
32: *
33: * @var string
34: */
35: private $_file = null;
36:
37: /**
38: * Cache of helper objects.
39: *
40: * @var array
41: */
42: private $_helpers = array();
43:
44: /**
45: * Encoding to use in escaping mechanisms.
46: *
47: * @var string
48: */
49: private $_encoding = 'UTF-8';
50:
51: /**
52: * Should we throw an error if helper methods collide?
53: *
54: * @var boolean
55: */
56: private $_throwOnHelperCollision = false;
57:
58: /**
59: * Protected properties.
60: *
61: * @var array
62: */
63: private $_protectedProperties;
64:
65: /**
66: * Constructor.
67: *
68: * @param array $config Configuration key-value pairs.
69: */
70: public function __construct($config = array())
71: {
72: // Encoding.
73: if (!empty($config['encoding'])) {
74: $this->setEncoding($config['encoding']);
75: }
76:
77: // User-defined template path.
78: if (!empty($config['templatePath'])) {
79: $this->addTemplatePath($config['templatePath']);
80: }
81:
82: $this->_protectedProperties = get_class_vars(__CLASS__);
83: }
84:
85: /**
86: * Undefined variables return null.
87: *
88: * @return null
89: */
90: public function __get($name)
91: {
92: return null;
93: }
94:
95: /**
96: * Accesses a helper object from within a template.
97: *
98: * @param string $method The helper method.
99: * @param array $args The parameters for the helper.
100: *
101: * @return string The result of the helper method.
102: * @throws Horde_View_Exception
103: */
104: public function __call($method, $args)
105: {
106: if (isset($this->_helpers[$method])) {
107: return call_user_func_array(array($this->_helpers[$method], $method), $args);
108: }
109:
110: throw new Horde_View_Exception('Helper for ' . $method . ' not found.');
111: }
112:
113: /**
114: * Adds to the stack of template paths in LIFO order.
115: *
116: * @param string|array The directory (-ies) to add.
117: */
118: public function addTemplatePath($path)
119: {
120: foreach ((array)$path as $dir) {
121: // Attempt to strip any possible separator and append a
122: // directory separator.
123: $dir = rtrim($dir, '\\/' . DIRECTORY_SEPARATOR) . '/';
124:
125: // Add to the top of the stack.
126: array_unshift($this->_templatePath, $dir);
127: }
128: }
129:
130: /**
131: * Resets the stack of template paths.
132: *
133: * To clear all paths, use Horde_View::setTemplatePath(null).
134: *
135: * @param string|array The directory (-ies) to set as the path.
136: */
137: public function setTemplatePath($path)
138: {
139: $this->_templatePath = array();
140: $this->addTemplatePath($path);
141: }
142:
143: /**
144: * Returns the template paths.
145: *
146: * @return array The stack of current template paths.
147: */
148: public function getTemplatePaths()
149: {
150: return $this->_templatePath;
151: }
152:
153: /**
154: * Adds to the stack of helpers in LIFO order.
155: *
156: * If the $helper parameter is a string instead of a Helper instance, then
157: * it will be treated as a class name. Names without "_" and that do not
158: * have "Helper" in them will be prefixed with Horde_View_Helper_; other
159: * names will be treated as literal class names. Examples:
160: *
161: * <code>
162: * // Adds a new Horde_View_Helper_Tag to the view:
163: * $v->addHelper('Tag');
164: * // Adds a new AppHelper object to the view if it exists, otherwise
165: * // throws an exception:
166: * $v->addHelper('AppHelper');
167: * </code>
168: *
169: * @param Horde_View_Helper|string $helper The helper instance to add.
170: *
171: * @return Horde_View_Helper Returns the helper object that was added.
172: * @throws Horde_View_Exception
173: */
174: public function addHelper($helper)
175: {
176: if (is_string($helper)) {
177: if (strpos($helper, '_') === false &&
178: strpos($helper, 'Helper') === false) {
179: $class = 'Horde_View_Helper_' . $helper;
180: } else {
181: $class = $helper;
182: }
183: if (!class_exists($class)) {
184: throw new Horde_View_Exception('Helper class ' . $helper . ' not found');
185: }
186: $helper = new $class($this);
187: }
188:
189: foreach (get_class_methods($helper) as $method) {
190: if (isset($this->_helpers[$method])) {
191: $msg = 'Helper method ' . get_class($this->_helpers[$method])
192: . '#' . $method . ' overridden by ' . get_class($helper)
193: . '#' . $method;
194: if ($this->_throwOnHelperCollision) {
195: throw new Horde_View_Exception($msg);
196: }
197: if ($this->logger) {
198: $this->logger->warn($msg);
199: }
200: }
201: $this->_helpers[$method] = $helper;
202: }
203:
204: return $helper;
205: }
206:
207: /**
208: * Assigns multiple variables to the view.
209: *
210: * The array keys are used as names, each assigned their corresponding
211: * array value.
212: *
213: * @param array $array The array of key/value pairs to assign.
214: */
215: public function assign($array)
216: {
217: foreach ($array as $key => $val) {
218: if (isset($this->_protectedProperties[$key])) {
219: throw new Horde_View_Exception('Cannott overwrite internal variables in assign()');
220: }
221: $this->$key = $val;
222: }
223: }
224:
225: /**
226: * Processes a template and returns the output.
227: *
228: * @param string $name The template to process.
229: *
230: * @return string The template output.
231: */
232: public function render($name, $locals = array())
233: {
234: // Render partial.
235: if (is_array($name) && $partial = $name['partial']) {
236: unset($name['partial']);
237: return $this->renderPartial($partial, $name);
238: }
239:
240: // Find the template file name.
241: $this->_file = $this->_template($name);
242:
243: // Remove $name from local scope.
244: unset($name);
245:
246: ob_start();
247: $this->_run($this->_file, $locals);
248: return ob_get_clean();
249: }
250:
251: /**
252: * Renders a partial template.
253: *
254: * Partial template filenames are named with a leading underscore, although
255: * this underscore is not used when specifying the name of the partial.
256: *
257: * We would reference the file /views/shared/_sidebarInfo.html in our
258: * template using:
259: *
260: * <code>
261: * <div>
262: * <?php echo $this->renderPartial('sidebarInfo') ?>
263: * </div>
264: * </code>
265: *
266: * @param string $name
267: * @param array $options
268: *
269: * @return string The template output.
270: */
271: public function renderPartial($name, $options = array())
272: {
273: // Pop name off of the path.
274: $parts = strstr($name, '/') ? explode('/', $name) : array($name);
275: $name = array_pop($parts);
276: $path = implode('/', $parts) . '/';
277:
278: // Check if they passed in a collection before validating keys.
279: $useCollection = array_key_exists('collection', $options);
280:
281: $valid = array('object' => null,
282: 'locals' => array(),
283: 'collection' => array());
284: $options = array_merge($valid, $options);
285: $locals = array($name => null);
286:
287: // Set the object variable.
288: if ($options['object']) {
289: $locals[$name] = $options['object'];
290: }
291:
292: // Set local variables to be used in the partial.
293: if (isset($options['locals']) &&
294: (is_array($options['locals']) ||
295: $options['locals'] instanceof Traversable)) {
296: foreach ($options['locals'] as $key => $val) {
297: $locals[$key] = $val;
298: }
299: }
300:
301: if ($useCollection) {
302: // Collection.
303: $rendered = '';
304: if (is_array($options['collection'])) {
305: $sz = count($options['collection']);
306: for ($i = 0; $i < $sz; $i++) {
307: $locals["{$name}Counter"] = $i;
308: $locals[$name] = $options['collection'][$i];
309: $rendered .= $this->render("{$path}_{$name}", $locals);
310: }
311: }
312: } else {
313: // Single render.
314: $rendered = $this->render("{$path}_{$name}", $locals);
315: }
316:
317: return $rendered;
318: }
319:
320: /**
321: * Sets the output encoding.
322: *
323: * @param string $encoding A character set name.
324: */
325: public function setEncoding($encoding)
326: {
327: $this->_encoding = $encoding;
328: }
329:
330: /**
331: * Returns the current output encoding.
332: *
333: * @return string The current character set.
334: */
335: public function getEncoding()
336: {
337: return $this->_encoding;
338: }
339:
340: /**
341: * Controls the behavior when a helper method is overridden by another
342: * helper.
343: *
344: * @param boolean $throw Throw an exception when helper methods collide?
345: */
346: public function throwOnHelperCollision($throw = true)
347: {
348: $this->_throwOnHelperCollision = $throw;
349: }
350:
351: /**
352: * Finds a template from the available directories.
353: *
354: * @param $name string The base name of the template.
355: *
356: * @return string The full path to the first matching template.
357: */
358: protected function _template($name)
359: {
360: // Append missing .html.
361: if (!strstr($name, '.')) {
362: $name .= '.html.php';
363: }
364:
365: if (!count($this->_templatePath)) {
366: throw new Horde_View_Exception('No template directory set; unable to locate ' . $name);
367: }
368:
369: foreach ($this->_templatePath as $dir) {
370: if (is_readable($dir . $name)) {
371: return $dir . $name;
372: }
373: }
374:
375: throw new Horde_View_Exception("\"$name\" not found in template path (\"" . implode(':', $this->_templatePath) . '")');
376: }
377:
378: /**
379: * Includes the template in a scope with only public variables.
380: *
381: * @param string The template to execute. Not declared in the function
382: * signature so it stays out of the view's public scope.
383: * @param array Any local variables to declare.
384: */
385: abstract protected function _run();
386: }
387: