1<?php 2 3/* 4 * This file is part of Twig. 5 * 6 * (c) Fabien Potencier 7 * (c) Armin Ronacher 8 * 9 * For the full copyright and license information, please view the LICENSE 10 * file that was distributed with this source code. 11 */ 12 13namespace Twig; 14 15use Twig\Node\Node; 16 17/** 18 * @author Fabien Potencier <fabien@symfony.com> 19 */ 20class Compiler 21{ 22 private $lastLine; 23 private $source; 24 private $indentation; 25 private $debugInfo = []; 26 private $sourceOffset; 27 private $sourceLine; 28 private $varNameSalt = 0; 29 private $didUseEcho = false; 30 private $didUseEchoStack = []; 31 32 public function __construct( 33 private Environment $env, 34 ) { 35 } 36 37 public function getEnvironment(): Environment 38 { 39 return $this->env; 40 } 41 42 public function getSource(): string 43 { 44 return $this->source; 45 } 46 47 /** 48 * @return $this 49 */ 50 public function reset(int $indentation = 0) 51 { 52 $this->lastLine = null; 53 $this->source = ''; 54 $this->debugInfo = []; 55 $this->sourceOffset = 0; 56 // source code starts at 1 (as we then increment it when we encounter new lines) 57 $this->sourceLine = 1; 58 $this->indentation = $indentation; 59 $this->varNameSalt = 0; 60 61 return $this; 62 } 63 64 /** 65 * @return $this 66 */ 67 public function compile(Node $node, int $indentation = 0) 68 { 69 $this->reset($indentation); 70 $this->didUseEchoStack[] = $this->didUseEcho; 71 72 try { 73 $this->didUseEcho = false; 74 $node->compile($this); 75 76 if ($this->didUseEcho) { 77 trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[\Twig\Attribute\YieldReady].', $this->didUseEcho, $node::class); 78 } 79 80 return $this; 81 } finally { 82 $this->didUseEcho = array_pop($this->didUseEchoStack); 83 } 84 } 85 86 /** 87 * @return $this 88 */ 89 public function subcompile(Node $node, bool $raw = true) 90 { 91 if (!$raw) { 92 $this->source .= str_repeat(' ', $this->indentation * 4); 93 } 94 95 $this->didUseEchoStack[] = $this->didUseEcho; 96 97 try { 98 $this->didUseEcho = false; 99 $node->compile($this); 100 101 if ($this->didUseEcho) { 102 trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[\Twig\Attribute\YieldReady].', $this->didUseEcho, $node::class); 103 } 104 105 return $this; 106 } finally { 107 $this->didUseEcho = array_pop($this->didUseEchoStack); 108 } 109 } 110 111 /** 112 * Adds a raw string to the compiled code. 113 * 114 * @return $this 115 */ 116 public function raw(string $string) 117 { 118 $this->checkForEcho($string); 119 $this->source .= $string; 120 121 return $this; 122 } 123 124 /** 125 * Writes a string to the compiled code by adding indentation. 126 * 127 * @return $this 128 */ 129 public function write(...$strings) 130 { 131 foreach ($strings as $string) { 132 $this->checkForEcho($string); 133 $this->source .= str_repeat(' ', $this->indentation * 4).$string; 134 } 135 136 return $this; 137 } 138 139 /** 140 * Adds a quoted string to the compiled code. 141 * 142 * @return $this 143 */ 144 public function string(string $value) 145 { 146 // Single quotes are encoded as \x27 (not \') as a defense-in-depth measure: 147 // it guarantees that the compiled output never contains a literal "'" derived 148 // from user input, which prevents breaking out of a surrounding single-quoted 149 // PHP context if a caller mistakenly concatenates the result into one. 150 // \' is not a recognized escape sequence in PHP double-quoted strings (the 151 // backslash would be kept literally), so \x27 is used instead. 152 $this->source .= \sprintf('"%s"', str_replace("'", '\\x27', addcslashes($value, "\0\t\"\$\\"))); 153 154 return $this; 155 } 156 157 /** 158 * Returns a PHP representation of a given value. 159 * 160 * @return $this 161 */ 162 public function repr($value) 163 { 164 if (\is_int($value) || \is_float($value)) { 165 if (false !== $locale = setlocale(\LC_NUMERIC, '0')) { 166 setlocale(\LC_NUMERIC, 'C'); 167 } 168 169 $this->raw(var_export($value, true)); 170 171 if (false !== $locale) { 172 setlocale(\LC_NUMERIC, $locale); 173 } 174 } elseif (null === $value) { 175 $this->raw('null'); 176 } elseif (\is_bool($value)) { 177 $this->raw($value ? 'true' : 'false'); 178 } elseif (\is_array($value)) { 179 $this->raw('['); 180 $first = true; 181 foreach ($value as $key => $v) { 182 if (!$first) { 183 $this->raw(', '); 184 } 185 $first = false; 186 $this->repr($key); 187 $this->raw(' => '); 188 $this->repr($v); 189 } 190 $this->raw(']'); 191 } else { 192 $this->string($value); 193 } 194 195 return $this; 196 } 197 198 /** 199 * @return $this 200 */ 201 public function addDebugInfo(Node $node) 202 { 203 if ($node->getTemplateLine() != $this->lastLine) { 204 $this->write(\sprintf("// line %d\n", $node->getTemplateLine())); 205 206 $this->sourceLine += substr_count($this->source, "\n", $this->sourceOffset); 207 $this->sourceOffset = \strlen($this->source); 208 $this->debugInfo[$this->sourceLine] = $node->getTemplateLine(); 209 210 $this->lastLine = $node->getTemplateLine(); 211 } 212 213 return $this; 214 } 215 216 public function getDebugInfo(): array 217 { 218 ksort($this->debugInfo); 219 220 return $this->debugInfo; 221 } 222 223 /** 224 * @return $this 225 */ 226 public function indent(int $step = 1) 227 { 228 $this->indentation += $step; 229 230 return $this; 231 } 232 233 /** 234 * @return $this 235 * 236 * @throws \LogicException When trying to outdent too much so the indentation would become negative 237 */ 238 public function outdent(int $step = 1) 239 { 240 // can't outdent by more steps than the current indentation level 241 if ($this->indentation < $step) { 242 throw new \LogicException('Unable to call outdent() as the indentation would become negative.'); 243 } 244 245 $this->indentation -= $step; 246 247 return $this; 248 } 249 250 public function getVarName(): string 251 { 252 return \sprintf('_v%d', $this->varNameSalt++); 253 } 254 255 private function checkForEcho(string $string): void 256 { 257 if ($this->didUseEcho) { 258 return; 259 } 260 261 $this->didUseEcho = preg_match('/^\s*+(echo|print)\b/', $string, $m) ? $m[1] : false; 262 } 263} 264