From f5c6eba83e2153132edf52296db38e6022309c26 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 5 Oct 2022 19:38:11 +0100 Subject: [PATCH 01/30] Improves performance of inline components --- src/Illuminate/View/Component.php | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/View/Component.php b/src/Illuminate/View/Component.php index 0acee35ca066..2e92a5b12474 100644 --- a/src/Illuminate/View/Component.php +++ b/src/Illuminate/View/Component.php @@ -26,6 +26,13 @@ abstract class Component */ protected static $methodCache = []; + /** + * The cache of blade view names, keyed by class. + * + * @var array + */ + protected static $bladeViewCache = []; + /** * The properties / methods that should not be exposed to the component. * @@ -74,9 +81,7 @@ public function resolveView() $resolver = function ($view) { $factory = Container::getInstance()->make('view'); - return strlen($view) <= PHP_MAXPATHLEN && $factory->exists($view) - ? $view - : $this->createBladeViewFromString($factory, $view); + return $this->extractBladeViewFromString($factory, $view); }; return $view instanceof Closure ? function (array $data = []) use ($view, $resolver) { @@ -92,8 +97,18 @@ public function resolveView() * @param string $contents * @return string */ - protected function createBladeViewFromString($factory, $contents) + protected function extractBladeViewFromString($factory, $contents) { + $class = get_class($this); + + if (isset(static::$bladeViewCache[$class])) { + return static::$bladeViewCache[$class]; + } + + if (strlen($contents) <= PHP_MAXPATHLEN && $factory->exists($contents)) { + return static::$bladeViewCache[$class] = $contents; + } + $factory->addNamespace( '__components', $directory = Container::getInstance()['config']->get('view.compiled') @@ -107,7 +122,7 @@ protected function createBladeViewFromString($factory, $contents) file_put_contents($viewFile, $contents); } - return '__components::'.basename($viewFile, '.blade.php'); + return static::$bladeViewCache[$class] = '__components::'.basename($viewFile, '.blade.php'); } /** From 981b684877ba7e2ab86d2446ee2ba71af070aafa Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 5 Oct 2022 20:51:54 +0100 Subject: [PATCH 02/30] Keys by content --- src/Illuminate/View/Component.php | 26 ++++++++++++++++++-------- tests/Integration/View/BladeTest.php | 7 +++++++ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/Illuminate/View/Component.php b/src/Illuminate/View/Component.php index 2e92a5b12474..afb534383bea 100644 --- a/src/Illuminate/View/Component.php +++ b/src/Illuminate/View/Component.php @@ -27,7 +27,7 @@ abstract class Component protected static $methodCache = []; /** - * The cache of blade view names, keyed by class. + * The cache of blade view names, keyed by contents. * * @var array */ @@ -81,7 +81,7 @@ public function resolveView() $resolver = function ($view) { $factory = Container::getInstance()->make('view'); - return $this->extractBladeViewFromString($factory, $view); + return $this->extractBladeViewFromString($factory, (string) $view); }; return $view instanceof Closure ? function (array $data = []) use ($view, $resolver) { @@ -99,14 +99,12 @@ public function resolveView() */ protected function extractBladeViewFromString($factory, $contents) { - $class = get_class($this); - - if (isset(static::$bladeViewCache[$class])) { - return static::$bladeViewCache[$class]; + if (isset(static::$bladeViewCache[$contents])) { + return static::$bladeViewCache[$contents]; } if (strlen($contents) <= PHP_MAXPATHLEN && $factory->exists($contents)) { - return static::$bladeViewCache[$class] = $contents; + return static::$bladeViewCache[$contents] = $contents; } $factory->addNamespace( @@ -122,7 +120,7 @@ protected function extractBladeViewFromString($factory, $contents) file_put_contents($viewFile, $contents); } - return static::$bladeViewCache[$class] = '__components::'.basename($viewFile, '.blade.php'); + return static::$bladeViewCache[$contents] = '__components::'.basename($viewFile, '.blade.php'); } /** @@ -307,4 +305,16 @@ public function shouldRender() { return true; } + + /** + * Flush the cache of components. + * + * @return void + */ + public static function flushCache() + { + static::$propertyCache = []; + static::$methodCache = []; + static::$bladeViewCache = []; + } } diff --git a/tests/Integration/View/BladeTest.php b/tests/Integration/View/BladeTest.php index b3d8f51eedc7..0fc250bec91e 100644 --- a/tests/Integration/View/BladeTest.php +++ b/tests/Integration/View/BladeTest.php @@ -9,6 +9,13 @@ class BladeTest extends TestCase { + public function tearDown(): void + { + Component::flushCache(); + + parent::tearDown(); + } + public function test_rendering_blade_string() { $this->assertSame('Hello Taylor', Blade::render('Hello {{ $name }}', ['name' => 'Taylor'])); From 3836f5c20a2804087bbb06a7d46c2cb8abcebab7 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 5 Oct 2022 21:37:08 +0100 Subject: [PATCH 03/30] Uses class on key too --- src/Illuminate/View/Component.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/View/Component.php b/src/Illuminate/View/Component.php index afb534383bea..665f362d8741 100644 --- a/src/Illuminate/View/Component.php +++ b/src/Illuminate/View/Component.php @@ -99,12 +99,14 @@ public function resolveView() */ protected function extractBladeViewFromString($factory, $contents) { - if (isset(static::$bladeViewCache[$contents])) { - return static::$bladeViewCache[$contents]; + $key = sprintf('%s::%s', static::class, $contents); + + if (isset(static::$bladeViewCache[$key])) { + return static::$bladeViewCache[$key]; } if (strlen($contents) <= PHP_MAXPATHLEN && $factory->exists($contents)) { - return static::$bladeViewCache[$contents] = $contents; + return static::$bladeViewCache[$key] = $contents; } $factory->addNamespace( @@ -120,7 +122,7 @@ protected function extractBladeViewFromString($factory, $contents) file_put_contents($viewFile, $contents); } - return static::$bladeViewCache[$contents] = '__components::'.basename($viewFile, '.blade.php'); + return static::$bladeViewCache[$key] = '__components::'.basename($viewFile, '.blade.php'); } /** From 0f6d05cda86d0f1c2fdabf1165cd6ed3746487e3 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 6 Oct 2022 11:48:57 +0100 Subject: [PATCH 04/30] Improves blade components performance --- src/Illuminate/View/AnonymousComponent.php | 11 ++++ .../Compilers/Concerns/CompilesComponents.php | 2 +- src/Illuminate/View/Component.php | 66 +++++++++++++++++-- src/Illuminate/View/DynamicComponent.php | 11 ++++ src/Illuminate/View/ViewServiceProvider.php | 5 ++ 5 files changed, 87 insertions(+), 8 deletions(-) diff --git a/src/Illuminate/View/AnonymousComponent.php b/src/Illuminate/View/AnonymousComponent.php index eba64365626b..8585d76ffa7a 100644 --- a/src/Illuminate/View/AnonymousComponent.php +++ b/src/Illuminate/View/AnonymousComponent.php @@ -57,4 +57,15 @@ public function data() ['attributes' => $this->attributes] ); } + + /** + * Acts as static factory for the component. + * + * @param array + * @return static + */ + public static function resolve($data) + { + return new static($data['view'], $data['data']); + } } diff --git a/src/Illuminate/View/Compilers/Concerns/CompilesComponents.php b/src/Illuminate/View/Compilers/Concerns/CompilesComponents.php index 0fe45be6d9fa..0790c41c1bff 100644 --- a/src/Illuminate/View/Compilers/Concerns/CompilesComponents.php +++ b/src/Illuminate/View/Compilers/Concerns/CompilesComponents.php @@ -64,7 +64,7 @@ public static function compileClassComponentOpening(string $component, string $a { return implode("\n", [ '', - 'getContainer()->make('.Str::finish($component, '::class').', '.($data ?: '[]').' + (isset($attributes) ? (array) $attributes->getIterator() : [])); ?>', + 'getIterator() : [])); ?>', 'withName('.$alias.'); ?>', 'shouldRender()): ?>', 'startComponent($component->resolveView(), $component->data()); ?>', diff --git a/src/Illuminate/View/Component.php b/src/Illuminate/View/Component.php index 0acee35ca066..6bcc563462fd 100644 --- a/src/Illuminate/View/Component.php +++ b/src/Illuminate/View/Component.php @@ -12,6 +12,13 @@ abstract class Component { + /** + * The view factory instance, if any. + * + * @var \Illuminate\Contracts\View\Factory + */ + protected static $factory; + /** * The cache of public property names, keyed by class. * @@ -72,11 +79,9 @@ public function resolveView() } $resolver = function ($view) { - $factory = Container::getInstance()->make('view'); - - return strlen($view) <= PHP_MAXPATHLEN && $factory->exists($view) + return strlen($view) <= PHP_MAXPATHLEN && $this->factory()->exists($view) ? $view - : $this->createBladeViewFromString($factory, $view); + : $this->createBladeViewFromString($view); }; return $view instanceof Closure ? function (array $data = []) use ($view, $resolver) { @@ -88,13 +93,12 @@ public function resolveView() /** * Create a Blade view with the raw component string content. * - * @param \Illuminate\Contracts\View\Factory $factory * @param string $contents * @return string */ - protected function createBladeViewFromString($factory, $contents) + protected function createBladeViewFromString($contents) { - $factory->addNamespace( + $this->factory()->addNamespace( '__components', $directory = Container::getInstance()['config']->get('view.compiled') ); @@ -292,4 +296,52 @@ public function shouldRender() { return true; } + + /** + * Set the component's view. + * + * @param string|null $view + * @param \Illuminate\Contracts\Support\Arrayable|array $data + * @param array $mergeData + * @return \Illuminate\Contracts\View\View + */ + public function view($view, $data = [], $mergeData = []) + { + return $this->factory()->make($view, $data, $mergeData); + } + + /** + * Get the view factory. + * + * @return \Illuminate\Contracts\View\Factory + */ + protected function factory() + { + if (is_null(static::$factory)) { + static::$factory = Container::getInstance()->make('view'); + } + + return static::$factory; + } + + /** + * Forget the component's factory. + * + * @return void + */ + public static function forgetFactory() + { + static::$factory = null; + } + + /** + * Acts as static factory for the component. + * + * @param array + * @return static + */ + public static function resolve($data) + { + return Container::getInstance()->make(static::class, $data); + } } diff --git a/src/Illuminate/View/DynamicComponent.php b/src/Illuminate/View/DynamicComponent.php index cea66e77b304..4bcdf9e82524 100644 --- a/src/Illuminate/View/DynamicComponent.php +++ b/src/Illuminate/View/DynamicComponent.php @@ -169,4 +169,15 @@ protected function compiler() return static::$compiler; } + + /** + * Acts as static factory for the component. + * + * @param array + * @return static + */ + public static function resolve($data) + { + return new static($data['component']); + } } diff --git a/src/Illuminate/View/ViewServiceProvider.php b/src/Illuminate/View/ViewServiceProvider.php index afae6bb70615..f7ee6977eda7 100755 --- a/src/Illuminate/View/ViewServiceProvider.php +++ b/src/Illuminate/View/ViewServiceProvider.php @@ -3,6 +3,7 @@ namespace Illuminate\View; use Illuminate\Support\ServiceProvider; +use Illuminate\View\Component; use Illuminate\View\Compilers\BladeCompiler; use Illuminate\View\Engines\CompilerEngine; use Illuminate\View\Engines\EngineResolver; @@ -48,6 +49,10 @@ public function registerFactory() $factory->share('app', $app); + $app->terminating(static function () { + Component::forgetFactory(); + }); + return $factory; }); } From a80dae1fc2452752d4e0e6dbe05cbc4f40f729b0 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 6 Oct 2022 10:49:33 +0000 Subject: [PATCH 05/30] Apply fixes from StyleCI --- src/Illuminate/View/ViewServiceProvider.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/View/ViewServiceProvider.php b/src/Illuminate/View/ViewServiceProvider.php index f7ee6977eda7..7b75ec1f5837 100755 --- a/src/Illuminate/View/ViewServiceProvider.php +++ b/src/Illuminate/View/ViewServiceProvider.php @@ -3,7 +3,6 @@ namespace Illuminate\View; use Illuminate\Support\ServiceProvider; -use Illuminate\View\Component; use Illuminate\View\Compilers\BladeCompiler; use Illuminate\View\Engines\CompilerEngine; use Illuminate\View\Engines\EngineResolver; From 9c3c416cd23cdd8a5e24da2db903b3176e1ca9bc Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 6 Oct 2022 12:03:14 +0100 Subject: [PATCH 06/30] Fixes cache --- src/Illuminate/View/Component.php | 2 +- src/Illuminate/View/ViewServiceProvider.php | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/View/Component.php b/src/Illuminate/View/Component.php index 533148558063..de7f1cd4f3ef 100644 --- a/src/Illuminate/View/Component.php +++ b/src/Illuminate/View/Component.php @@ -101,7 +101,7 @@ public function resolveView() * @param string $contents * @return string */ - protected function extractBladeViewFromString($factory, $contents) + protected function extractBladeViewFromString($contents) { $key = sprintf('%s::%s', static::class, $contents); diff --git a/src/Illuminate/View/ViewServiceProvider.php b/src/Illuminate/View/ViewServiceProvider.php index 7b75ec1f5837..4e63127eb3b6 100755 --- a/src/Illuminate/View/ViewServiceProvider.php +++ b/src/Illuminate/View/ViewServiceProvider.php @@ -3,6 +3,7 @@ namespace Illuminate\View; use Illuminate\Support\ServiceProvider; +use Illuminate\View\Component; use Illuminate\View\Compilers\BladeCompiler; use Illuminate\View\Engines\CompilerEngine; use Illuminate\View\Engines\EngineResolver; @@ -22,6 +23,10 @@ public function register() $this->registerViewFinder(); $this->registerBladeCompiler(); $this->registerEngineResolver(); + + $this->app->terminating(static function () { + Component::flushCache(); + }); } /** From 12a299ed632c7881d31154f335036fae6062f42d Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 6 Oct 2022 13:19:02 +0100 Subject: [PATCH 07/30] Forgets factory in tests --- tests/Integration/View/BladeTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Integration/View/BladeTest.php b/tests/Integration/View/BladeTest.php index 0fc250bec91e..5dd83c9a4b24 100644 --- a/tests/Integration/View/BladeTest.php +++ b/tests/Integration/View/BladeTest.php @@ -12,6 +12,7 @@ class BladeTest extends TestCase public function tearDown(): void { Component::flushCache(); + Component::forgetFactory(); parent::tearDown(); } From ac1da93bb67fcce2f2867e0ba4180c4c9f74fb16 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 6 Oct 2022 12:19:31 +0000 Subject: [PATCH 08/30] Apply fixes from StyleCI --- src/Illuminate/View/ViewServiceProvider.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/View/ViewServiceProvider.php b/src/Illuminate/View/ViewServiceProvider.php index 4e63127eb3b6..67450ed204ec 100755 --- a/src/Illuminate/View/ViewServiceProvider.php +++ b/src/Illuminate/View/ViewServiceProvider.php @@ -3,7 +3,6 @@ namespace Illuminate\View; use Illuminate\Support\ServiceProvider; -use Illuminate\View\Component; use Illuminate\View\Compilers\BladeCompiler; use Illuminate\View\Engines\CompilerEngine; use Illuminate\View\Engines\EngineResolver; From 1553bdd2f4000759772810910f4c5deec60aa42f Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 6 Oct 2022 15:54:22 +0100 Subject: [PATCH 09/30] Fixes tests --- tests/View/Blade/AbstractBladeTestCase.php | 11 ++- .../Blade/BladeComponentTagCompilerTest.php | 89 ++++++++++++------- tests/View/Blade/BladeComponentsTest.php | 23 +++-- tests/View/ComponentTest.php | 8 +- 4 files changed, 88 insertions(+), 43 deletions(-) diff --git a/tests/View/Blade/AbstractBladeTestCase.php b/tests/View/Blade/AbstractBladeTestCase.php index 99510b1dee66..ed61b1703700 100644 --- a/tests/View/Blade/AbstractBladeTestCase.php +++ b/tests/View/Blade/AbstractBladeTestCase.php @@ -2,7 +2,9 @@ namespace Illuminate\Tests\View\Blade; +use Illuminate\Container\Container; use Illuminate\Filesystem\Filesystem; +use Illuminate\View\Component; use Illuminate\View\Compilers\BladeCompiler; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -16,12 +18,17 @@ abstract class AbstractBladeTestCase extends TestCase protected function setUp(): void { - $this->compiler = new BladeCompiler($this->getFiles(), __DIR__); parent::setUp(); + + $this->compiler = new BladeCompiler($this->getFiles(), __DIR__); } protected function tearDown(): void - { + { + Container::setInstance(null); + Component::flushCache(); + Component::forgetFactory(); + m::close(); parent::tearDown(); diff --git a/tests/View/Blade/BladeComponentTagCompilerTest.php b/tests/View/Blade/BladeComponentTagCompilerTest.php index 781adb9ef556..a91eeadfa35f 100644 --- a/tests/View/Blade/BladeComponentTagCompilerTest.php +++ b/tests/View/Blade/BladeComponentTagCompilerTest.php @@ -15,13 +15,9 @@ class BladeComponentTagCompilerTest extends AbstractBladeTestCase { - protected function tearDown(): void - { - m::close(); - } - public function testSlotsCanBeCompiled() { + $this->mockViewFactory(); $result = $this->compiler()->compileSlots(' '); @@ -30,6 +26,7 @@ public function testSlotsCanBeCompiled() public function testInlineSlotsCanBeCompiled() { + $this->mockViewFactory(); $result = $this->compiler()->compileSlots(' '); @@ -38,6 +35,7 @@ public function testInlineSlotsCanBeCompiled() public function testDynamicSlotsCanBeCompiled() { + $this->mockViewFactory(); $result = $this->compiler()->compileSlots(' '); @@ -46,6 +44,7 @@ public function testDynamicSlotsCanBeCompiled() public function testDynamicSlotsCanBeCompiledWithKeyOfObjects() { + $this->mockViewFactory(); $result = $this->compiler()->compileSlots(' '); @@ -54,6 +53,7 @@ public function testDynamicSlotsCanBeCompiledWithKeyOfObjects() public function testSlotsWithAttributesCanBeCompiled() { + $this->mockViewFactory(); $result = $this->compiler()->compileSlots(' '); @@ -62,6 +62,7 @@ public function testSlotsWithAttributesCanBeCompiled() public function testInlineSlotsWithAttributesCanBeCompiled() { + $this->mockViewFactory(); $result = $this->compiler()->compileSlots(' '); @@ -70,6 +71,7 @@ public function testInlineSlotsWithAttributesCanBeCompiled() public function testSlotsWithDynamicAttributesCanBeCompiled() { + $this->mockViewFactory(); $result = $this->compiler()->compileSlots(' '); @@ -78,6 +80,7 @@ public function testSlotsWithDynamicAttributesCanBeCompiled() public function testSlotsWithClassDirectiveCanBeCompiled() { + $this->mockViewFactory(); $result = $this->compiler()->compileSlots(' '); @@ -105,6 +108,7 @@ public function testBasicComponentParsing() public function testBasicComponentWithEmptyAttributesParsing() { + $this->mockViewFactory(); $result = $this->compiler(['alert' => TestAlertComponent::class])->compileTags('
'); $this->assertSame("
##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', []) @@ -117,6 +121,7 @@ public function testBasicComponentWithEmptyAttributesParsing() public function testDataCamelCasing() { + $this->mockViewFactory(); $result = $this->compiler(['profile' => TestProfileComponent::class])->compileTags(''); $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', ['userId' => '1']) @@ -128,6 +133,7 @@ public function testDataCamelCasing() public function testColonData() { + $this->mockViewFactory(); $result = $this->compiler(['profile' => TestProfileComponent::class])->compileTags(''); $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', ['userId' => 1]) @@ -139,6 +145,7 @@ public function testColonData() public function testColonDataShortSyntax() { + $this->mockViewFactory(); $result = $this->compiler(['profile' => TestProfileComponent::class])->compileTags(''); $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', ['userId' => \$userId]) @@ -150,6 +157,7 @@ public function testColonDataShortSyntax() public function testColonDataWithStaticClassProperty() { + $this->mockViewFactory(); $result = $this->compiler(['profile' => TestProfileComponent::class])->compileTags(''); $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', ['userId' => User::\$id]) @@ -161,6 +169,7 @@ public function testColonDataWithStaticClassProperty() public function testColonDataWithStaticClassPropertyAndMultipleAttributes() { + $this->mockViewFactory(); $result = $this->compiler(['input' => TestInputComponent::class])->compileTags(''); $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestInputComponent', 'input', ['label' => Input::\$label,'name' => \$name,'value' => 'Joe']) @@ -180,6 +189,7 @@ public function testColonDataWithStaticClassPropertyAndMultipleAttributes() public function testSelfClosingComponentWithColonDataShortSyntax() { + $this->mockViewFactory(); $result = $this->compiler(['profile' => TestProfileComponent::class])->compileTags(''); $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', ['userId' => \$userId]) @@ -192,6 +202,7 @@ public function testSelfClosingComponentWithColonDataShortSyntax() public function testSelfClosingComponentWithColonDataAndStaticClassPropertyShortSyntax() { + $this->mockViewFactory(); $result = $this->compiler(['profile' => TestProfileComponent::class])->compileTags(''); $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', ['userId' => User::\$id]) @@ -204,6 +215,7 @@ public function testSelfClosingComponentWithColonDataAndStaticClassPropertyShort public function testSelfClosingComponentWithColonDataMultipleAttributesAndStaticClassPropertyShortSyntax() { + $this->mockViewFactory(); $result = $this->compiler(['input' => TestInputComponent::class])->compileTags(''); $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestInputComponent', 'input', ['label' => Input::\$label,'value' => 'Joe','name' => \$name]) @@ -225,6 +237,7 @@ public function testSelfClosingComponentWithColonDataMultipleAttributesAndStatic public function testEscapedColonAttribute() { + $this->mockViewFactory(); $result = $this->compiler(['profile' => TestProfileComponent::class])->compileTags(''); $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', ['userId' => 1]) @@ -236,6 +249,7 @@ public function testEscapedColonAttribute() public function testColonAttributesIsEscapedIfStrings() { + $this->mockViewFactory(); $result = $this->compiler(['profile' => TestProfileComponent::class])->compileTags(''); $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', []) @@ -259,6 +273,7 @@ public function testClassDirective() public function testColonNestedComponentParsing() { + $this->mockViewFactory(); $result = $this->compiler(['foo:alert' => TestAlertComponent::class])->compileTags(''); $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'foo:alert', []) @@ -270,6 +285,7 @@ public function testColonNestedComponentParsing() public function testColonStartingNestedComponentParsing() { + $this->mockViewFactory(); $result = $this->compiler(['foo:alert' => TestAlertComponent::class])->compileTags(''); $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'foo:alert', []) @@ -281,6 +297,7 @@ public function testColonStartingNestedComponentParsing() public function testSelfClosingComponentsCanBeCompiled() { + $this->mockViewFactory(); $result = $this->compiler(['alert' => TestAlertComponent::class])->compileTags('
'); $this->assertSame("
##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', []) @@ -295,7 +312,7 @@ public function testClassNamesCanBeGuessed() { $container = new Container; $container->instance(Application::class, $app = m::mock(Application::class)); - $app->shouldReceive('getNamespace')->andReturn('App\\'); + $app->shouldReceive('getNamespace')->once()->andReturn('App\\'); Container::setInstance($container); $result = $this->compiler()->guessClassName('alert'); @@ -309,7 +326,7 @@ public function testClassNamesCanBeGuessedWithNamespaces() { $container = new Container; $container->instance(Application::class, $app = m::mock(Application::class)); - $app->shouldReceive('getNamespace')->andReturn('App\\'); + $app->shouldReceive('getNamespace')->once()->andReturn('App\\'); Container::setInstance($container); $result = $this->compiler()->guessClassName('base.alert'); @@ -335,6 +352,7 @@ public function testComponentsCanBeCompiledWithHyphenAttributes() public function testSelfClosingComponentsCanBeCompiledWithDataAndAttributes() { + $this->mockViewFactory(); $result = $this->compiler(['alert' => TestAlertComponent::class])->compileTags(''); $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', ['title' => 'foo']) @@ -348,6 +366,7 @@ public function testSelfClosingComponentsCanBeCompiledWithDataAndAttributes() public function testComponentCanReceiveAttributeBag() { $this->mockViewFactory(); + $result = $this->compiler(['profile' => TestProfileComponent::class])->compileTags(''); $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', []) @@ -373,6 +392,7 @@ public function testSelfClosingComponentCanReceiveAttributeBag() public function testComponentsCanHaveAttachedWord() { + $this->mockViewFactory(); $result = $this->compiler(['profile' => TestProfileComponent::class])->compileTags('Words'); $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', []) @@ -384,6 +404,7 @@ public function testComponentsCanHaveAttachedWord() public function testSelfClosingComponentsCanHaveAttachedWord() { + $this->mockViewFactory(); $result = $this->compiler(['alert' => TestAlertComponent::class])->compileTags('Words'); $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', []) @@ -396,6 +417,7 @@ public function testSelfClosingComponentsCanHaveAttachedWord() public function testSelfClosingComponentsCanBeCompiledWithBoundData() { + $this->mockViewFactory(); $result = $this->compiler(['alert' => TestAlertComponent::class])->compileTags(''); $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', ['title' => \$title]) @@ -408,6 +430,7 @@ public function testSelfClosingComponentsCanBeCompiledWithBoundData() public function testPairedComponentTags() { + $this->mockViewFactory(); $result = $this->compiler(['alert' => TestAlertComponent::class])->compileTags(' '); @@ -424,8 +447,8 @@ public function testClasslessComponents() $container = new Container; $container->instance(Application::class, $app = m::mock(Application::class)); $container->instance(Factory::class, $factory = m::mock(Factory::class)); - $app->shouldReceive('getNamespace')->andReturn('App\\'); - $factory->shouldReceive('exists')->andReturn(true); + $app->shouldReceive('getNamespace')->once()->andReturn('App\\'); + $factory->shouldReceive('exists')->once()->andReturn(true); Container::setInstance($container); $result = $this->compiler()->compileTags(''); @@ -483,8 +506,8 @@ public function testClasslessComponentsWithAnonymousComponentNamespace() $container->instance(Application::class, $app = m::mock(Application::class)); $container->instance(Factory::class, $factory = m::mock(Factory::class)); - $app->shouldReceive('getNamespace')->andReturn('App\\'); - $factory->shouldReceive('exists')->andReturnUsing(function ($arg) { + $app->shouldReceive('getNamespace')->once()->andReturn('App\\'); + $factory->shouldReceive('exists')->times(3)->andReturnUsing(function ($arg) { // In our test, we'll do as if the 'public.frontend.anonymous-component' // view exists and not the others. return $arg === 'public.frontend.anonymous-component'; @@ -494,7 +517,7 @@ public function testClasslessComponentsWithAnonymousComponentNamespace() $blade = m::mock(BladeCompiler::class)->makePartial(); - $blade->shouldReceive('getAnonymousComponentNamespaces')->andReturn([ + $blade->shouldReceive('getAnonymousComponentNamespaces')->once()->andReturn([ 'frontend' => 'public.frontend', ]); @@ -517,8 +540,8 @@ public function testClasslessComponentsWithAnonymousComponentNamespaceWithIndexV $container->instance(Application::class, $app = m::mock(Application::class)); $container->instance(Factory::class, $factory = m::mock(Factory::class)); - $app->shouldReceive('getNamespace')->andReturn('App\\'); - $factory->shouldReceive('exists')->andReturnUsing(function (string $viewNameBeingCheckedForExistence) { + $app->shouldReceive('getNamespace')->once()->andReturn('App\\'); + $factory->shouldReceive('exists')->times(4)->andReturnUsing(function (string $viewNameBeingCheckedForExistence) { // In our test, we'll do as if the 'public.frontend.anonymous-component' // view exists and not the others. return $viewNameBeingCheckedForExistence === 'admin.auth.components.anonymous-component.index'; @@ -528,7 +551,7 @@ public function testClasslessComponentsWithAnonymousComponentNamespaceWithIndexV $blade = m::mock(BladeCompiler::class)->makePartial(); - $blade->shouldReceive('getAnonymousComponentNamespaces')->andReturn([ + $blade->shouldReceive('getAnonymousComponentNamespaces')->once()->andReturn([ 'admin.auth' => 'admin.auth.components', ]); @@ -546,6 +569,7 @@ public function testClasslessComponentsWithAnonymousComponentNamespaceWithIndexV public function testAttributeSanitization() { + $this->mockViewFactory(); $class = new class { public function __toString() @@ -567,12 +591,7 @@ public function __toString() public function testItThrowsAnExceptionForNonExistingAliases() { - $container = new Container; - $container->instance(Application::class, $app = m::mock(Application::class)); - $container->instance(Factory::class, $factory = m::mock(Factory::class)); - $app->shouldReceive('getNamespace')->andReturn('App\\'); - $factory->shouldReceive('exists')->andReturn(false); - Container::setInstance($container); + $this->mockViewFactory(false); $this->expectException(InvalidArgumentException::class); @@ -584,8 +603,8 @@ public function testItThrowsAnExceptionForNonExistingClass() $container = new Container; $container->instance(Application::class, $app = m::mock(Application::class)); $container->instance(Factory::class, $factory = m::mock(Factory::class)); - $app->shouldReceive('getNamespace')->andReturn('App\\'); - $factory->shouldReceive('exists')->andReturn(false); + $app->shouldReceive('getNamespace')->once()->andReturn('App\\'); + $factory->shouldReceive('exists')->twice()->andReturn(false); Container::setInstance($container); $this->expectException(InvalidArgumentException::class); @@ -598,23 +617,26 @@ public function testAttributesTreatedAsPropsAreRemovedFromFinalAttributes() $container = new Container; $container->instance(Application::class, $app = m::mock(Application::class)); $container->instance(Factory::class, $factory = m::mock(Factory::class)); - $app->shouldReceive('getNamespace')->andReturn('App\\'); - $factory->shouldReceive('exists')->andReturn(false); + $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']); $component = m::mock(Component::class); - $component->shouldReceive('withName', 'test'); - $component->shouldReceive('shouldRender')->andReturn(true); - $component->shouldReceive('resolveView')->andReturn(''); - $component->shouldReceive('data')->andReturn([]); - $component->shouldReceive('withAttributes'); + $component->shouldReceive('withName')->with('profile')->once(); + $component->shouldReceive('shouldRender')->once()->andReturn(true); + $component->shouldReceive('resolveView')->once()->andReturn(''); + $component->shouldReceive('data')->once()->andReturn([]); + $component->shouldReceive('withAttributes')->once(); + + $container->bind(TestProfileComponent::class, fn () => $component); $__env = m::mock(\Illuminate\View\Factory::class); - $__env->shouldReceive('getContainer->make')->with(TestProfileComponent::class, ['userId' => 'bar', 'other' => 'ok'])->andReturn($component); - $__env->shouldReceive('startComponent'); - $__env->shouldReceive('renderComponent'); + $__env->shouldReceive('startComponent')->once(); + $__env->shouldReceive('renderComponent')->once();; $template = $this->compiler(['profile' => TestProfileComponent::class])->compileTags(''); $template = $this->compiler->compileString($template); @@ -631,6 +653,7 @@ protected function mockViewFactory($existsSucceeds = true) { $container = new Container; $container->instance(Factory::class, $factory = m::mock(Factory::class)); + $container->alias(Factory::class, 'view'); $factory->shouldReceive('exists')->andReturn($existsSucceeds); Container::setInstance($container); } diff --git a/tests/View/Blade/BladeComponentsTest.php b/tests/View/Blade/BladeComponentsTest.php index d650330dbe8d..cac5feee0c83 100644 --- a/tests/View/Blade/BladeComponentsTest.php +++ b/tests/View/Blade/BladeComponentsTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\View\Blade; +use Illuminate\Container\Container; use Illuminate\View\Component; use Illuminate\View\ComponentAttributeBag; use Illuminate\View\Factory; @@ -17,11 +18,11 @@ public function testComponentsAreCompiled() public function testClassComponentsAreCompiled() { - $this->assertSame(' -getContainer()->make(Test::class, ["foo" => "bar"] + (isset($attributes) ? (array) $attributes->getIterator() : [])); ?> + $this->assertSame(' + "bar"] + (isset($attributes) ? (array) $attributes->getIterator() : [])); ?> withName(\'test\'); ?> shouldRender()): ?> -startComponent($component->resolveView(), $component->data()); ?>', $this->compiler->compileString('@component(\'Test::class\', \'test\', ["foo" => "bar"])')); +startComponent($component->resolveView(), $component->data()); ?>', $this->compiler->compileString('@component(\'Illuminate\Tests\View\Blade\ComponentStub::class\', \'test\', ["foo" => "bar"])')); } public function testEndComponentsAreCompiled() @@ -62,13 +63,23 @@ public function testPropsAreExtractedFromParentAttributesCorrectlyForClassCompon $component->shouldReceive('withName', 'test'); $component->shouldReceive('shouldRender')->andReturn(false); - $__env = m::mock(Factory::class); - $__env->shouldReceive('getContainer->make')->with('Test', ['foo' => 'bar', 'other' => 'ok'])->andReturn($component); + $mock = m::mock(Container::class); + Container::setInstance($mock); - $template = $this->compiler->compileString('@component(\'Test::class\', \'test\', ["foo" => "bar"])'); + $mock->shouldReceive('make')->with(ComponentStub::class, ['foo' => 'bar', 'other' => 'ok'])->andReturn($component); + + $template = $this->compiler->compileString('@component(\'Illuminate\Tests\View\Blade\ComponentStub::class\', \'test\', ["foo" => "bar"])'); ob_start(); eval(" ?> $template config = m::mock(Config::class); $container = new Container; @@ -33,8 +35,6 @@ protected function setUp(): void Container::setInstance($container); Facade::setFacadeApplication($container); - - parent::setUp(); } protected function tearDown(): void @@ -44,6 +44,10 @@ protected function tearDown(): void Facade::clearResolvedInstances(); Facade::setFacadeApplication(null); Container::setInstance(null); + Component::flushCache(); + Component::forgetFactory(); + + parent::tearDown(); } public function testInlineViewsGetCreated() From 616b191c83ef58eb0e531065e456f38d60a14df2 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 6 Oct 2022 14:54:48 +0000 Subject: [PATCH 10/30] Apply fixes from StyleCI --- tests/View/Blade/AbstractBladeTestCase.php | 4 ++-- tests/View/Blade/BladeComponentTagCompilerTest.php | 2 +- tests/View/Blade/BladeComponentsTest.php | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/View/Blade/AbstractBladeTestCase.php b/tests/View/Blade/AbstractBladeTestCase.php index ed61b1703700..7fb0ad1a29b7 100644 --- a/tests/View/Blade/AbstractBladeTestCase.php +++ b/tests/View/Blade/AbstractBladeTestCase.php @@ -4,8 +4,8 @@ use Illuminate\Container\Container; use Illuminate\Filesystem\Filesystem; -use Illuminate\View\Component; use Illuminate\View\Compilers\BladeCompiler; +use Illuminate\View\Component; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -24,7 +24,7 @@ protected function setUp(): void } protected function tearDown(): void - { + { Container::setInstance(null); Component::flushCache(); Component::forgetFactory(); diff --git a/tests/View/Blade/BladeComponentTagCompilerTest.php b/tests/View/Blade/BladeComponentTagCompilerTest.php index a91eeadfa35f..66b87863932b 100644 --- a/tests/View/Blade/BladeComponentTagCompilerTest.php +++ b/tests/View/Blade/BladeComponentTagCompilerTest.php @@ -636,7 +636,7 @@ public function testAttributesTreatedAsPropsAreRemovedFromFinalAttributes() $__env = m::mock(\Illuminate\View\Factory::class); $__env->shouldReceive('startComponent')->once(); - $__env->shouldReceive('renderComponent')->once();; + $__env->shouldReceive('renderComponent')->once(); $template = $this->compiler(['profile' => TestProfileComponent::class])->compileTags(''); $template = $this->compiler->compileString($template); diff --git a/tests/View/Blade/BladeComponentsTest.php b/tests/View/Blade/BladeComponentsTest.php index cac5feee0c83..91490d5bbaec 100644 --- a/tests/View/Blade/BladeComponentsTest.php +++ b/tests/View/Blade/BladeComponentsTest.php @@ -5,7 +5,6 @@ use Illuminate\Container\Container; use Illuminate\View\Component; use Illuminate\View\ComponentAttributeBag; -use Illuminate\View\Factory; use Mockery as m; class BladeComponentsTest extends AbstractBladeTestCase @@ -78,7 +77,7 @@ public function testPropsAreExtractedFromParentAttributesCorrectlyForClassCompon class ComponentStub extends Component { - function render() + public function render() { return ''; } From c70ec4c984c5224e9baba5b0b6dbde5f23a59573 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 6 Oct 2022 17:53:33 +0100 Subject: [PATCH 11/30] Avoids usage of container in regular components --- src/Illuminate/View/AnonymousComponent.php | 11 --- .../View/Compilers/ComponentTagCompiler.php | 1 + src/Illuminate/View/Component.php | 74 ++++++++++++++++++- src/Illuminate/View/DynamicComponent.php | 11 --- tests/View/Blade/AbstractBladeTestCase.php | 1 + .../Blade/BladeComponentTagCompilerTest.php | 2 +- tests/View/Blade/BladeComponentsTest.php | 5 +- 7 files changed, 75 insertions(+), 30 deletions(-) diff --git a/src/Illuminate/View/AnonymousComponent.php b/src/Illuminate/View/AnonymousComponent.php index 8585d76ffa7a..eba64365626b 100644 --- a/src/Illuminate/View/AnonymousComponent.php +++ b/src/Illuminate/View/AnonymousComponent.php @@ -57,15 +57,4 @@ public function data() ['attributes' => $this->attributes] ); } - - /** - * Acts as static factory for the component. - * - * @param array - * @return static - */ - public static function resolve($data) - { - return new static($data['view'], $data['data']); - } } diff --git a/src/Illuminate/View/Compilers/ComponentTagCompiler.php b/src/Illuminate/View/Compilers/ComponentTagCompiler.php index 3870ca95c71e..6a19c289f6e4 100644 --- a/src/Illuminate/View/Compilers/ComponentTagCompiler.php +++ b/src/Illuminate/View/Compilers/ComponentTagCompiler.php @@ -408,6 +408,7 @@ public function partitionDataAndAttributes($class, array $attributes) return [collect($attributes), collect($attributes)]; } + // @todo Reuse paramters... $constructor = (new ReflectionClass($class))->getConstructor(); $parameterNames = $constructor diff --git a/src/Illuminate/View/Component.php b/src/Illuminate/View/Component.php index de7f1cd4f3ef..7bc7b62b3ee9 100644 --- a/src/Illuminate/View/Component.php +++ b/src/Illuminate/View/Component.php @@ -12,6 +12,20 @@ abstract class Component { + /** + * The components resolver used withing views. + * + * @var (\Closure(string, array $data): Component)|null + */ + protected static $componentsResolver; + + /** + * The cache of blade view names, keyed by contents. + * + * @var array + */ + protected static $bladeViewCache = []; + /** * The view factory instance, if any. * @@ -34,11 +48,11 @@ abstract class Component protected static $methodCache = []; /** - * The cache of blade view names, keyed by contents. + * The cache of constructor parameters, keyed by class. * * @var array */ - protected static $bladeViewCache = []; + protected static $constructorParametersCache = []; /** * The properties / methods that should not be exposed to the component. @@ -361,14 +375,68 @@ public static function forgetFactory() static::$factory = null; } + /** + * Forget the component resolver. + * + * @return void + * + * @internal + */ + public static function forgetComponentsResolver() + { + static::$componentsResolver = null; + } + /** * Acts as static factory for the component. * - * @param array + * @param array $data * @return static */ public static function resolve($data) { + if (static::$componentsResolver) { + return call_user_func(static::$componentsResolver, static::class, $data); + } + + $parameters = static::extractConstructorParameters(); + + $dataKeys = array_keys($data); + + if (empty(array_diff($parameters, $dataKeys))) { + return new static(...array_intersect_key($data, array_flip($parameters))); + } + return Container::getInstance()->make(static::class, $data); } + + /** + * Set the callback to be used to resolve components within views. + * + * @param \Closure(string $component, array $data): Component $resolver + * @return void + */ + public static function resolveComponentsUsing($resolver) + { + static::$componentsResolver = $resolver; + } + + /** + * Extract the constructor parameters for the component. + * + * @return array + */ + protected static function extractConstructorParameters() + { + if (! isset(static::$constructorParametersCache[static::class])) { + $class = new ReflectionClass(static::class); + $constructor = $class->getConstructor(); + + static::$constructorParametersCache[static::class] = $constructor + ? collect($constructor->getParameters())->map->getName()->all() + : []; + } + + return static::$constructorParametersCache[static::class]; + } } diff --git a/src/Illuminate/View/DynamicComponent.php b/src/Illuminate/View/DynamicComponent.php index 4bcdf9e82524..cea66e77b304 100644 --- a/src/Illuminate/View/DynamicComponent.php +++ b/src/Illuminate/View/DynamicComponent.php @@ -169,15 +169,4 @@ protected function compiler() return static::$compiler; } - - /** - * Acts as static factory for the component. - * - * @param array - * @return static - */ - public static function resolve($data) - { - return new static($data['component']); - } } diff --git a/tests/View/Blade/AbstractBladeTestCase.php b/tests/View/Blade/AbstractBladeTestCase.php index 7fb0ad1a29b7..93e0739404bf 100644 --- a/tests/View/Blade/AbstractBladeTestCase.php +++ b/tests/View/Blade/AbstractBladeTestCase.php @@ -27,6 +27,7 @@ protected function tearDown(): void { Container::setInstance(null); Component::flushCache(); + Component::forgetComponentsResolver(); Component::forgetFactory(); m::close(); diff --git a/tests/View/Blade/BladeComponentTagCompilerTest.php b/tests/View/Blade/BladeComponentTagCompilerTest.php index 66b87863932b..970127539e62 100644 --- a/tests/View/Blade/BladeComponentTagCompilerTest.php +++ b/tests/View/Blade/BladeComponentTagCompilerTest.php @@ -632,7 +632,7 @@ public function testAttributesTreatedAsPropsAreRemovedFromFinalAttributes() $component->shouldReceive('data')->once()->andReturn([]); $component->shouldReceive('withAttributes')->once(); - $container->bind(TestProfileComponent::class, fn () => $component); + Component::resolveComponentsUsing(fn () => $component); $__env = m::mock(\Illuminate\View\Factory::class); $__env->shouldReceive('startComponent')->once(); diff --git a/tests/View/Blade/BladeComponentsTest.php b/tests/View/Blade/BladeComponentsTest.php index 91490d5bbaec..574eb16a9369 100644 --- a/tests/View/Blade/BladeComponentsTest.php +++ b/tests/View/Blade/BladeComponentsTest.php @@ -62,10 +62,7 @@ public function testPropsAreExtractedFromParentAttributesCorrectlyForClassCompon $component->shouldReceive('withName', 'test'); $component->shouldReceive('shouldRender')->andReturn(false); - $mock = m::mock(Container::class); - Container::setInstance($mock); - - $mock->shouldReceive('make')->with(ComponentStub::class, ['foo' => 'bar', 'other' => 'ok'])->andReturn($component); + Component::resolveComponentsUsing(fn () => $component); $template = $this->compiler->compileString('@component(\'Illuminate\Tests\View\Blade\ComponentStub::class\', \'test\', ["foo" => "bar"])'); From 1ff8c82f67fe804fb016478783c648a9b9dda274 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 6 Oct 2022 16:53:54 +0000 Subject: [PATCH 12/30] Apply fixes from StyleCI --- src/Illuminate/View/Component.php | 2 +- tests/View/Blade/BladeComponentsTest.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Illuminate/View/Component.php b/src/Illuminate/View/Component.php index 7bc7b62b3ee9..ec79bd5b5105 100644 --- a/src/Illuminate/View/Component.php +++ b/src/Illuminate/View/Component.php @@ -15,7 +15,7 @@ abstract class Component /** * The components resolver used withing views. * - * @var (\Closure(string, array $data): Component)|null + * @var (\Closure(string, array): Component)|null */ protected static $componentsResolver; diff --git a/tests/View/Blade/BladeComponentsTest.php b/tests/View/Blade/BladeComponentsTest.php index 574eb16a9369..bb0bbcd4863c 100644 --- a/tests/View/Blade/BladeComponentsTest.php +++ b/tests/View/Blade/BladeComponentsTest.php @@ -2,7 +2,6 @@ namespace Illuminate\Tests\View\Blade; -use Illuminate\Container\Container; use Illuminate\View\Component; use Illuminate\View\ComponentAttributeBag; use Mockery as m; From f8fe3a5815216c41ab0fc5ede845ee5041d60866 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 6 Oct 2022 18:09:05 +0100 Subject: [PATCH 13/30] Removes todo --- src/Illuminate/View/Compilers/ComponentTagCompiler.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/View/Compilers/ComponentTagCompiler.php b/src/Illuminate/View/Compilers/ComponentTagCompiler.php index 6a19c289f6e4..3870ca95c71e 100644 --- a/src/Illuminate/View/Compilers/ComponentTagCompiler.php +++ b/src/Illuminate/View/Compilers/ComponentTagCompiler.php @@ -408,7 +408,6 @@ public function partitionDataAndAttributes($class, array $attributes) return [collect($attributes), collect($attributes)]; } - // @todo Reuse paramters... $constructor = (new ReflectionClass($class))->getConstructor(); $parameterNames = $constructor From b6ab56ff71d1bc854be4b7dc0f391b92bb658421 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 6 Oct 2022 19:05:45 +0100 Subject: [PATCH 14/30] Avoids extra calls checking if a view is expired --- .../View/Engines/CompilerEngine.php | 21 ++++++++++++++++++- src/Illuminate/View/ViewServiceProvider.php | 8 +++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/View/Engines/CompilerEngine.php b/src/Illuminate/View/Engines/CompilerEngine.php index dca6a8710560..328aa265215b 100755 --- a/src/Illuminate/View/Engines/CompilerEngine.php +++ b/src/Illuminate/View/Engines/CompilerEngine.php @@ -16,6 +16,13 @@ class CompilerEngine extends PhpEngine */ protected $compiler; + /** + * The caches paths that were compiled or are not expired, keyed by paths. + * + * @var array + */ + protected $compiledOrNotExpired = []; + /** * A stack of the last compiled templates. * @@ -51,10 +58,12 @@ public function get($path, array $data = []) // If this given view has expired, which means it has simply been edited since // it was last compiled, we will re-compile the views so we can evaluate a // fresh copy of the view. We'll pass the compiler the path of the view. - if ($this->compiler->isExpired($path)) { + if (! isset($this->compiledOrNotExpired[$path]) && $this->compiler->isExpired($path)) { $this->compiler->compile($path); } + $this->compiledOrNotExpired[$path] = true; + // Once we have the path to the compiled file, we will evaluate the paths with // typical PHP just like any other templates. We also keep a stack of views // which have been rendered for right exception messages to be generated. @@ -101,4 +110,14 @@ public function getCompiler() { return $this->compiler; } + + /** + * Forgets the views that were compiled or not expired. + * + * @return void + */ + public function forgetCompiledOrNotExpired() + { + $this->compiledOrNotExpired = []; + } } diff --git a/src/Illuminate/View/ViewServiceProvider.php b/src/Illuminate/View/ViewServiceProvider.php index 67450ed204ec..f3d17b69df57 100755 --- a/src/Illuminate/View/ViewServiceProvider.php +++ b/src/Illuminate/View/ViewServiceProvider.php @@ -112,7 +112,7 @@ public function registerBladeCompiler() */ public function registerEngineResolver() { - $this->app->singleton('view.engine.resolver', function () { + $this->app->singleton('view.engine.resolver', function ($app) { $resolver = new EngineResolver; // Next, we will register the various view engines with the resolver so that the @@ -161,7 +161,11 @@ public function registerPhpEngine($resolver) public function registerBladeEngine($resolver) { $resolver->register('blade', function () { - return new CompilerEngine($this->app['blade.compiler'], $this->app['files']); + $compiler = new CompilerEngine($this->app['blade.compiler'], $this->app['files']); + + $this->app->terminating(fn () => $compiler->forgetCompiledOrNotExpired()); + + return $compiler; }); } } From 7ead72d92d5432ad2c38b1fa4aa489efc7726e0f Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 6 Oct 2022 19:06:38 +0100 Subject: [PATCH 15/30] Removes non needed include --- src/Illuminate/View/ViewServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/View/ViewServiceProvider.php b/src/Illuminate/View/ViewServiceProvider.php index f3d17b69df57..686d1ba5efd9 100755 --- a/src/Illuminate/View/ViewServiceProvider.php +++ b/src/Illuminate/View/ViewServiceProvider.php @@ -112,7 +112,7 @@ public function registerBladeCompiler() */ public function registerEngineResolver() { - $this->app->singleton('view.engine.resolver', function ($app) { + $this->app->singleton('view.engine.resolver', function () { $resolver = new EngineResolver; // Next, we will register the various view engines with the resolver so that the From cdbb41396b51f370940657c99b499226dc665e06 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 7 Oct 2022 12:41:57 +0100 Subject: [PATCH 16/30] Avoids calling creators / composers when is not necessary --- .../View/Concerns/ManagesEvents.php | 47 +++++- tests/View/ViewFactoryTest.php | 157 +++++++++++++++++- 2 files changed, 200 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/View/Concerns/ManagesEvents.php b/src/Illuminate/View/Concerns/ManagesEvents.php index d6cd9d81b113..2ebce0a1af4c 100644 --- a/src/Illuminate/View/Concerns/ManagesEvents.php +++ b/src/Illuminate/View/Concerns/ManagesEvents.php @@ -4,10 +4,25 @@ use Closure; use Illuminate\Contracts\View\View as ViewContract; +use Illuminate\Support\Arr; use Illuminate\Support\Str; trait ManagesEvents { + /** + * If the factory should call the creators. + * + * @var array|true + */ + protected $shouldCallCreators = []; + + /** + * If the factory should call the composers. + * + * @var array|true + */ + protected $shouldCallComposers = []; + /** * Register a view creator event. * @@ -17,6 +32,16 @@ trait ManagesEvents */ public function creator($views, $callback) { + if (is_array($this->shouldCallCreators)) { + if ($views == '*') { + $this->shouldCallCreators = true; + } else { + foreach (Arr::wrap($views) as $view) { + $this->shouldCallCreators[$this->normalizeName($view)] = true; + } + } + } + $creators = []; foreach ((array) $views as $view) { @@ -52,6 +77,16 @@ public function composers(array $composers) */ public function composer($views, $callback) { + if (is_array($this->shouldCallComposers)) { + if ($views == '*') { + $this->shouldCallComposers = true; + } else { + foreach (Arr::wrap($views) as $view) { + $this->shouldCallComposers[$this->normalizeName($view)] = true; + } + } + } + $composers = []; foreach ((array) $views as $view) { @@ -174,7 +209,11 @@ protected function addEventListener($name, $callback) */ public function callComposer(ViewContract $view) { - $this->events->dispatch('composing: '.$view->name(), [$view]); + if ($this->shouldCallComposers === true || isset($this->shouldCallComposers[ + $this->normalizeName((string) $view->name()) + ])) { + $this->events->dispatch('composing: '.$view->name(), [$view]); + } } /** @@ -185,6 +224,10 @@ public function callComposer(ViewContract $view) */ public function callCreator(ViewContract $view) { - $this->events->dispatch('creating: '.$view->name(), [$view]); + if ($this->shouldCallCreators === true || isset($this->shouldCallCreators[ + $this->normalizeName((string) $view->name()) + ])) { + $this->events->dispatch('creating: '.$view->name(), [$view]); + } } } diff --git a/tests/View/ViewFactoryTest.php b/tests/View/ViewFactoryTest.php index dddc7f261464..ead59863bb3c 100755 --- a/tests/View/ViewFactoryTest.php +++ b/tests/View/ViewFactoryTest.php @@ -182,6 +182,151 @@ public function testPrependedExtensionOverridesExistingExtensions() $this->assertSame('baz', key($extensions)); } + public function testCallCreatorsDoesNotDispatchEventsWhenIsNotNecessary() + { + $factory = $this->getFactory(); + $factory->getDispatcher()->shouldReceive('listen')->never(); + $view = m::mock(View::class); + $view->shouldReceive('name')->once()->andReturn('name'); + + $factory->callCreator($view); + } + + public function testCallCreatorsDoesDispatchEventsWhenIsNecessary() + { + $factory = $this->getFactory(); + $factory->getDispatcher() + ->shouldReceive('listen') + ->with('creating: name', m::type(Closure::class)) + ->once(); + + $factory->getDispatcher() + ->shouldReceive('dispatch') + ->with('creating: name', m::type('array')) + ->once(); + + $view = m::mock(View::class); + $view->shouldReceive('name')->twice()->andReturn('name'); + + $factory->creator('name', fn () => true); + + $factory->callCreator($view); + } + + public function testCallCreatorsDoesDispatchEventsWhenIsNecessaryUsingWildcards() + { + $factory = $this->getFactory(); + $factory->getDispatcher() + ->shouldReceive('listen') + ->with('creating: *', m::type(Closure::class)) + ->once(); + + $factory->getDispatcher() + ->shouldReceive('dispatch') + ->with('creating: name', m::type('array')) + ->once(); + + $view = m::mock(View::class); + $view->shouldReceive('name')->once()->andReturn('name'); + + $factory->creator('*', fn () => true); + + $factory->callCreator($view); + } + + public function testCallCreatorsDoesDispatchEventsWhenIsNecessaryUsingNormalizedNames() + { + $factory = $this->getFactory(); + $factory->getDispatcher() + ->shouldReceive('listen') + ->with('creating: components.button', m::type(Closure::class)) + ->once(); + + $factory->getDispatcher() + ->shouldReceive('dispatch') + ->with('creating: components/button', m::type('array')) + ->once(); + + $view = m::mock(View::class); + $view->shouldReceive('name')->twice()->andReturn('components/button'); + + $factory->creator('components.button', fn () => true); + + $factory->callCreator($view); + } + + public function testCallComposersDoesNotDispatchEventsWhenIsNotNecessary() + { + $factory = $this->getFactory(); + + $view = m::mock(View::class); + $view->shouldReceive('name')->once(); + $factory->callComposer($view); + } + + public function testCallComposerDoesDispatchEventsWhenIsNecessary() + { + $factory = $this->getFactory(); + $factory->getDispatcher() + ->shouldReceive('listen') + ->with('composing: name', m::type(Closure::class)) + ->once(); + + $factory->getDispatcher() + ->shouldReceive('dispatch') + ->with('composing: name', m::type('array')) + ->once(); + + $view = m::mock(View::class); + $view->shouldReceive('name')->twice()->andReturn('name'); + + $factory->composer('name', fn () => true); + + $factory->callComposer($view); + } + + public function testCallComposersDoesDispatchEventsWhenIsNecessaryUsingWildcards() + { + $factory = $this->getFactory(); + $factory->getDispatcher() + ->shouldReceive('listen') + ->with('composing: *', m::type(Closure::class)) + ->once(); + + $factory->getDispatcher() + ->shouldReceive('dispatch') + ->with('composing: name', m::type('array')) + ->once(); + + $view = m::mock(View::class); + $view->shouldReceive('name')->once()->andReturn('name'); + + $factory->composer('*', fn () => true); + + $factory->callComposer($view); + } + + public function testCallComposersDoesDispatchEventsWhenIsNecessaryUsingNormalizedNames() + { + $factory = $this->getFactory(); + $factory->getDispatcher() + ->shouldReceive('listen') + ->with('composing: components.button', m::type(Closure::class)) + ->once(); + + $factory->getDispatcher() + ->shouldReceive('dispatch') + ->with('composing: components/button', m::type('array')) + ->once(); + + $view = m::mock(View::class); + $view->shouldReceive('name')->twice()->andReturn('components/button'); + + $factory->composer('components.button', fn () => true); + + $factory->callComposer($view); + } + public function testComposersAreProperlyRegistered() { $factory = $this->getFactory(); @@ -244,7 +389,15 @@ public function testCallComposerCallsProperEvent() { $factory = $this->getFactory(); $view = m::mock(View::class); - $view->shouldReceive('name')->once()->andReturn('name'); + $dispatcher = m::mock(DispatcherContract::class); + $factory->setDispatcher($dispatcher); + + $dispatcher->shouldReceive('listen', m::any())->once(); + + $view->shouldReceive('name')->twice()->andReturn('name'); + + $factory->composer('name', fn () => true); + $factory->getDispatcher()->shouldReceive('dispatch')->once()->with('composing: name', [$view]); $factory->callComposer($view); @@ -589,7 +742,7 @@ public function testExceptionsInSectionsAreThrown() $factory->getEngineResolver()->shouldReceive('resolve')->twice()->andReturn($engine); $factory->getFinder()->shouldReceive('find')->once()->with('layout')->andReturn(__DIR__.'/fixtures/section-exception-layout.php'); $factory->getFinder()->shouldReceive('find')->once()->with('view')->andReturn(__DIR__.'/fixtures/section-exception.php'); - $factory->getDispatcher()->shouldReceive('dispatch')->times(4); + $factory->getDispatcher()->shouldReceive('dispatch')->never(); $factory->make('view')->render(); } From 796592003ab36fd648eab3c6851015d0407bd511 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Fri, 7 Oct 2022 11:42:32 +0000 Subject: [PATCH 17/30] Apply fixes from StyleCI --- src/Illuminate/View/Concerns/ManagesEvents.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/View/Concerns/ManagesEvents.php b/src/Illuminate/View/Concerns/ManagesEvents.php index 2ebce0a1af4c..45aedf0bf600 100644 --- a/src/Illuminate/View/Concerns/ManagesEvents.php +++ b/src/Illuminate/View/Concerns/ManagesEvents.php @@ -212,7 +212,7 @@ public function callComposer(ViewContract $view) if ($this->shouldCallComposers === true || isset($this->shouldCallComposers[ $this->normalizeName((string) $view->name()) ])) { - $this->events->dispatch('composing: '.$view->name(), [$view]); + $this->events->dispatch('composing: '.$view->name(), [$view]); } } From 10bbc6008ebb63853351f14358c4b9fb1adbfa24 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 7 Oct 2022 14:12:27 +0100 Subject: [PATCH 18/30] Minor changes --- src/Illuminate/View/Component.php | 12 +++++++----- src/Illuminate/View/Concerns/ManagesEvents.php | 2 +- tests/View/ViewFactoryTest.php | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/View/Component.php b/src/Illuminate/View/Component.php index ec79bd5b5105..3cee09263b81 100644 --- a/src/Illuminate/View/Component.php +++ b/src/Illuminate/View/Component.php @@ -22,14 +22,14 @@ abstract class Component /** * The cache of blade view names, keyed by contents. * - * @var array + * @var array */ protected static $bladeViewCache = []; /** * The view factory instance, if any. * - * @var \Illuminate\Contracts\View\Factory + * @var \Illuminate\Contracts\View\Factory|null */ protected static $factory; @@ -50,7 +50,7 @@ abstract class Component /** * The cache of constructor parameters, keyed by class. * - * @var array + * @var array> */ protected static $constructorParametersCache = []; @@ -327,7 +327,7 @@ public function shouldRender() } /** - * Flush the cache of components. + * Flush the components cache. * * @return void */ @@ -339,7 +339,7 @@ public static function flushCache() } /** - * Set the component's view. + * Get the evaluated view contents for the given view. * * @param string|null $view * @param \Illuminate\Contracts\Support\Arrayable|array $data @@ -415,6 +415,8 @@ public static function resolve($data) * * @param \Closure(string $component, array $data): Component $resolver * @return void + * + * @internal */ public static function resolveComponentsUsing($resolver) { diff --git a/src/Illuminate/View/Concerns/ManagesEvents.php b/src/Illuminate/View/Concerns/ManagesEvents.php index 45aedf0bf600..119033c912d5 100644 --- a/src/Illuminate/View/Concerns/ManagesEvents.php +++ b/src/Illuminate/View/Concerns/ManagesEvents.php @@ -210,7 +210,7 @@ protected function addEventListener($name, $callback) public function callComposer(ViewContract $view) { if ($this->shouldCallComposers === true || isset($this->shouldCallComposers[ - $this->normalizeName((string) $view->name()) + $this->normalizeName($view->name()) ])) { $this->events->dispatch('composing: '.$view->name(), [$view]); } diff --git a/tests/View/ViewFactoryTest.php b/tests/View/ViewFactoryTest.php index ead59863bb3c..2ee0e45087ab 100755 --- a/tests/View/ViewFactoryTest.php +++ b/tests/View/ViewFactoryTest.php @@ -260,7 +260,7 @@ public function testCallComposersDoesNotDispatchEventsWhenIsNotNecessary() $factory = $this->getFactory(); $view = m::mock(View::class); - $view->shouldReceive('name')->once(); + $view->shouldReceive('name')->once()->andReturn('name'); $factory->callComposer($view); } From 547f31d543297d991f0f94726a2d7b7a44634186 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 7 Oct 2022 14:14:27 +0100 Subject: [PATCH 19/30] Improves tests --- tests/View/ViewFactoryTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/View/ViewFactoryTest.php b/tests/View/ViewFactoryTest.php index 2ee0e45087ab..232c4edaac21 100755 --- a/tests/View/ViewFactoryTest.php +++ b/tests/View/ViewFactoryTest.php @@ -186,6 +186,8 @@ public function testCallCreatorsDoesNotDispatchEventsWhenIsNotNecessary() { $factory = $this->getFactory(); $factory->getDispatcher()->shouldReceive('listen')->never(); + $factory->getDispatcher()->shouldReceive('dispatch')->never(); + $view = m::mock(View::class); $view->shouldReceive('name')->once()->andReturn('name'); @@ -258,9 +260,12 @@ public function testCallCreatorsDoesDispatchEventsWhenIsNecessaryUsingNormalized public function testCallComposersDoesNotDispatchEventsWhenIsNotNecessary() { $factory = $this->getFactory(); + $factory->getDispatcher()->shouldReceive('listen')->never(); + $factory->getDispatcher()->shouldReceive('dispatch')->never(); $view = m::mock(View::class); $view->shouldReceive('name')->once()->andReturn('name'); + $factory->callComposer($view); } From f0e397dd5d44252b69b9a8a684085d534438edef Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 7 Oct 2022 15:11:18 +0100 Subject: [PATCH 20/30] Adds more tests --- src/Illuminate/View/Component.php | 4 +- tests/View/ComponentTest.php | 155 +++++++++++++++++++++++++++++- 2 files changed, 154 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/View/Component.php b/src/Illuminate/View/Component.php index 3cee09263b81..6c7f781199ad 100644 --- a/src/Illuminate/View/Component.php +++ b/src/Illuminate/View/Component.php @@ -333,9 +333,9 @@ public function shouldRender() */ public static function flushCache() { - static::$propertyCache = []; - static::$methodCache = []; static::$bladeViewCache = []; + static::$methodCache = []; + static::$propertyCache = []; } /** diff --git a/tests/View/ComponentTest.php b/tests/View/ComponentTest.php index bdc990a0c3cd..a5ce2c7d93db 100644 --- a/tests/View/ComponentTest.php +++ b/tests/View/ComponentTest.php @@ -17,6 +17,7 @@ class ComponentTest extends TestCase { protected $viewFactory; + protected $config; protected function setUp(): void @@ -60,12 +61,22 @@ public function testInlineViewsGetCreated() $this->assertSame('__components::c6327913fef3fca4518bcd7df1d0ff630758e241', $component->resolveView()); } - public function testRegularViewsGetReturned() + public function testRegularViewsGetReturnedUsingViewHelper() + { + $view = m::mock(View::class); + $this->viewFactory->shouldReceive('make')->once()->with('alert', [], [])->andReturn($view); + + $component = new TestRegularViewComponentUsingViewHelper; + + $this->assertSame($view, $component->resolveView()); + } + + public function testRegularViewsGetReturnedUsingViewMethod() { $view = m::mock(View::class); $this->viewFactory->shouldReceive('make')->once()->with('alert', [], [])->andReturn($view); - $component = new TestRegularViewComponent; + $component = new TestRegularViewComponentUsingViewMethod; $this->assertSame($view, $component->resolveView()); } @@ -89,6 +100,114 @@ public function testHtmlablesGetReturned() $this->assertInstanceOf(Htmlable::class, $view); $this->assertSame('

Hello foo

', $view->toHtml()); } + + public function testResolveComponentsUsing() + { + $component = new TestInlineViewComponent; + + Component::resolveComponentsUsing(fn () => $component); + + $this->assertSame($component, Component::resolve('bar')); + } + + public function testBladeViewCacheWithRegularViewNameViewComponent() + { + $component = new TestRegularViewNameViewComponent; + + $this->viewFactory->shouldReceive('exists')->twice()->andReturn(true); + + $this->assertSame('alert', $component->resolveView()); + $this->assertSame('alert', $component->resolveView()); + $this->assertSame('alert', $component->resolveView()); + $this->assertSame('alert', $component->resolveView()); + + $cache = (fn () => $component::$bladeViewCache)->call($component); + $this->assertSame([$component::class.'::alert' => 'alert'], $cache); + + $component::flushCache(); + + $cache = (fn () => $component::$bladeViewCache)->call($component); + $this->assertSame([], $cache); + + $this->assertSame('alert', $component->resolveView()); + $this->assertSame('alert', $component->resolveView()); + $this->assertSame('alert', $component->resolveView()); + $this->assertSame('alert', $component->resolveView()); + } + + public function testBladeViewCacheWithInlineViewComponent() + { + $component = new TestInlineViewComponent; + + $this->viewFactory->shouldReceive('exists')->twice()->andReturn(false); + + $this->config->shouldReceive('get')->twice()->with('view.compiled')->andReturn('/tmp'); + + $this->viewFactory->shouldReceive('addNamespace') + ->with('__components', '/tmp') + ->twice(); + + $compiledViewName = '__components::c6327913fef3fca4518bcd7df1d0ff630758e241'; + $contents = '::Hello {{ $title }}'; + $cacheKey = $component::class.$contents; + + $this->assertSame($compiledViewName, $component->resolveView()); + $this->assertSame($compiledViewName, $component->resolveView()); + $this->assertSame($compiledViewName, $component->resolveView()); + $this->assertSame($compiledViewName, $component->resolveView()); + + $cache = (fn () => $component::$bladeViewCache)->call($component); + $this->assertSame([$cacheKey => $compiledViewName], $cache); + + $component::flushCache(); + + $cache = (fn () => $component::$bladeViewCache)->call($component); + $this->assertSame([], $cache); + + $this->assertSame($compiledViewName, $component->resolveView()); + $this->assertSame($compiledViewName, $component->resolveView()); + $this->assertSame($compiledViewName, $component->resolveView()); + $this->assertSame($compiledViewName, $component->resolveView()); + } + + public function testBladeViewCacheWithInlineViewComponentWhereRenderDependsOnProps() + { + $componentA = new TestInlineViewComponentWhereRenderDependsOnProps('A'); + $componentB = new TestInlineViewComponentWhereRenderDependsOnProps('B'); + + $this->viewFactory->shouldReceive('exists')->twice()->andReturn(false); + + $this->config->shouldReceive('get')->twice()->with('view.compiled')->andReturn('/tmp'); + + $this->viewFactory->shouldReceive('addNamespace') + ->with('__components', '/tmp') + ->twice(); + + $compiledViewNameA = '__components::6dcd4ce23d88e2ee9568ba546c007c63d9131c1b'; + $compiledViewNameB = '__components::ae4f281df5a5d0ff3cad6371f76d5c29b6d953ec'; + $cacheAKey = $componentA::class.'::A'; + $cacheBKey = $componentB::class.'::B'; + + $this->assertSame($compiledViewNameA, $componentA->resolveView()); + $this->assertSame($compiledViewNameA, $componentA->resolveView()); + $this->assertSame($compiledViewNameB, $componentB->resolveView()); + $this->assertSame($compiledViewNameB, $componentB->resolveView()); + + $cacheA = (fn () => $componentA::$bladeViewCache)->call($componentA); + $cacheB = (fn () => $componentB::$bladeViewCache)->call($componentB); + $this->assertSame($cacheA, $cacheB); + $this->assertSame([ + $cacheAKey => $compiledViewNameA, + $cacheBKey => $compiledViewNameB, + ], $cacheA); + + $componentA::flushCache(); + + $cacheA = (fn () => $componentA::$bladeViewCache)->call($componentA); + $cacheB = (fn () => $componentB::$bladeViewCache)->call($componentB); + $this->assertSame($cacheA, $cacheB); + $this->assertSame([], $cacheA); + } } class TestInlineViewComponent extends Component @@ -106,7 +225,22 @@ public function render() } } -class TestRegularViewComponent extends Component +class TestInlineViewComponentWhereRenderDependsOnProps extends Component +{ + public $content; + + public function __construct($content) + { + $this->content = $content; + } + + public function render() + { + return $this->content; + } +} + +class TestRegularViewComponentUsingViewHelper extends Component { public $title; @@ -121,6 +255,21 @@ public function render() } } +class TestRegularViewComponentUsingViewMethod extends Component +{ + public $title; + + public function __construct($title = 'foo') + { + $this->title = $title; + } + + public function render() + { + return $this->view('alert'); + } +} + class TestRegularViewNameViewComponent extends Component { public $title; From 83595e43b9e4aa14ad65177b54ec5bcb58909b7c Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 7 Oct 2022 15:25:42 +0100 Subject: [PATCH 21/30] More tests --- tests/View/ComponentTest.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/View/ComponentTest.php b/tests/View/ComponentTest.php index a5ce2c7d93db..2d298f55650b 100644 --- a/tests/View/ComponentTest.php +++ b/tests/View/ComponentTest.php @@ -208,6 +208,22 @@ public function testBladeViewCacheWithInlineViewComponentWhereRenderDependsOnPro $this->assertSame($cacheA, $cacheB); $this->assertSame([], $cacheA); } + + public function testFactoryGetsSharedBetweenComponents() + { + $regular = new TestRegularViewNameViewComponent; + $inline = new TestInlineViewComponent; + + $getFactory = fn ($component) => (fn () => $component->factory())->call($component); + + $this->assertSame($this->viewFactory, $getFactory($regular)); + + Container::getInstance()->instance('view', 'foo'); + $this->assertSame($this->viewFactory, $getFactory($inline)); + + Component::forgetFactory(); + $this->assertNotSame($this->viewFactory, $getFactory($inline)); + } } class TestInlineViewComponent extends Component From c8595136fe110ce226a5e3d017fb2e5ae9c5cb15 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 7 Oct 2022 17:18:27 +0100 Subject: [PATCH 22/30] More tests --- tests/View/ComponentTest.php | 64 ++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/View/ComponentTest.php b/tests/View/ComponentTest.php index 2d298f55650b..5f9bfe66f0ba 100644 --- a/tests/View/ComponentTest.php +++ b/tests/View/ComponentTest.php @@ -4,6 +4,7 @@ use Illuminate\Config\Repository as Config; use Illuminate\Container\Container; +use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Contracts\Support\Htmlable; use Illuminate\Contracts\View\Factory as FactoryContract; use Illuminate\Support\Facades\Facade; @@ -101,6 +102,31 @@ public function testHtmlablesGetReturned() $this->assertSame('

Hello foo

', $view->toHtml()); } + public function testResolveWithUnresolvableDependency() + { + $this->expectException(BindingResolutionException::class); + $this->expectExceptionMessage('Unresolvable dependency resolving'); + + TestInlineViewComponentWhereRenderDependsOnProps::resolve([]); + } + + public function testResolveDependenciesWithoutContainer() + { + $component = TestInlineViewComponentWhereRenderDependsOnProps::resolve(['content' => 'foo']); + $this->assertSame('foo', $component->render()); + } + + public function testResolveDependenciesWithContainerIfNecessary() + { + $component = TestInlineViewComponentWithContainerDependencies::resolve([]); + $this->assertSame($this->viewFactory, $component->dependency); + + $component = TestInlineViewComponentWithContainerDependenciesAndProps::resolve(['content' => 'foo']); + $this->assertSame($this->viewFactory, $component->dependency); + $this->assertSame('foo', $component->render()); + + } + public function testResolveComponentsUsing() { $component = new TestInlineViewComponent; @@ -241,6 +267,44 @@ public function render() } } +class TestInlineViewComponentWithContainerDependencies extends Component +{ + public $dependency; + + public function __construct(FactoryContract $dependency) { + $this->dependency = $dependency; + } + + public function render() + { + return ''; + } +} + +class TestInlineViewComponentWithContainerDependenciesAndProps extends Component +{ + public $content; + public $dependency; + + public function __construct(FactoryContract $dependency, $content) { + $this->content = $content; + $this->dependency = $dependency; + } + + public function render() + { + return $this->content; + } +} + +class TestInlineViewComponentWithoutDependencies extends Component +{ + public function render() + { + return 'alert'; + } +} + class TestInlineViewComponentWhereRenderDependsOnProps extends Component { public $content; From df1ac305a748997bfabae436a50e576a48a8cc8c Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Fri, 7 Oct 2022 16:18:43 +0000 Subject: [PATCH 23/30] Apply fixes from StyleCI --- tests/View/ComponentTest.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/View/ComponentTest.php b/tests/View/ComponentTest.php index 5f9bfe66f0ba..483cbe4fb0dc 100644 --- a/tests/View/ComponentTest.php +++ b/tests/View/ComponentTest.php @@ -124,7 +124,6 @@ public function testResolveDependenciesWithContainerIfNecessary() $component = TestInlineViewComponentWithContainerDependenciesAndProps::resolve(['content' => 'foo']); $this->assertSame($this->viewFactory, $component->dependency); $this->assertSame('foo', $component->render()); - } public function testResolveComponentsUsing() @@ -271,7 +270,8 @@ class TestInlineViewComponentWithContainerDependencies extends Component { public $dependency; - public function __construct(FactoryContract $dependency) { + public function __construct(FactoryContract $dependency) + { $this->dependency = $dependency; } @@ -286,7 +286,8 @@ class TestInlineViewComponentWithContainerDependenciesAndProps extends Component public $content; public $dependency; - public function __construct(FactoryContract $dependency, $content) { + public function __construct(FactoryContract $dependency, $content) + { $this->content = $content; $this->dependency = $dependency; } From 68e1d3c2584d62140d2cb33f876a250bf63dca40 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 7 Oct 2022 17:20:36 +0100 Subject: [PATCH 24/30] Flushes parameters cache too --- src/Illuminate/View/Component.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/View/Component.php b/src/Illuminate/View/Component.php index 6c7f781199ad..99e77d503fcd 100644 --- a/src/Illuminate/View/Component.php +++ b/src/Illuminate/View/Component.php @@ -334,6 +334,7 @@ public function shouldRender() public static function flushCache() { static::$bladeViewCache = []; + static::$constructorParametersCache = p[] static::$methodCache = []; static::$propertyCache = []; } From 6b69ca2351c52b627905b1b7143d61f3822618e4 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 7 Oct 2022 17:28:45 +0100 Subject: [PATCH 25/30] Makes forget static --- src/Illuminate/View/Component.php | 2 +- src/Illuminate/View/ViewServiceProvider.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/View/Component.php b/src/Illuminate/View/Component.php index 99e77d503fcd..c555e375df04 100644 --- a/src/Illuminate/View/Component.php +++ b/src/Illuminate/View/Component.php @@ -334,7 +334,7 @@ public function shouldRender() public static function flushCache() { static::$bladeViewCache = []; - static::$constructorParametersCache = p[] + static::$constructorParametersCache = []; static::$methodCache = []; static::$propertyCache = []; } diff --git a/src/Illuminate/View/ViewServiceProvider.php b/src/Illuminate/View/ViewServiceProvider.php index 686d1ba5efd9..c40c3b9fc5bb 100755 --- a/src/Illuminate/View/ViewServiceProvider.php +++ b/src/Illuminate/View/ViewServiceProvider.php @@ -163,7 +163,9 @@ public function registerBladeEngine($resolver) $resolver->register('blade', function () { $compiler = new CompilerEngine($this->app['blade.compiler'], $this->app['files']); - $this->app->terminating(fn () => $compiler->forgetCompiledOrNotExpired()); + $this->app->terminating(static function () use ($compiler) { + $compiler->forgetCompiledOrNotExpired(); + }); return $compiler; }); From 84948d5f24cc35936e38d1e6fd29f878f7da33c4 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 7 Oct 2022 17:46:12 +0100 Subject: [PATCH 26/30] More tests --- src/Illuminate/View/Engines/CompilerEngine.php | 2 +- tests/View/ViewCompilerEngineTest.php | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/View/Engines/CompilerEngine.php b/src/Illuminate/View/Engines/CompilerEngine.php index 328aa265215b..a4cfc8b7738e 100755 --- a/src/Illuminate/View/Engines/CompilerEngine.php +++ b/src/Illuminate/View/Engines/CompilerEngine.php @@ -26,7 +26,7 @@ class CompilerEngine extends PhpEngine /** * A stack of the last compiled templates. * - * @var array + * @var array */ protected $lastCompiled = []; diff --git a/tests/View/ViewCompilerEngineTest.php b/tests/View/ViewCompilerEngineTest.php index f826e9656ac3..a194a64b7150 100755 --- a/tests/View/ViewCompilerEngineTest.php +++ b/tests/View/ViewCompilerEngineTest.php @@ -39,6 +39,22 @@ public function testViewsAreNotRecompiledIfTheyAreNotExpired() ', $results); } + public function testThatViewsAreNotAskTwiceIfTheyAreExpired() + { + $engine = $this->getEngine(); + $engine->getCompiler()->shouldReceive('getCompiledPath')->with(__DIR__.'/fixtures/foo.php')->andReturn(__DIR__.'/fixtures/basic.php'); + $engine->getCompiler()->shouldReceive('isExpired')->twice()->andReturn(false); + $engine->getCompiler()->shouldReceive('compile')->never(); + + $engine->get(__DIR__.'/fixtures/foo.php'); + $engine->get(__DIR__.'/fixtures/foo.php'); + $engine->get(__DIR__.'/fixtures/foo.php'); + + $engine->forgetCompiledOrNotExpired(); + + $engine->get(__DIR__.'/fixtures/foo.php'); + } + protected function getEngine() { return new CompilerEngine(m::mock(CompilerInterface::class), new Filesystem); From cb2fc237ccadf3302b417b522415570bafafcc5b Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 7 Oct 2022 17:47:22 +0100 Subject: [PATCH 27/30] Docs --- src/Illuminate/View/Engines/CompilerEngine.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/View/Engines/CompilerEngine.php b/src/Illuminate/View/Engines/CompilerEngine.php index a4cfc8b7738e..fa746cf97181 100755 --- a/src/Illuminate/View/Engines/CompilerEngine.php +++ b/src/Illuminate/View/Engines/CompilerEngine.php @@ -19,14 +19,14 @@ class CompilerEngine extends PhpEngine /** * The caches paths that were compiled or are not expired, keyed by paths. * - * @var array + * @var array */ protected $compiledOrNotExpired = []; /** * A stack of the last compiled templates. * - * @var array + * @var array */ protected $lastCompiled = []; From 0631ccb0c90b6efb141818140688549d8b09d144 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 7 Oct 2022 19:12:07 +0100 Subject: [PATCH 28/30] More tests --- tests/View/ComponentTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/View/ComponentTest.php b/tests/View/ComponentTest.php index 483cbe4fb0dc..c2b1eb771408 100644 --- a/tests/View/ComponentTest.php +++ b/tests/View/ComponentTest.php @@ -114,6 +114,23 @@ public function testResolveDependenciesWithoutContainer() { $component = TestInlineViewComponentWhereRenderDependsOnProps::resolve(['content' => 'foo']); $this->assertSame('foo', $component->render()); + + $component = new class extends Component + { + public function __construct($a = null, $b = null) + { + $this->content = $a.$b; + } + + public function render() + { + return $this->content; + } + }; + + $component = $component::resolve(['a' => 'a', 'b' => 'b']); + $component = $component::resolve(['b' => 'b', 'a' => 'a']); + $this->assertSame('ab', $component->render()); } public function testResolveDependenciesWithContainerIfNecessary() @@ -284,6 +301,7 @@ public function render() class TestInlineViewComponentWithContainerDependenciesAndProps extends Component { public $content; + public $dependency; public function __construct(FactoryContract $dependency, $content) From 993f698acc883c652b5a851bddd8bff64fd17b08 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 12 Oct 2022 09:36:59 -0500 Subject: [PATCH 29/30] formatting --- src/Illuminate/View/Component.php | 164 +++++++++--------- .../View/Concerns/ManagesEvents.php | 4 +- .../View/Engines/CompilerEngine.php | 14 +- 3 files changed, 91 insertions(+), 91 deletions(-) diff --git a/src/Illuminate/View/Component.php b/src/Illuminate/View/Component.php index c555e375df04..931b9b8af655 100644 --- a/src/Illuminate/View/Component.php +++ b/src/Illuminate/View/Component.php @@ -13,18 +13,25 @@ abstract class Component { /** - * The components resolver used withing views. + * The properties / methods that should not be exposed to the component. * - * @var (\Closure(string, array): Component)|null + * @var array */ - protected static $componentsResolver; + protected $except = []; /** - * The cache of blade view names, keyed by contents. + * The component alias name. * - * @var array + * @var string */ - protected static $bladeViewCache = []; + public $componentName; + + /** + * The component attributes. + * + * @var \Illuminate\View\ComponentAttributeBag + */ + public $attributes; /** * The view factory instance, if any. @@ -33,6 +40,20 @@ abstract class Component */ protected static $factory; + /** + * The component resolver callback. + * + * @var (\Closure(string, array): Component)|null + */ + protected static $componentsResolver; + + /** + * The cache of blade view names, keyed by contents. + * + * @var array + */ + protected static $bladeViewCache = []; + /** * The cache of public property names, keyed by class. * @@ -55,32 +76,54 @@ abstract class Component protected static $constructorParametersCache = []; /** - * The properties / methods that should not be exposed to the component. + * Get the view / view contents that represent the component. * - * @var array + * @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\Support\Htmlable|\Closure|string */ - protected $except = []; + abstract public function render(); /** - * The component alias name. + * Resolve the component instance with the given data. * - * @var string + * @param array $data + * @return static */ - public $componentName; + public static function resolve($data) + { + if (static::$componentsResolver) { + return call_user_func(static::$componentsResolver, static::class, $data); + } - /** - * The component attributes. - * - * @var \Illuminate\View\ComponentAttributeBag - */ - public $attributes; + $parameters = static::extractConstructorParameters(); + + $dataKeys = array_keys($data); + + if (empty(array_diff($parameters, $dataKeys))) { + return new static(...array_intersect_key($data, array_flip($parameters))); + } + + return Container::getInstance()->make(static::class, $data); + } /** - * Get the view / view contents that represent the component. + * Extract the constructor parameters for the component. * - * @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\Support\Htmlable|\Closure|string + * @return array */ - abstract public function render(); + protected static function extractConstructorParameters() + { + if (! isset(static::$constructorParametersCache[static::class])) { + $class = new ReflectionClass(static::class); + + $constructor = $class->getConstructor(); + + static::$constructorParametersCache[static::class] = $constructor + ? collect($constructor->getParameters())->map->getName()->all() + : []; + } + + return static::$constructorParametersCache[static::class]; + } /** * Resolve the Blade view or view file that should be used when rendering the component. @@ -105,8 +148,7 @@ public function resolveView() return $view instanceof Closure ? function (array $data = []) use ($view, $resolver) { return $resolver($view($data)); - } - : $resolver($view); + } : $resolver($view); } /** @@ -326,19 +368,6 @@ public function shouldRender() return true; } - /** - * Flush the components cache. - * - * @return void - */ - public static function flushCache() - { - static::$bladeViewCache = []; - static::$constructorParametersCache = []; - static::$methodCache = []; - static::$propertyCache = []; - } - /** * Get the evaluated view contents for the given view. * @@ -353,7 +382,7 @@ public function view($view, $data = [], $mergeData = []) } /** - * Get the view factory. + * Get the view factory instance. * * @return \Illuminate\Contracts\View\Factory */ @@ -367,52 +396,42 @@ protected function factory() } /** - * Forget the component's factory. + * Flush the component's cached state. * * @return void */ - public static function forgetFactory() + public static function flushCache() { - static::$factory = null; + static::$bladeViewCache = []; + static::$constructorParametersCache = []; + static::$methodCache = []; + static::$propertyCache = []; } /** - * Forget the component resolver. + * Forget the component's factory instance. * * @return void - * - * @internal */ - public static function forgetComponentsResolver() + public static function forgetFactory() { - static::$componentsResolver = null; + static::$factory = null; } /** - * Acts as static factory for the component. + * Forget the component's resolver callback. * - * @param array $data - * @return static + * @return void + * + * @internal */ - public static function resolve($data) + public static function forgetComponentsResolver() { - if (static::$componentsResolver) { - return call_user_func(static::$componentsResolver, static::class, $data); - } - - $parameters = static::extractConstructorParameters(); - - $dataKeys = array_keys($data); - - if (empty(array_diff($parameters, $dataKeys))) { - return new static(...array_intersect_key($data, array_flip($parameters))); - } - - return Container::getInstance()->make(static::class, $data); + static::$componentsResolver = null; } /** - * Set the callback to be used to resolve components within views. + * Set the callback that should be used to resolve components within views. * * @param \Closure(string $component, array $data): Component $resolver * @return void @@ -423,23 +442,4 @@ public static function resolveComponentsUsing($resolver) { static::$componentsResolver = $resolver; } - - /** - * Extract the constructor parameters for the component. - * - * @return array - */ - protected static function extractConstructorParameters() - { - if (! isset(static::$constructorParametersCache[static::class])) { - $class = new ReflectionClass(static::class); - $constructor = $class->getConstructor(); - - static::$constructorParametersCache[static::class] = $constructor - ? collect($constructor->getParameters())->map->getName()->all() - : []; - } - - return static::$constructorParametersCache[static::class]; - } } diff --git a/src/Illuminate/View/Concerns/ManagesEvents.php b/src/Illuminate/View/Concerns/ManagesEvents.php index 119033c912d5..10c3bda31190 100644 --- a/src/Illuminate/View/Concerns/ManagesEvents.php +++ b/src/Illuminate/View/Concerns/ManagesEvents.php @@ -10,14 +10,14 @@ trait ManagesEvents { /** - * If the factory should call the creators. + * An array of views and whether they have registered "creators". * * @var array|true */ protected $shouldCallCreators = []; /** - * If the factory should call the composers. + * An array of views and whether they have registered "composers". * * @var array|true */ diff --git a/src/Illuminate/View/Engines/CompilerEngine.php b/src/Illuminate/View/Engines/CompilerEngine.php index fa746cf97181..5b0234a5d13b 100755 --- a/src/Illuminate/View/Engines/CompilerEngine.php +++ b/src/Illuminate/View/Engines/CompilerEngine.php @@ -17,18 +17,18 @@ class CompilerEngine extends PhpEngine protected $compiler; /** - * The caches paths that were compiled or are not expired, keyed by paths. + * A stack of the last compiled templates. * - * @var array + * @var array */ - protected $compiledOrNotExpired = []; + protected $lastCompiled = []; /** - * A stack of the last compiled templates. + * The view paths that were compiled or are not expired, keyed by the path. * - * @var array + * @var array */ - protected $lastCompiled = []; + protected $compiledOrNotExpired = []; /** * Create a new compiler engine instance. @@ -112,7 +112,7 @@ public function getCompiler() } /** - * Forgets the views that were compiled or not expired. + * Clear the cache of views that were compiled or not expired. * * @return void */ From 0745b1c89c70d3b93c8bcd3b230007ead7bc65e2 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 12 Oct 2022 14:37:30 +0000 Subject: [PATCH 30/30] Apply fixes from StyleCI --- src/Illuminate/View/Component.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/View/Component.php b/src/Illuminate/View/Component.php index 931b9b8af655..4f5186692ae7 100644 --- a/src/Illuminate/View/Component.php +++ b/src/Illuminate/View/Component.php @@ -148,7 +148,8 @@ public function resolveView() return $view instanceof Closure ? function (array $data = []) use ($view, $resolver) { return $resolver($view($data)); - } : $resolver($view); + } + : $resolver($view); } /**