From 4aeb95eab7eccaaff3f878c7623d9ae6a026b728 Mon Sep 17 00:00:00 2001 From: memleakd <121398829+memleakd@users.noreply.github.com> Date: Mon, 18 May 2026 14:23:00 +0200 Subject: [PATCH 1/6] feat(http): add source-specific typed request input Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com> --- system/HTTP/IncomingRequest.php | 54 ++++++++ tests/system/HTTP/IncomingRequestTest.php | 128 ++++++++++++++++++ user_guide_src/source/changelogs/v4.8.0.rst | 1 + .../source/incoming/incomingrequest.rst | 42 +++++- .../source/incoming/incomingrequest/046.php | 5 + 5 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 user_guide_src/source/incoming/incomingrequest/046.php diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index f9a57a4098ba..e6fc82a8cf41 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -17,6 +17,7 @@ use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\HTTP\Files\FileCollection; use CodeIgniter\HTTP\Files\UploadedFile; +use CodeIgniter\Input\InputData; use Config\App; use Config\Services; use Locale; @@ -555,6 +556,59 @@ public function getRawInputVar($index = null, ?int $filter = null, $flags = null return $output; } + /** + * Returns query-string parameters as a typed input object. + */ + public function getQueryInput(): InputData + { + $data = $this->getGet(); + + return service('inputdatafactory')->create(is_array($data) ? $data : []); + } + + /** + * Returns POST body parameters as a typed input object. + */ + public function getPostInput(): InputData + { + $data = $this->getPost(); + + return service('inputdatafactory')->create(is_array($data) ? $data : []); + } + + /** + * Returns request body payload parameters as a typed input object. + */ + public function getPayloadInput(): InputData + { + $contentType = $this->getHeaderLine('Content-Type'); + + if (str_contains($contentType, 'application/json')) { + $data = $this->getJSON(true) ?? []; + + if (! is_array($data)) { + throw HTTPException::forUnsupportedJSONFormat(); + } + + return service('inputdatafactory')->create($data); + } + + if ( + in_array($this->getMethod(), [Method::PUT, Method::PATCH, Method::DELETE], true) + && ! str_contains($contentType, 'multipart/form-data') + ) { + return service('inputdatafactory')->create($this->getRawInput()); + } + + if (in_array($this->getMethod(), [Method::GET, Method::HEAD], true)) { + return service('inputdatafactory')->create([]); + } + + $data = $this->getPost(); + + return service('inputdatafactory')->create(is_array($data) ? $data : []); + } + /** * Fetch an item from GET data. * diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index aec26069fdcf..adb708411bba 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -19,6 +19,7 @@ use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\HTTP\Files\UploadedFile; +use CodeIgniter\Input\InputData; use CodeIgniter\Superglobals; use CodeIgniter\Test\CIUnitTestCase; use Config\App; @@ -86,6 +87,35 @@ public function testCanGrabPostVars(): void $this->assertNull($this->request->getPost('TESTY')); } + public function testGetQueryInputReadsQueryData(): void + { + service('superglobals')->setGet('page', '3'); + service('superglobals')->setGet('filters', ['active' => 'true']); + service('superglobals')->setPost('page', '10'); + + $request = $this->createRequest(); + $input = $request->getQueryInput(); + + $this->assertInstanceOf(InputData::class, $input); + $this->assertSame(3, $input->integer('page')); + $this->assertTrue($input->boolean('filters.active')); + $this->assertSame(1, $input->integer('missing', 1)); + } + + public function testGetPostInputReadsPostData(): void + { + service('superglobals')->setGet('remember', '0'); + service('superglobals')->setPost('remember', '1'); + service('superglobals')->setPost('tags', ['php', 'ci4']); + + $request = $this->createRequest(); + $input = $request->getPostInput(); + + $this->assertInstanceOf(InputData::class, $input); + $this->assertTrue($input->boolean('remember')); + $this->assertSame(['php', 'ci4'], $input->array('tags')); + } + public function testCanGrabPostBeforeGet(): void { service('superglobals')->setPost('TEST', '5'); @@ -572,6 +602,104 @@ public function testCanGrabGetRawInput(): void $this->assertSame($expected, $request->getRawInput()); } + public function testGetPayloadInputReadsJsonBody(): void + { + $json = json_encode([ + 'page' => '4', + 'filters' => ['active' => 'true'], + 'nullable' => null, + ]); + + $request = $this->createRequest(new App(), $json); + $request->setHeader('Content-Type', 'application/json'); + + $input = $request->getPayloadInput(); + + $this->assertInstanceOf(InputData::class, $input); + $this->assertSame(4, $input->integer('page')); + $this->assertTrue($input->boolean('filters.active')); + $this->assertTrue($input->has('nullable')); + } + + #[DataProvider('provideGetPayloadInputReadsRawBodyForWriteRequests')] + public function testGetPayloadInputReadsRawBodyForWriteRequests(string $method): void + { + $request = $this->createRequest(new App(), 'title=Hello&published=1') + ->withMethod($method); + + $input = $request->getPayloadInput(); + + $this->assertSame('Hello', $input->string('title')); + $this->assertTrue($input->boolean('published')); + } + + /** + * @return iterable + */ + public static function provideGetPayloadInputReadsRawBodyForWriteRequests(): iterable + { + yield 'PUT' => ['PUT']; + + yield 'PATCH' => ['PATCH']; + + yield 'DELETE' => ['DELETE']; + } + + public function testGetPayloadInputReadsPostBodyForPostRequests(): void + { + service('superglobals')->setGet('title', 'Query title'); + service('superglobals')->setPost('title', 'Post title'); + + $request = $this->createRequest()->withMethod('POST'); + $input = $request->getPayloadInput(); + + $this->assertSame('Post title', $input->string('title')); + } + + public function testGetPayloadInputDoesNotReadQueryDataForGetRequests(): void + { + service('superglobals')->setGet('page', '2'); + + $request = $this->createRequest()->withMethod('GET'); + $input = $request->getPayloadInput(); + + $this->assertFalse($input->has('page')); + $this->assertSame(1, $input->integer('page', 1)); + } + + public function testGetPayloadInputReturnsEmptyInputForEmptyJsonBody(): void + { + $request = $this->createRequest(new App()); + $request->setHeader('Content-Type', 'application/json'); + + $input = $request->getPayloadInput(); + + $this->assertInstanceOf(InputData::class, $input); + $this->assertFalse($input->has('name')); + } + + public function testGetPayloadInputRejectsScalarJsonBody(): void + { + $this->expectException(HTTPException::class); + $this->expectExceptionMessage('The provided JSON format is not supported.'); + + $request = $this->createRequest(new App(), '"hello"'); + $request->setHeader('Content-Type', 'application/json'); + + $request->getPayloadInput(); + } + + public function testGetPayloadInputKeepsInvalidJsonError(): void + { + $this->expectException(HTTPException::class); + $this->expectExceptionMessage('Failed to parse JSON string. Error: Syntax error'); + + $request = $this->createRequest(new App(), 'Invalid JSON string'); + $request->setHeader('Content-Type', 'application/json'); + + $request->getPayloadInput(); + } + /** * @param string $rawstring * @param mixed $var diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index 9301149cb5ef..d418d6fb1d5d 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -270,6 +270,7 @@ HTTP - Added the ``retry`` option to ``CURLRequest`` for retrying failed responses with configurable delays, retryable status codes, optional transient cURL error retries, and ``Retry-After`` support. See :ref:`curlrequest-request-options-retry`. - Added :ref:`Form Requests ` - a new ``FormRequest`` base class that encapsulates validation rules, custom error messages, and authorization logic for a single HTTP request. +- Added ``IncomingRequest::getQueryInput()``, ``getPostInput()``, and ``getPayloadInput()`` to read source-specific request data through ``InputData``. - Added ``SSEResponse`` class for streaming Server-Sent Events (SSE) over HTTP. See :ref:`server-sent-events`. - ``Response`` and its child classes no longer require ``Config\App`` passed to their constructors. Consequently, ``CURLRequest``'s ``$config`` parameter is unused and will be removed in a future release. diff --git a/user_guide_src/source/incoming/incomingrequest.rst b/user_guide_src/source/incoming/incomingrequest.rst index a96353dadfba..5f72eb417d88 100644 --- a/user_guide_src/source/incoming/incomingrequest.rst +++ b/user_guide_src/source/incoming/incomingrequest.rst @@ -161,6 +161,32 @@ The ``getVar()`` method will pull from ``$_REQUEST``, so will return any data fr .. note:: If the incoming request has a ``Content-Type`` header set to ``application/json``, the ``getVar()`` method returns the JSON data instead of ``$_REQUEST`` data. +.. _incomingrequest-typed-source-input: + +Typed Source Input +================== + +.. versionadded:: 4.8.0 + +``getQueryInput()``, ``getPostInput()``, and ``getPayloadInput()`` return +request data as a ``CodeIgniter\Input\InputData`` object. Use these methods +when you want source-explicit access with typed fallback helpers: + +.. literalinclude:: incomingrequest/046.php + :lines: 2- + +``getQueryInput()`` reads query-string parameters. ``getPostInput()`` reads +POST body parameters. ``getPayloadInput()`` reads the request body payload: +JSON requests use the decoded JSON body, ``PUT``, ``PATCH``, and ``DELETE`` +requests use ``getRawInput()`` when they are not multipart requests, and +ordinary form requests use POST body parameters. +For non-JSON ``GET`` and ``HEAD`` requests, use ``getQueryInput()``; +``getPayloadInput()`` returns an empty input object. + +These methods do not validate input. They are fallback-friendly helpers for +reading raw request data. Use Validation or :ref:`form-requests` when input +must satisfy application rules before it is consumed. + .. _incomingrequest-getting-json-data: Getting JSON Data @@ -406,6 +432,11 @@ The methods provided by the parent classes that are available are: .. literalinclude:: incomingrequest/045.php + .. php:method:: getQueryInput() + + :returns: Query-string parameters as a typed input object. + :rtype: CodeIgniter\\Input\\InputData + .. php:method:: getPost([$index = null[, $filter = null[, $flags = null]]]) :param string $index: The name of the variable/key to look for. @@ -418,6 +449,16 @@ The methods provided by the parent classes that are available are: This method is identical to ``getGet()``, only it fetches POST data. + .. php:method:: getPostInput() + + :returns: POST body parameters as a typed input object. + :rtype: CodeIgniter\\Input\\InputData + + .. php:method:: getPayloadInput() + + :returns: Request body payload parameters as a typed input object. + :rtype: CodeIgniter\\Input\\InputData + .. php:method:: getPostGet([$index = null[, $filter = null[, $flags = null]]]) :param string $index: The name of the variable/key to look for. @@ -519,4 +560,3 @@ The methods provided by the parent classes that are available are: .. note:: Prior to v4.4.0, this was the safest method to determine the "current URI", since ``IncomingRequest::$uri`` might not be aware of the complete App configuration for base URLs. - diff --git a/user_guide_src/source/incoming/incomingrequest/046.php b/user_guide_src/source/incoming/incomingrequest/046.php new file mode 100644 index 000000000000..70ee5d516c6d --- /dev/null +++ b/user_guide_src/source/incoming/incomingrequest/046.php @@ -0,0 +1,5 @@ +getQueryInput()->integer('page', 1); +$remember = $request->getPostInput()->boolean('remember', false); +$name = $request->getPayloadInput()->string('name'); From 5cde0cb3ec25fe479d5ad0f94ff83d362dc2a2cc Mon Sep 17 00:00:00 2001 From: memleakd <121398829+memleakd@users.noreply.github.com> Date: Fri, 22 May 2026 10:25:59 +0200 Subject: [PATCH 2/6] trigger CI Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com> From 09150cd5456103ae54316d265a9ef3a6604cc9b8 Mon Sep 17 00:00:00 2001 From: memleakd <121398829+memleakd@users.noreply.github.com> Date: Fri, 22 May 2026 10:41:34 +0200 Subject: [PATCH 3/6] fix(http): allow input dependency in Structarmed Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com> --- structarmed.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structarmed.php b/structarmed.php index 276886d219e4..28c74e556b9e 100644 --- a/structarmed.php +++ b/structarmed.php @@ -100,7 +100,7 @@ 'Files' => ['I18n'], 'Filters' => ['HTTP'], 'Honeypot' => ['Filters', 'HTTP'], - 'HTTP' => ['Cookie', 'Files', 'I18n', 'Security', 'URI'], + 'HTTP' => ['Cookie', 'Files', 'I18n', 'Input', 'Security', 'URI'], 'Images' => ['Files', 'I18n'], 'Lock' => ['Cache'], 'Model' => ['Database', 'DataCaster', 'DataConverter', 'Entity', 'I18n', 'Pager', 'Validation'], From 6f38ae656a3faf10fa8c6a4d4140ecf80250f99b Mon Sep 17 00:00:00 2001 From: memleakd <121398829+memleakd@users.noreply.github.com> Date: Sat, 23 May 2026 11:07:29 +0200 Subject: [PATCH 4/6] refactor(http): clarify typed request input sources Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com> --- system/HTTP/IncomingRequest.php | 35 +++------- tests/system/HTTP/IncomingRequestTest.php | 70 +++---------------- user_guide_src/source/changelogs/v4.8.0.rst | 2 +- .../source/incoming/incomingrequest.rst | 30 ++++---- .../source/incoming/incomingrequest/046.php | 4 +- 5 files changed, 35 insertions(+), 106 deletions(-) diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index e6fc82a8cf41..be9b4675cf3b 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -557,9 +557,9 @@ public function getRawInputVar($index = null, ?int $filter = null, $flags = null } /** - * Returns query-string parameters as a typed input object. + * Returns GET parameters as a typed input object. */ - public function getQueryInput(): InputData + public function getGetInput(): InputData { $data = $this->getGet(); @@ -577,36 +577,17 @@ public function getPostInput(): InputData } /** - * Returns request body payload parameters as a typed input object. + * Returns JSON body parameters as a typed input object. */ - public function getPayloadInput(): InputData + public function getJSONInput(): InputData { - $contentType = $this->getHeaderLine('Content-Type'); + $data = $this->getJSON(true) ?? []; - if (str_contains($contentType, 'application/json')) { - $data = $this->getJSON(true) ?? []; - - if (! is_array($data)) { - throw HTTPException::forUnsupportedJSONFormat(); - } - - return service('inputdatafactory')->create($data); - } - - if ( - in_array($this->getMethod(), [Method::PUT, Method::PATCH, Method::DELETE], true) - && ! str_contains($contentType, 'multipart/form-data') - ) { - return service('inputdatafactory')->create($this->getRawInput()); - } - - if (in_array($this->getMethod(), [Method::GET, Method::HEAD], true)) { - return service('inputdatafactory')->create([]); + if (! is_array($data)) { + throw HTTPException::forUnsupportedJSONFormat(); } - $data = $this->getPost(); - - return service('inputdatafactory')->create(is_array($data) ? $data : []); + return service('inputdatafactory')->create($data); } /** diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index adb708411bba..a7d93934f0e2 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -87,14 +87,14 @@ public function testCanGrabPostVars(): void $this->assertNull($this->request->getPost('TESTY')); } - public function testGetQueryInputReadsQueryData(): void + public function testGetGetInputReadsGetData(): void { service('superglobals')->setGet('page', '3'); service('superglobals')->setGet('filters', ['active' => 'true']); service('superglobals')->setPost('page', '10'); $request = $this->createRequest(); - $input = $request->getQueryInput(); + $input = $request->getGetInput(); $this->assertInstanceOf(InputData::class, $input); $this->assertSame(3, $input->integer('page')); @@ -602,7 +602,7 @@ public function testCanGrabGetRawInput(): void $this->assertSame($expected, $request->getRawInput()); } - public function testGetPayloadInputReadsJsonBody(): void + public function testGetJSONInputReadsJsonBody(): void { $json = json_encode([ 'page' => '4', @@ -611,9 +611,8 @@ public function testGetPayloadInputReadsJsonBody(): void ]); $request = $this->createRequest(new App(), $json); - $request->setHeader('Content-Type', 'application/json'); - $input = $request->getPayloadInput(); + $input = $request->getJSONInput(); $this->assertInstanceOf(InputData::class, $input); $this->assertSame(4, $input->integer('page')); @@ -621,83 +620,34 @@ public function testGetPayloadInputReadsJsonBody(): void $this->assertTrue($input->has('nullable')); } - #[DataProvider('provideGetPayloadInputReadsRawBodyForWriteRequests')] - public function testGetPayloadInputReadsRawBodyForWriteRequests(string $method): void - { - $request = $this->createRequest(new App(), 'title=Hello&published=1') - ->withMethod($method); - - $input = $request->getPayloadInput(); - - $this->assertSame('Hello', $input->string('title')); - $this->assertTrue($input->boolean('published')); - } - - /** - * @return iterable - */ - public static function provideGetPayloadInputReadsRawBodyForWriteRequests(): iterable - { - yield 'PUT' => ['PUT']; - - yield 'PATCH' => ['PATCH']; - - yield 'DELETE' => ['DELETE']; - } - - public function testGetPayloadInputReadsPostBodyForPostRequests(): void - { - service('superglobals')->setGet('title', 'Query title'); - service('superglobals')->setPost('title', 'Post title'); - - $request = $this->createRequest()->withMethod('POST'); - $input = $request->getPayloadInput(); - - $this->assertSame('Post title', $input->string('title')); - } - - public function testGetPayloadInputDoesNotReadQueryDataForGetRequests(): void - { - service('superglobals')->setGet('page', '2'); - - $request = $this->createRequest()->withMethod('GET'); - $input = $request->getPayloadInput(); - - $this->assertFalse($input->has('page')); - $this->assertSame(1, $input->integer('page', 1)); - } - - public function testGetPayloadInputReturnsEmptyInputForEmptyJsonBody(): void + public function testGetJSONInputReturnsEmptyInputForEmptyJsonBody(): void { $request = $this->createRequest(new App()); - $request->setHeader('Content-Type', 'application/json'); - $input = $request->getPayloadInput(); + $input = $request->getJSONInput(); $this->assertInstanceOf(InputData::class, $input); $this->assertFalse($input->has('name')); } - public function testGetPayloadInputRejectsScalarJsonBody(): void + public function testGetJSONInputRejectsScalarJsonBody(): void { $this->expectException(HTTPException::class); $this->expectExceptionMessage('The provided JSON format is not supported.'); $request = $this->createRequest(new App(), '"hello"'); - $request->setHeader('Content-Type', 'application/json'); - $request->getPayloadInput(); + $request->getJSONInput(); } - public function testGetPayloadInputKeepsInvalidJsonError(): void + public function testGetJSONInputKeepsInvalidJsonError(): void { $this->expectException(HTTPException::class); $this->expectExceptionMessage('Failed to parse JSON string. Error: Syntax error'); $request = $this->createRequest(new App(), 'Invalid JSON string'); - $request->setHeader('Content-Type', 'application/json'); - $request->getPayloadInput(); + $request->getJSONInput(); } /** diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index d418d6fb1d5d..e7aa476859b5 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -270,7 +270,7 @@ HTTP - Added the ``retry`` option to ``CURLRequest`` for retrying failed responses with configurable delays, retryable status codes, optional transient cURL error retries, and ``Retry-After`` support. See :ref:`curlrequest-request-options-retry`. - Added :ref:`Form Requests ` - a new ``FormRequest`` base class that encapsulates validation rules, custom error messages, and authorization logic for a single HTTP request. -- Added ``IncomingRequest::getQueryInput()``, ``getPostInput()``, and ``getPayloadInput()`` to read source-specific request data through ``InputData``. +- Added ``IncomingRequest::getGetInput()``, ``getPostInput()``, and ``getJSONInput()`` to read GET, POST, and JSON request data through ``InputData``. - Added ``SSEResponse`` class for streaming Server-Sent Events (SSE) over HTTP. See :ref:`server-sent-events`. - ``Response`` and its child classes no longer require ``Config\App`` passed to their constructors. Consequently, ``CURLRequest``'s ``$config`` parameter is unused and will be removed in a future release. diff --git a/user_guide_src/source/incoming/incomingrequest.rst b/user_guide_src/source/incoming/incomingrequest.rst index 5f72eb417d88..f48cc1f5cce6 100644 --- a/user_guide_src/source/incoming/incomingrequest.rst +++ b/user_guide_src/source/incoming/incomingrequest.rst @@ -161,27 +161,25 @@ The ``getVar()`` method will pull from ``$_REQUEST``, so will return any data fr .. note:: If the incoming request has a ``Content-Type`` header set to ``application/json``, the ``getVar()`` method returns the JSON data instead of ``$_REQUEST`` data. -.. _incomingrequest-typed-source-input: +.. _incomingrequest-typed-request-input: -Typed Source Input -================== +Typed Request Input +=================== .. versionadded:: 4.8.0 -``getQueryInput()``, ``getPostInput()``, and ``getPayloadInput()`` return -request data as a ``CodeIgniter\Input\InputData`` object. Use these methods -when you want source-explicit access with typed fallback helpers: +``getGetInput()``, ``getPostInput()``, and ``getJSONInput()`` return +request data as a ``CodeIgniter\Input\InputData`` object. Use these methods to +read values from a specific part of the request with typed fallback helpers: .. literalinclude:: incomingrequest/046.php :lines: 2- -``getQueryInput()`` reads query-string parameters. ``getPostInput()`` reads -POST body parameters. ``getPayloadInput()`` reads the request body payload: -JSON requests use the decoded JSON body, ``PUT``, ``PATCH``, and ``DELETE`` -requests use ``getRawInput()`` when they are not multipart requests, and -ordinary form requests use POST body parameters. -For non-JSON ``GET`` and ``HEAD`` requests, use ``getQueryInput()``; -``getPayloadInput()`` returns an empty input object. +``getGetInput()`` reads query-string parameters. ``getPostInput()`` reads +POST body parameters. ``getJSONInput()`` reads JSON request body parameters. +These methods keep GET, POST, and JSON data separate. They do not combine +multiple request sources for you. For raw ``PUT``, ``PATCH``, or ``DELETE`` +data, continue using ``getRawInput()`` or ``getRawInputVar()``. These methods do not validate input. They are fallback-friendly helpers for reading raw request data. Use Validation or :ref:`form-requests` when input @@ -432,7 +430,7 @@ The methods provided by the parent classes that are available are: .. literalinclude:: incomingrequest/045.php - .. php:method:: getQueryInput() + .. php:method:: getGetInput() :returns: Query-string parameters as a typed input object. :rtype: CodeIgniter\\Input\\InputData @@ -454,9 +452,9 @@ The methods provided by the parent classes that are available are: :returns: POST body parameters as a typed input object. :rtype: CodeIgniter\\Input\\InputData - .. php:method:: getPayloadInput() + .. php:method:: getJSONInput() - :returns: Request body payload parameters as a typed input object. + :returns: JSON body parameters as a typed input object. :rtype: CodeIgniter\\Input\\InputData .. php:method:: getPostGet([$index = null[, $filter = null[, $flags = null]]]) diff --git a/user_guide_src/source/incoming/incomingrequest/046.php b/user_guide_src/source/incoming/incomingrequest/046.php index 70ee5d516c6d..749d939aac73 100644 --- a/user_guide_src/source/incoming/incomingrequest/046.php +++ b/user_guide_src/source/incoming/incomingrequest/046.php @@ -1,5 +1,5 @@ getQueryInput()->integer('page', 1); +$page = $request->getGetInput()->integer('page', 1); $remember = $request->getPostInput()->boolean('remember', false); -$name = $request->getPayloadInput()->string('name'); +$name = $request->getJSONInput()->string('name'); From 11c2581d997b41c0101d9c8af4f1c5c497601867 Mon Sep 17 00:00:00 2001 From: memleakd <121398829+memleakd@users.noreply.github.com> Date: Tue, 26 May 2026 12:21:21 +0200 Subject: [PATCH 5/6] refactor(http): reshape typed request input API Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com> --- system/HTTP/IncomingRequest.php | 43 ++---- system/HTTP/RequestInput.php | 74 ++++++++++ tests/system/HTTP/IncomingRequestTest.php | 78 ---------- tests/system/HTTP/RequestInputTest.php | 134 ++++++++++++++++++ user_guide_src/source/changelogs/v4.8.0.rst | 2 +- .../source/incoming/incomingrequest.rst | 32 ++--- .../source/incoming/incomingrequest/046.php | 7 +- 7 files changed, 232 insertions(+), 138 deletions(-) create mode 100644 system/HTTP/RequestInput.php create mode 100644 tests/system/HTTP/RequestInputTest.php diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index be9b4675cf3b..13b8f56e0b6c 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -17,7 +17,6 @@ use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\HTTP\Files\FileCollection; use CodeIgniter\HTTP\Files\UploadedFile; -use CodeIgniter\Input\InputData; use Config\App; use Config\Services; use Locale; @@ -556,40 +555,6 @@ public function getRawInputVar($index = null, ?int $filter = null, $flags = null return $output; } - /** - * Returns GET parameters as a typed input object. - */ - public function getGetInput(): InputData - { - $data = $this->getGet(); - - return service('inputdatafactory')->create(is_array($data) ? $data : []); - } - - /** - * Returns POST body parameters as a typed input object. - */ - public function getPostInput(): InputData - { - $data = $this->getPost(); - - return service('inputdatafactory')->create(is_array($data) ? $data : []); - } - - /** - * Returns JSON body parameters as a typed input object. - */ - public function getJSONInput(): InputData - { - $data = $this->getJSON(true) ?? []; - - if (! is_array($data)) { - throw HTTPException::forUnsupportedJSONFormat(); - } - - return service('inputdatafactory')->create($data); - } - /** * Fetch an item from GET data. * @@ -604,6 +569,14 @@ public function getGet($index = null, $filter = null, $flags = null) return $this->fetchGlobal('get', $index, $filter, $flags); } + /** + * Returns a typed input data selector. + */ + public function input(): RequestInput + { + return new RequestInput($this, service('inputdatafactory')); + } + /** * Fetch an item from POST. * diff --git a/system/HTTP/RequestInput.php b/system/HTTP/RequestInput.php new file mode 100644 index 000000000000..4c94949cbe4f --- /dev/null +++ b/system/HTTP/RequestInput.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP; + +use CodeIgniter\HTTP\Exceptions\HTTPException; +use CodeIgniter\Input\InputData; +use CodeIgniter\Input\InputDataFactory; + +/** + * Provides typed input access for request data sources. + * + * @see \CodeIgniter\HTTP\RequestInputTest + */ +final readonly class RequestInput +{ + public function __construct( + private IncomingRequest $request, + private InputDataFactory $factory, + ) { + } + + /** + * Returns GET parameters as a typed input object. + */ + public function get(): InputData + { + $data = $this->request->getGet(); + + return $this->factory->create(is_array($data) ? $data : []); + } + + /** + * Returns POST body parameters as a typed input object. + */ + public function post(): InputData + { + $data = $this->request->getPost(); + + return $this->factory->create(is_array($data) ? $data : []); + } + + /** + * Returns JSON body parameters as a typed input object. + */ + public function json(): InputData + { + $data = $this->request->getJSON(true) ?? []; + + if (! is_array($data)) { + throw HTTPException::forUnsupportedJSONFormat(); + } + + return $this->factory->create($data); + } + + /** + * Returns raw input parameters as a typed input object. + */ + public function raw(): InputData + { + return $this->factory->create($this->request->getRawInput()); + } +} diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index a7d93934f0e2..aec26069fdcf 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -19,7 +19,6 @@ use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\HTTP\Files\UploadedFile; -use CodeIgniter\Input\InputData; use CodeIgniter\Superglobals; use CodeIgniter\Test\CIUnitTestCase; use Config\App; @@ -87,35 +86,6 @@ public function testCanGrabPostVars(): void $this->assertNull($this->request->getPost('TESTY')); } - public function testGetGetInputReadsGetData(): void - { - service('superglobals')->setGet('page', '3'); - service('superglobals')->setGet('filters', ['active' => 'true']); - service('superglobals')->setPost('page', '10'); - - $request = $this->createRequest(); - $input = $request->getGetInput(); - - $this->assertInstanceOf(InputData::class, $input); - $this->assertSame(3, $input->integer('page')); - $this->assertTrue($input->boolean('filters.active')); - $this->assertSame(1, $input->integer('missing', 1)); - } - - public function testGetPostInputReadsPostData(): void - { - service('superglobals')->setGet('remember', '0'); - service('superglobals')->setPost('remember', '1'); - service('superglobals')->setPost('tags', ['php', 'ci4']); - - $request = $this->createRequest(); - $input = $request->getPostInput(); - - $this->assertInstanceOf(InputData::class, $input); - $this->assertTrue($input->boolean('remember')); - $this->assertSame(['php', 'ci4'], $input->array('tags')); - } - public function testCanGrabPostBeforeGet(): void { service('superglobals')->setPost('TEST', '5'); @@ -602,54 +572,6 @@ public function testCanGrabGetRawInput(): void $this->assertSame($expected, $request->getRawInput()); } - public function testGetJSONInputReadsJsonBody(): void - { - $json = json_encode([ - 'page' => '4', - 'filters' => ['active' => 'true'], - 'nullable' => null, - ]); - - $request = $this->createRequest(new App(), $json); - - $input = $request->getJSONInput(); - - $this->assertInstanceOf(InputData::class, $input); - $this->assertSame(4, $input->integer('page')); - $this->assertTrue($input->boolean('filters.active')); - $this->assertTrue($input->has('nullable')); - } - - public function testGetJSONInputReturnsEmptyInputForEmptyJsonBody(): void - { - $request = $this->createRequest(new App()); - - $input = $request->getJSONInput(); - - $this->assertInstanceOf(InputData::class, $input); - $this->assertFalse($input->has('name')); - } - - public function testGetJSONInputRejectsScalarJsonBody(): void - { - $this->expectException(HTTPException::class); - $this->expectExceptionMessage('The provided JSON format is not supported.'); - - $request = $this->createRequest(new App(), '"hello"'); - - $request->getJSONInput(); - } - - public function testGetJSONInputKeepsInvalidJsonError(): void - { - $this->expectException(HTTPException::class); - $this->expectExceptionMessage('Failed to parse JSON string. Error: Syntax error'); - - $request = $this->createRequest(new App(), 'Invalid JSON string'); - - $request->getJSONInput(); - } - /** * @param string $rawstring * @param mixed $var diff --git a/tests/system/HTTP/RequestInputTest.php b/tests/system/HTTP/RequestInputTest.php new file mode 100644 index 000000000000..b12fdf646c70 --- /dev/null +++ b/tests/system/HTTP/RequestInputTest.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP; + +use CodeIgniter\Config\Services; +use CodeIgniter\HTTP\Exceptions\HTTPException; +use CodeIgniter\Input\InputData; +use CodeIgniter\Superglobals; +use CodeIgniter\Test\CIUnitTestCase; +use Config\App; +use PHPUnit\Framework\Attributes\BackupGlobals; +use PHPUnit\Framework\Attributes\Group; + +/** + * @internal + */ +#[BackupGlobals(true)] +#[Group('SeparateProcess')] +final class RequestInputTest extends CIUnitTestCase +{ + protected function setUp(): void + { + parent::setUp(); + + Services::injectMock('superglobals', new Superglobals([], [], [], [], [], [])); + } + + private function createRequest(?App $config = null, ?string $body = null): IncomingRequest + { + $config ??= new App(); + + return new IncomingRequest( + $config, + new SiteURI($config, ''), + $body, + new UserAgent(), + ); + } + + public function testInputReturnsRequestInput(): void + { + $request = $this->createRequest(); + $input = $request->input(); + + $this->assertInstanceOf(RequestInput::class, $input); + } + + public function testGetReadsGetData(): void + { + service('superglobals')->setGet('page', '3'); + service('superglobals')->setGet('filters', ['active' => 'true']); + service('superglobals')->setPost('page', '10'); + + $input = $this->createRequest()->input()->get(); + + $this->assertInstanceOf(InputData::class, $input); + $this->assertSame(3, $input->integer('page')); + $this->assertTrue($input->boolean('filters.active')); + $this->assertSame(1, $input->integer('missing', 1)); + } + + public function testPostReadsPostData(): void + { + service('superglobals')->setGet('remember', '0'); + service('superglobals')->setPost('remember', '1'); + service('superglobals')->setPost('tags', ['php', 'ci4']); + + $input = $this->createRequest()->input()->post(); + + $this->assertInstanceOf(InputData::class, $input); + $this->assertTrue($input->boolean('remember')); + $this->assertSame(['php', 'ci4'], $input->array('tags')); + } + + public function testJsonReadsJsonBody(): void + { + $json = json_encode([ + 'page' => '4', + 'filters' => ['active' => 'true'], + 'nullable' => null, + ]); + + $input = $this->createRequest(new App(), $json)->input()->json(); + + $this->assertInstanceOf(InputData::class, $input); + $this->assertSame(4, $input->integer('page')); + $this->assertTrue($input->boolean('filters.active')); + $this->assertTrue($input->has('nullable')); + } + + public function testJsonReturnsEmptyInputForEmptyJsonBody(): void + { + $input = $this->createRequest(new App())->input()->json(); + + $this->assertInstanceOf(InputData::class, $input); + $this->assertFalse($input->has('name')); + } + + public function testJsonRejectsScalarJsonBody(): void + { + $this->expectException(HTTPException::class); + $this->expectExceptionMessage('The provided JSON format is not supported.'); + + $this->createRequest(new App(), '"hello"')->input()->json(); + } + + public function testJsonKeepsInvalidJsonError(): void + { + $this->expectException(HTTPException::class); + $this->expectExceptionMessage('Failed to parse JSON string. Error: Syntax error'); + + $this->createRequest(new App(), 'Invalid JSON string')->input()->json(); + } + + public function testRawReadsRawInputData(): void + { + $input = $this->createRequest(new App(), 'title=Hello&published=1')->input()->raw(); + + $this->assertInstanceOf(InputData::class, $input); + $this->assertSame('Hello', $input->string('title')); + $this->assertTrue($input->boolean('published')); + } +} diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index e7aa476859b5..1116e8440930 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -270,7 +270,7 @@ HTTP - Added the ``retry`` option to ``CURLRequest`` for retrying failed responses with configurable delays, retryable status codes, optional transient cURL error retries, and ``Retry-After`` support. See :ref:`curlrequest-request-options-retry`. - Added :ref:`Form Requests ` - a new ``FormRequest`` base class that encapsulates validation rules, custom error messages, and authorization logic for a single HTTP request. -- Added ``IncomingRequest::getGetInput()``, ``getPostInput()``, and ``getJSONInput()`` to read GET, POST, and JSON request data through ``InputData``. +- Added ``IncomingRequest::input()`` to read GET, POST, JSON, and raw request data through ``InputData``. - Added ``SSEResponse`` class for streaming Server-Sent Events (SSE) over HTTP. See :ref:`server-sent-events`. - ``Response`` and its child classes no longer require ``Config\App`` passed to their constructors. Consequently, ``CURLRequest``'s ``$config`` parameter is unused and will be removed in a future release. diff --git a/user_guide_src/source/incoming/incomingrequest.rst b/user_guide_src/source/incoming/incomingrequest.rst index f48cc1f5cce6..8e30fabb425f 100644 --- a/user_guide_src/source/incoming/incomingrequest.rst +++ b/user_guide_src/source/incoming/incomingrequest.rst @@ -168,18 +168,18 @@ Typed Request Input .. versionadded:: 4.8.0 -``getGetInput()``, ``getPostInput()``, and ``getJSONInput()`` return -request data as a ``CodeIgniter\Input\InputData`` object. Use these methods to -read values from a specific part of the request with typed fallback helpers: +``input()`` returns a ``CodeIgniter\HTTP\RequestInput`` object. Use it to read +values from a specific part of the request with typed fallback helpers: .. literalinclude:: incomingrequest/046.php :lines: 2- -``getGetInput()`` reads query-string parameters. ``getPostInput()`` reads -POST body parameters. ``getJSONInput()`` reads JSON request body parameters. -These methods keep GET, POST, and JSON data separate. They do not combine -multiple request sources for you. For raw ``PUT``, ``PATCH``, or ``DELETE`` -data, continue using ``getRawInput()`` or ``getRawInputVar()``. +``input()->get()`` reads query-string parameters. ``input()->post()`` reads +POST body parameters. ``input()->json()`` reads JSON request body parameters. +``input()->raw()`` reads raw input parameters, like ``getRawInput()``. + +These methods keep GET, POST, JSON, and raw data separate. They do not combine +multiple request sources for you. These methods do not validate input. They are fallback-friendly helpers for reading raw request data. Use Validation or :ref:`form-requests` when input @@ -430,10 +430,10 @@ The methods provided by the parent classes that are available are: .. literalinclude:: incomingrequest/045.php - .. php:method:: getGetInput() + .. php:method:: input() - :returns: Query-string parameters as a typed input object. - :rtype: CodeIgniter\\Input\\InputData + :returns: A typed input data selector. + :rtype: CodeIgniter\\HTTP\\RequestInput .. php:method:: getPost([$index = null[, $filter = null[, $flags = null]]]) @@ -447,16 +447,6 @@ The methods provided by the parent classes that are available are: This method is identical to ``getGet()``, only it fetches POST data. - .. php:method:: getPostInput() - - :returns: POST body parameters as a typed input object. - :rtype: CodeIgniter\\Input\\InputData - - .. php:method:: getJSONInput() - - :returns: JSON body parameters as a typed input object. - :rtype: CodeIgniter\\Input\\InputData - .. php:method:: getPostGet([$index = null[, $filter = null[, $flags = null]]]) :param string $index: The name of the variable/key to look for. diff --git a/user_guide_src/source/incoming/incomingrequest/046.php b/user_guide_src/source/incoming/incomingrequest/046.php index 749d939aac73..d0c6aca18158 100644 --- a/user_guide_src/source/incoming/incomingrequest/046.php +++ b/user_guide_src/source/incoming/incomingrequest/046.php @@ -1,5 +1,6 @@ getGetInput()->integer('page', 1); -$remember = $request->getPostInput()->boolean('remember', false); -$name = $request->getJSONInput()->string('name'); +$page = $request->input()->get()->integer('page', 1); +$remember = $request->input()->post()->boolean('remember', false); +$name = $request->input()->json()->string('name'); +$published = $request->input()->raw()->boolean('published', false); From bb1490da64fa5f258233910c8f2876e672bc0009 Mon Sep 17 00:00:00 2001 From: memleakd <121398829+memleakd@users.noreply.github.com> Date: Wed, 27 May 2026 11:51:53 +0200 Subject: [PATCH 6/6] refactor(http): address feedbacks --- system/HTTP/IncomingRequest.php | 12 +++++++++++- tests/system/HTTP/RequestInputTest.php | 17 +++++++++++++++++ .../source/incoming/incomingrequest.rst | 1 + 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 13b8f56e0b6c..0e52eacd9d4c 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -123,6 +123,11 @@ class IncomingRequest extends Request */ protected $userAgent; + /** + * Typed input data selector. + */ + protected ?RequestInput $input = null; + /** * Constructor * @@ -169,6 +174,11 @@ public function __construct($config, ?URI $uri = null, $body = 'php://input', ?U $this->detectLocale($config); } + public function __clone() + { + $this->input = null; + } + private function getPostMaxSize(): int { $postMaxSize = ini_get('post_max_size'); @@ -574,7 +584,7 @@ public function getGet($index = null, $filter = null, $flags = null) */ public function input(): RequestInput { - return new RequestInput($this, service('inputdatafactory')); + return $this->input ??= new RequestInput($this, service('inputdatafactory')); } /** diff --git a/tests/system/HTTP/RequestInputTest.php b/tests/system/HTTP/RequestInputTest.php index b12fdf646c70..44d094b0ada1 100644 --- a/tests/system/HTTP/RequestInputTest.php +++ b/tests/system/HTTP/RequestInputTest.php @@ -56,6 +56,23 @@ public function testInputReturnsRequestInput(): void $this->assertInstanceOf(RequestInput::class, $input); } + public function testInputReturnsSameRequestInputInstance(): void + { + $request = $this->createRequest(); + + $this->assertSame($request->input(), $request->input()); + } + + public function testClonedRequestGetsNewRequestInputInstance(): void + { + $request = $this->createRequest(); + $input = $request->input(); + + $clonedRequest = $request->withMethod(Method::POST); + + $this->assertNotSame($input, $clonedRequest->input()); + } + public function testGetReadsGetData(): void { service('superglobals')->setGet('page', '3'); diff --git a/user_guide_src/source/incoming/incomingrequest.rst b/user_guide_src/source/incoming/incomingrequest.rst index 8e30fabb425f..680f0b807dce 100644 --- a/user_guide_src/source/incoming/incomingrequest.rst +++ b/user_guide_src/source/incoming/incomingrequest.rst @@ -251,6 +251,7 @@ Filtering a POST variable would look like this: All of the methods mentioned above support the filter type passed in as the second parameter, with the exception of ``getJSON()`` and ``getRawInput()``. +The typed input helpers returned by ``input()`` do not accept filter parameters. Retrieving Headers ******************