diff --git a/CHANGELOG.md b/CHANGELOG.md index ee7e8b3..17b63c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Adds a new "Compile Time Rendering" system, which can render components at compile time and inline the static output - Adds compiler support for circular component references, like nested comment threads - Adds a `#cache` compiler attribute, which may be used to cache the results of any Dagger component - Bumps the minimum Laravel version to `11.23`, for `Cache::flexible` support diff --git a/README.md b/README.md index 07a9836..00b9412 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,10 @@ The main visual difference when working with Dagger components is the use of the - [Dynamic Components](#dynamic-components) - [Custom Component Paths and Namespaces](#custom-component-paths-and-namespaces) - [Blade Component Prefix](#blade-component-prefix) +- [Compile Time Rendering](#compile-time-rendering) + - [Disabling Compile Time Rendering on a Component](#disabling-compile-time-rendering-on-a-component) + - [Enabling/Disabling Optimizations on Classes or Methods](#enablingdisabling-optimizations-on-classes-or-methods) + - [Notes on Compile Time Rendering](#notes-on-compile-time-rendering) - [The View Manifest](#the-view-manifest) - [License](#license) @@ -1389,6 +1393,151 @@ Custom components can leverage all features of the Dagger compiler using their c You are **not** allowed to register the prefix `x` with the Dagger compiler; attempting to do so will raise an `InvalidArgumentException`. +## Compile Time Rendering + +The Dagger compiler contains a subsystem known as the Compile Time Renderer, or CTR. This checks to see if all the props on a component are resolvable at runtime; if so, it may elect to compile the component at runtime and insert the pre-rendered results into Blade's compiled output. + +This feature has a number of internal safe guards, and here are a few of the things that will internally disable this feature: + +- Dynamic/interpolated variable references +- Using Mixins +- Most static method calls +- Referencing PHP's [superglobals](https://php.net/superglobals), such as `$_GET` or `$_POST` +- Using debugging-related functions in a component, such as `dd`, `dump`, `var_dump`, etc. +- Calling functions such as `time`, `now`, or `date` +- Enabling the Attribute Cache on a component +- Components with slots + +Imagine we have the following alert component: + +```blade + + +@props(['type' => 'info', 'message']) + +
merge(['class' => 'alert alert-'.$type]) }}> + {{ $message }} +
+``` + +If we were to call the alert component like so: + +```blade + +``` + +The compiler would detect that all props are resolvable, and the following would be emitted in the compiled Blade output: + +```html +
+ The awesome message +
+``` + +However, if we were to call our component like so, the compiler would not attempt to render the component at compile time: + +```blade + +``` + +### Disabling Compile Time Rendering on a Component + +The CTR system should be transparent from a component author's point-of-view, however, if the rare event that you need to disable compiler optimizations, you may do so using the `compiler` helper method: + +```blade + +@php + \Stillat\Dagger\component() + ->props(['title']) + ->compiler( + allowOptimizations: false + ); +@endphp + +{{ $title }} +``` + +If you find yourself disabling optimizations on a component, please open a discussion or an issue with details on which behaviors led to that decision. + +### Enabling/Disabling Optimizations on Classes or Methods + +The CTR system will aggressively disable itself whenever it detects static method calls within component templates. You may choose to mark these methods as safe using the `EnableOptimization` attribute: + +```php + [ + 'unsafe_variables' => [ + '$_GET', '$_POST', '$_FILES', '$_REQUEST', '$_SESSION', + '$_ENV', '$_COOKIE', '$http_response_header', '$argc', '$argv', + ], + 'unsafe_functions' => [ + 'now', 'time', 'date', 'env', 'getenv', 'cookie', + 'request', 'session', 'dd', 'dump', 'var_dump', + 'debug_backtrace', 'phpinfo', 'extract', + 'get_defined_vars', 'parse_str', 'abort', 'abort_if', + 'abort_unless', + ], + ], +]; diff --git a/src/AbstractComponent.php b/src/AbstractComponent.php index d491a19..7a81080 100644 --- a/src/AbstractComponent.php +++ b/src/AbstractComponent.php @@ -4,6 +4,7 @@ use Illuminate\Support\Fluent; use Illuminate\View\ComponentAttributeBag; +use Stillat\Dagger\Exceptions\RuntimeException; use Stillat\Dagger\Parser\ComponentTap; use Stillat\Dagger\Runtime\SlotContainer; @@ -96,6 +97,14 @@ abstract public function trimOutput(): static; abstract public function cache(): static; + /** + * @throws RuntimeException + */ + public function compiler(?bool $allowOptimizations = null): static + { + throw new RuntimeException('Cannot call compiler method at runtime.'); + } + public function __get(string $name) { return $this->data->get($name); diff --git a/src/Compiler/ComponentState.php b/src/Compiler/ComponentState.php index 4bad5d6..6a6b7cc 100644 --- a/src/Compiler/ComponentState.php +++ b/src/Compiler/ComponentState.php @@ -6,6 +6,7 @@ use InvalidArgumentException; use Stillat\BladeParser\Nodes\Components\ComponentNode; use Stillat\Dagger\Cache\CacheProperties; +use Stillat\Dagger\ComponentOptions; use Stillat\Dagger\Support\Utils; class ComponentState @@ -63,17 +64,36 @@ class ComponentState public string $validationMessages = '[]'; + public ?bool $canCompileTimeRender = null; + public int $lineOffset = 0; + public ?Extractions $extractions = null; + + public ComponentOptions $options; + + /** + * Indicates if the component is eligible for compile-time rendering. + * + * We will keep it false by default, and only enable this if needed. + */ + public bool $isCtrEligible = false; + public ?CacheProperties $cacheProperties = null; public function __construct( public ?ComponentNode $node, public string $varSuffix, ) { + $this->options = new ComponentOptions; $this->updateNodeDetails($this->node, $this->varSuffix); } + public function getDynamicVariables(): array + { + return $this->dynamicVariables; + } + /** * @internal */ @@ -287,6 +307,11 @@ public function setPropDefaults(array $defaults): static return $this; } + public function getAllPropNames(): array + { + return array_merge($this->getPropNames(), $this->getAwareVariables()); + } + public function getPropDefaults(): array { return $this->defaultPropValues; @@ -342,6 +367,11 @@ public function setMixins(string $classes): static return $this; } + public function hasMixins(): bool + { + return $this->mixins != ''; + } + public function getMixins(): string { return $this->mixins; diff --git a/src/Compiler/Concerns/ManagesComponentCtrState.php b/src/Compiler/Concerns/ManagesComponentCtrState.php new file mode 100644 index 0000000..02d2d59 --- /dev/null +++ b/src/Compiler/Concerns/ManagesComponentCtrState.php @@ -0,0 +1,77 @@ +ctrVisitor) { + return $this->ctrVisitor; + } + + return $this->ctrVisitor = new CompileTimeRendererVisitor; + } + + protected function containsOtherComponents(string $template): bool + { + preg_match_all( + '/<([a-zA-Z0-9_]+)([-:])[a-zA-Z0-9_\-:]*(?:\s[^>]*)?>/', + $template, + $matches, + PREG_SET_ORDER + ); + + if (! $matches) { + return false; + } + + foreach ($matches as $match) { + if (! in_array(mb_strtolower($match[1]), $this->componentNamespaces)) { + return true; + } + } + + return false; + } + + protected function checkForCtrEligibility(string $originalTemplate, string $compiledTemplate): void + { + if (! $this->activeComponent->options->allowOptimizations) { + $this->activeComponent->isCtrEligible = false; + + return; + } + + if ($this->containsOtherComponents($originalTemplate)) { + $this->activeComponent->isCtrEligible = false; + + return; + } + + $traverser = new NodeTraverser; + + $ast = PhpParser::makeParser()->parse($compiledTemplate); + $parentingVisitor = new ParentConnectingVisitor; + $traverser->addVisitor($parentingVisitor); + $traverser->traverse($ast); + + $traverser->removeVisitor($parentingVisitor); + + $ctrVisitor = $this->getCtrVisitor() + ->reset() + ->setComponentState($this->activeComponent); + + $traverser->addVisitor($ctrVisitor); + $traverser->traverse($ast); + + $this->activeComponent->isCtrEligible = $ctrVisitor->isEligibleForCtr(); + } +} diff --git a/src/Compiler/Ctr/CompileTimeRendererVisitor.php b/src/Compiler/Ctr/CompileTimeRendererVisitor.php new file mode 100644 index 0000000..8b16125 --- /dev/null +++ b/src/Compiler/Ctr/CompileTimeRendererVisitor.php @@ -0,0 +1,242 @@ +isCtrEligible = true; + + return $this; + } + + public function setUnsafeVariableNames(array $unsafeVariableNames): static + { + $this->unsafeVariableNames = $unsafeVariableNames; + + return $this; + } + + public function setUnsafeFunctionCalls(array $unsafeFunctionCalls): static + { + $this->unsafeFunctionCalls = $unsafeFunctionCalls; + + return $this; + } + + public function setAppAliases(array $aliases): static + { + $this->appAliases = $aliases; + + return $this; + } + + public function setComponentState(ComponentState $componentState): self + { + $this->componentState = $componentState; + + return $this; + } + + protected function isComponentVar($node): bool + { + if (! $node instanceof Node\Expr\Variable) { + return false; + } + + return $node->name == $this->componentState->getVariableName(); + } + + public function enterNode(Node $node) + { + if ($node instanceof Node\Expr\PropertyFetch) { + if (! $this->isComponentVar($node->var)) { + return; + } + + $name = $node->name->toString(); + + if (in_array($name, $this->restrictedComponentProperties)) { + $this->isCtrEligible = false; + + return; + } + } elseif ($node instanceof Node\Expr\MethodCall) { + if (! $this->isComponentVar($node->var)) { + return; + } + + if ($node->name->toString() === 'parent') { + $this->isCtrEligible = false; + } + } elseif ($node instanceof Node\Expr\FuncCall) { + if (! $node->name instanceof Node\Name) { + return; + } + + $name = $node->name->toString(); + + if (in_array($name, $this->unsafeFunctionCalls)) { + $this->isCtrEligible = false; + + return; + } + + if (in_array($name, $this->nonCtrMethodNames)) { + $this->isCtrEligible = false; + } + } elseif ($node instanceof Node\Expr\Variable && $this->isUnsafeVariable($node->name)) { + $this->isCtrEligible = false; + + return; + } elseif ($node instanceof Node\Expr\StaticCall) { + $name = $this->getStaticCallName($node); + + if (! $name) { + $this->isCtrEligible = false; + + return; + } + + if (! class_exists($name)) { + // The class may be available at runtime; disable CTR. + $this->isCtrEligible = false; + + return; + } + + if (in_array($name, $this->allowedFrameworkClasses)) { + return; + } + + $methodName = $node->name->toString(); + $reflectionClass = new ReflectionClass($name); + + if (! $reflectionClass->hasMethod($methodName)) { + $this->isCtrEligible = false; + + return; + } + + $this->isCtrEligible = $this->isCtrAllowed($reflectionClass, $reflectionClass->getMethod($methodName)); + } elseif (in_array(get_class($node), $this->disabledExpressions)) { + $this->isCtrEligible = false; + } + } + + protected function isCtrAllowed(ReflectionClass $class, ReflectionMethod $method): bool + { + /** @var \ReflectionAttribute $methodCtrAttribute */ + if ($methodCtrAttribute = $this->getCtrAttribute($method)) { + return $methodCtrAttribute->getName() == EnableOptimization::class; + } + + if ($this->getCtrAttribute($class)?->getName() == EnableOptimization::class) { + return true; + } + + return false; + } + + protected function getCtrAttribute($reflectedObject): ?ReflectionAttribute + { + $ctrAllowed = $reflectedObject->getAttributes(EnableOptimization::class); + + if (! empty($ctrAllowed)) { + return $ctrAllowed[0]; + } + + $ctrDisabled = $reflectedObject->getAttributes(DisableOptimization::class); + + if (! empty($ctrDisabled)) { + return $ctrDisabled[0]; + } + + return null; + } + + protected function isUnsafeVariable(string $name): bool + { + $prefixed = '$'.$name; + + return in_array($name, $this->unsafeVariableNames) || in_array($prefixed, $this->unsafeVariableNames); + } + + protected function getStaticCallName(Node\Expr\StaticCall $call): string + { + $name = ''; + + if ($call->class instanceof Node\Name\FullyQualified) { + $name = $call->class->toString(); + } elseif ($call->class instanceof Node\Name) { + $name = $call->class->toString(); + + $name = $this->appAliases[$name] ?? $name; + } + + return $name; + } + + public function isEligibleForCtr(): bool + { + return $this->isCtrEligible; + } + + public function beforeTraverse(array $nodes) {} + + public function leaveNode(Node $node) {} + + public function afterTraverse(array $nodes) {} +} diff --git a/src/Compiler/DisableOptimization.php b/src/Compiler/DisableOptimization.php new file mode 100644 index 0000000..bf82824 --- /dev/null +++ b/src/Compiler/DisableOptimization.php @@ -0,0 +1,8 @@ +compiler = $compiler; + $this->bladeCompiler = $bladeCompiler; + } + + public function reset(): void + { + $this->pendingProps = []; + } + + public function clear(): void + { + $this->reset(); + + $this->disabledComponents = []; + } + + protected function containsDynamicAttributes(ComponentNode $component): bool + { + foreach ($component->parameters as $param) { + if ( + $param->type == ParameterType::DynamicVariable || + $param->type == ParameterType::ShorthandDynamicVariable || + $param->type == ParameterType::AttributeEcho || + $param->type == ParameterType::AttributeRawEcho || + $param->type == ParameterType::AttributeTripleEcho || + $param->type == ParameterType::UnknownEcho || + $param->type == ParameterType::UnknownRawEcho || + $param->type == ParameterType::UnknownTripleEcho + ) { + return true; + } + } + + return false; + } + + protected function attributesSatisfyProps(ComponentState $component, array $propNames): bool + { + $attributeNames = collect($component->node->parameters) + ->map(fn (ParameterNode $param) => $param->materializedName) + ->unique() + ->flip() + ->all(); + + $defaultProps = array_flip(array_keys($component->getPropDefaults())); + + foreach ($propNames as $propName) { + if (isset($this->pendingProps[$propName])) { + // We can satisfy this one now. Let's clear it out. + unset($this->pendingProps[$propName]); + } + + if (isset($attributeNames[$propName])) { + continue; + } + + if (isset($defaultProps[$propName])) { + continue; + } + + $this->pendingProps[$propName] = true; + } + + if (! empty($this->pendingProps)) { + return false; + } + + return true; + } + + protected function canRenderSlots(ComponentState $componentState): bool + { + if ($componentState->extractions === null) { + return true; + } + + if ($componentState->extractions->content != '') { + return false; + } + + if (! empty($componentState->extractions->forwardedSlots)) { + return false; + } + + if (! empty($componentState->extractions->namedSlots)) { + return false; + } + + return true; + } + + public function canRender(ComponentState $componentState): bool + { + if (isset($this->disabledComponents[$componentState->componentPath])) { + return false; + } + + if (! $componentState->isCtrEligible) { + // The parsers have determined that this component + // is not eligible for compile time rendering. + // This is typically due to either nested + // components also not being eligible. + $this->compiler->disableCompileTimeRenderOnStack(); + + return false; + } + + if ($componentState->shouldCache) { + // If a developer has explicitly indicated + // the cache is enabled, assume they are + // doing things the way they want to + $this->compiler->disableCompileTimeRenderOnStack(); + + return false; + } + + if ($componentState->hasMixins()) { + // Mixins may contain data that is resolved + // only at runtime. Because of this, we + // cannot assume it is safe to CTR. + $this->compiler->disableCompileTimeRenderOnStack(); + + return false; + } + + if (! empty($componentState->cacheReplacements)) { + // The existence of cache replacements are + // a good indicator of dynamic behavior + $this->compiler->disableCompileTimeRenderOnStack(); + + return false; + } + + if ($this->compiler->hasBoundScopeVariables()) { + // We won't disable CTR on the stack at this time. + // It is possible that parents can satisfy the + // compile time rendering requirements... + return false; + } + + if (! $this->canRenderSlots($componentState)) { + // Slots may be evaluated in an unresolvable state + // at compile time, therefore we disable CTR. + $this->compiler->disableCompileTimeRenderOnStack(); + + return false; + } + + if ($this->containsDynamicAttributes($componentState->node)) { + $this->compiler->disableCompileTimeRenderOnStack(); + + return false; + } + + if (empty($componentState->getPropNames()) && empty($componentState->getAwareVariables())) { + return true; + } + + if ($this->attributesSatisfyProps($componentState, $componentState->getAllPropNames())) { + return true; + } + + return false; + } + + protected function normalizeTemplate(ComponentState $componentState, string $template): string + { + $replacements = [ + $componentState->varSuffix => '_ctrVarSuffix', + ]; + + foreach ($componentState->getDynamicVariables() as $varName => $value) { + $replacements[$value] = '__ctrRep'.$varName; + } + + return Str::swap($replacements, $template); + } + + protected function getTemporaryPath(): string + { + return $this->compiler->getOptions()->viewCachePath.Utils::makeRandomString().'.php'; + } + + protected function renderFile(string $path) + { + $__env = view(); + + return (static function () use ($path, $__env) { + return require $path; + })(); + } + + protected function reduceAttributes(ComponentNode $component): array + { + // If we are calling this method, we should know + // that the attributes/props supplied to the + // component are all static. It is safe + // to convert simplify to key/value + return collect($component->parameters) + ->sortBy(fn (ParameterNode $param) => $param->materializedName) + ->mapWithKeys(fn (ParameterNode $param) => [$param->materializedName => $param->value]) + ->all(); + } + + /** + * @throws CompilerRenderException + */ + public function render(ComponentState $componentState, string $template): string + { + $template = $this->normalizeTemplate($componentState, $template); + + $key = implode('', [ + md5($template), + AttributeCache::getAttributeCacheKey($this->reduceAttributes($componentState->node)), + ]); + + if (isset($this->renderCache[$key])) { + return $this->renderCache[$key]; + } + + $obLevel = ob_get_level(); + $tmpPath = $this->getTemporaryPath(); + try { + file_put_contents($tmpPath, $template); + + ob_start(); + $this->renderFile($tmpPath); + $results = ob_get_clean(); + + return $this->renderCache[$key] = ltrim($results); + } catch (Throwable) { + if ($componentState->componentPath != '') { + $this->disabledComponents[$componentState->componentPath] = true; + } + + while (ob_get_level() > $obLevel) { + ob_end_clean(); + } + + throw new CompilerRenderException; + } finally { + @unlink($tmpPath); + } + } +} diff --git a/src/Compiler/StaticTemplateCompiler.php b/src/Compiler/StaticTemplateCompiler.php deleted file mode 100644 index be5fda0..0000000 --- a/src/Compiler/StaticTemplateCompiler.php +++ /dev/null @@ -1,71 +0,0 @@ -phpParser = PhpParser::makeParser(); - $this->printer = new Standard; - } - - public function compile(string $template): string - { - return $this->printPhpFile($this->phpParser->parse($template)); - } - - public function isStaticTemplate(string $template, array $placeholders = []): bool - { - if (Str::contains($template, $placeholders)) { - return false; - } - - foreach (token_get_all(Blade::compileString($template)) as $token) { - if (is_array($token) && in_array($token[0], $this->phpOpeningTokens, true)) { - return false; - } - } - - return true; - } - - protected function compileStaticComponentTemplate(ComponentState $component, string $template): string - { - if ($component->getTrimOutput()) { - $template = trim($template); - } - - return $template; - } - - public function testTemplate(ComponentState $component, string $template, array $placeholders = []): array - { - $compiled = $this->compile($template); - $isStatic = $this->isStaticTemplate($compiled, $placeholders); - - if ($isStatic) { - $compiled = $this->compileStaticComponentTemplate($component, $compiled); - } - - return [ - $isStatic, - $isStatic ? $compiled : $template, - ]; - } -} diff --git a/src/Compiler/TemplateCompiler.php b/src/Compiler/TemplateCompiler.php index 276fe38..a48b3a5 100644 --- a/src/Compiler/TemplateCompiler.php +++ b/src/Compiler/TemplateCompiler.php @@ -23,8 +23,10 @@ use Stillat\Dagger\Compiler\Concerns\CompilesPhp; use Stillat\Dagger\Compiler\Concerns\CompilesSlots; use Stillat\Dagger\Compiler\Concerns\CompilesStencils; +use Stillat\Dagger\Compiler\Concerns\ManagesComponentCtrState; use Stillat\Dagger\Compiler\Concerns\ManagesExceptions; use Stillat\Dagger\Exceptions\CompilerException; +use Stillat\Dagger\Exceptions\CompilerRenderException; use Stillat\Dagger\Exceptions\ComponentException; use Stillat\Dagger\Exceptions\InvalidCompilerParameterException; use Stillat\Dagger\Exceptions\Mapping\LineMapper; @@ -34,6 +36,7 @@ use Stillat\Dagger\Parser\ComponentParser; use Stillat\Dagger\Runtime\ViewManifest; use Stillat\Dagger\Support\Utils; +use Throwable; final class TemplateCompiler { @@ -49,6 +52,7 @@ final class TemplateCompiler CompilesPhp, CompilesSlots, CompilesStencils, + ManagesComponentCtrState, ManagesExceptions; protected array $compilerDirectiveParams = [ @@ -96,18 +100,22 @@ final class TemplateCompiler protected ReplacementManager $replacementManager; - protected StaticTemplateCompiler $staticTemplateCompiler; - protected LineMapper $lineMapper; protected ?ComponentState $activeComponent = null; protected CompilerOptions $options; + protected Renderer $renderer; + protected ?string $currentViewName = null; protected ?string $currentCachePath = null; + protected bool $enabled = true; + + protected bool $ctrEnabled = true; + public function __construct(ViewManifest $manifest, Factory $factory, LineMapper $lineMapper, string $cachePath) { $this->options = new CompilerOptions; @@ -121,12 +129,12 @@ public function __construct(ViewManifest $manifest, Factory $factory, LineMapper $this->storeRawBlockProxy->setAccessible(true); $this->printer = new Standard; - $this->staticTemplateCompiler = new StaticTemplateCompiler; $this->replacementManager = new ReplacementManager; $this->componentCompiler = new ComponentCompiler; $this->componentParser = new ComponentParser(new ComponentCache); $this->compiler = Blade::getFacadeRoot(); $this->attributeCompiler = new AttributeCompiler; + $this->renderer = new Renderer($this, $this->compiler); } public function getOptions(): CompilerOptions @@ -271,9 +279,24 @@ protected function stopCompilingComponent(): void if (count($this->componentStack) > 0) { $this->activeComponent = $this->componentStack[array_key_last($this->componentStack)]; + } else { + $this->cleanupAfterComponentStack(); } } + protected function cleanupAfterComponentStack(): void + { + $this->renderer->reset(); + } + + /** + * @internal + */ + public function disableCompileTimeRenderOnStack(): void + { + $this->ctrEnabled = false; + } + protected function compileBoundScopeVariables(): string { $depVars = ''; @@ -288,6 +311,21 @@ protected function compileBoundScopeVariables(): string return $depVars; } + /** + * @internal + */ + public function hasBoundScopeVariables(): bool + { + /** @var ComponentState $componentState */ + foreach ($this->componentStack as $componentState) { + if (! empty($componentState->boundScopeVariables)) { + return true; + } + } + + return false; + } + protected function incrementCompilerDepth(): void { $this->compilerDepth += 1; @@ -415,13 +453,13 @@ protected function compileNodes(array $nodes): string $slotContainerVar = '__slotContainer'.$varSuffix; - $extractions = $this->extractDetails($node); - $innerContent = $extractions->content; + $this->activeComponent->extractions = $this->extractDetails($node); + $innerContent = $this->activeComponent->extractions->content; - $compiledSlots = $this->compileForwardedSlots($extractions->forwardedSlots); + $compiledSlots = $this->compileForwardedSlots($this->activeComponent->extractions->forwardedSlots); $compiledSlots .= $this->compileSlotContent($innerContent, $slotContainerVar); - $compiledSlots .= $this->compileNamedSlots($extractions->namedSlots, $slotContainerVar); + $compiledSlots .= $this->compileNamedSlots($this->activeComponent->extractions->namedSlots, $slotContainerVar); if ($this->activeComponent->hasUserSuppliedId) { $compiledSlots .= $this->forwardSlots($slotContainerVar); @@ -432,22 +470,7 @@ protected function compileNodes(array $nodes): string } $innerTemplate = $componentModel->getTemplate(); - - [$isStaticTemplate, $staticTemplate] = $this->staticTemplateCompiler->testTemplate( - $this->activeComponent, - $innerTemplate, - $this->getDynamicStrings() - ); - - if ($isStaticTemplate) { - $this->stopCompilingComponent(); - - $compiled .= $staticTemplate; - - continue; - } - - $innerTemplate = $this->compileStencils($componentModel, $extractions, $innerTemplate); + $innerTemplate = $this->compileStencils($componentModel, $this->activeComponent->extractions, $innerTemplate); $compiledComponentParams = $this->attributeCompiler->compile( $this->filterComponentParams($node->parameters ?? []), @@ -533,6 +556,12 @@ protected function compileNodes(array $nodes): string $innerContent = $this->componentCompiler->compile( $this->compiler->compileString($this->compile($innerTemplate)) ); + + try { + $this->checkForCtrEligibility($innerTemplate, $innerContent); + } catch (Throwable) { + $this->activeComponent->isCtrEligible = false; + } } catch (Error $error) { throw SourceMapper::convertParserError($error, $innerTemplate, $sourcePath, $componentModel->lineOffset); } @@ -552,10 +581,7 @@ protected function compileNodes(array $nodes): string $compiledComponentTemplate = $this->compileCompilerDirectives($compiledComponentTemplate); - $propNames = array_flip(array_merge( - $componentModel->getPropNames(), - $componentModel->getAwareVariables(), - )); + $propNames = array_flip($componentModel->getAllPropNames()); $swapVars = [ '#cachePath#' => $cachePath ?? '', @@ -576,9 +602,24 @@ protected function compileNodes(array $nodes): string ]; $compiledComponentTemplate = Str::swap($swapVars, $compiledComponentTemplate); - $compiledComponentTemplate = $this->compileExceptions($compiledComponentTemplate); - $compiled .= $this->finalizeCompiledComponent($compiledComponentTemplate); + if (! $this->ctrEnabled || ! $this->renderer->canRender($this->activeComponent)) { + $compiled .= $this->finalizeCompiledComponent($compiledComponentTemplate); + } else { + $this->enabled = false; + try { + $renderedResult = $this->renderer->render( + $this->activeComponent, + $this->resolveBlocks($compiledComponentTemplate) + ); + + $compiled .= $this->storeComponentBlock($renderedResult); + } catch (CompilerRenderException) { + $compiled .= $this->finalizeCompiledComponent($compiledComponentTemplate); + } finally { + $this->enabled = true; + } + } $this->stopCompilingComponent(); } @@ -594,6 +635,8 @@ protected function compileNodes(array $nodes): string protected function finalizeCompiledComponent(string $compiled): string { + $compiled = $this->compileExceptions($compiled); + if ($this->activeComponent->cacheProperties != null) { $compiled = $this->compileCache($compiled); } @@ -604,8 +647,10 @@ protected function finalizeCompiledComponent(string $compiled): string public function cleanup(): void { $this->componentParser->getComponentCache()->clear(); - $this->componentBlocks = []; + $this->renderer->clear(); $this->replacementManager->clear(); + + $this->componentBlocks = []; } protected function compileVariableCleanup(array $variables): string @@ -622,6 +667,7 @@ protected function compileVariableUnset(string $variableName): string protected function resetCompilerState(): void { + $this->ctrEnabled = true; $this->compilerDepth = 0; } @@ -644,6 +690,10 @@ public function compileWithLineNumbers(string $template): string */ public function compile(string $template): string { + if (! $this->enabled) { + return $template; + } + $this->newlineStyle = Utils::getNewlineStyle($template); if ($this->isRoot()) { diff --git a/src/ComponentOptions.php b/src/ComponentOptions.php new file mode 100644 index 0000000..ebef1b9 --- /dev/null +++ b/src/ComponentOptions.php @@ -0,0 +1,8 @@ +options ?? new ComponentOptions; + } + public function validateProps(array|string $props, array|string $messages = []): static { $this->assertIsString($props, 'Supplied "props" variables must be a string.'); @@ -69,6 +77,14 @@ public function aware(array|string $aware): static return $this; } + public function compiler(?bool $allowOptimizations = null): static + { + $this->options = new ComponentOptions; + $this->options->allowOptimizations = $allowOptimizations ?? $this->options->allowOptimizations; + + return $this; + } + public function trimOutput(): static { $this->trimOutput = true; diff --git a/src/Parser/ComponentParser.php b/src/Parser/ComponentParser.php index f2ba56d..e052495 100644 --- a/src/Parser/ComponentParser.php +++ b/src/Parser/ComponentParser.php @@ -179,6 +179,7 @@ public function parse(?ComponentNode $component, string $template, string $varSu $model = $this->evaluateComponentModel($this->printer->prettyPrintFile($componentModelAst)); $innerTemplate = $this->printer->prettyPrintFile($newAst); + $componentState->options = $model->getComponentOptions(); $componentState->shouldCache = $model->getShouldCache(); $componentState->trimOutput = $model->getTrimOutput(); diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 0ee7db9..bf33ced 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -41,12 +41,25 @@ public function register() $app['config']['view.compiled'] ); + $ctrConfig = config('dagger.ctr') ?? []; + + $ctrVisitor = $compiler->getCtrVisitor(); + $ctrVisitor->setUnsafeFunctionCalls($ctrConfig['unsafe_functions'] ?? []) + ->setUnsafeVariableNames($ctrConfig['unsafe_variables'] ?? []) + ->setAppAliases(config('app.aliases') ?? []); + return $compiler; }); } public function boot() { + $this->publishes([ + __DIR__.'/../config/dagger.php' => config_path('dagger.php'), + ]); + + $this->mergeConfigFrom(__DIR__.'/../config/dagger.php', 'dagger'); + $this->loadTranslationsFrom(__DIR__.'/../lang', 'dagger'); if ($this->app->runningInConsole()) { diff --git a/tests/Compiler/CompleTimeRenderingTest.php b/tests/Compiler/CompleTimeRenderingTest.php new file mode 100644 index 0000000..45aa1d9 --- /dev/null +++ b/tests/Compiler/CompleTimeRenderingTest.php @@ -0,0 +1,427 @@ +assertSame( + '', + trim($this->compile('')) + ); +}); + +test('rendered and compile time contents are equivalent', function () { + $template = <<<'BLADE' + +BLADE; + + $expected = <<<'EXPECTED' +
+ The Title +
+EXPECTED; + + $this->assertSame( + $expected, + trim($this->render($template)) + ); + + $this->assertSame( + $expected, + trim($this->compile($template)) + ); +}); + +test('nested components can be compile time rendered', function () { + $expected = <<<'EXPECTED' +Simple Parent Start + +Child: The Title + +Child: The Other Title + +Simple Parent End +EXPECTED; + + $this->assertSame( + $expected, + $this->compile('') + ); + + $this->assertSame( + $expected, + $this->render('') + ); +}); + +test('aware props for nested children can be inferred at compile time', function () { + // The parent "title" prop being defined should satisfy the nested ctr.child_aware value. + $this->assertSame( + 'Child: The Title', + $this->compile('') + ); + + // No title prop available. Compiler should not render at compile time. + $this->assertNotSame( + 'Child: The Title', + $this->compile('') + ); +}); + +test('props that are not satisfied at compile time disable ctr', function () { + $this->assertStringNotContainsString( + 'Title: The Title', + $this->compile('') + ); + + $this->assertStringContainsString( + 'Title: The Title', + $this->render('') + ); +}); + +test('components using parent instance method are not rendered', function () { + $this->assertStringNotContainsString( + 'Title: The Title', + $this->compile('') + ); + + $this->assertStringContainsString( + 'Title: The Title', + $this->render('') + ); +}); + +test('components using parent instance variable are not compile time rendered', function () { + $this->assertStringNotContainsString( + 'Title: The Title', + $this->compile('') + ); + + $this->assertStringContainsString( + 'Title: The Title', + $this->render('') + ); +}); + +test('components using parent utility function are not compile time rendered', function () { + $this->assertStringNotContainsString( + 'Title: The Title', + $this->compile('') + ); + + $this->assertStringContainsString( + 'Title: The Title', + $this->render('') + ); +}); + +test('components using component utility function are not compile time rendered', function () { + $this->assertStringNotContainsString( + 'Title: The Title', + $this->compile('') + ); + + $this->assertStringContainsString( + 'Title: The Title', + $this->render('') + ); +}); + +test('components using current utility function are not compile time rendered', function () { + $this->assertStringNotContainsString( + 'Title: The Title', + $this->compile('') + ); + + $this->assertStringContainsString( + 'Title: The Title', + $this->render('') + ); +}); + +test('components using render utility function are not compile time rendered', function () { + $this->assertStringNotContainsString( + 'Title: The Title', + $this->compile('') + ); + + $this->assertStringContainsString( + 'Title: The Title', + $this->render('') + ); +}); + +test('components called with slots are not eligible for ctr', function () { + $this->assertStringNotContainsString( + 'With Slot: Slot Contents', + $this->compile('Slot Contents') + ); + + $this->assertStringContainsString( + 'With Slot: Slot Contents', + $this->render('Slot Contents') + ); +}); + +test('components that trigger exceptions disable ctr for that component', function () { + $this->assertStringContainsString( + 'echo e($title)', + $this->compile('') + ); +}); + +test('compiler rendering can escape Blade', function () { + $expected = <<<'EXPECTED' + + +The Title: The Title +{{ $title }} +{!! $title !!} +@if + Yes +EXPECTED; + + $this->assertSame( + $expected, + $this->compile('') + ); +}); + +test('nested compile-time rendered templates do not double escape escaped Blade', function () { + $expected = <<<'EXPECTED' +{{ $escapedInParent }} + + +The Title: The First Title +{{ $title }} +{!! $title !!} +@if + Yes + + +The Title: The Second Title +{{ $title }} +{!! $title !!} +@if + Yes +EXPECTED; + + $this->assertSame( + $expected, + $this->compile('') + ); +}); + +test('compile time rendering respects trim output', function () { + $this->assertSame( + 'The Title: The Title', + $this->compile('') + ); +}); + +test('compile time rendering can be disabled via compiler options', function () { + $compiled = $this->compile(''); + $rendered = $this->render(''); + + $this->assertNotEquals('The Title', $compiled); + $this->assertEquals('The Title', $rendered); +}); + +test('compile time rendering skips components with "unsafe" function calls', function () { + $compiled = $this->compile(''); + + $this->assertStringContainsString('e(time());', $compiled); + $this->assertStringContainsString('e(now());', $compiled); + $this->assertStringContainsString("e(date('l'));", $compiled); +}); + +test('compile time rendering skips components with "unsafe" variables', function () { + $expected = <<<'EXPECTED' +echo e($title . $_GET['something']); +EXPECTED; + + $this->assertStringContainsString( + $expected, + $this->compile(''), + ); +}); + +test('compile time rendering with static methods', function () { + $this->assertSame( + 'THE TITLE', + $this->compile(''), + ); +}); + +test('compile time rendering can be disabled on a class and re-enabled on a specific method', function () { + $template = ''; + $expected = 'Hello, world.'; + + $this->assertSame($expected, $this->render($template)); + $this->assertSame($expected, $this->compile($template)); +}); + +test('compile time rendering can be disabled on a class', function () { + $template = ''; + + $this->assertStringContainsString( + 'echo e(\Stillat\Dagger\Tests\CtrDisabledClass::methodOne());', + $this->compile($template), + ); + + $this->assertSame('Hello, world.', $this->render($template)); +}); + +test('compile time rendering can be enabled on a class', function () { + $template = ''; + $expected = 'Hello, world.'; + + $this->assertSame($expected, $this->render($template)); + $this->assertSame($expected, $this->compile($template)); +}); + +test('compile time rendering can be enabled on a class and disabled on a specific method', function () { + $template = ''; + + $this->assertStringContainsString( + 'echo e(\Stillat\Dagger\Tests\CtrEnabledClass::methodTwo());', + $this->compile($template), + ); + + $this->assertSame('Hello, world.', $this->render($template)); +}); + +test('satisfied roots do not precompile if nested components are not satisfied', function () { + $expected = <<<'EXPECTED' +Root: The Title + + +Child One: Child One + + + +Child Two: Child One to child two +EXPECTED; + + $template = <<<'BLADE' + +BLADE; + + $compiled = $this->compile($template); + + $this->assertStringContainsString('echo e($title)', $compiled); + + $this->assertNotSame($expected, $compiled); + + $this->assertSame($expected, trim($this->render($template))); +}); + +test('Laravel Blade component prefix disables compile time rendering', function () { + $this->assertStringContainsString( + "echo e('The String');", + $this->compile('') + ); +}); + +test('Flux component prefix disables compile time rendering', function () { + $this->assertStringContainsString( + "echo e('The String');", + $this->compile('') + ); +}); + +test('Livewire component prefix disables compile time rendering', function () { + $this->assertStringContainsString( + "echo e('The String');", + $this->compile('') + ); +}); + +test('Other component prefix disables compile time rendering', function () { + $this->assertStringContainsString( + "echo e('The String');", + $this->compile('') + ); +}); + +test('var disables compile time rendering', function ($varName) { + $this->assertStringContainsString( + "echo e('The String');", + $this->compile('') + ); +})->with([ + 'argc', 'argv', 'cookie', 'env', 'files', 'get', 'post', 'request', 'session', +]); + +test('function disables compile time rendering', function ($functionName) { + $this->assertStringContainsString( + "echo e('The String');", + $this->compile('') + ); +})->with([ + 'now', 'time', 'date', 'env', 'getenv', 'cookie', + 'request', 'session', 'dd', 'dump', 'var_dump', + 'debug_backtrace', 'phpinfo', 'include', 'include_once', + 'require', 'require_once', 'eval', 'extract', + 'get_defined_vars', 'parse_str', 'abort', 'abort_if', 'abort_unless', +]); + +test('exit disables compile time rendering', function () { + $this->assertStringContainsString( + "echo e('The String');", + $this->compile('') + ); +}); + +test('stencils can disable compile time rendering', function () { + $template = <<<'BLADE' + + + + {{ $theVar }} + + +BLADE; + + $expected = <<<'EXPECTED' +Before + Value +After +EXPECTED; + + $this->assertSame($expected, $this->compile($template)); + $this->assertSame($expected, $this->render($template)); + + $template = <<<'BLADE' + + + + {{ $theVar }} + + +BLADE; + + $compiled = $this->compile($template); + + $this->assertStringContainsString( + 'echo e($theVar);', + $compiled, + ); + + $this->assertNotSame($expected, $compiled); + $this->assertSame($expected, $this->render($template)); +}); diff --git a/tests/Compiler/DocsTest.php b/tests/Compiler/DocsTest.php index d37e2cb..23e063d 100644 --- a/tests/Compiler/DocsTest.php +++ b/tests/Compiler/DocsTest.php @@ -259,7 +259,9 @@ $expected = <<<'EXPECTED'
-
+ + + EXPECTED; $this->assertSame( @@ -538,3 +540,32 @@ $this->render($template) ); }); + +test('ctr alert example', function () { + $template = <<<'BLADE' + +BLADE; + + $expected = <<<'EXPECTED' +
+ The awesome message +
+EXPECTED; + + $this->assertSame($expected, $this->compile($template)); + $this->assertSame($expected, $this->render($template)); + + $template = <<<'BLADE' + +BLADE; + + $this->assertNotSame( + $expected, + $this->compile($template), + ); + + $this->assertSame( + $expected, + $this->render($template, ['message' => 'The awesome message']) + ); +}); diff --git a/tests/Compiler/DynamicComponentsTest.php b/tests/Compiler/DynamicComponentsTest.php index cf877ce..df554f5 100644 --- a/tests/Compiler/DynamicComponentsTest.php +++ b/tests/Compiler/DynamicComponentsTest.php @@ -8,12 +8,12 @@ test('it renders dynamic components', function () { $template = <<<'BLADE' + BLADE; $expected = <<<'EXPECTED' Component A: The Title - Component B: The Title EXPECTED; @@ -26,12 +26,12 @@ test('it renders dynamic components with proxy alias', function () { $template = <<<'BLADE' + BLADE; $expected = <<<'EXPECTED' Component A: The Title - Component B: The Title EXPECTED; @@ -44,12 +44,12 @@ test('it renders dynamic components with delegate-component alias', function () { $template = <<<'BLADE' + BLADE; $expected = <<<'EXPECTED' Component A: The Title - Component B: The Title EXPECTED; diff --git a/tests/CtrDisabledClass.php b/tests/CtrDisabledClass.php new file mode 100644 index 0000000..5689a99 --- /dev/null +++ b/tests/CtrDisabledClass.php @@ -0,0 +1,21 @@ + The Title - - Another Title EXPECTED; @@ -128,6 +126,7 @@ Before The Default. After + Before Some changed content #1
@@ -138,10 +137,8 @@ Before The Default. After + EXPECTED; - $this->assertSame( - $expected, - $this->render($template) - ); + $this->assertSame($expected, $this->render($template)); }); diff --git a/tests/resources/components/alert.blade.php b/tests/resources/components/alert.blade.php new file mode 100644 index 0000000..40f6dd8 --- /dev/null +++ b/tests/resources/components/alert.blade.php @@ -0,0 +1,5 @@ +@props(['type' => 'info', 'message']) + +
merge(['class' => 'alert alert-'.$type]) }}> + {{ $message }} +
\ No newline at end of file diff --git a/tests/resources/components/ctr/button.blade.php b/tests/resources/components/ctr/button.blade.php new file mode 100644 index 0000000..68dbb17 --- /dev/null +++ b/tests/resources/components/ctr/button.blade.php @@ -0,0 +1,3 @@ +@props(['text']) + + \ No newline at end of file diff --git a/tests/resources/components/ctr/child_aware.blade.php b/tests/resources/components/ctr/child_aware.blade.php new file mode 100644 index 0000000..b267270 --- /dev/null +++ b/tests/resources/components/ctr/child_aware.blade.php @@ -0,0 +1,3 @@ +@aware(['title']) + +Child: {{ $title }} \ No newline at end of file diff --git a/tests/resources/components/ctr/child_one.blade.php b/tests/resources/components/ctr/child_one.blade.php new file mode 100644 index 0000000..adef300 --- /dev/null +++ b/tests/resources/components/ctr/child_one.blade.php @@ -0,0 +1,5 @@ +@props(['title']) + +Child One: {{ $title }} + + \ No newline at end of file diff --git a/tests/resources/components/ctr/child_simple.blade.php b/tests/resources/components/ctr/child_simple.blade.php new file mode 100644 index 0000000..52a3962 --- /dev/null +++ b/tests/resources/components/ctr/child_simple.blade.php @@ -0,0 +1,3 @@ +@props(['title']) + +Child: {{ $title }} \ No newline at end of file diff --git a/tests/resources/components/ctr/child_two.blade.php b/tests/resources/components/ctr/child_two.blade.php new file mode 100644 index 0000000..afc8b33 --- /dev/null +++ b/tests/resources/components/ctr/child_two.blade.php @@ -0,0 +1,3 @@ +@props(['title']) + +Child Two: {{ $title }} \ No newline at end of file diff --git a/tests/resources/components/ctr/child_unsatisfiable.blade.php b/tests/resources/components/ctr/child_unsatisfiable.blade.php new file mode 100644 index 0000000..cfdd0e8 --- /dev/null +++ b/tests/resources/components/ctr/child_unsatisfiable.blade.php @@ -0,0 +1,4 @@ +@aware(['title', 'message']) + +Title: {{ $title }} +Message: {{ $message ?? 'Nope' }} \ No newline at end of file diff --git a/tests/resources/components/ctr/component_utility_function.blade.php b/tests/resources/components/ctr/component_utility_function.blade.php new file mode 100644 index 0000000..9de6a0e --- /dev/null +++ b/tests/resources/components/ctr/component_utility_function.blade.php @@ -0,0 +1,8 @@ +@php + use function Stillat\Dagger\{component}; + + component()->props(['title']); +@endphp + +Title: {{ $title }} +Parent: {{ component()->parent()?->name ?? '' }} \ No newline at end of file diff --git a/tests/resources/components/ctr/current_utility_function.blade.php b/tests/resources/components/ctr/current_utility_function.blade.php new file mode 100644 index 0000000..2893f97 --- /dev/null +++ b/tests/resources/components/ctr/current_utility_function.blade.php @@ -0,0 +1,8 @@ +@php + use function Stillat\Dagger\{component, current}; + + component()->props(['title']); +@endphp + +Title: {{ $title }} +Parent: {{ current()->parent()?->name ?? '' }} \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled.blade.php b/tests/resources/components/ctr/disabled.blade.php new file mode 100644 index 0000000..36e9883 --- /dev/null +++ b/tests/resources/components/ctr/disabled.blade.php @@ -0,0 +1,9 @@ +@php + \Stillat\Dagger\component() + ->props(['title']) + ->compiler( + allowOptimizations: false + ); +@endphp + +{{ $title }} \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/exit.blade.php b/tests/resources/components/ctr/disabled/exit.blade.php new file mode 100644 index 0000000..e71bcf5 --- /dev/null +++ b/tests/resources/components/ctr/disabled/exit.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_abort.blade.php b/tests/resources/components/ctr/disabled/func_abort.blade.php new file mode 100644 index 0000000..fb9efd0 --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_abort.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_abort_if.blade.php b/tests/resources/components/ctr/disabled/func_abort_if.blade.php new file mode 100644 index 0000000..462b5da --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_abort_if.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_abort_unless.blade.php b/tests/resources/components/ctr/disabled/func_abort_unless.blade.php new file mode 100644 index 0000000..8cf1dcd --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_abort_unless.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_cookie.blade.php b/tests/resources/components/ctr/disabled/func_cookie.blade.php new file mode 100644 index 0000000..1c085ae --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_cookie.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_date.blade.php b/tests/resources/components/ctr/disabled/func_date.blade.php new file mode 100644 index 0000000..021d2dd --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_date.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_dd.blade.php b/tests/resources/components/ctr/disabled/func_dd.blade.php new file mode 100644 index 0000000..7e765b9 --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_dd.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_debug_backtrace.blade.php b/tests/resources/components/ctr/disabled/func_debug_backtrace.blade.php new file mode 100644 index 0000000..f42e66a --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_debug_backtrace.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_dump.blade.php b/tests/resources/components/ctr/disabled/func_dump.blade.php new file mode 100644 index 0000000..ef69151 --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_dump.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_env.blade.php b/tests/resources/components/ctr/disabled/func_env.blade.php new file mode 100644 index 0000000..7c41d62 --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_env.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_eval.blade.php b/tests/resources/components/ctr/disabled/func_eval.blade.php new file mode 100644 index 0000000..4a59772 --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_eval.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_extract.blade.php b/tests/resources/components/ctr/disabled/func_extract.blade.php new file mode 100644 index 0000000..e89cb7a --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_extract.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_get_defined_vars.blade.php b/tests/resources/components/ctr/disabled/func_get_defined_vars.blade.php new file mode 100644 index 0000000..d46c2ec --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_get_defined_vars.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_getenv.blade.php b/tests/resources/components/ctr/disabled/func_getenv.blade.php new file mode 100644 index 0000000..0879228 --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_getenv.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_include.blade.php b/tests/resources/components/ctr/disabled/func_include.blade.php new file mode 100644 index 0000000..895bdd4 --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_include.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_include_once.blade.php b/tests/resources/components/ctr/disabled/func_include_once.blade.php new file mode 100644 index 0000000..191d273 --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_include_once.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_now.blade.php b/tests/resources/components/ctr/disabled/func_now.blade.php new file mode 100644 index 0000000..9a0592e --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_now.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_parse_str.blade.php b/tests/resources/components/ctr/disabled/func_parse_str.blade.php new file mode 100644 index 0000000..b1f43c0 --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_parse_str.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_phpinfo.blade.php b/tests/resources/components/ctr/disabled/func_phpinfo.blade.php new file mode 100644 index 0000000..fa8dcd9 --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_phpinfo.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_request.blade.php b/tests/resources/components/ctr/disabled/func_request.blade.php new file mode 100644 index 0000000..42e6f62 --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_request.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_require.blade.php b/tests/resources/components/ctr/disabled/func_require.blade.php new file mode 100644 index 0000000..5db4562 --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_require.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_require_once.blade.php b/tests/resources/components/ctr/disabled/func_require_once.blade.php new file mode 100644 index 0000000..86bf1b2 --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_require_once.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_session.blade.php b/tests/resources/components/ctr/disabled/func_session.blade.php new file mode 100644 index 0000000..8e6c264 --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_session.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_time.blade.php b/tests/resources/components/ctr/disabled/func_time.blade.php new file mode 100644 index 0000000..7682b52 --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_time.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/func_var_dump.blade.php b/tests/resources/components/ctr/disabled/func_var_dump.blade.php new file mode 100644 index 0000000..4c59386 --- /dev/null +++ b/tests/resources/components/ctr/disabled/func_var_dump.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/var_argc.blade.php b/tests/resources/components/ctr/disabled/var_argc.blade.php new file mode 100644 index 0000000..5cba657 --- /dev/null +++ b/tests/resources/components/ctr/disabled/var_argc.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} +{{ $_GET['thing'] }} \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/var_argv.blade.php b/tests/resources/components/ctr/disabled/var_argv.blade.php new file mode 100644 index 0000000..a52742e --- /dev/null +++ b/tests/resources/components/ctr/disabled/var_argv.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} +{{ $argv['thing'] }} \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/var_cookie.blade.php b/tests/resources/components/ctr/disabled/var_cookie.blade.php new file mode 100644 index 0000000..899140a --- /dev/null +++ b/tests/resources/components/ctr/disabled/var_cookie.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} +{{ $_COOKIE['thing'] }} \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/var_env.blade.php b/tests/resources/components/ctr/disabled/var_env.blade.php new file mode 100644 index 0000000..1a8404c --- /dev/null +++ b/tests/resources/components/ctr/disabled/var_env.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} +{{ $_ENV['thing'] }} \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/var_files.blade.php b/tests/resources/components/ctr/disabled/var_files.blade.php new file mode 100644 index 0000000..d817e47 --- /dev/null +++ b/tests/resources/components/ctr/disabled/var_files.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} +{{ $_FILES['thing'] }} \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/var_get.blade.php b/tests/resources/components/ctr/disabled/var_get.blade.php new file mode 100644 index 0000000..5cba657 --- /dev/null +++ b/tests/resources/components/ctr/disabled/var_get.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} +{{ $_GET['thing'] }} \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/var_post.blade.php b/tests/resources/components/ctr/disabled/var_post.blade.php new file mode 100644 index 0000000..1917385 --- /dev/null +++ b/tests/resources/components/ctr/disabled/var_post.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} +{{ $_POST['thing'] }} \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/var_request.blade.php b/tests/resources/components/ctr/disabled/var_request.blade.php new file mode 100644 index 0000000..a6e2500 --- /dev/null +++ b/tests/resources/components/ctr/disabled/var_request.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} +{{ $_REQUEST['thing'] }} \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled/var_session.blade.php b/tests/resources/components/ctr/disabled/var_session.blade.php new file mode 100644 index 0000000..d0d49ae --- /dev/null +++ b/tests/resources/components/ctr/disabled/var_session.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} +{{ $_SESSION['thing'] }} \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled_class.blade.php b/tests/resources/components/ctr/disabled_class.blade.php new file mode 100644 index 0000000..f4eb680 --- /dev/null +++ b/tests/resources/components/ctr/disabled_class.blade.php @@ -0,0 +1 @@ +{{ \Stillat\Dagger\Tests\CtrDisabledClass::methodOne() }} \ No newline at end of file diff --git a/tests/resources/components/ctr/disabled_class_enabled_method.blade.php b/tests/resources/components/ctr/disabled_class_enabled_method.blade.php new file mode 100644 index 0000000..38d9712 --- /dev/null +++ b/tests/resources/components/ctr/disabled_class_enabled_method.blade.php @@ -0,0 +1 @@ +{{ \Stillat\Dagger\Tests\CtrDisabledClass::methodTwo() }} \ No newline at end of file diff --git a/tests/resources/components/ctr/enabled_class.blade.php b/tests/resources/components/ctr/enabled_class.blade.php new file mode 100644 index 0000000..5dec025 --- /dev/null +++ b/tests/resources/components/ctr/enabled_class.blade.php @@ -0,0 +1 @@ +{{ \Stillat\Dagger\Tests\CtrEnabledClass::methodOne() }} \ No newline at end of file diff --git a/tests/resources/components/ctr/enabled_class_disabled_method.blade.php b/tests/resources/components/ctr/enabled_class_disabled_method.blade.php new file mode 100644 index 0000000..7a27d9e --- /dev/null +++ b/tests/resources/components/ctr/enabled_class_disabled_method.blade.php @@ -0,0 +1 @@ +{{ \Stillat\Dagger\Tests\CtrEnabledClass::methodTwo() }} \ No newline at end of file diff --git a/tests/resources/components/ctr/escaped.blade.php b/tests/resources/components/ctr/escaped.blade.php new file mode 100644 index 0000000..7518192 --- /dev/null +++ b/tests/resources/components/ctr/escaped.blade.php @@ -0,0 +1,13 @@ +@props(['title']) + + + +The Title: {{ $title }} +@{{ $title }} +@{!! $title !!} +@@if +@if ($title != '') Yes @else no @endif \ No newline at end of file diff --git a/tests/resources/components/ctr/escaped_parent.blade.php b/tests/resources/components/ctr/escaped_parent.blade.php new file mode 100644 index 0000000..9a986a6 --- /dev/null +++ b/tests/resources/components/ctr/escaped_parent.blade.php @@ -0,0 +1,3 @@ +@{{ $escapedInParent }} + + \ No newline at end of file diff --git a/tests/resources/components/ctr/other_prefixes/flux.blade.php b/tests/resources/components/ctr/other_prefixes/flux.blade.php new file mode 100644 index 0000000..0e4e0bf --- /dev/null +++ b/tests/resources/components/ctr/other_prefixes/flux.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/other_prefixes/laravel.blade.php b/tests/resources/components/ctr/other_prefixes/laravel.blade.php new file mode 100644 index 0000000..28d28ea --- /dev/null +++ b/tests/resources/components/ctr/other_prefixes/laravel.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/other_prefixes/livewire.blade.php b/tests/resources/components/ctr/other_prefixes/livewire.blade.php new file mode 100644 index 0000000..8b946d5 --- /dev/null +++ b/tests/resources/components/ctr/other_prefixes/livewire.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/other_prefixes/other.blade.php b/tests/resources/components/ctr/other_prefixes/other.blade.php new file mode 100644 index 0000000..3ac1b28 --- /dev/null +++ b/tests/resources/components/ctr/other_prefixes/other.blade.php @@ -0,0 +1,2 @@ +{{ 'The String' }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/parent_access_instance_method.blade.php b/tests/resources/components/ctr/parent_access_instance_method.blade.php new file mode 100644 index 0000000..904a560 --- /dev/null +++ b/tests/resources/components/ctr/parent_access_instance_method.blade.php @@ -0,0 +1,4 @@ +@props(['title']) + +Title: {{ $title }} +Parent: {{ $component->parent()?->name ?? '' }} \ No newline at end of file diff --git a/tests/resources/components/ctr/parent_access_instance_variable.blade.php b/tests/resources/components/ctr/parent_access_instance_variable.blade.php new file mode 100644 index 0000000..258b8ad --- /dev/null +++ b/tests/resources/components/ctr/parent_access_instance_variable.blade.php @@ -0,0 +1,4 @@ +@props(['title']) + +Title: {{ $title }} +Parent: {{ $component->parent?->name ?? '' }} \ No newline at end of file diff --git a/tests/resources/components/ctr/parent_access_utility_function.blade.php b/tests/resources/components/ctr/parent_access_utility_function.blade.php new file mode 100644 index 0000000..233893e --- /dev/null +++ b/tests/resources/components/ctr/parent_access_utility_function.blade.php @@ -0,0 +1,8 @@ +@php + use function Stillat\Dagger\{component, _parent}; + + component()->props(['title']); +@endphp + +Title: {{ $title }} +Parent: {{ _parent()?->name ?? '' }} \ No newline at end of file diff --git a/tests/resources/components/ctr/parent_props.blade.php b/tests/resources/components/ctr/parent_props.blade.php new file mode 100644 index 0000000..b8d25f5 --- /dev/null +++ b/tests/resources/components/ctr/parent_props.blade.php @@ -0,0 +1,3 @@ +@props(['title']) + + \ No newline at end of file diff --git a/tests/resources/components/ctr/parent_simple.blade.php b/tests/resources/components/ctr/parent_simple.blade.php new file mode 100644 index 0000000..db9d772 --- /dev/null +++ b/tests/resources/components/ctr/parent_simple.blade.php @@ -0,0 +1,7 @@ +Simple Parent Start + + + + + +Simple Parent End diff --git a/tests/resources/components/ctr/parent_unsatisfiable.blade.php b/tests/resources/components/ctr/parent_unsatisfiable.blade.php new file mode 100644 index 0000000..9398433 --- /dev/null +++ b/tests/resources/components/ctr/parent_unsatisfiable.blade.php @@ -0,0 +1,3 @@ +@props(['title']) + + \ No newline at end of file diff --git a/tests/resources/components/ctr/render_utility_function.blade.php b/tests/resources/components/ctr/render_utility_function.blade.php new file mode 100644 index 0000000..519f100 --- /dev/null +++ b/tests/resources/components/ctr/render_utility_function.blade.php @@ -0,0 +1,8 @@ +@php + use function Stillat\Dagger\{component, render}; + + component()->props(['title']); +@endphp + +Title: {{ $title }} +Parent: {{ render(time()) }} \ No newline at end of file diff --git a/tests/resources/components/ctr/root.blade.php b/tests/resources/components/ctr/root.blade.php new file mode 100644 index 0000000..7d77055 --- /dev/null +++ b/tests/resources/components/ctr/root.blade.php @@ -0,0 +1,4 @@ +@props(['title']) + +Root: {{ $title }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/static_methods.blade.php b/tests/resources/components/ctr/static_methods.blade.php new file mode 100644 index 0000000..8485f0b --- /dev/null +++ b/tests/resources/components/ctr/static_methods.blade.php @@ -0,0 +1,3 @@ +@props(['title']) + +{{ Str::upper($title) }} \ No newline at end of file diff --git a/tests/resources/components/ctr/throws_exception.blade.php b/tests/resources/components/ctr/throws_exception.blade.php new file mode 100644 index 0000000..8ca824c --- /dev/null +++ b/tests/resources/components/ctr/throws_exception.blade.php @@ -0,0 +1,4 @@ +@props(['title']) + +Title: {{ $title }} + \ No newline at end of file diff --git a/tests/resources/components/ctr/trimmed_output.blade.php b/tests/resources/components/ctr/trimmed_output.blade.php new file mode 100644 index 0000000..879ed90 --- /dev/null +++ b/tests/resources/components/ctr/trimmed_output.blade.php @@ -0,0 +1,15 @@ +@php + \Stillat\Dagger\component() + ->props(['title']) + ->trimOutput(); +@endphp + +The Title: {{ $title }} + + + + + + + + diff --git a/tests/resources/components/ctr/unsafe_calls.blade.php b/tests/resources/components/ctr/unsafe_calls.blade.php new file mode 100644 index 0000000..a1ca1d3 --- /dev/null +++ b/tests/resources/components/ctr/unsafe_calls.blade.php @@ -0,0 +1,3 @@ +{{ time() }} +{{ now() }} +{{ date('l') }} \ No newline at end of file diff --git a/tests/resources/components/ctr/unsafe_vars.blade.php b/tests/resources/components/ctr/unsafe_vars.blade.php new file mode 100644 index 0000000..8caab66 --- /dev/null +++ b/tests/resources/components/ctr/unsafe_vars.blade.php @@ -0,0 +1,3 @@ +@props(['title']) + +{{ $title.$_GET['something'] }} \ No newline at end of file diff --git a/tests/resources/components/ctr/with_slot.blade.php b/tests/resources/components/ctr/with_slot.blade.php new file mode 100644 index 0000000..434d68c --- /dev/null +++ b/tests/resources/components/ctr/with_slot.blade.php @@ -0,0 +1 @@ +With Slot: {{ $slot }} \ No newline at end of file