Revision 399 | Blame | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed
<?phpnamespace Illuminate\View\Compilers;use Illuminate\Container\Container;use Illuminate\Contracts\Support\Htmlable;use Illuminate\Contracts\View\Factory as ViewFactory;use Illuminate\Contracts\View\View;use Illuminate\Support\Arr;use Illuminate\Support\Str;use Illuminate\Support\Traits\ReflectsClosures;use Illuminate\View\Component;use InvalidArgumentException;class BladeCompiler extends Compiler implements CompilerInterface{use Concerns\CompilesAuthorizations,Concerns\CompilesClasses,Concerns\CompilesComments,Concerns\CompilesComponents,Concerns\CompilesConditionals,Concerns\CompilesEchos,Concerns\CompilesErrors,Concerns\CompilesFragments,Concerns\CompilesHelpers,Concerns\CompilesIncludes,Concerns\CompilesInjections,Concerns\CompilesJson,Concerns\CompilesJs,Concerns\CompilesLayouts,Concerns\CompilesLoops,Concerns\CompilesRawPhp,Concerns\CompilesStacks,Concerns\CompilesStyles,Concerns\CompilesTranslations,ReflectsClosures;/*** All of the registered extensions.** @var array*/protected $extensions = [];/*** All custom "directive" handlers.** @var array*/protected $customDirectives = [];/*** All custom "condition" handlers.** @var array*/protected $conditions = [];/*** All of the registered precompilers.** @var array*/protected $precompilers = [];/*** The file currently being compiled.** @var string*/protected $path;/*** All of the available compiler functions.** @var string[]*/protected $compilers = [// 'Comments','Extensions','Statements','Echos',];/*** Array of opening and closing tags for raw echos.** @var string[]*/protected $rawTags = ['{!!', '!!}'];/*** Array of opening and closing tags for regular echos.** @var string[]*/protected $contentTags = ['{{', '}}'];/*** Array of opening and closing tags for escaped echos.** @var string[]*/protected $escapedTags = ['{{{', '}}}'];/*** The "regular" / legacy echo string format.** @var string*/protected $echoFormat = 'e(%s)';/*** Array of footer lines to be added to the template.** @var array*/protected $footer = [];/*** Array to temporarily store the raw blocks found in the template.** @var array*/protected $rawBlocks = [];/*** The array of anonymous component paths to search for components in.** @var array*/protected $anonymousComponentPaths = [];/*** The array of anonymous component namespaces to autoload from.** @var array*/protected $anonymousComponentNamespaces = [];/*** The array of class component aliases and their class names.** @var array*/protected $classComponentAliases = [];/*** The array of class component namespaces to autoload from.** @var array*/protected $classComponentNamespaces = [];/*** Indicates if component tags should be compiled.** @var bool*/protected $compilesComponentTags = true;/*** Compile the view at the given path.** @param string|null $path* @return void*/public function compile($path = null){if ($path) {$this->setPath($path);}if (! is_null($this->cachePath)) {$contents = $this->compileString($this->files->get($this->getPath()));if (! empty($this->getPath())) {$contents = $this->appendFilePath($contents);}$this->ensureCompiledDirectoryExists($compiledPath = $this->getCompiledPath($this->getPath()));$this->files->put($compiledPath, $contents);}}/*** Append the file path to the compiled string.** @param string $contents* @return string*/protected function appendFilePath($contents){$tokens = $this->getOpenAndClosingPhpTokens($contents);if ($tokens->isNotEmpty() && $tokens->last() !== T_CLOSE_TAG) {$contents .= ' ?>';}return $contents."<?php /**PATH {$this->getPath()} ENDPATH**/ ?>";}/*** Get the open and closing PHP tag tokens from the given string.** @param string $contents* @return \Illuminate\Support\Collection*/protected function getOpenAndClosingPhpTokens($contents){return collect(token_get_all($contents))->pluck(0)->filter(function ($token) {return in_array($token, [T_OPEN_TAG, T_OPEN_TAG_WITH_ECHO, T_CLOSE_TAG]);});}/*** Get the path currently being compiled.** @return string*/public function getPath(){return $this->path;}/*** Set the path currently being compiled.** @param string $path* @return void*/public function setPath($path){$this->path = $path;}/*** Compile the given Blade template contents.** @param string $value* @return string*/public function compileString($value){[$this->footer, $result] = [[], ''];// First we will compile the Blade component tags. This is a precompile style// step which compiles the component Blade tags into @component directives// that may be used by Blade. Then we should call any other precompilers.$value = $this->compileComponentTags($this->compileComments($this->storeUncompiledBlocks($value)));foreach ($this->precompilers as $precompiler) {$value = $precompiler($value);}// Here we will loop through all of the tokens returned by the Zend lexer and// parse each one into the corresponding valid PHP. We will then have this// template as the correctly rendered PHP that can be rendered natively.foreach (token_get_all($value) as $token) {$result .= is_array($token) ? $this->parseToken($token) : $token;}if (! empty($this->rawBlocks)) {$result = $this->restoreRawContent($result);}// If there are any footer lines that need to get added to a template we will// add them here at the end of the template. This gets used mainly for the// template inheritance via the extends keyword that should be appended.if (count($this->footer) > 0) {$result = $this->addFooters($result);}if (! empty($this->echoHandlers)) {$result = $this->addBladeCompilerVariable($result);}return str_replace(['##BEGIN-COMPONENT-CLASS##', '##END-COMPONENT-CLASS##'],'',$result);}/*** Evaluate and render a Blade string to HTML.** @param string $string* @param array $data* @param bool $deleteCachedView* @return string*/public static function render($string, $data = [], $deleteCachedView = false){$component = new class($string) extends Component{protected $template;public function __construct($template){$this->template = $template;}public function render(){return $this->template;}};$view = Container::getInstance()->make(ViewFactory::class)->make($component->resolveView(), $data);return tap($view->render(), function () use ($view, $deleteCachedView) {if ($deleteCachedView) {unlink($view->getPath());}});}/*** Render a component instance to HTML.** @param \Illuminate\View\Component $component* @return string*/public static function renderComponent(Component $component){$data = $component->data();$view = value($component->resolveView(), $data);if ($view instanceof View) {return $view->with($data)->render();} elseif ($view instanceof Htmlable) {return $view->toHtml();} else {return Container::getInstance()->make(ViewFactory::class)->make($view, $data)->render();}}/*** Store the blocks that do not receive compilation.** @param string $value* @return string*/protected function storeUncompiledBlocks($value){if (str_contains($value, '@verbatim')) {$value = $this->storeVerbatimBlocks($value);}if (str_contains($value, '@php')) {$value = $this->storePhpBlocks($value);}return $value;}/*** Store the verbatim blocks and replace them with a temporary placeholder.** @param string $value* @return string*/protected function storeVerbatimBlocks($value){return preg_replace_callback('/(?<!@)@verbatim(.*?)@endverbatim/s', function ($matches) {return $this->storeRawBlock($matches[1]);}, $value);}/*** Store the PHP blocks and replace them with a temporary placeholder.** @param string $value* @return string*/protected function storePhpBlocks($value){return preg_replace_callback('/(?<!@)@php(.*?)@endphp/s', function ($matches) {return $this->storeRawBlock("<?php{$matches[1]}?>");}, $value);}/*** Store a raw block and return a unique raw placeholder.** @param string $value* @return string*/protected function storeRawBlock($value){return $this->getRawPlaceholder(array_push($this->rawBlocks, $value) - 1);}/*** Compile the component tags.** @param string $value* @return string*/protected function compileComponentTags($value){if (! $this->compilesComponentTags) {return $value;}return (new ComponentTagCompiler($this->classComponentAliases, $this->classComponentNamespaces, $this))->compile($value);}/*** Replace the raw placeholders with the original code stored in the raw blocks.** @param string $result* @return string*/protected function restoreRawContent($result){$result = preg_replace_callback('/'.$this->getRawPlaceholder('(\d+)').'/', function ($matches) {return $this->rawBlocks[$matches[1]];}, $result);$this->rawBlocks = [];return $result;}/*** Get a placeholder to temporarily mark the position of raw blocks.** @param int|string $replace* @return string*/protected function getRawPlaceholder($replace){return str_replace('#', $replace, '@__raw_block_#__@');}/*** Add the stored footers onto the given content.** @param string $result* @return string*/protected function addFooters($result){return ltrim($result, "\n")."\n".implode("\n", array_reverse($this->footer));}/*** Parse the tokens from the template.** @param array $token* @return string*/protected function parseToken($token){[$id, $content] = $token;if ($id == T_INLINE_HTML) {foreach ($this->compilers as $type) {$content = $this->{"compile{$type}"}($content);}}return $content;}/*** Execute the user defined extensions.** @param string $value* @return string*/protected function compileExtensions($value){foreach ($this->extensions as $compiler) {$value = $compiler($value, $this);}return $value;}/*** Compile Blade statements that start with "@".** @param string $template* @return string*/protected function compileStatements($template){preg_match_all('/\B@(@?\w+(?:::\w+)?)([ \t]*)(\( ( [\S\s]*? ) \))?/x', $template, $matches);$offset = 0;for ($i = 0; isset($matches[0][$i]); $i++) {$match = [$matches[0][$i],$matches[1][$i],$matches[2][$i],$matches[3][$i] ?: null,$matches[4][$i] ?: null,];// Here we check to see if we have properly found the closing parenthesis by// regex pattern or not, and will recursively continue on to the next ")"// then check again until the tokenizer confirms we find the right one.while (isset($match[4]) &&Str::endsWith($match[0], ')') &&! $this->hasEvenNumberOfParentheses($match[0])) {if (($after = Str::after($template, $match[0])) === $template) {break;}$rest = Str::before($after, ')');if (isset($matches[0][$i + 1]) && Str::contains($rest.')', $matches[0][$i + 1])) {unset($matches[0][$i + 1]);$i++;}$match[0] = $match[0].$rest.')';$match[3] = $match[3].$rest.')';$match[4] = $match[4].$rest;}[$template, $offset] = $this->replaceFirstStatement($match[0],$this->compileStatement($match),$template,$offset);}return $template;}/*** Replace the first match for a statement compilation operation.** @param string $search* @param string $replace* @param string $subject* @param int $offset* @return array*/protected function replaceFirstStatement($search, $replace, $subject, $offset){$search = (string) $search;if ($search === '') {return $subject;}$position = strpos($subject, $search, $offset);if ($position !== false) {return [substr_replace($subject, $replace, $position, strlen($search)),$position + strlen($replace),];}return [$subject, 0];}/*** Determine if the given expression has the same number of opening and closing parentheses.** @param string $expression* @return bool*/protected function hasEvenNumberOfParentheses(string $expression){$tokens = token_get_all('<?php '.$expression);if (Arr::last($tokens) !== ')') {return false;}$opening = 0;$closing = 0;foreach ($tokens as $token) {if ($token == ')') {$closing++;} elseif ($token == '(') {$opening++;}}return $opening === $closing;}/*** Compile a single Blade @ statement.** @param array $match* @return string*/protected function compileStatement($match){if (str_contains($match[1], '@')) {$match[0] = isset($match[3]) ? $match[1].$match[3] : $match[1];} elseif (isset($this->customDirectives[$match[1]])) {$match[0] = $this->callCustomDirective($match[1], Arr::get($match, 3));} elseif (method_exists($this, $method = 'compile'.ucfirst($match[1]))) {$match[0] = $this->$method(Arr::get($match, 3));} else {return $match[0];}return isset($match[3]) ? $match[0] : $match[0].$match[2];}/*** Call the given directive with the given value.** @param string $name* @param string|null $value* @return string*/protected function callCustomDirective($name, $value){$value ??= '';if (str_starts_with($value, '(') && str_ends_with($value, ')')) {$value = Str::substr($value, 1, -1);}return call_user_func($this->customDirectives[$name], trim($value));}/*** Strip the parentheses from the given expression.** @param string $expression* @return string*/public function stripParentheses($expression){if (Str::startsWith($expression, '(')) {$expression = substr($expression, 1, -1);}return $expression;}/*** Register a custom Blade compiler.** @param callable $compiler* @return void*/public function extend(callable $compiler){$this->extensions[] = $compiler;}/*** Get the extensions used by the compiler.** @return array*/public function getExtensions(){return $this->extensions;}/*** Register an "if" statement directive.** @param string $name* @param callable $callback* @return void*/public function if($name, callable $callback){$this->conditions[$name] = $callback;$this->directive($name, function ($expression) use ($name) {return $expression !== ''? "<?php if (\Illuminate\Support\Facades\Blade::check('{$name}', {$expression})): ?>": "<?php if (\Illuminate\Support\Facades\Blade::check('{$name}')): ?>";});$this->directive('unless'.$name, function ($expression) use ($name) {return $expression !== ''? "<?php if (! \Illuminate\Support\Facades\Blade::check('{$name}', {$expression})): ?>": "<?php if (! \Illuminate\Support\Facades\Blade::check('{$name}')): ?>";});$this->directive('else'.$name, function ($expression) use ($name) {return $expression !== ''? "<?php elseif (\Illuminate\Support\Facades\Blade::check('{$name}', {$expression})): ?>": "<?php elseif (\Illuminate\Support\Facades\Blade::check('{$name}')): ?>";});$this->directive('end'.$name, function () {return '<?php endif; ?>';});}/*** Check the result of a condition.** @param string $name* @param mixed ...$parameters* @return bool*/public function check($name, ...$parameters){return call_user_func($this->conditions[$name], ...$parameters);}/*** Register a class-based component alias directive.** @param string $class* @param string|null $alias* @param string $prefix* @return void*/public function component($class, $alias = null, $prefix = ''){if (! is_null($alias) && str_contains($alias, '\\')) {[$class, $alias] = [$alias, $class];}if (is_null($alias)) {$alias = str_contains($class, '\\View\\Components\\')? collect(explode('\\', Str::after($class, '\\View\\Components\\')))->map(function ($segment) {return Str::kebab($segment);})->implode(':'): Str::kebab(class_basename($class));}if (! empty($prefix)) {$alias = $prefix.'-'.$alias;}$this->classComponentAliases[$alias] = $class;}/*** Register an array of class-based components.** @param array $components* @param string $prefix* @return void*/public function components(array $components, $prefix = ''){foreach ($components as $key => $value) {if (is_numeric($key)) {$this->component($value, null, $prefix);} else {$this->component($key, $value, $prefix);}}}/*** Get the registered class component aliases.** @return array*/public function getClassComponentAliases(){return $this->classComponentAliases;}/*** Register a new anonymous component path.** @param string $path* @param string|null $prefix* @return void*/public function anonymousComponentPath(string $path, string $prefix = null){$prefixHash = md5($prefix ?: $path);$this->anonymousComponentPaths[] = ['path' => $path,'prefix' => $prefix,'prefixHash' => $prefixHash,];Container::getInstance()->make(ViewFactory::class)->addNamespace($prefixHash, $path);}/*** Register an anonymous component namespace.** @param string $directory* @param string|null $prefix* @return void*/public function anonymousComponentNamespace(string $directory, string $prefix = null){$prefix ??= $directory;$this->anonymousComponentNamespaces[$prefix] = Str::of($directory)->replace('/', '.')->trim('. ')->toString();}/*** Register a class-based component namespace.** @param string $namespace* @param string $prefix* @return void*/public function componentNamespace($namespace, $prefix){$this->classComponentNamespaces[$prefix] = $namespace;}/*** Get the registered anonymous component paths.** @return array*/public function getAnonymousComponentPaths(){return $this->anonymousComponentPaths;}/*** Get the registered anonymous component namespaces.** @return array*/public function getAnonymousComponentNamespaces(){return $this->anonymousComponentNamespaces;}/*** Get the registered class component namespaces.** @return array*/public function getClassComponentNamespaces(){return $this->classComponentNamespaces;}/*** Register a component alias directive.** @param string $path* @param string|null $alias* @return void*/public function aliasComponent($path, $alias = null){$alias = $alias ?: Arr::last(explode('.', $path));$this->directive($alias, function ($expression) use ($path) {return $expression? "<?php \$__env->startComponent('{$path}', {$expression}); ?>": "<?php \$__env->startComponent('{$path}'); ?>";});$this->directive('end'.$alias, function ($expression) {return '<?php echo $__env->renderComponent(); ?>';});}/*** Register an include alias directive.** @param string $path* @param string|null $alias* @return void*/public function include($path, $alias = null){$this->aliasInclude($path, $alias);}/*** Register an include alias directive.** @param string $path* @param string|null $alias* @return void*/public function aliasInclude($path, $alias = null){$alias = $alias ?: Arr::last(explode('.', $path));$this->directive($alias, function ($expression) use ($path) {$expression = $this->stripParentheses($expression) ?: '[]';return "<?php echo \$__env->make('{$path}', {$expression}, \Illuminate\Support\Arr::except(get_defined_vars(), ['__data', '__path']))->render(); ?>";});}/*** Register a handler for custom directives.** @param string $name* @param callable $handler* @return void** @throws \InvalidArgumentException*/public function directive($name, callable $handler){if (! preg_match('/^\w+(?:::\w+)?$/x', $name)) {throw new InvalidArgumentException("The directive name [{$name}] is not valid. Directive names must only contain alphanumeric characters and underscores.");}$this->customDirectives[$name] = $handler;}/*** Get the list of custom directives.** @return array*/public function getCustomDirectives(){return $this->customDirectives;}/*** Register a new precompiler.** @param callable $precompiler* @return void*/public function precompiler(callable $precompiler){$this->precompilers[] = $precompiler;}/*** Set the echo format to be used by the compiler.** @param string $format* @return void*/public function setEchoFormat($format){$this->echoFormat = $format;}/*** Set the "echo" format to double encode entities.** @return void*/public function withDoubleEncoding(){$this->setEchoFormat('e(%s, true)');}/*** Set the "echo" format to not double encode entities.** @return void*/public function withoutDoubleEncoding(){$this->setEchoFormat('e(%s, false)');}/*** Indicate that component tags should not be compiled.** @return void*/public function withoutComponentTags(){$this->compilesComponentTags = false;}}