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. 33 * 34 * @author Fabien Potencier <fabien@symfony.com> 35 */ 36class Error extends \Exception 37{ 38 private $lineno; 39 private $rawMessage; 40 private ?Source $source; 41 private string $phpFile; 42 private int $phpLine; 43 44 /** 45 * Constructor. 46 * 47 * By default, automatic guessing is enabled. 48 * 49 * @param string $message The error message 50 * @param int $lineno The template line where the error occurred 51 * @param Source|null $source The source context where the error occurred 52 */ 53 public function __construct(string $message, int $lineno = -1, ?Source $source = null, ?\Throwable $previous = null) 54 { 55 parent::__construct('', 0, $previous); 56 57 $this->phpFile = $this->getFile(); 58 $this->phpLine = $this->getLine(); 59 $this->lineno = $lineno; 60 $this->source = $source; 61 $this->rawMessage = $message; 62 $this->updateRepr(); 63 } 64 65 public function getRawMessage(): string 66 { 67 return $this->rawMessage; 68 } 69 70 public function getTemplateLine(): int 71 { 72 return $this->lineno; 73 } 74 75 public function setTemplateLine(int $lineno): void 76 { 77 $this->lineno = $lineno; 78 $this->updateRepr(); 79 } 80 81 public function getSourceContext(): ?Source 82 { 83 return $this->source; 84 } 85 86 public function setSourceContext(?Source $source = null): void 87 { 88 $this->source = $source; 89 $this->updateRepr(); 90 } 91 92 public function guess(): void 93 { 94 if ($this->lineno > -1) { 95 return; 96 } 97 98 $this->guessTemplateInfo(); 99 $this->updateRepr(); 100 } 101 102 public function appendMessage($rawMessage): void 103 { 104 $this->rawMessage .= $rawMessage; 105 $this->updateRepr(); 106 } 107 108 private function updateRepr(): void 109 { 110 if ($this->source && $this->source->getPath()) { 111 // we only update the file and the line together 112 $this->file = $this->source->getPath(); 113 if ($this->lineno > 0) { 114 $this->line = $this->lineno; 115 } else { 116 $this->line = -1; 117 } 118 } 119 120 $this->message = $this->rawMessage; 121 $last = substr($this->message, -1); 122 if ($punctuation = '.' === $last || '?' === $last ? $last : '') { 123 $this->message = substr($this->message, 0, -1); 124 } 125 if ($this->source && $this->source->getName()) { 126 $this->message .= \sprintf(' in "%s"', $this->source->getName()); 127 } 128 if ($this->lineno > 0) { 129 $this->message .= \sprintf(' at line %d', $this->lineno); 130 } 131 if ($punctuation) { 132 $this->message .= $punctuation; 133 } 134 } 135 136 private function guessTemplateInfo(): void 137 { 138 // $this->source is never null here (see guess() usage in Template) 139 140 $this->lineno = 0; 141 $template = null; 142 $backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT); 143 foreach ($backtrace as $trace) { 144 if (isset($trace['object']) && $trace['object'] instanceof Template && $this->source->getName() === $trace['object']->getTemplateName()) { 145 $template = $trace['object']; 146 147 break; 148 } 149 } 150 151 if (null === $template) { 152 return; // Impossible to guess the info as the template was not found in the backtrace 153 } 154 155 $r = new \ReflectionObject($template); 156 $file = $r->getFileName(); 157 158 $exceptions = [$e = $this]; 159 while ($e = $e->getPrevious()) { 160 $exceptions[] = $e; 161 } 162 163 while ($e = array_pop($exceptions)) { 164 $traces = $e->getTrace(); 165 array_unshift($traces, ['file' => $e instanceof self ? $e->phpFile : $e->getFile(), 'line' => $e instanceof self ? $e->phpLine : $e->getLine()]); 166 while ($trace = array_shift($traces)) { 167 if (!isset($trace['file']) || !isset($trace['line']) || $file != $trace['file']) { 168 continue; 169 } 170 171 foreach ($template->getDebugInfo() as $codeLine => $templateLine) { 172 if ($codeLine <= $trace['line']) { 173 // update template line 174 $this->lineno = $templateLine; 175 176 return; 177 } 178 } 179 } 180 } 181 } 182} 183