Repository: symfony/ux (php)

Issue:
[TwigComponent][Icons] Inconsistency between allowed attributes
Twig components allows to provide an integer as attibute values and casts it, while `UX:Icon` doesn't allow it.

Valid example:
```twig
<twig:Test a="1" b="{{ 2 }}">
  test
</twig:Test>
```
Currently invalid:
```twig
<twig:UX:Icon a="1" b="{{ 2 }}" />
```

Pull request description:
[Icons] Add support for int/float attribute to `ux_icon` function
| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no <!-- please update src/**/CHANGELOG.md files -->
| Issues        | Fix #2148 <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead -->
| License       | MIT

<!--
Replace this notice by a description of your feature/bugfix.
This will help reviewers and should be a good start for the documentation.

Additionally (see https://symfony.com/releases):
 - Always add tests and ensure they pass.
 - For new features, provide some code snippets to help understand usage.
 - Features and deprecations must be submitted against branch main.
 - Changelog entry should follow https://symfony.com/doc/current/contributing/code/conventions.html#writing-a-changelog-entry
 - Never break backward compatibility (see https://symfony.com/bc).
-->

I'm not really sure why do we have checks on attributes values, do you remember why @kbond? 🙏🏻 

Patch:
diff --git a/src/Icons/CHANGELOG.md b/src/Icons/CHANGELOG.md
index 71eb76d0..8ff2aad1 100644
--- a/src/Icons/CHANGELOG.md
+++ b/src/Icons/CHANGELOG.md
@@ -3,6 +3,7 @@
 ## 2.20.0
 
 -   Add `aliases` configuration option to define icon alternative names.
+-   Add support for `int` and `float` attribute values in `<twig:ux:icon />`.
 
 ## 2.19.0
 
diff --git a/src/Icons/src/Icon.php b/src/Icons/src/Icon.php
index 41883a98..e7a9e73b 100644
--- a/src/Icons/src/Icon.php
+++ b/src/Icons/src/Icon.php
@@ -176,7 +176,7 @@ final class Icon implements \Stringable
     }
 
     /**
-     * @param array<string, string|bool> $attributes
+     * @param array<string, string|bool|int|float> $attributes
      */
     public function withAttributes(array $attributes): self
     {
@@ -189,8 +189,8 @@ final class Icon implements \Stringable
                 throw new \InvalidArgumentException(\sprintf('Invalid attribute name "%s".', $name));
             }
 
-            if (!\is_string($value) && !\is_bool($value)) {
-                throw new \InvalidArgumentException(\sprintf('Invalid value type for attribute "%s". Boolean or string allowed, "%s" provided. ', $name, get_debug_type($value)));
+            if (!\is_string($value) && !\is_bool($value) && !\is_int($value) && !\is_float($value)) {
+                throw new \InvalidArgumentException(\sprintf('Invalid value type for attribute "%s". Boolean, string, int or float allowed, "%s" provided. ', $name, get_debug_type($value)));
             }
         }
 
diff --git a/src/LiveComponent/assets/dist/Backend/Backend.d.ts b/src/LiveComponent/assets/dist/Backend/Backend.d.ts
index c664be92..53e5daae 100644
--- a/src/LiveComponent/assets/dist/Backend/Backend.d.ts
+++ b/src/LiveComponent/assets/dist/Backend/Backend.d.ts
@@ -13,7 +13,6 @@ export interface BackendInterface {
     }, files: {
         [key: string]: FileList;
     }): BackendRequest;
-    updateCsrfToken(csrfToken: string): void;
 }
 export interface BackendAction {
     name: string;
@@ -29,5 +28,4 @@ export default class implements BackendInterface {
     }, files: {
         [key: string]: FileList;
     }): BackendRequest;
-    updateCsrfToken(csrfToken: string): void;
 }
