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'])
+
+
+```
+
+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'
\ 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