blob: a68be65f203a4e371f45ee6ca1e2d23c3e5017df [file] [log] [blame]
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +01001<?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 private $lineno;
42 private $name;
43 private $rawMessage;
44 private $sourcePath;
45 private $sourceCode;
46
47 /**
48 * Constructor.
49 *
50 * By default, automatic guessing is enabled.
51 *
52 * @param string $message The error message
53 * @param int $lineno The template line where the error occurred
54 * @param Source|null $source The source context where the error occurred
55 */
56 public function __construct(string $message, int $lineno = -1, Source $source = null, \Exception $previous = null)
57 {
58 parent::__construct('', 0, $previous);
59
60 if (null === $source) {
61 $name = null;
62 } else {
63 $name = $source->getName();
64 $this->sourceCode = $source->getCode();
65 $this->sourcePath = $source->getPath();
66 }
67
68 $this->lineno = $lineno;
69 $this->name = $name;
70 $this->rawMessage = $message;
71 $this->updateRepr();
72 }
73
74 public function getRawMessage(): string
75 {
76 return $this->rawMessage;
77 }
78
79 public function getTemplateLine(): int
80 {
81 return $this->lineno;
82 }
83
84 public function setTemplateLine(int $lineno): void
85 {
86 $this->lineno = $lineno;
87
88 $this->updateRepr();
89 }
90
91 public function getSourceContext(): ?Source
92 {
93 return $this->name ? new Source($this->sourceCode, $this->name, $this->sourcePath) : null;
94 }
95
96 public function setSourceContext(Source $source = null): void
97 {
98 if (null === $source) {
99 $this->sourceCode = $this->name = $this->sourcePath = null;
100 } else {
101 $this->sourceCode = $source->getCode();
102 $this->name = $source->getName();
103 $this->sourcePath = $source->getPath();
104 }
105
106 $this->updateRepr();
107 }
108
109 public function guess(): void
110 {
111 $this->guessTemplateInfo();
112 $this->updateRepr();
113 }
114
115 public function appendMessage($rawMessage): void
116 {
117 $this->rawMessage .= $rawMessage;
118 $this->updateRepr();
119 }
120
121 private function updateRepr(): void
122 {
123 $this->message = $this->rawMessage;
124
125 if ($this->sourcePath && $this->lineno > 0) {
126 $this->file = $this->sourcePath;
127 $this->line = $this->lineno;
128
129 return;
130 }
131
132 $dot = false;
133 if ('.' === substr($this->message, -1)) {
134 $this->message = substr($this->message, 0, -1);
135 $dot = true;
136 }
137
138 $questionMark = false;
139 if ('?' === substr($this->message, -1)) {
140 $this->message = substr($this->message, 0, -1);
141 $questionMark = true;
142 }
143
144 if ($this->name) {
145 if (\is_string($this->name) || (\is_object($this->name) && method_exists($this->name, '__toString'))) {
146 $name = sprintf('"%s"', $this->name);
147 } else {
148 $name = json_encode($this->name);
149 }
150 $this->message .= sprintf(' in %s', $name);
151 }
152
153 if ($this->lineno && $this->lineno >= 0) {
154 $this->message .= sprintf(' at line %d', $this->lineno);
155 }
156
157 if ($dot) {
158 $this->message .= '.';
159 }
160
161 if ($questionMark) {
162 $this->message .= '?';
163 }
164 }
165
166 private function guessTemplateInfo(): void
167 {
168 $template = null;
169 $templateClass = null;
170
171 $backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT);
172 foreach ($backtrace as $trace) {
173 if (isset($trace['object']) && $trace['object'] instanceof Template) {
174 $currentClass = \get_class($trace['object']);
175 $isEmbedContainer = null === $templateClass ? false : 0 === strpos($templateClass, $currentClass);
176 if (null === $this->name || ($this->name == $trace['object']->getTemplateName() && !$isEmbedContainer)) {
177 $template = $trace['object'];
178 $templateClass = \get_class($trace['object']);
179 }
180 }
181 }
182
183 // update template name
184 if (null !== $template && null === $this->name) {
185 $this->name = $template->getTemplateName();
186 }
187
188 // update template path if any
189 if (null !== $template && null === $this->sourcePath) {
190 $src = $template->getSourceContext();
191 $this->sourceCode = $src->getCode();
192 $this->sourcePath = $src->getPath();
193 }
194
195 if (null === $template || $this->lineno > -1) {
196 return;
197 }
198
199 $r = new \ReflectionObject($template);
200 $file = $r->getFileName();
201
202 $exceptions = [$e = $this];
203 while ($e = $e->getPrevious()) {
204 $exceptions[] = $e;
205 }
206
207 while ($e = array_pop($exceptions)) {
208 $traces = $e->getTrace();
209 array_unshift($traces, ['file' => $e->getFile(), 'line' => $e->getLine()]);
210
211 while ($trace = array_shift($traces)) {
212 if (!isset($trace['file']) || !isset($trace['line']) || $file != $trace['file']) {
213 continue;
214 }
215
216 foreach ($template->getDebugInfo() as $codeLine => $templateLine) {
217 if ($codeLine <= $trace['line']) {
218 // update template line
219 $this->lineno = $templateLine;
220
221 return;
222 }
223 }
224 }
225 }
226 }
227}