Repository: symfony/ux (php)

Issue:
[Icons] Website's search seems to be broken
Hello,

On https://ux.symfony.com/icons?query&set=mdi when I make a search like "b", I have the following JavaScript error:

![Image](https://github.com/user-attachments/assets/4b969a7f-a21d-49d0-923a-583929304b73)

![Image](https://github.com/user-attachments/assets/55743233-50b9-4b83-b9e6-0fe920c00442)

How to reproduce:
 
- Go to https://ux.symfony.com/icons?query&set=mdi 
- type "b" inside the search's input
- new icons appears then I have the error
- I can't search for a second letter like "br"

Cheers!

Pull request:
[LiveComponent] Fix query string decoding with no `=`
| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no
| Docs?         | no
| Issues        | Fix #2826 (when released and a website update)
| License       | MIT

The root cause of #2826 is in fact a bug  in LiveComponent ... that i'm very surprised no one already experienced oO

In url_utils: fromQueryString missed a nullcheck. Triggering a TypeError.

`
?foo&bar=
`


Patch:
diff --git a/src/Dropzone/src/DependencyInjection/DropzoneExtension.php b/src/Dropzone/src/DependencyInjection/DropzoneExtension.php
index 697d1249..a40bc5ff 100644
--- a/src/Dropzone/src/DependencyInjection/DropzoneExtension.php
+++ b/src/Dropzone/src/DependencyInjection/DropzoneExtension.php
@@ -25,7 +25,7 @@ use Symfony\UX\Dropzone\Form\DropzoneType;
  */
 class DropzoneExtension extends Extension implements PrependExtensionInterface
 {
-    public function prepend(ContainerBuilder $container): void
+    public function prepend(ContainerBuilder $container)
     {
         // Register the Dropzone form theme if TwigBundle is available
         $bundles = $container->getParameter('kernel.bundles');
diff --git a/src/LiveComponent/assets/dist/live_controller.js b/src/LiveComponent/assets/dist/live_controller.js
index 41177512..8591ab0f 100644
--- a/src/LiveComponent/assets/dist/live_controller.js
+++ b/src/LiveComponent/assets/dist/live_controller.js
@@ -2798,7 +2798,7 @@ function fromQueryString(search) {
     const entries = search.split('&').map((i) => i.split('='));
     const data = {};
     entries.forEach(([key, value]) => {
-        value = decodeURIComponent(value.replace(/\+/g, '%20'));
+        value = decodeURIComponent(String(value || '').replace(/\+/g, '%20'));
         if (!key.includes('[')) {
             data[key] = value;
         }
diff --git a/src/LiveComponent/assets/src/url_utils.ts b/src/LiveComponent/assets/src/url_utils.ts
index 93fedacf..c988d7ef 100644
--- a/src/LiveComponent/assets/src/url_utils.ts
+++ b/src/LiveComponent/assets/src/url_utils.ts
@@ -104,7 +104,7 @@ function fromQueryString(search: string) {
     const data: any = {};
 
     entries.forEach(([key, value]) => {
-        value = decodeURIComponent(value.replace(/\+/g, '%20'));
+        value = decodeURIComponent(String(value || '').replace(/\+/g, '%20'));
 
         if (!key.includes('[')) {
             data[key] = value;
diff --git a/src/StimulusBundle/src/DependencyInjection/StimulusExtension.php b/src/StimulusBundle/src/DependencyInjection/StimulusExtension.php
index 97a7ec2f..aa0a1060 100644
--- a/src/StimulusBundle/src/DependencyInjection/StimulusExtension.php
+++ b/src/StimulusBundle/src/DependencyInjection/StimulusExtension.php
@@ -43,7 +43,7 @@ final class StimulusExtension extends Extension implements PrependExtensionInter
         }
     }
 
-    public function prepend(ContainerBuilder $container): void
+    public function prepend(ContainerBuilder $container)
     {
         if (!$this->isAssetMapperAvailable($container)) {
             return;
diff --git a/src/Translator/src/DependencyInjection/UxTranslatorExtension.php b/src/Translator/src/DependencyInjection/UxTranslatorExtension.php
index 3ebfbe51..9ba3a029 100644
--- a/src/Translator/src/DependencyInjection/UxTranslatorExtension.php
+++ b/src/Translator/src/DependencyInjection/UxTranslatorExtension.php
@@ -27,7 +27,7 @@ use Symfony\Component\HttpKernel\DependencyInjection\Extension;
  */
 class UxTranslatorExtension extends Extension implements PrependExtensionInterface
 {
-    public function load(array $configs, ContainerBuilder $container): void
+    public function load(array $configs, ContainerBuilder $container)
     {
         $configuration = new Configuration();
         $config = $this->processConfiguration($configuration, $configs);
@@ -46,7 +46,7 @@ class UxTranslatorExtension extends Extension implements PrependExtensionInterfa
         }
     }
 
-    public function prepend(ContainerBuilder $container): void
+    public function prepend(ContainerBuilder $container)
     {
         if (!$this->isAssetMapperAvailable($container)) {
             return;


Test patch:
diff --git a/src/Chartjs/tests/Kernel/TwigAppKernel.php b/src/Chartjs/tests/Kernel/TwigAppKernel.php
index 76de3dbd..5d629bd2 100644
--- a/src/Chartjs/tests/Kernel/TwigAppKernel.php
+++ b/src/Chartjs/tests/Kernel/TwigAppKernel.php
@@ -31,7 +31,7 @@ class TwigAppKernel extends Kernel
         return [new FrameworkBundle(), new TwigBundle(), new StimulusBundle(), new ChartjsBundle()];
     }
 
-    public function registerContainerConfiguration(LoaderInterface $loader): void
+    public function registerContainerConfiguration(LoaderInterface $loader)
     {
         $loader->load(function (ContainerBuilder $container) {
             $container->loadFromExtension('framework', ['secret' => '$ecret', 'test' => true, 'http_method_override' => false]);
diff --git a/src/Cropperjs/tests/Kernel/EmptyAppKernel.php b/src/Cropperjs/tests/Kernel/EmptyAppKernel.php
index a4a7c89b..71926a98 100644
--- a/src/Cropperjs/tests/Kernel/EmptyAppKernel.php
+++ b/src/Cropperjs/tests/Kernel/EmptyAppKernel.php
@@ -29,7 +29,7 @@ class EmptyAppKernel extends Kernel
         return [new CropperjsBundle()];
     }
 
-    public function registerContainerConfiguration(LoaderInterface $loader): void
+    public function registerContainerConfiguration(LoaderInterface $loader)
     {
     }
 }
diff --git a/src/Cropperjs/tests/Kernel/FrameworkAppKernel.php b/src/Cropperjs/tests/Kernel/FrameworkAppKernel.php
index 63535d3a..1944eace 100644
--- a/src/Cropperjs/tests/Kernel/FrameworkAppKernel.php
+++ b/src/Cropperjs/tests/Kernel/FrameworkAppKernel.php
@@ -31,7 +31,7 @@ class FrameworkAppKernel extends Kernel
         return [new FrameworkBundle(), new CropperjsBundle()];
     }
 
-    public function registerContainerConfiguration(LoaderInterface $loader): void
+    public function registerContainerConfiguration(LoaderInterface $loader)
     {
         $loader->load(function (ContainerBuilder $container) {
             $frameworkConfig = [
diff --git a/src/Cropperjs/tests/Kernel/TwigAppKernel.php b/src/Cropperjs/tests/Kernel/TwigAppKernel.php
index 9b3a9ce8..95cad520 100644
--- a/src/Cropperjs/tests/Kernel/TwigAppKernel.php
+++ b/src/Cropperjs/tests/Kernel/TwigAppKernel.php
@@ -34,7 +34,7 @@ class TwigAppKernel extends Kernel
         return [new FrameworkBundle(), new TwigBundle(), new CropperjsBundle()];
     }
 
-    public function registerContainerConfiguration(LoaderInterface $loader): void
+    public function registerContainerConfiguration(LoaderInterface $loader)
     {
         $loader->load(function (ContainerBuilder $container) {
             $frameworkConfig = [
diff --git a/src/Dropzone/tests/Kernel/EmptyAppKernel.php b/src/Dropzone/tests/Kernel/EmptyAppKernel.php
index e1541b77..f5734e0a 100644
--- a/src/Dropzone/tests/Kernel/EmptyAppKernel.php
+++ b/src/Dropzone/tests/Kernel/EmptyAppKernel.php
@@ -29,7 +29,7 @@ class EmptyAppKernel extends Kernel
         return [new DropzoneBundle()];
     }
 
-    public function registerContainerConfiguration(LoaderInterface $loader): void
+    public function registerContainerConfiguration(LoaderInterface $loader)
     {
     }
 }
diff --git a/src/Dropzone/tests/Kernel/FrameworkAppKernel.php b/src/Dropzone/tests/Kernel/FrameworkAppKernel.php
index 0198c674..4ab07836 100644
--- a/src/Dropzone/tests/Kernel/FrameworkAppKernel.php
+++ b/src/Dropzone/tests/Kernel/FrameworkAppKernel.php
@@ -31,7 +31,7 @@ class FrameworkAppKernel extends Kernel
         return [new FrameworkBundle(), new DropzoneBundle()];
     }
 
-    public function registerContainerConfiguration(LoaderInterface $loader): void
+    public function registerContainerConfiguration(LoaderInterface $loader)
     {
         $loader->load(function (ContainerBuilder $container) {
             $container->loadFromExtension('framework', ['secret' => '$ecret', 'test' => true]);
diff --git a/src/Dropzone/tests/Kernel/TwigAppKernel.php b/src/Dropzone/tests/Kernel/TwigAppKernel.php
index 0da830a9..8970425a 100644
--- a/src/Dropzone/tests/Kernel/TwigAppKernel.php
+++ b/src/Dropzone/tests/Kernel/TwigAppKernel.php
@@ -34,7 +34,7 @@ class TwigAppKernel extends Kernel
         return [new FrameworkBundle(), new TwigBundle(), new DropzoneBundle()];
     }
 
-    public function registerContainerConfiguration(LoaderInterface $loader): void
+    public function registerContainerConfiguration(LoaderInterface $loader)
     {
         $loader->load(function (ContainerBuilder $container) {
             $container->loadFromExtension('framework', ['secret' => '$ecret', 'test' => true, 'http_method_override' => false]);
diff --git a/src/LazyImage/tests/BlurHash/BlurHashTest.php b/src/LazyImage/tests/BlurHash/BlurHashTest.php
index 1304d1bb..60baaf42 100644
--- a/src/LazyImage/tests/BlurHash/BlurHashTest.php
+++ b/src/LazyImage/tests/BlurHash/BlurHashTest.php
@@ -47,7 +47,7 @@ class BlurHashTest extends TestCase
     public function testWithCustomGetImageContent(): void
     {
         $kernel = new class('test', true) extends TwigAppKernel {
-            public function registerContainerConfiguration(LoaderInterface $loader): void
+            public function registerContainerConfiguration(LoaderInterface $loader)
             {
                 parent::registerContainerConfiguration($loader);
 
@@ -99,7 +99,7 @@ class BlurHashTest extends TestCase
     public function testEnsureCacheIsUsedWhenConfigured()
     {
         $kernel = new class('test', true) extends TwigAppKernel {
-            public function registerContainerConfiguration(LoaderInterface $loader): void
+            public function registerContainerConfiguration(LoaderInterface $loader)
             {
                 parent::registerContainerConfiguration($loader);
 
@@ -154,7 +154,7 @@ class BlurHashTest extends TestCase
     public function testCreateDataUriThumbnailWithCache()
     {
         $kernel = new class('test', true) extends TwigAppKernel {
-            public function registerContainerConfiguration(LoaderInterface $loader): void
+            public function registerContainerConfiguration(LoaderInterface $loader)
             {
                 parent::registerContainerConfiguration($loader);
 
diff --git a/src/LazyImage/tests/Kernel/EmptyAppKernel.php b/src/LazyImage/tests/Kernel/EmptyAppKernel.php
index 03394163..4d215d31 100644
--- a/src/LazyImage/tests/Kernel/EmptyAppKernel.php
+++ b/src/LazyImage/tests/Kernel/EmptyAppKernel.php
@@ -29,7 +29,7 @@ class EmptyAppKernel extends Kernel
         return [new LazyImageBundle()];
     }
 
-    public function registerContainerConfiguration(LoaderInterface $loader): void
+    public function registerContainerConfiguration(LoaderInterface $loader)
     {
     }
 }
diff --git a/src/LazyImage/tests/Kernel/FrameworkAppKernel.php b/src/LazyImage/tests/Kernel/FrameworkAppKernel.php
index dac1de81..2dd2e4b7 100644
--- a/src/LazyImage/tests/Kernel/FrameworkAppKernel.php
+++ b/src/LazyImage/tests/Kernel/FrameworkAppKernel.php
@@ -31,7 +31,7 @@ class FrameworkAppKernel extends Kernel
         return [new FrameworkBundle(), new LazyImageBundle()];
     }
 
-    public function registerContainerConfiguration(LoaderInterface $loader): void
+    public function registerContainerConfiguration(LoaderInterface $loader)
     {
         $loader->load(function (ContainerBuilder $container) {
             $container->loadFromExtension('framework', ['secret' => '$ecret', 'test' => true]);
diff --git a/src/LazyImage/tests/Kernel/TwigAppKernel.php b/src/LazyImage/tests/Kernel/TwigAppKernel.php
index 51869833..185798d3 100644
--- a/src/LazyImage/tests/Kernel/TwigAppKernel.php
+++ b/src/LazyImage/tests/Kernel/TwigAppKernel.php
@@ -32,7 +32,7 @@ class TwigAppKernel extends Kernel
         return [new FrameworkBundle(), new TwigBundle(), new LazyImageBundle()];
     }
 
-    public function registerContainerConfiguration(LoaderInterface $loader): void
+    public function registerContainerConfiguration(LoaderInterface $loader)
     {
         $loader->load(function (ContainerBuilder $container) {
             $container->loadFromExtension('framework', ['secret' => '$ecret', 'test' => true, 'http_method_override' => false]);
diff --git a/src/LiveComponent/assets/test/url_utils.test.ts b/src/LiveComponent/assets/test/url_utils.test.ts
index fcb711f5..bce7f86b 100644
--- a/src/LiveComponent/assets/test/url_utils.test.ts
+++ b/src/LiveComponent/assets/test/url_utils.test.ts
@@ -115,6 +115,32 @@ describe('url_utils', () => {
                 expect(urlUtils.search).toEqual('');
             });
         });
+
+        describe('fromQueryString', () => {
+            const urlUtils: UrlUtils = new UrlUtils(window.location.href);
+
+            beforeEach(() => {
+                // Reset search before each test
+                urlUtils.search = '';
+            });
+
+            it('parses a query string with value', () => {
+                urlUtils.search = '?param1=value1';
+                expect(urlUtils.get('param1')).toEqual('value1');
+            });
+
+            it('parses a query string with empty value', () => {
+                urlUtils.search = '?param1=&param2=value2';
+                expect(urlUtils.get('param1')).toEqual('');
+                expect(urlUtils.get('param2')).toEqual('value2');
+            });
+
+            it('parses a query string without equal sign', () => {
+                urlUtils.search = '?param1&param2=value2';
+                expect(urlUtils.get('param1')).toEqual('');
+                expect(urlUtils.get('param2')).toEqual('value2');
+            });
+        });
     });
 
     describe('HistoryStrategy', () => {
diff --git a/src/Notify/tests/Kernel/TwigAppKernel.php b/src/Notify/tests/Kernel/TwigAppKernel.php
index 59910fd3..de278e8f 100644
--- a/src/Notify/tests/Kernel/TwigAppKernel.php
+++ b/src/Notify/tests/Kernel/TwigAppKernel.php
@@ -36,7 +36,7 @@ class TwigAppKernel extends Kernel
         yield new NotifyBundle();
     }
 
-    public function registerContainerConfiguration(LoaderInterface $loader): void
+    public function registerContainerConfiguration(LoaderInterface $loader)
     {
         $loader->load(function (ContainerBuilder $container) {
             $container->loadFromExtension('framework', ['secret' => '$ecret', 'test' => true, 'http_method_override' => false]);
diff --git a/src/React/tests/Kernel/TwigAppKernel.php b/src/React/tests/Kernel/TwigAppKernel.php
index 762c1bcb..cb1027dd 100644
--- a/src/React/tests/Kernel/TwigAppKernel.php
+++ b/src/React/tests/Kernel/TwigAppKernel.php
@@ -31,7 +31,7 @@ class TwigAppKernel extends Kernel
         return [new FrameworkBundle(), new StimulusBundle(), new TwigBundle(), new ReactBundle()];
     }
 
-    public function registerContainerConfiguration(LoaderInterface $loader): void
+    public function registerContainerConfiguration(LoaderInterface $loader)
     {
         $loader->load(function (ContainerBuilder $container) {
             $container->loadFromExtension('framework', ['secret' => '$ecret', 'test' => true, 'http_method_override' => false]);
diff --git a/src/Svelte/tests/Kernel/TwigAppKernel.php b/src/Svelte/tests/Kernel/TwigAppKernel.php
index 29c82c6f..e61a6386 100644
--- a/src/Svelte/tests/Kernel/TwigAppKernel.php
+++ b/src/Svelte/tests/Kernel/TwigAppKernel.php
@@ -32,7 +32,7 @@ class TwigAppKernel extends Kernel
         return [new StimulusBundle(), new FrameworkBundle(), new TwigBundle(), new SvelteBundle()];
     }
 
-    public function registerContainerConfiguration(LoaderInterface $loader): void
+    public function registerContainerConfiguration(LoaderInterface $loader)
     {
         $loader->load(function (ContainerBuilder $container) {
             $container->loadFromExtension('framework', ['secret' => '$ecret', 'test' => true, 'http_method_override' => false]);
diff --git a/src/Translator/tests/Kernel/EmptyAppKernel.php b/src/Translator/tests/Kernel/EmptyAppKernel.php
index b92997f6..648443bc 100644
--- a/src/Translator/tests/Kernel/EmptyAppKernel.php
+++ b/src/Translator/tests/Kernel/EmptyAppKernel.php
@@ -29,7 +29,7 @@ class EmptyAppKernel extends Kernel
         return [new UxTranslatorBundle()];
     }
 
-    public function registerContainerConfiguration(LoaderInterface $loader): void
+    public function registerContainerConfiguration(LoaderInterface $loader)
     {
     }
 }
diff --git a/src/Translator/tests/Kernel/FrameworkAppKernel.php b/src/Translator/tests/Kernel/FrameworkAppKernel.php
index 3e61e1ef..98c0ca6a 100644
--- a/src/Translator/tests/Kernel/FrameworkAppKernel.php
+++ b/src/Translator/tests/Kernel/FrameworkAppKernel.php
@@ -31,7 +31,7 @@ class FrameworkAppKernel extends Kernel
         return [new FrameworkBundle(), new UxTranslatorBundle()];
     }
 
-    public function registerContainerConfiguration(LoaderInterface $loader): void
+    public function registerContainerConfiguration(LoaderInterface $loader)
     {
         $loader->load(function (ContainerBuilder $container) {
             $container->loadFromExtension('framework', [
diff --git a/src/Vue/tests/Kernel/TwigAppKernel.php b/src/Vue/tests/Kernel/TwigAppKernel.php
index 54a5be3b..5221f7e3 100644
--- a/src/Vue/tests/Kernel/TwigAppKernel.php
+++ b/src/Vue/tests/Kernel/TwigAppKernel.php
@@ -32,7 +32,7 @@ class TwigAppKernel extends Kernel
         return [new StimulusBundle(), new FrameworkBundle(), new TwigBundle(), new VueBundle()];
     }
 
-    public function registerContainerConfiguration(LoaderInterface $loader): void
+    public function registerContainerConfiguration(LoaderInterface $loader)
     {
         $loader->load(function (ContainerBuilder $container) {
             $container->loadFromExtension('framework', ['secret' => '$ecret', 'test' => true, 'http_method_override' => false]);