From 828e34e5e972d66d6e5c3daf87181d496c838f49 Mon Sep 17 00:00:00 2001 From: Nicolas Rigaud Date: Wed, 8 Nov 2023 09:31:50 +0100 Subject: [PATCH] [LiveComponent] Alias URL bound props --- src/LiveComponent/CHANGELOG.md | 1 + .../test/controller/query-binding.test.ts | 23 +++++- src/LiveComponent/doc/index.rst | 70 +++++++++++++++++-- src/LiveComponent/src/Attribute/LiveProp.php | 19 +++-- .../src/Metadata/LiveComponentMetadata.php | 2 +- .../src/Metadata/LivePropMetadata.php | 4 +- src/LiveComponent/src/Metadata/UrlMapping.php | 28 ++++++++ .../Util/LiveControllerAttributesCreator.php | 8 +-- .../src/Util/QueryStringPropsExtractor.php | 4 +- .../Component/ComponentWithUrlBoundProps.php | 53 +++++++++----- .../component_with_url_bound_props.html.twig | 18 ++--- ...r_component_with_url_bound_props.html.twig | 3 +- .../AddLiveAttributesSubscriberTest.php | 16 +++-- .../QueryStringInitializerSubscriberTest.php | 33 ++++++--- .../LiveComponentMetadataFactoryTest.php | 26 +++---- .../Util/QueryStringPropsExtractorTest.php | 23 +++--- 16 files changed, 240 insertions(+), 91 deletions(-) create mode 100644 src/LiveComponent/src/Metadata/UrlMapping.php diff --git a/src/LiveComponent/CHANGELOG.md b/src/LiveComponent/CHANGELOG.md index 304c232b4c2..15e2c613ffb 100644 --- a/src/LiveComponent/CHANGELOG.md +++ b/src/LiveComponent/CHANGELOG.md @@ -8,6 +8,7 @@ page is rendered, either when the page loads (`loading="defer"`) or when the component becomes visible in the viewport (`loading="lazy"`). - Deprecate the `defer` attribute. +- Add `UrlMapping` configuration object for URL bindings in LiveComponents ## 2.16.0 diff --git a/src/LiveComponent/assets/test/controller/query-binding.test.ts b/src/LiveComponent/assets/test/controller/query-binding.test.ts index f62754fa2d4..530e1504bea 100644 --- a/src/LiveComponent/assets/test/controller/query-binding.test.ts +++ b/src/LiveComponent/assets/test/controller/query-binding.test.ts @@ -144,7 +144,6 @@ describe('LiveController query string binding', () => { expectCurrentSearch().toEqual('?prop='); }); - it('updates the URL with props changed by the server', async () => { const test = await createTest({ prop: ''}, (data: any) => `
@@ -165,4 +164,26 @@ describe('LiveController query string binding', () => { expectCurrentSearch().toEqual('?prop=foo'); }); + + it('uses custom name instead of prop name in the URL', async () => { + const test = await createTest({ prop1: ''}, (data: any) => ` +
+ `) + + // Set value + test.expectsAjaxCall() + .expectUpdatedData({prop1: 'foo'}); + + await test.component.set('prop1', 'foo', true); + + expectCurrentSearch().toEqual('?alias1=foo'); + + // Remove value + test.expectsAjaxCall() + .expectUpdatedData({prop1: ''}); + + await test.component.set('prop1', '', true); + + expectCurrentSearch().toEqual('?alias1='); + }); }) diff --git a/src/LiveComponent/doc/index.rst b/src/LiveComponent/doc/index.rst index 335ea5b1ca7..ec5fef4950c 100644 --- a/src/LiveComponent/doc/index.rst +++ b/src/LiveComponent/doc/index.rst @@ -2489,11 +2489,6 @@ If you load this URL in your browser, the ``LiveProp`` value will be initialized The URL is changed via ``history.replaceState()``. So no new entry is added. -.. warning:: - - You can use multiple components with URL bindings in the same page, as long as bound field names don't collide. - Otherwise, you will observe unexpected behaviors. - Supported Data Types ~~~~~~~~~~~~~~~~~~~~ @@ -2537,6 +2532,65 @@ For example, if you declare the following bindings:: And you only set the ``query`` value, then your URL will be updated to ``https://my.domain/search?query=my+query+string&mode=fulltext``. +Controlling the Query Parameter Name +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 2.17 + + The ``as`` option was added in LiveComponents 2.17. + + +Instead of using the prop's field name as the query parameter name, you can use the ``as`` option in your ``LiveProp`` +definition:: + + // ... + use Symfony\UX\LiveComponent\Metadata\UrlMapping; + + #[AsLiveComponent] + class SearchModule + { + #[LiveProp(writable: true, url: new UrlMapping(as: 'q')] + public string $query = ''; + + // ... + } + +Then the ``query`` value will appear in the URL like ``https://my.domain/search?q=my+query+string``. + +If you need to change the parameter name on a specific page, you can leverage the :ref:`modifier ` option:: + + // ... + use Symfony\UX\LiveComponent\Metadata\UrlMapping; + + #[AsLiveComponent] + class SearchModule + { + #[LiveProp(writable: true, url: true, modifier: 'modifyQueryProp')] + public string $query = ''; + + #[LiveProp] + public ?string $alias = null; + + public function modifyQueryProp(LiveProp $liveProp): LiveProp + { + if ($this->alias) { + $liveProp = $liveProp->withUrl(new UrlMapping(as: $this->alias)); + } + return $liveProp; + } + } + +.. code-block:: html+twig + + + +This way you can also use the component multiple times in the same page and avoid collisions in parameter names: + +.. code-block:: html+twig + + + + Validating the Query Parameter Values ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -2564,8 +2618,8 @@ validated. To validate it, you have to set up a `PostMount hook`_:: #[PostMount] public function postMount(): void { - // Validate 'mode' field without throwing an exception, so the component can be mounted anyway and a - // validation error can be shown to the user + // Validate 'mode' field without throwing an exception, so the component can + // be mounted anyway and a validation error can be shown to the user if (!$this->validateField('mode', false)) { // Do something when validation fails } @@ -3501,6 +3555,8 @@ the change of one specific key:: } } +.. _modifier: + Set LiveProp Options Dynamically ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/LiveComponent/src/Attribute/LiveProp.php b/src/LiveComponent/src/Attribute/LiveProp.php index 26ed12f486b..11382848dba 100644 --- a/src/LiveComponent/src/Attribute/LiveProp.php +++ b/src/LiveComponent/src/Attribute/LiveProp.php @@ -11,6 +11,8 @@ namespace Symfony\UX\LiveComponent\Attribute; +use Symfony\UX\LiveComponent\Metadata\UrlMapping; + /** * An attribute to mark a property as a "LiveProp". * @@ -97,10 +99,11 @@ public function __construct( private string|array|null $onUpdated = null, /** - * If true, this property will be synchronized with a query parameter - * in the URL. + * Whether to synchronize this property with a query parameter + * in the URL. Pass true to configure the mapping automatically, or a + * {@see UrlMapping} instance to configure the mapping. */ - private bool $url = false, + private bool|UrlMapping $url = false, /** * A hook that will be called when this LiveProp is used. @@ -114,6 +117,10 @@ public function __construct( private ?string $modifier = null, ) { self::validateHydrationStrategy($this); + + if (true === $url) { + $this->url = new UrlMapping(); + } } /** @@ -277,15 +284,15 @@ public function withOnUpdated(string|array|null $onUpdated): self return $clone; } - public function url(): bool + public function url(): UrlMapping|false { return $this->url; } - public function withUrl(bool $url): self + public function withUrl(bool|UrlMapping $url): self { $clone = clone $this; - $clone->url = $url; + $clone->url = (true === $url) ? new UrlMapping() : $url; return $clone; } diff --git a/src/LiveComponent/src/Metadata/LiveComponentMetadata.php b/src/LiveComponent/src/Metadata/LiveComponentMetadata.php index fe058e0f6b2..667449151f3 100644 --- a/src/LiveComponent/src/Metadata/LiveComponentMetadata.php +++ b/src/LiveComponent/src/Metadata/LiveComponentMetadata.php @@ -69,7 +69,7 @@ public function getOnlyPropsThatAcceptUpdatesFromParent(array $inputProps): arra public function hasQueryStringBindings($component): bool { foreach ($this->getAllLivePropsMetadata($component) as $livePropMetadata) { - if ($livePropMetadata->queryStringMapping()) { + if ($livePropMetadata->urlMapping()) { return true; } } diff --git a/src/LiveComponent/src/Metadata/LivePropMetadata.php b/src/LiveComponent/src/Metadata/LivePropMetadata.php index 49f8861cded..1878a159b03 100644 --- a/src/LiveComponent/src/Metadata/LivePropMetadata.php +++ b/src/LiveComponent/src/Metadata/LivePropMetadata.php @@ -51,9 +51,9 @@ public function allowsNull(): bool return $this->allowsNull; } - public function queryStringMapping(): bool + public function urlMapping(): ?UrlMapping { - return $this->liveProp->url(); + return $this->liveProp->url() ?: null; } public function calculateFieldName(object $component, string $fallback): string diff --git a/src/LiveComponent/src/Metadata/UrlMapping.php b/src/LiveComponent/src/Metadata/UrlMapping.php new file mode 100644 index 00000000000..be7fd86195e --- /dev/null +++ b/src/LiveComponent/src/Metadata/UrlMapping.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\LiveComponent\Metadata; + +/** + * Mapping configuration to bind a LiveProp to a URL query parameter. + * + * @author Nicolas Rigaud + */ +final class UrlMapping +{ + public function __construct( + /** + * The name of the prop that appears in the URL. If null, the LiveProp's field name is used. + */ + public readonly ?string $as = null, + ) { + } +} diff --git a/src/LiveComponent/src/Util/LiveControllerAttributesCreator.php b/src/LiveComponent/src/Util/LiveControllerAttributesCreator.php index 4eaa0ae6f72..cb1d8167788 100644 --- a/src/LiveComponent/src/Util/LiveControllerAttributesCreator.php +++ b/src/LiveComponent/src/Util/LiveControllerAttributesCreator.php @@ -104,14 +104,14 @@ public function attributesForRendering(MountedComponent $mounted, ComponentMetad } if ($liveMetadata->hasQueryStringBindings($mounted->getComponent())) { - $queryMapping = []; + $mappings = []; foreach ($liveMetadata->getAllLivePropsMetadata($mounted->getComponent()) as $livePropMetadata) { - if ($livePropMetadata->queryStringMapping()) { + if ($urlMapping = $livePropMetadata->urlMapping()) { $frontendName = $livePropMetadata->calculateFieldName($mounted->getComponent(), $livePropMetadata->getName()); - $queryMapping[$frontendName] = ['name' => $frontendName]; + $mappings[$frontendName] = ['name' => $urlMapping->as ?? $frontendName]; } } - $attributesCollection->setQueryUrlMapping($queryMapping); + $attributesCollection->setQueryUrlMapping($mappings); } if ($isChildComponent) { diff --git a/src/LiveComponent/src/Util/QueryStringPropsExtractor.php b/src/LiveComponent/src/Util/QueryStringPropsExtractor.php index 9bbe48bd75b..48e852d70ba 100644 --- a/src/LiveComponent/src/Util/QueryStringPropsExtractor.php +++ b/src/LiveComponent/src/Util/QueryStringPropsExtractor.php @@ -41,9 +41,9 @@ public function extract(Request $request, LiveComponentMetadata $metadata, objec $data = []; foreach ($metadata->getAllLivePropsMetadata($component) as $livePropMetadata) { - if ($livePropMetadata->queryStringMapping()) { + if ($queryMapping = $livePropMetadata->urlMapping()) { $frontendName = $livePropMetadata->calculateFieldName($component, $livePropMetadata->getName()); - if (null !== ($value = $query[$frontendName] ?? null)) { + if (null !== ($value = $query[$queryMapping->as ?? $frontendName] ?? null)) { if ('' === $value && null !== $livePropMetadata->getType() && (!$livePropMetadata->isBuiltIn() || 'array' === $livePropMetadata->getType())) { // Cast empty string to empty array for objects and arrays $value = []; diff --git a/src/LiveComponent/tests/Fixtures/Component/ComponentWithUrlBoundProps.php b/src/LiveComponent/tests/Fixtures/Component/ComponentWithUrlBoundProps.php index cd67eceb2d2..24ffdf10614 100644 --- a/src/LiveComponent/tests/Fixtures/Component/ComponentWithUrlBoundProps.php +++ b/src/LiveComponent/tests/Fixtures/Component/ComponentWithUrlBoundProps.php @@ -14,6 +14,7 @@ use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\DefaultActionTrait; +use Symfony\UX\LiveComponent\Metadata\UrlMapping; use Symfony\UX\LiveComponent\Tests\Fixtures\Dto\Address; #[AsLiveComponent('component_with_url_bound_props')] @@ -22,39 +23,59 @@ class ComponentWithUrlBoundProps use DefaultActionTrait; #[LiveProp(url: true)] - public ?string $prop1 = null; + public ?string $stringProp = null; #[LiveProp(url: true)] - public ?int $prop2 = null; + public ?int $intProp = null; #[LiveProp(url: true)] - public array $prop3 = []; + public array $arrayProp = []; #[LiveProp] - public ?string $prop4 = null; + public ?string $unboundProp = null; #[LiveProp(url: true)] - public ?Address $prop5 = null; + public ?Address $objectProp = null; - #[LiveProp(fieldName: 'field6', url: true)] - public ?string $prop6 = null; + #[LiveProp(fieldName: 'field1', url: true)] + public ?string $propWithField1 = null; - #[LiveProp(fieldName: 'getProp7Name()', url: true)] - public ?string $prop7 = null; + #[LiveProp(fieldName: 'getField2()', url: true)] + public ?string $propWithField2 = null; - #[LiveProp(modifier: 'modifyProp8')] - public ?string $prop8 = null; + #[LiveProp(modifier: 'modifyMaybeBoundProp')] + public ?string $maybeBoundProp = null; #[LiveProp] - public ?bool $prop8InUrl = false; + public ?bool $maybeBoundPropInUrl = false; - public function getProp7Name(): string + public function getField2(): string { - return 'field7'; + return 'field2'; } - public function modifyProp8(LiveProp $prop): LiveProp + public function modifyMaybeBoundProp(LiveProp $prop): LiveProp { - return $prop->withUrl($this->prop8InUrl); + return $prop->withUrl($this->maybeBoundPropInUrl); } + + #[LiveProp(url: new UrlMapping(as: 'q'))] + public ?string $boundPropWithAlias = null; + + #[LiveProp(url: true, modifier: 'modifyBoundPropWithCustomAlias')] + public ?string $boundPropWithCustomAlias = null; + + #[LiveProp] + public ?string $customAlias = null; + + public function modifyBoundPropWithCustomAlias(LiveProp $liveProp): LiveProp + { + if ($this->customAlias) { + $liveProp = $liveProp->withUrl(new UrlMapping(as: $this->customAlias)); + } + + return $liveProp; + } + + } diff --git a/src/LiveComponent/tests/Fixtures/templates/components/component_with_url_bound_props.html.twig b/src/LiveComponent/tests/Fixtures/templates/components/component_with_url_bound_props.html.twig index 0ddd6d9b253..21073e218f9 100644 --- a/src/LiveComponent/tests/Fixtures/templates/components/component_with_url_bound_props.html.twig +++ b/src/LiveComponent/tests/Fixtures/templates/components/component_with_url_bound_props.html.twig @@ -1,10 +1,12 @@
- Prop1: {{ prop1 }} - Prop2: {{ prop2 }} - Prop3: {{ prop3|join(',') }} - Prop4: {{ prop4 }} - Prop5: address: {{ prop5.address ?? '' }} city: {{ prop5.city ?? '' }} - Prop6: {{ prop6 }} - Prop7: {{ prop7 }} - Prop8: {{ prop8 }} + StringProp: {{ stringProp }} + IntProp: {{ intProp }} + ArrayProp: {{ arrayProp|join(',') }} + UnboundProp: {{ unboundProp }} + ObjectProp: address: {{ objectProp.address ?? '' }} city: {{ objectProp.city ?? '' }} + PropWithField1: {{ propWithField1 }} + PropWithField2: {{ propWithField2 }} + MaybeBoundProp: {{ maybeBoundProp }} + BoundPropWithAlias: {{ boundPropWithAlias }} + BoundPropWithCustomAlias: {{ boundPropWithCustomAlias }}
diff --git a/src/LiveComponent/tests/Fixtures/templates/render_component_with_url_bound_props.html.twig b/src/LiveComponent/tests/Fixtures/templates/render_component_with_url_bound_props.html.twig index 9d368e059d2..d5eee88a558 100644 --- a/src/LiveComponent/tests/Fixtures/templates/render_component_with_url_bound_props.html.twig +++ b/src/LiveComponent/tests/Fixtures/templates/render_component_with_url_bound_props.html.twig @@ -1,3 +1,4 @@ {{ component('component_with_url_bound_props', { - prop8InUrl: true + maybeBoundPropInUrl: true, + customAlias: 'customAlias', }) }} diff --git a/src/LiveComponent/tests/Functional/EventListener/AddLiveAttributesSubscriberTest.php b/src/LiveComponent/tests/Functional/EventListener/AddLiveAttributesSubscriberTest.php index ead3102afaf..564a83c6773 100644 --- a/src/LiveComponent/tests/Functional/EventListener/AddLiveAttributesSubscriberTest.php +++ b/src/LiveComponent/tests/Functional/EventListener/AddLiveAttributesSubscriberTest.php @@ -146,13 +146,15 @@ public function testQueryStringMappingAttribute() $queryMapping = json_decode($div->attr('data-live-query-mapping-value'), true); $expected = [ - 'prop1' => ['name' => 'prop1'], - 'prop2' => ['name' => 'prop2'], - 'prop3' => ['name' => 'prop3'], - 'prop5' => ['name' => 'prop5'], - 'field6' => ['name' => 'field6'], - 'field7' => ['name' => 'field7'], - 'prop8' => ['name' => 'prop8'], + 'stringProp' => ['name' => 'stringProp'], + 'intProp' => ['name' => 'intProp'], + 'arrayProp' => ['name' => 'arrayProp'], + 'objectProp' => ['name' => 'objectProp'], + 'field1' => ['name' => 'field1'], + 'field2' => ['name' => 'field2'], + 'maybeBoundProp' => ['name' => 'maybeBoundProp'], + 'boundPropWithAlias' => ['name' => 'q'], + 'boundPropWithCustomAlias' => ['name' => 'customAlias'], ]; $this->assertEquals($expected, $queryMapping); diff --git a/src/LiveComponent/tests/Functional/EventListener/QueryStringInitializerSubscriberTest.php b/src/LiveComponent/tests/Functional/EventListener/QueryStringInitializerSubscriberTest.php index a63610b08cb..aa5955c6378 100644 --- a/src/LiveComponent/tests/Functional/EventListener/QueryStringInitializerSubscriberTest.php +++ b/src/LiveComponent/tests/Functional/EventListener/QueryStringInitializerSubscriberTest.php @@ -20,18 +20,31 @@ class QueryStringInitializerSubscriberTest extends KernelTestCase public function testQueryStringPropsInitialization() { + $queryString = '?' + .'stringProp=foo' + .'&intProp=42' + .'&arrayProp[]=foo&arrayProp[]=bar' + .'&unboundProp=unbound' + .'&objectProp[address]=foo&objectProp[city]=bar' + .'&field1=foo' + .'&field2=foo' + .'&maybeBoundProp=foo' + .'&q=foo' + .'&customAlias=foo' + ; $this->browser() - ->throwExceptions() - ->get('/render-template/render_component_with_url_bound_props?prop1=foo&prop2=42&prop3[]=foo&prop3[]=bar&prop4=unbound&prop5[address]=foo&prop5[city]=bar&field6=foo&field7=foo&prop8=foo') + ->get('/render-template/render_component_with_url_bound_props'.$queryString) ->assertSuccessful() - ->assertContains('Prop1: foo') - ->assertContains('Prop2: 42') - ->assertContains('Prop3: foo,bar') - ->assertContains('Prop4:') - ->assertContains('Prop5: address: foo city: bar') - ->assertContains('Prop6: foo') - ->assertContains('Prop7: foo') - ->assertContains('Prop8: foo') + ->assertContains('StringProp: foo') + ->assertContains('IntProp: 42') + ->assertContains('ArrayProp: foo,bar') + ->assertContains('UnboundProp:') + ->assertContains('ObjectProp: address: foo city: bar') + ->assertContains('PropWithField1: foo') + ->assertContains('PropWithField2: foo') + ->assertContains('MaybeBoundProp: foo') + ->assertContains('BoundPropWithAlias: foo') + ->assertContains('BoundPropWithCustomAlias: foo') ; } } diff --git a/src/LiveComponent/tests/Functional/Metadata/LiveComponentMetadataFactoryTest.php b/src/LiveComponent/tests/Functional/Metadata/LiveComponentMetadataFactoryTest.php index c8bd6194b1c..3529804443d 100644 --- a/src/LiveComponent/tests/Functional/Metadata/LiveComponentMetadataFactoryTest.php +++ b/src/LiveComponent/tests/Functional/Metadata/LiveComponentMetadataFactoryTest.php @@ -13,6 +13,7 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadataFactory; +use Symfony\UX\LiveComponent\Metadata\UrlMapping; use Symfony\UX\LiveComponent\Tests\Fixtures\Component\ComponentWithUrlBoundProps; class LiveComponentMetadataFactoryTest extends KernelTestCase @@ -30,20 +31,15 @@ public function testQueryStringMapping() $propsMetadataByName[$propMetadata->getName()] = $propMetadata; } - $this->assertTrue($propsMetadataByName['prop1']->queryStringMapping()); - - $this->assertTrue($propsMetadataByName['prop2']->queryStringMapping()); - - $this->assertTrue($propsMetadataByName['prop3']->queryStringMapping()); - - $this->assertFalse($propsMetadataByName['prop4']->queryStringMapping()); - - $this->assertTrue($propsMetadataByName['prop5']->queryStringMapping()); - - $this->assertTrue($propsMetadataByName['prop6']->queryStringMapping()); - - $this->assertTrue($propsMetadataByName['prop7']->queryStringMapping()); - - $this->assertFalse($propsMetadataByName['prop8']->queryStringMapping()); + $this->assertNotNull($propsMetadataByName['stringProp']->urlMapping()); + $this->assertNotNull($propsMetadataByName['intProp']->urlMapping()); + $this->assertNotNull($propsMetadataByName['arrayProp']->urlMapping()); + $this->assertNull($propsMetadataByName['unboundProp']->urlMapping()); + $this->assertNotNull($propsMetadataByName['objectProp']->urlMapping()); + $this->assertNotNull($propsMetadataByName['propWithField1']->urlMapping()); + $this->assertNotNull($propsMetadataByName['propWithField2']->urlMapping()); + $this->assertNull($propsMetadataByName['maybeBoundProp']->urlMapping()); + $this->assertEquals(new UrlMapping(as: 'q'), $propsMetadataByName['boundPropWithAlias']->urlMapping()); + $this->assertNotNull($propsMetadataByName['boundPropWithCustomAlias']); } } diff --git a/src/LiveComponent/tests/Functional/Util/QueryStringPropsExtractorTest.php b/src/LiveComponent/tests/Functional/Util/QueryStringPropsExtractorTest.php index 161a69a6b11..cabfb98e406 100644 --- a/src/LiveComponent/tests/Functional/Util/QueryStringPropsExtractorTest.php +++ b/src/LiveComponent/tests/Functional/Util/QueryStringPropsExtractorTest.php @@ -46,14 +46,14 @@ public function getQueryStringTests(): iterable { yield from [ 'no query string' => ['', []], - 'empty value for nullable string' => ['prop1=', ['prop1' => null]], - 'string value' => ['prop1=foo', ['prop1' => 'foo']], - 'empty value for nullable int' => ['prop2=', ['prop2' => null]], - 'int value' => ['prop2=42', ['prop2' => 42]], - 'array value' => ['prop3[]=foo&prop3[]=bar', ['prop3' => ['foo', 'bar']]], - 'array value indexed' => ['prop3[1]=foo&prop3[0]=bar', ['prop3' => [1 => 'foo', 0 => 'bar']]], - 'not bound prop' => ['prop4=foo', []], - 'object value' => ['prop5[address]=foo&prop5[city]=bar', ['prop5' => (function () { + 'empty value for nullable string' => ['stringProp=', ['stringProp' => null]], + 'string value' => ['stringProp=foo', ['stringProp' => 'foo']], + 'empty value for nullable int' => ['intProp=', ['intProp' => null]], + 'int value' => ['intProp=42', ['intProp' => 42]], + 'array value' => ['arrayProp[]=foo&arrayProp[]=bar', ['arrayProp' => ['foo', 'bar']]], + 'array value indexed' => ['arrayProp[1]=foo&arrayProp[0]=bar', ['arrayProp' => [1 => 'foo', 0 => 'bar']]], + 'not bound prop' => ['unboundProp=foo', []], + 'object value' => ['objectProp[address]=foo&objectProp[city]=bar', ['objectProp' => (function () { $address = new Address(); $address->address = 'foo'; $address->city = 'bar'; @@ -61,9 +61,10 @@ public function getQueryStringTests(): iterable return $address; })(), ]], - 'invalid scalar value' => ['prop1[]=foo&prop1[]=bar', []], - 'invalid array value' => ['prop3=foo', []], - 'invalid object value' => ['prop5=foo', []], + 'invalid scalar value' => ['stringProp[]=foo&stringProp[]=bar', []], + 'invalid array value' => ['arrayProp=foo', []], + 'invalid object value' => ['objectProp=foo', []], + 'aliased prop' => ['q=foo', ['boundPropWithAlias' => 'foo']], ]; } }