1<?php 2 3/* 4 * This file is part of Twig. 5 * 6 * (c) Fabien Potencier 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Twig\Error; 13 14use Twig\Source; 15use Twig\Template; 16 17/** 18 * Twig base exception. 19 * 20 * This exception class and its children must only be used when 21 * an error occurs during the loading of a template, when a syntax error 22 * is detected in a template, or when rendering a template. Other 23 * errors must use regular PHP exception classes (like when the template 24 * cache directory is not writable for instance). 25 * 26 * To help debugging template issues, this class tracks the original template 27 * name and line where the error occurred. 28 * 29 * Whenever possible, you must set these information (original template name 30 * and line number) yourself by passing them to the constructor. If some or all 31 * these information are not available from where you throw the exception, then 32 * this class will guess them automatically (when the line number is set to -1 33 * and/or the name is set to null). As this is a costly operation, this 34 * can be disabled by passing false for both the name and the line number 35 * when creating a new instance of this class. 36 * 37 * @author Fabien Potencier <fabien@symfony.com> 38 */ 39class Error extends \Exception 40{ 41 protected $lineno; 42 // to be renamed to name in 2.0 43 protected $filename; 44 protected $rawMessage; 45 46 private $sourcePath; 47 private $sourceCode; 48 49 /** 50 * Constructor. 51 * 52 * Set both the line number and the name to false to 53 * disable automatic guessing of the original template name 54 * and line number. 55 * 56 * Set the line number to -1 to enable its automatic guessing. 57 * Set the name to null to enable its automatic guessing. 58 * 59 * By default, automatic guessing is enabled. 60 * 61 * @param string $message The error message 62 * @param int $lineno The template line where the error occurred 63 * @param Source|string|null $source The source context where the error occurred 64 * @param \Exception $previous The previous exception 65 */ 66 public function __construct($message, $lineno = -1, $source = null, \Exception $previous = null, $autoGuess = true) 67 { 68 if (null === $source) { 69 $name = null; 70 } elseif (!$source instanceof Source) { 71 // for compat with the Twig C ext., passing the template name as string is accepted 72 $name = $source; 73 } else { 74 $name = $source->getName(); 75 $this->sourceCode = $source->getCode(); 76 $this->sourcePath = $source->getPath(); 77 } 78 parent::__construct('', 0, $previous); 79 80 $this->lineno = $lineno; 81 $this->filename = $name; 82 83 if ($autoGuess && (-1 === $lineno || null === $name || null === $this->sourcePath)) { 84 $this->guessTemplateInfo(); 85 } 86 87 $this->rawMessage = $message; 88 89 $this->updateRepr(); 90 } 91 92 /** 93 * Gets the raw message. 94 * 95 * @return string The raw message 96 */ 97 public function getRawMessage() 98 { 99 return $this->rawMessage; 100 } 101 102 /** 103 * Gets the logical name where the error occurred. 104 * 105 * @return string The name 106 * 107 * @deprecated since 1.27 (to be removed in 2.0). Use getSourceContext() instead. 108 */ 109 public function getTemplateFile() 110 { 111 @trigger_error(sprintf('The "%s" method is deprecated since version 1.27 and will be removed in 2.0. Use getSourceContext() instead.', __METHOD__), E_USER_DEPRECATED); 112 113 return $this->filename; 114 } 115 116 /** 117 * Sets the logical name where the error occurred. 118 * 119 * @param string $name The name 120 * 121 * @deprecated since 1.27 (to be removed in 2.0). Use setSourceContext() instead. 122 */ 123 public function setTemplateFile($name) 124 { 125 @trigger_error(sprintf('The "%s" method is deprecated since version 1.27 and will be removed in 2.0. Use setSourceContext() instead.', __METHOD__), E_USER_DEPRECATED); 126 127 $this->filename = $name; 128 129 $this->updateRepr(); 130 } 131 132 /** 133 * Gets the logical name where the error occurred. 134 * 135 * @return string The name 136 * 137 * @deprecated since 1.29 (to be removed in 2.0). Use getSourceContext() instead. 138 */ 139 public function getTemplateName() 140 { 141 @trigger_error(sprintf('The "%s" method is deprecated since version 1.29 and will be removed in 2.0. Use getSourceContext() instead.', __METHOD__), E_USER_DEPRECATED); 142 143 return $this->filename; 144 } 145 146 /** 147 * Sets the logical name where the error occurred. 148 * 149 * @param string $name The name 150 * 151 * @deprecated since 1.29 (to be removed in 2.0). Use setSourceContext() instead. 152 */ 153 public function setTemplateName($name) 154 { 155 @trigger_error(sprintf('The "%s" method is deprecated since version 1.29 and will be removed in 2.0. Use setSourceContext() instead.', __METHOD__), E_USER_DEPRECATED); 156 157 $this->filename = $name; 158 $this->sourceCode = $this->sourcePath = null; 159 160 $this->updateRepr(); 161 } 162 163 /** 164 * Gets the template line where the error occurred. 165 * 166 * @return int The template line 167 */ 168 public function getTemplateLine() 169 { 170 return $this->lineno; 171 } 172 173 /** 174 * Sets the template line where the error occurred. 175 * 176 * @param int $lineno The template line 177 */ 178 public function setTemplateLine($lineno) 179 { 180 $this->lineno = $lineno; 181 182 $this->updateRepr(); 183 } 184 185 /** 186 * Gets the source context of the Twig template where the error occurred. 187 * 188 * @return Source|null 189 */ 190 public function getSourceContext() 191 { 192 return $this->filename ? new Source($this->sourceCode, $this->filename, $this->sourcePath) : null; 193 } 194 195 /** 196 * Sets the source context of the Twig template where the error occurred. 197 */ 198 public function setSourceContext(Source $source = null) 199 { 200 if (null === $source) { 201 $this->sourceCode = $this->filename = $this->sourcePath = null; 202 } else { 203 $this->sourceCode = $source->getCode(); 204 $this->filename = $source->getName(); 205 $this->sourcePath = $source->getPath(); 206 } 207 208 $this->updateRepr(); 209 } 210 211 public function guess() 212 { 213 $this->guessTemplateInfo(); 214 $this->updateRepr(); 215 } 216 217 public function appendMessage($rawMessage) 218 { 219 $this->rawMessage .= $rawMessage; 220 $this->updateRepr(); 221 } 222 223 /** 224 * @internal 225 */ 226 protected function updateRepr() 227 { 228 $this->message = $this->rawMessage; 229 230 if ($this->sourcePath && $this->lineno > 0) { 231 $this->file = $this->sourcePath; 232 $this->line = $this->lineno; 233 234 return; 235 } 236 237 $dot = false; 238 if ('.' === substr($this->message, -1)) { 239 $this->message = substr($this->message, 0, -1); 240 $dot = true; 241 } 242 243 $questionMark = false; 244 if ('?' === substr($this->message, -1)) { 245 $this->message = substr($this->message, 0, -1); 246 $questionMark = true; 247 } 248 249 if ($this->filename) { 250 if (\is_string($this->filename) || (\is_object($this->filename) && method_exists($this->filename, '__toString'))) { 251 $name = sprintf('"%s"', $this->filename); 252 } else { 253 $name = json_encode($this->filename); 254 } 255 $this->message .= sprintf(' in %s', $name); 256 } 257 258 if ($this->lineno && $this->lineno >= 0) { 259 $this->message .= sprintf(' at line %d', $this->lineno); 260 } 261 262 if ($dot) { 263 $this->message .= '.'; 264 } 265 266 if ($questionMark) { 267 $this->message .= '?'; 268 } 269 } 270 271 /** 272 * @internal 273 */ 274 protected function guessTemplateInfo() 275 { 276 $template = null; 277 $templateClass = null; 278 279 $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT); 280 foreach ($backtrace as $trace) { 281 if (isset($trace['object']) && $trace['object'] instanceof Template && 'Twig_Template' !== \get_class($trace['object'])) { 282 $currentClass = \get_class($trace['object']); 283 $isEmbedContainer = 0 === strpos($templateClass, $currentClass); 284 if (null === $this->filename || ($this->filename == $trace['object']->getTemplateName() && !$isEmbedContainer)) { 285 $template = $trace['object']; 286 $templateClass = \get_class($trace['object']); 287 } 288 } 289 } 290 291 // update template name 292 if (null !== $template && null === $this->filename) { 293 $this->filename = $template->getTemplateName(); 294 } 295 296 // update template path if any 297 if (null !== $template && null === $this->sourcePath) { 298 $src = $template->getSourceContext(); 299 $this->sourceCode = $src->getCode(); 300 $this->sourcePath = $src->getPath(); 301 } 302 303 if (null === $template || $this->lineno > -1) { 304 return; 305 } 306 307 $r = new \ReflectionObject($template); 308 $file = $r->getFileName(); 309 310 $exceptions = [$e = $this]; 311 while ($e instanceof self && $e = $e->getPrevious()) { 312 $exceptions[] = $e; 313 } 314 315 while ($e = array_pop($exceptions)) { 316 $traces = $e->getTrace(); 317 array_unshift($traces, ['file' => $e->getFile(), 'line' => $e->getLine()]); 318 319 while ($trace = array_shift($traces)) { 320 if (!isset($trace['file']) || !isset($trace['line']) || $file != $trace['file']) { 321 continue; 322 } 323 324 foreach ($template->getDebugInfo() as $codeLine => $templateLine) { 325 if ($codeLine <= $trace['line']) { 326 // update template line 327 $this->lineno = $templateLine; 328 329 return; 330 } 331 } 332 } 333 } 334 } 335} 336 337class_alias('Twig\Error\Error', 'Twig_Error'); 338