diff --git a/src/TwigComponent/src/ComponentFactory.php b/src/TwigComponent/src/ComponentFactory.php index 02cf0cecf14..3877889f888 100644 --- a/src/TwigComponent/src/ComponentFactory.php +++ b/src/TwigComponent/src/ComponentFactory.php @@ -217,6 +217,32 @@ private function isAnonymousComponent(string $name): bool */ private function throwUnknownComponentException(string $name): void { - throw new \InvalidArgumentException(sprintf('Unknown component "%s". The registered components are: %s. And no matching anonymous component template was found', $name, implode(', ', array_keys($this->config)))); + $message = sprintf('Unknown component "%s".', $name); + $lowerName = strtolower($name); + $nameLength = \strlen($lowerName); + $alternatives = []; + + foreach (array_keys($this->config) as $type) { + $lowerType = strtolower($type); + $lev = levenshtein($lowerName, $lowerType); + + if ($lev <= $nameLength / 3 || str_contains($lowerType, $lowerName)) { + $alternatives[] = $type; + } + } + + if ($alternatives) { + if (1 === \count($alternatives)) { + $message .= ' Did you mean this: "'; + } else { + $message .= ' Did you mean one of these: "'; + } + + $message .= implode('", "', $alternatives).'"?'; + } else { + $message .= ' And no matching anonymous component template was found.'; + } + + throw new \InvalidArgumentException($message); } } diff --git a/src/TwigComponent/tests/Integration/ComponentFactoryTest.php b/src/TwigComponent/tests/Integration/ComponentFactoryTest.php index edbab16788a..19e6f889418 100644 --- a/src/TwigComponent/tests/Integration/ComponentFactoryTest.php +++ b/src/TwigComponent/tests/Integration/ComponentFactoryTest.php @@ -152,17 +152,24 @@ public function testCanGetMetadataForSameComponentWithDifferentName(): void public function testCannotGetConfigByNameForNonRegisteredComponent(): void { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessageMatches('/^Unknown component "invalid"\. The registered components are:.* component_a/'); + $this->expectExceptionMessage('Unknown component "tabl". Did you mean this: "table"?'); - $this->factory()->metadataFor('invalid'); + $this->factory()->metadataFor('tabl'); } - public function testCannotGetInvalidComponent(): void + /** + * @testWith ["tabl", "Unknown component \"tabl\". Did you mean this: \"table\"?"] + * ["Basic", "Unknown component \"Basic\". Did you mean this: \"BasicComponent\"?"] + * ["basic", "Unknown component \"basic\". Did you mean this: \"BasicComponent\"?"] + * ["with", "Unknown component \"with\". Did you mean one of these: \"with_attributes\", \"with_exposed_variables\", \"WithSlots\"?"] + * ["anonAnon", "Unknown component \"anonAnon\". And no matching anonymous component template was found."] + */ + public function testCannotGetInvalidComponent(string $name, string $expectedExceptionMessage): void { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessageMatches('/^Unknown component "invalid"\. The registered components are:.* component_a/'); + $this->expectExceptionMessage($expectedExceptionMessage); - $this->factory()->get('invalid'); + $this->factory()->get($name); } public function testInputPropsStoredOnMountedComponent(): void