Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions src/BladeRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function getTemporaryCachePath(): string
/**
* Render a Blade template string in isolation by freezing and restoring compiler state.
*/
public function render(ComponentNode $component, ComponentSource $source): string
public function render(ComponentNode $component, string $path): string
{
$temporaryCachePath = $this->getTemporaryCachePath();

Expand Down Expand Up @@ -93,17 +93,21 @@ function ($input) {
]);

$obLevel = ob_get_level();
$hash = Utils::hash($source->path);
$path = $temporaryCachePath . '/' . $hash . '.php';
$hash = Utils::hash($path);
$compiled = $temporaryCachePath . '/' . $hash . '.php';
$fn = '__' . $hash;

$this->manager->startFolding();

try {
if (! file_exists($path)) {
$this->blade->compile($source->path);
if (! file_exists($compiled)) {
$this->blade->compile($path);
}

$awareData = Arr::mapWithKeys($component->parentsAttributes, function (Attribute $attribute) {
return [$attribute->name => $attribute->getStaticValue()];
});

$attributes = Arr::mapWithKeys($component->attributes, function (Attribute $attribute) {
return [$attribute->name => $attribute->getStaticValue()];
});
Expand All @@ -112,12 +116,13 @@ function ($input) {
return [$slot->name => new ComponentSlot($slot->content())];
});

$this->runtime->pushData($awareData);
$this->runtime->pushData($attributes);
$this->runtime->pushSlots($slots);

ob_start();

require_once $path;
require_once $compiled;

$fn(
__blaze: $this->runtime,
Expand All @@ -131,6 +136,7 @@ function ($input) {
ob_end_clean();
}

$this->runtime->popData();
$this->runtime->popData();
$this->manager->stopFolding();

Expand Down
6 changes: 0 additions & 6 deletions src/Compiler/AwareCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,6 @@ public function compile(string $expression): string
? sprintf('$%s = $__blaze->getConsumableData(\'%s\', $__awareDefaults[\'%s\']);', $name, $name, $name)
: sprintf('$%s = $__blaze->getConsumableData(\'%s\');', $name, $name);

$kebab = Str::kebab($name);

$output .= $kebab !== $name
? sprintf(' unset($attributes[\'%s\'], $attributes[\'%s\']);', $name, $kebab)
: sprintf(' unset($attributes[\'%s\']);', $name);

$output .= "\n";
}

Expand Down
105 changes: 27 additions & 78 deletions src/Folder/Foldable.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class Foldable

public function __construct(
protected ComponentNode $node,
protected ComponentSource $source,
protected string $path,
protected BladeRenderer $renderer,
protected BladeService $blade,
) {
Expand All @@ -42,17 +42,13 @@ public function fold(): string
$this->renderable = new ComponentNode(
name: $this->node->name,
prefix: $this->node->prefix,
attributeString: '',
children: [],
selfClosing: $this->node->selfClosing,
parentsAttributes: $this->node->parentsAttributes,
);

$this->setupAttributes();
$this->setupSlots();
$this->mergeAwareProps();

$this->html = $this->renderer->render($this->renderable, $this->source);
$this->html = $this->renderer->render($this->renderable, $this->path);

$this->processUncompiledAttributes();
$this->restorePlaceholders();
Expand All @@ -62,28 +58,38 @@ public function fold(): string
}

/**
* Replace dynamic attributes with placeholders, keep static ones as-is.
* Prepare component and inherited attributes for isolated rendering.
*/
protected function setupAttributes(): void
{
foreach ($this->node->attributes as $key => $attribute) {
if (! $attribute->isStaticValue()) {
$placeholder = 'BLAZE_PLACEHOLDER_' . $this->placeholderIndex++ . '_';
$this->renderable->attributes[$key] = $this->prepareAttribute($attribute);
}

$this->attributeByPlaceholder[$placeholder] = $attribute;
foreach ($this->node->parentsAttributes as $key => $attribute) {
$this->renderable->parentsAttributes[$key] = $this->prepareAttribute($attribute);
}
}

$this->renderable->attributes[$key] = new Attribute(
name: $attribute->name,
value: $placeholder,
propName: $attribute->propName,
prefix: '',
dynamic: false,
quotes: '"',
);
} else {
$this->renderable->attributes[$key] = clone $attribute;
}
/**
* Replace a dynamic attribute with a static placeholder for isolated rendering.
*/
protected function prepareAttribute(Attribute $attribute): Attribute
{
if ($attribute->isStaticValue()) {
return clone $attribute;
}

$placeholder = 'BLAZE_PLACEHOLDER_' . $this->placeholderIndex++ . '_';

$this->attributeByPlaceholder[$placeholder] = $attribute;

return new Attribute(
name: $attribute->name,
value: $placeholder,
propName: $attribute->propName,
dynamic: false,
);
}

/**
Expand Down Expand Up @@ -140,63 +146,6 @@ protected function setupSlots(): void
$this->renderable->children = $slots;
}

/**
* Merge @aware-declared props from parent attributes into the renderable node.
*/
protected function mergeAwareProps(): void
{
$aware = $this->source->directives->array('aware') ?? [];

foreach ($aware as $prop => $default) {
if (is_int($prop)) {
$prop = $default;
$default = null;
}

if (isset($this->renderable->attributes[$prop])) {
continue;
}

if (isset($this->node->parentsAttributes[$prop])) {
$attribute = $this->node->parentsAttributes[$prop];

if (! $attribute->isStaticValue()) {
$placeholder = 'BLAZE_PLACEHOLDER_' . $this->placeholderIndex++ . '_';

$this->attributeByPlaceholder[$placeholder] = $attribute;

$this->renderable->attributes[$prop] = new Attribute(
name: $prop,
value: $placeholder,
propName: $prop,
dynamic: false,
);
} else {
$this->renderable->attributes[$prop] = new Attribute(
name: $attribute->name,
value: $attribute->value,
propName: $attribute->propName,
dynamic: $attribute->dynamic,
quotes: $attribute->quotes,
prefix: $attribute->prefix,
);
}
} else if ($default !== null) {
// TODO: test this, we might not need to add the default attributes because they will be added inside the component?
// When the value is null and no parent provides a value, we intentionally
// skip adding the attribute. This lets @aware and @props handle defaults
// at runtime, matching the non-folded behavior. Adding an attribute with
// null value would render as prop="" in HTML, corrupting null to empty string.
$this->renderable->attributes[$prop] = new Attribute(
name: $prop,
value: $default,
propName: $prop,
dynamic: false,
);
}
}
}

/**
* Convert [BLAZE_ATTR:...] markers into conditional PHP for dynamic attributes.
*/
Expand Down
6 changes: 5 additions & 1 deletion src/Folder/Folder.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public function fold(Node $node): Node
$this->checkProblematicPatterns($source);

try {
$foldable = new Foldable($node, $source, $this->renderer, $this->blade);
$foldable = new Foldable($node, $source->path, $this->renderer, $this->blade);

$html = $foldable->fold();

Expand Down Expand Up @@ -103,6 +103,10 @@ protected function isSafeToFold(ComponentSource $source, ComponentNode $node): b
return false;
}

if (array_key_exists('attributes', $node->parentsAttributes)) {
return false;
}

$dynamicAttributes = array_filter($node->attributes, fn ($attribute) => ! $attribute->isStaticValue());

foreach ($source->directives->aware() as $prop) {
Expand Down
71 changes: 71 additions & 0 deletions tests/BladeRendererTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

use Illuminate\Support\Facades\File;
use Livewire\Blaze\BladeRenderer;
use Livewire\Blaze\Parser\Parser;
use Livewire\Blaze\Support\AttributeParser;
use Livewire\Blaze\Support\Utils;

afterEach(fn () => app(BladeRenderer::class)->deleteTemporaryCacheDirectory());

test('compiles component source into the temporary cache', function () {
$path = fixture_path('views/components/foldable/input.blade.php');
$node = app(Parser::class)->parse('<x-foldable.input />')[0];

app(BladeRenderer::class)->render($node, $path);

expect(File::exists(config('view.compiled').'/blaze/'.Utils::hash($path).'.php'))->toBeTrue();
});

test('makes attributes available to aware props', function () {
$node = app(Parser::class)->parse('<x-foldable.input-aware type="number" />')[0];

$output = app(BladeRenderer::class)->render($node, fixture_path('views/components/foldable/input-aware.blade.php'));

expect($output)->toEqualCollapsingWhitespace('<input type="number" >');
});

test('makes slots available to aware props', function () {
$node = app(Parser::class)->parse('<x-foldable.input-aware><x-slot:type>number</x-slot:type></x-foldable.input-aware>')[0];

$output = app(BladeRenderer::class)->render($node, fixture_path('views/components/foldable/input-aware.blade.php'));

expect($output)->toEqualCollapsingWhitespace('<input type="number" >');
});

test('makes parents attributes available to aware props', function () {
$node = app(Parser::class)->parse('<x-foldable.input-aware />')[0];

$node->setParentsAttributes(
app(AttributeParser::class)->parse('type="number"')
);

$output = app(BladeRenderer::class)->render($node, fixture_path('views/components/foldable/input-aware.blade.php'));

expect($output)->toEqualCollapsingWhitespace('<input type="number" >');
});

test('processes unblaze blocks', function () {
$node = app(Parser::class)->parse('<x-foldable.input-unblaze name="address" />')[0];

$output = app(BladeRenderer::class)->render($node, fixture_path('views/components/foldable/input-unblaze.blade.php'));

expect($output)->toEqualCollapsingWhitespace(sprintf('<input %s >', join('', [
'<?php if (isset($scope)) $__scope = $scope; ?>',
'<?php $scope = array ( \'name\' => \'address\', ); ?>',
' {{ $errors->has($scope[\'name\']) }} ',
'<?php if (isset($__scope)) { $scope = $__scope; unset($__scope); } ?>',
])));
});

test('deletes the temporary cache directory', function () {
$node = app(Parser::class)->parse('<x-foldable.input />')[0];

app(BladeRenderer::class)->render($node, fixture_path('views/components/foldable/input.blade.php'));

expect(File::isDirectory(config('view.compiled').'/blaze'))->toBeTrue();

app(BladeRenderer::class)->deleteTemporaryCacheDirectory();

expect(File::isDirectory(config('view.compiled').'/blaze'))->toBeFalse();
});
5 changes: 5 additions & 0 deletions tests/ComparisonTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,8 @@
</x-foldable.card>
BLADE
));

test('foldable aware without props', fn () => compare(<<<'BLADE'
<x-foldable.input-aware-no-props type="number" />
BLADE
));
2 changes: 1 addition & 1 deletion tests/Compiler/WrapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
'ob_start(); ?> ',
'@blaze ',
'<?php $__awareDefaults = [\'type\' => \'text\']; ',
'$type = $__blaze->getConsumableData(\'type\', $__awareDefaults[\'type\']); unset($attributes[\'type\']); ',
'$type = $__blaze->getConsumableData(\'type\', $__awareDefaults[\'type\']); ',
'unset($__awareDefaults); ?> ',
'<?php $__defaults = [\'type\' => \'text\', \'disabled\' => false]; ',
'$type ??= $attributes[\'type\'] ?? $__defaults[\'type\']; unset($attributes[\'type\']); ',
Expand Down
Loading
Loading