Loading core/lib/Drupal/Core/Template/ComponentsTwigExtension.php +26 −0 Original line number Diff line number Diff line Loading @@ -8,6 +8,7 @@ use Drupal\Core\Theme\Component\ComponentValidator; use Drupal\Core\Theme\ComponentPluginManager; use Twig\Extension\AbstractExtension; use Twig\Template; use Twig\TwigFunction; /** Loading Loading @@ -129,6 +130,31 @@ protected function doValidateProps(array $context, string $component_id): bool { catch (ComponentNotFoundException $e) { throw new InvalidComponentException($e->getMessage(), $e->getCode(), $e); } catch (InvalidComponentException $e) { // Inspect the backtrace to find the template that embedded the component. $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 20); $calling_template = 'Unknown Template'; foreach ($backtrace as $trace) { if (isset($trace['object']) && $trace['object'] instanceof Template) { $template_name = $trace['object']->getTemplateName(); // Skip the component's own template to find its parent container. if ($template_name === $component_id) { continue; } $calling_template = $template_name; break; } } throw new InvalidComponentException( sprintf('Component validation failed in Twig file [%s] for component [%s]: %s', $calling_template, $component_id, $e->getMessage()), $e->getCode(), $e ); } } } core/tests/Drupal/KernelTests/Components/ComponentRenderTest.php +36 −0 Original line number Diff line number Diff line Loading @@ -6,6 +6,7 @@ use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Render\Component\Exception\InvalidComponentDataException; use Drupal\Core\Render\Component\Exception\InvalidComponentException; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Template\Attribute; use Drupal\Core\Theme\ComponentPluginManager; Loading Loading @@ -158,6 +159,41 @@ public function testRenderPropValidation(): void { } } /** * Ensures nested invalid props report the calling parent template. */ public function testRenderNestedPropValidationIncludesCallingTemplate(): void { // Omit ctaText so my-banner still validates, but nested my-cta does not. $build = [ '#type' => 'component', '#component' => 'sdc_test:my-banner', '#props' => [ 'heading' => 'I am a banner', 'ctaHref' => 'https://www.example.org', 'ctaTarget' => '', ], '#slots' => [ 'banner_body' => [ '#plain_text' => 'Banner body', ], ], ]; try { $this->renderComponentRenderArray($build); $this->fail('Invalid nested prop did not cause an exception'); } catch (\Throwable $e) { // Find the "InvalidComponentException": while ($e && !$e instanceof InvalidComponentException) { $e = $e->getPrevious(); } $this->assertInstanceOf(InvalidComponentException::class, $e); // Check, that the error message contains the error calling template, as // well as the component, whose props actually fail validation: $this->assertStringContainsString('Component validation failed in Twig file [sdc_test:my-banner] for component [sdc_test:my-cta]:', $e->getMessage()); } } /** * Ensure fuzzy coercing of arrays and objects works properly. */ Loading Loading
core/lib/Drupal/Core/Template/ComponentsTwigExtension.php +26 −0 Original line number Diff line number Diff line Loading @@ -8,6 +8,7 @@ use Drupal\Core\Theme\Component\ComponentValidator; use Drupal\Core\Theme\ComponentPluginManager; use Twig\Extension\AbstractExtension; use Twig\Template; use Twig\TwigFunction; /** Loading Loading @@ -129,6 +130,31 @@ protected function doValidateProps(array $context, string $component_id): bool { catch (ComponentNotFoundException $e) { throw new InvalidComponentException($e->getMessage(), $e->getCode(), $e); } catch (InvalidComponentException $e) { // Inspect the backtrace to find the template that embedded the component. $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 20); $calling_template = 'Unknown Template'; foreach ($backtrace as $trace) { if (isset($trace['object']) && $trace['object'] instanceof Template) { $template_name = $trace['object']->getTemplateName(); // Skip the component's own template to find its parent container. if ($template_name === $component_id) { continue; } $calling_template = $template_name; break; } } throw new InvalidComponentException( sprintf('Component validation failed in Twig file [%s] for component [%s]: %s', $calling_template, $component_id, $e->getMessage()), $e->getCode(), $e ); } } }
core/tests/Drupal/KernelTests/Components/ComponentRenderTest.php +36 −0 Original line number Diff line number Diff line Loading @@ -6,6 +6,7 @@ use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Render\Component\Exception\InvalidComponentDataException; use Drupal\Core\Render\Component\Exception\InvalidComponentException; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Template\Attribute; use Drupal\Core\Theme\ComponentPluginManager; Loading Loading @@ -158,6 +159,41 @@ public function testRenderPropValidation(): void { } } /** * Ensures nested invalid props report the calling parent template. */ public function testRenderNestedPropValidationIncludesCallingTemplate(): void { // Omit ctaText so my-banner still validates, but nested my-cta does not. $build = [ '#type' => 'component', '#component' => 'sdc_test:my-banner', '#props' => [ 'heading' => 'I am a banner', 'ctaHref' => 'https://www.example.org', 'ctaTarget' => '', ], '#slots' => [ 'banner_body' => [ '#plain_text' => 'Banner body', ], ], ]; try { $this->renderComponentRenderArray($build); $this->fail('Invalid nested prop did not cause an exception'); } catch (\Throwable $e) { // Find the "InvalidComponentException": while ($e && !$e instanceof InvalidComponentException) { $e = $e->getPrevious(); } $this->assertInstanceOf(InvalidComponentException::class, $e); // Check, that the error message contains the error calling template, as // well as the component, whose props actually fail validation: $this->assertStringContainsString('Component validation failed in Twig file [sdc_test:my-banner] for component [sdc_test:my-cta]:', $e->getMessage()); } } /** * Ensure fuzzy coercing of arrays and objects works properly. */ Loading