diff --git a/src/LiveComponent/assets/dist/Backend/RequestBuilder.d.ts b/src/LiveComponent/assets/dist/Backend/RequestBuilder.d.ts
index 8a29af3b..2cee6c2f 100644
--- a/src/LiveComponent/assets/dist/Backend/RequestBuilder.d.ts
+++ b/src/LiveComponent/assets/dist/Backend/RequestBuilder.d.ts
@@ -2,7 +2,7 @@ import type { BackendAction, ChildrenFingerprints } from './Backend';
 export default class {
     private url;
     private method;
-    private csrfToken;
+    private readonly csrfToken;
     constructor(url: string, method?: 'get' | 'post', csrfToken?: string | null);
     buildRequest(props: any, actions: BackendAction[], updated: {
         [key: string]: any;
@@ -15,5 +15,4 @@ export default class {
         fetchOptions: RequestInit;
     };
     private willDataFitInUrl;
-    updateCsrfToken(csrfToken: string): void;
 }
diff --git a/src/LiveComponent/assets/dist/live_controller.js b/src/LiveComponent/assets/dist/live_controller.js
index 22b6b68f..05af8986 100644
--- a/src/LiveComponent/assets/dist/live_controller.js
+++ b/src/LiveComponent/assets/dist/live_controller.js
@@ -2052,9 +2052,6 @@ class Component {
                 return response;
             }
             this.processRerender(html, backendResponse);
-            if (this.element.dataset.liveCsrfValue) {
-                this.backend.updateCsrfToken(this.element.dataset.liveCsrfValue);
-            }
             this.backendRequest = null;
             thisPromiseResolve(backendResponse);
             if (this.isRequestPending) {
@@ -2328,9 +2325,6 @@ class RequestBuilder {
         const urlEncodedJsonData = new URLSearchParams(propsJson + updatedJson + childrenJson + propsFromParentJson).toString();
         return (urlEncodedJsonData + params.toString()).length < 1500;
     }
-    updateCsrfToken(csrfToken) {
-        this.csrfToken = csrfToken;
-    }
 }
 
 class Backend {
@@ -2341,9 +2335,6 @@ class Backend {
         const { url, fetchOptions } = this.requestBuilder.buildRequest(props, actions, updated, children, updatedPropsFromParent, files);
         return new BackendRequest(fetch(url, fetchOptions), actions.map((backendAction) => backendAction.name), Object.keys(updated));
     }
-    updateCsrfToken(csrfToken) {
-        this.requestBuilder.updateCsrfToken(csrfToken);
-    }
 }
 
 class StimulusElementDriver {
diff --git a/src/LiveComponent/assets/src/Backend/Backend.ts b/src/LiveComponent/assets/src/Backend/Backend.ts
index b3d019a4..b7bb6445 100644
--- a/src/LiveComponent/assets/src/Backend/Backend.ts
+++ b/src/LiveComponent/assets/src/Backend/Backend.ts
@@ -15,7 +15,6 @@ export interface BackendInterface {
         updatedPropsFromParent: { [key: string]: any },
         files: { [key: string]: FileList }
     ): BackendRequest;
-    updateCsrfToken(csrfToken: string): void;
 }
 
 export interface BackendAction {
@@ -53,8 +52,4 @@ export default class implements BackendInterface {
             Object.keys(updated)
         );
     }
-
-    updateCsrfToken(csrfToken: string) {
-        this.requestBuilder.updateCsrfToken(csrfToken);
-    }
 }
diff --git a/src/LiveComponent/assets/src/Backend/RequestBuilder.ts b/src/LiveComponent/assets/src/Backend/RequestBuilder.ts
index b9bd2fc1..328f0647 100644
--- a/src/LiveComponent/assets/src/Backend/RequestBuilder.ts
+++ b/src/LiveComponent/assets/src/Backend/RequestBuilder.ts
@@ -3,7 +3,7 @@ import type { BackendAction, ChildrenFingerprints } from './Backend';
 export default class {
     private url: string;
     private method: 'get' | 'post';
-    private csrfToken: string | null;
+    private readonly csrfToken: string | null;
 
     constructor(url: string, method: 'get' | 'post' = 'post', csrfToken: string | null = null) {
         this.url = url;
@@ -117,8 +117,4 @@ export default class {
         // if the URL gets remotely close to 2000 chars, it may not fit
         return (urlEncodedJsonData + params.toString()).length < 1500;
     }
-
-    updateCsrfToken(csrfToken: string) {
-        this.csrfToken = csrfToken;
-    }
 }
diff --git a/src/LiveComponent/assets/src/Component/index.ts b/src/LiveComponent/assets/src/Component/index.ts
index 18c6a5de..28807c57 100644
--- a/src/LiveComponent/assets/src/Component/index.ts
+++ b/src/LiveComponent/assets/src/Component/index.ts
@@ -329,11 +329,6 @@ export default class Component {
 
             this.processRerender(html, backendResponse);
 
-            // Store updated csrf token
-            if (this.element.dataset.liveCsrfValue) {
-                this.backend.updateCsrfToken(this.element.dataset.liveCsrfValue);
-            }
-
             // finally resolve this promise
             this.backendRequest = null;
             thisPromiseResolve(backendResponse);


Test patch:
diff --git a/src/Icons/tests/Fixtures/templates/template1.html.twig b/src/Icons/tests/Fixtures/templates/template1.html.twig
index 98247030..096a3d53 100644
--- a/src/Icons/tests/Fixtures/templates/template1.html.twig
+++ b/src/Icons/tests/Fixtures/templates/template1.html.twig
@@ -2,8 +2,10 @@
     <li id="first">{{ ux_icon('user', {class: 'h-8 w-8'}) }}</li>
     <li id="second">{{ ux_icon('user', {"aria-label": "AriaLabel"}) }}</li>
     <li id="third">{{ ux_icon('sub:check', {'data-action': 'string "with" quotes'}) }}</li>
+    <li id="fourth">{{ ux_icon('sub:check', { height: 24, width: "24" }) }}</li>
     <li id="fifth"><twig:UX:Icon name="user" class="h-8 w-8" /></li>
     <li id="sixth"><twig:ux:icon name="sub:check" aria-labelledby="foo" /></li>
     <li id="seventh"><twig:Ux:Icon :name="'sub:'~'check'" /></li>
     <li id="eighth">{{ ux_icon('iconamoon:3d-duotone') }}</li>
+    <li id="ninth">{{ ux_icon('iconamoon:3d-duotone', { height: 24, width: "24" }) }}</li>
 </ul>
diff --git a/src/Icons/tests/Integration/RenderIconsInTwigTest.php b/src/Icons/tests/Integration/RenderIconsInTwigTest.php
index 0522a916..8789f29a 100644
--- a/src/Icons/tests/Integration/RenderIconsInTwigTest.php
+++ b/src/Icons/tests/Integration/RenderIconsInTwigTest.php
@@ -29,10 +29,12 @@ final class RenderIconsInTwigTest extends KernelTestCase
                 <li id="first"><svg viewBox="0 0 24 24" fill="currentColor" class="h-8 w-8" aria-hidden="true"><path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z" clip-rule="evenodd"></path></svg></li>
                 <li id="second"><svg viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6" aria-label="AriaLabel"><path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z" clip-rule="evenodd"></path></svg></li>
                 <li id="third"><svg viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6" data-action="string &quot;with&quot; quotes" aria-hidden="true"><path fill-rule="evenodd" d="M19.916 4.626a.75.75 0 0 1 .208 1.04l-9 13.5a.75.75 0 0 1-1.154.114l-6-6a.75.75 0 0 1 1.06-1.06l5.353 5.353 8.493-12.74a.75.75 0 0 1 1.04-.207Z" clip-rule="evenodd"></path></svg></li>
+                <li id="fourth"><svg viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6" height="24" width="24" aria-hidden="true"><path fill-rule="evenodd" d="M19.916 4.626a.75.75 0 0 1 .208 1.04l-9 13.5a.75.75 0 0 1-1.154.114l-6-6a.75.75 0 0 1 1.06-1.06l5.353 5.353 8.493-12.74a.75.75 0 0 1 1.04-.207Z" clip-rule="evenodd"></path></svg></li>
                 <li id="fifth"><svg viewBox="0 0 24 24" fill="currentColor" class="h-8 w-8" aria-hidden="true"><path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z" clip-rule="evenodd"></path></svg></li>
                 <li id="sixth"><svg viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6" aria-labelledby="foo"><path fill-rule="evenodd" d="M19.916 4.626a.75.75 0 0 1 .208 1.04l-9 13.5a.75.75 0 0 1-1.154.114l-6-6a.75.75 0 0 1 1.06-1.06l5.353 5.353 8.493-12.74a.75.75 0 0 1 1.04-.207Z" clip-rule="evenodd"></path></svg></li>
                 <li id="seventh"><svg viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6" aria-hidden="true"><path fill-rule="evenodd" d="M19.916 4.626a.75.75 0 0 1 .208 1.04l-9 13.5a.75.75 0 0 1-1.154.114l-6-6a.75.75 0 0 1 1.06-1.06l5.353 5.353 8.493-12.74a.75.75 0 0 1 1.04-.207Z" clip-rule="evenodd"></path></svg></li>
                 <li id="eighth"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><g fill="none"><path fill="currentColor" d="m12 3l7.794 4.5v7.845a2 2 0 0 1-1 1.732L13 20.423a2 2 0 0 1-2 0l-5.794-3.346a2 2 0 0 1-1-1.732V7.5z" opacity=".16"/><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m12 3l7.794 4.5v7.845a2 2 0 0 1-1 1.732L13 20.423a2 2 0 0 1-2 0l-5.794-3.346a2 2 0 0 1-1-1.732V7.5z"/><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 7v5l-4.33 2.5M12 12l4.33 2.5"/></g></svg></li>
+                <li id="ninth"><svg viewBox="0 0 24 24" fill="currentColor" height="24" width="24" aria-hidden="true"><g fill="none"><path fill="currentColor" d="m12 3l7.794 4.5v7.845a2 2 0 0 1-1 1.732L13 20.423a2 2 0 0 1-2 0l-5.794-3.346a2 2 0 0 1-1-1.732V7.5z" opacity=".16"/><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m12 3l7.794 4.5v7.845a2 2 0 0 1-1 1.732L13 20.423a2 2 0 0 1-2 0l-5.794-3.346a2 2 0 0 1-1-1.732V7.5z"/><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 7v5l-4.33 2.5M12 12l4.33 2.5"/></g></svg></li>
             </ul>
             HTML,
             trim($output)
diff --git a/src/LiveComponent/assets/test/controller/render.test.ts b/src/LiveComponent/assets/test/controller/render.test.ts
index 0e328a61..efa875c0 100644
--- a/src/LiveComponent/assets/test/controller/render.test.ts
+++ b/src/LiveComponent/assets/test/controller/render.test.ts
@@ -630,23 +630,4 @@ describe('LiveController rendering Tests', () => {
         // verify the selectedIndex of the select option 2 is 0
         expect(selectOption2.selectedIndex).toBe(0);
     });
-
-    it('backend will have a new csrf token', async () => {
-        const test = await createTest(
-            {},
-            (data: any) => `
-            <div ${initComponent(data)} data-live-csrf-value="${data.csrf}">
-            </div>
-        `
-        );
-
-        test.expectsAjaxCall().serverWillChangeProps((data: any) => {
-            // change csrf token
-            data.csrf = 'Hello';
-        });
-
-        await test.component.render();
-
-        expect(test.mockedBackend.csrfToken).toEqual('Hello');
-    });
 });
diff --git a/src/LiveComponent/assets/test/tools.ts b/src/LiveComponent/assets/test/tools.ts
index 958fdcc8..4b0f6799 100644
--- a/src/LiveComponent/assets/test/tools.ts
+++ b/src/LiveComponent/assets/test/tools.ts
@@ -98,8 +98,6 @@ class FunctionalTest {
 class MockedBackend implements BackendInterface {
     private expectedMockedAjaxCalls: Array<MockedAjaxCall> = [];
 
-    public csrfToken: string | null = null;
-
     addMockedAjaxCall(mock: MockedAjaxCall) {
         this.expectedMockedAjaxCalls.push(mock);
     }
@@ -141,10 +139,6 @@ class MockedBackend implements BackendInterface {
         return matchedMock.createBackendRequest();
     }
 
-    updateCsrfToken(csrfToken: string) {
-        this.csrfToken = csrfToken;
-    }
-
     getExpectedMockedAjaxCalls(): Array<MockedAjaxCall> {
         return this.expectedMockedAjaxCalls;
     }