From 37cb3450150cf75af787dc9bcd8de08370024008 Mon Sep 17 00:00:00 2001 From: Roj Vroemen Date: Fri, 1 Dec 2023 23:04:08 +0100 Subject: [PATCH] [10.x] Fix loss of attributes after calling child component (#49216) * Store and restore original attributes when rendering component * Move attribute assertion to mock This is needed because the original attributes are now restored after rendering a component. * Assert withAttribute params on child component prop * Fix assertion * Add assertion to ensure original attributes are restored --- .../Compilers/Concerns/CompilesComponents.php | 5 ++ .../Blade/BladeComponentTagCompilerTest.php | 62 ++++++++++++++++++- tests/View/Blade/BladeComponentsTest.php | 5 ++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/View/Compilers/Concerns/CompilesComponents.php b/src/Illuminate/View/Compilers/Concerns/CompilesComponents.php index 8e986553abb1..88617186fb20 100644 --- a/src/Illuminate/View/Compilers/Concerns/CompilesComponents.php +++ b/src/Illuminate/View/Compilers/Concerns/CompilesComponents.php @@ -67,6 +67,7 @@ public static function compileClassComponentOpening(string $component, string $a { return implode("\n", [ '', + '', 'getIterator() : [])); ?>', 'withName('.$alias.'); ?>', 'shouldRender()): ?>', @@ -94,6 +95,10 @@ public function compileEndComponentClass() $hash = array_pop(static::$componentHashStack); return $this->compileEndComponent()."\n".implode("\n", [ + '', + '', + '', + '', '', '', '', diff --git a/tests/View/Blade/BladeComponentTagCompilerTest.php b/tests/View/Blade/BladeComponentTagCompilerTest.php index ec100b50ee58..5143ae503c72 100644 --- a/tests/View/Blade/BladeComponentTagCompilerTest.php +++ b/tests/View/Blade/BladeComponentTagCompilerTest.php @@ -715,7 +715,7 @@ public function testAttributesTreatedAsPropsAreRemovedFromFinalAttributes() $component->shouldReceive('shouldRender')->once()->andReturn(true); $component->shouldReceive('resolveView')->once()->andReturn(''); $component->shouldReceive('data')->once()->andReturn([]); - $component->shouldReceive('withAttributes')->once(); + $component->shouldReceive('withAttributes')->with(['attributes' => new ComponentAttributeBag(['other' => 'ok'])])->once(); Component::resolveComponentsUsing(fn () => $component); @@ -730,7 +730,57 @@ public function testAttributesTreatedAsPropsAreRemovedFromFinalAttributes() eval(" ?> $template assertNull($attributes->get('userId')); + $this->assertSame($attributes->get('userId'), 'bar'); + $this->assertSame($attributes->get('other'), 'ok'); + } + + public function testOriginalAttributesAreRestoredAfterRenderingChildComponentWithProps() + { + $container = new Container; + $container->instance(Application::class, $app = m::mock(Application::class)); + $container->instance(Factory::class, $factory = m::mock(Factory::class)); + $container->alias(Factory::class, 'view'); + $app->shouldReceive('getNamespace')->never()->andReturn('App\\'); + $factory->shouldReceive('exists')->never(); + + Container::setInstance($container); + + $attributes = new ComponentAttributeBag(['userId' => 'bar', 'other' => 'ok']); + + $containerComponent = m::mock(Component::class); + $containerComponent->shouldReceive('withName')->with('container')->once(); + $containerComponent->shouldReceive('shouldRender')->once()->andReturn(true); + $containerComponent->shouldReceive('resolveView')->once()->andReturn(''); + $containerComponent->shouldReceive('data')->once()->andReturn([]); + $containerComponent->shouldReceive('withAttributes')->once(); + + $profileComponent = m::mock(Component::class); + $profileComponent->shouldReceive('withName')->with('profile')->once(); + $profileComponent->shouldReceive('shouldRender')->once()->andReturn(true); + $profileComponent->shouldReceive('resolveView')->once()->andReturn(''); + $profileComponent->shouldReceive('data')->once()->andReturn([]); + $profileComponent->shouldReceive('withAttributes')->with(['attributes' => new ComponentAttributeBag(['other' => 'ok'])])->once(); + + Component::resolveComponentsUsing(fn ($component) => match ($component) { + TestContainerComponent::class => $containerComponent, + TestProfileComponent::class => $profileComponent, + }); + + $__env = m::mock(\Illuminate\View\Factory::class); + $__env->shouldReceive('startComponent')->twice(); + $__env->shouldReceive('renderComponent')->twice(); + + $template = $this->compiler([ + 'container' => TestContainerComponent::class, + 'profile' => TestProfileComponent::class, + ])->compileTags(''); + $template = $this->compiler->compileString($template); + + ob_start(); + eval(" ?> $template assertSame($attributes->get('userId'), 'bar'); $this->assertSame($attributes->get('other'), 'ok'); } @@ -797,3 +847,11 @@ public function render() return 'input'; } } + +class TestContainerComponent extends Component +{ + public function render() + { + return 'container'; + } +} diff --git a/tests/View/Blade/BladeComponentsTest.php b/tests/View/Blade/BladeComponentsTest.php index 406f9290965f..2615edf91452 100644 --- a/tests/View/Blade/BladeComponentsTest.php +++ b/tests/View/Blade/BladeComponentsTest.php @@ -17,6 +17,7 @@ public function testComponentsAreCompiled() public function testClassComponentsAreCompiled() { $this->assertSame(' + "bar"] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? (array) $attributes->getIterator() : [])); ?> withName(\'test\'); ?> shouldRender()): ?> @@ -36,6 +37,10 @@ public function testEndComponentClassesAreCompiled() $this->assertSame('renderComponent(); ?> + + + +