Skip to content

Commit 7c31ebe

Browse files
committed
feat(http): add source-specific typed request input
Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
1 parent 37b8b37 commit 7c31ebe

6 files changed

Lines changed: 230 additions & 1 deletion

File tree

deptrac.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ deptrac:
210210
- Cookie
211211
- Files
212212
- I18n
213+
- Input
213214
- Security
214215
- URI
215216
Images:

system/HTTP/IncomingRequest.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use CodeIgniter\HTTP\Exceptions\HTTPException;
1818
use CodeIgniter\HTTP\Files\FileCollection;
1919
use CodeIgniter\HTTP\Files\UploadedFile;
20+
use CodeIgniter\Input\InputData;
2021
use Config\App;
2122
use Config\Services;
2223
use Locale;
@@ -555,6 +556,59 @@ public function getRawInputVar($index = null, ?int $filter = null, $flags = null
555556
return $output;
556557
}
557558

559+
/**
560+
* Returns query-string parameters as a typed input object.
561+
*/
562+
public function getQueryInput(): InputData
563+
{
564+
$data = $this->getGet();
565+
566+
return service('inputdatafactory')->create(is_array($data) ? $data : []);
567+
}
568+
569+
/**
570+
* Returns POST body parameters as a typed input object.
571+
*/
572+
public function getPostInput(): InputData
573+
{
574+
$data = $this->getPost();
575+
576+
return service('inputdatafactory')->create(is_array($data) ? $data : []);
577+
}
578+
579+
/**
580+
* Returns request body payload parameters as a typed input object.
581+
*/
582+
public function getPayloadInput(): InputData
583+
{
584+
$contentType = $this->getHeaderLine('Content-Type');
585+
586+
if (str_contains($contentType, 'application/json')) {
587+
$data = $this->getJSON(true) ?? [];
588+
589+
if (! is_array($data)) {
590+
throw HTTPException::forUnsupportedJSONFormat();
591+
}
592+
593+
return service('inputdatafactory')->create($data);
594+
}
595+
596+
if (
597+
in_array($this->getMethod(), [Method::PUT, Method::PATCH, Method::DELETE], true)
598+
&& ! str_contains($contentType, 'multipart/form-data')
599+
) {
600+
return service('inputdatafactory')->create($this->getRawInput());
601+
}
602+
603+
if (in_array($this->getMethod(), [Method::GET, Method::HEAD], true)) {
604+
return service('inputdatafactory')->create([]);
605+
}
606+
607+
$data = $this->getPost();
608+
609+
return service('inputdatafactory')->create(is_array($data) ? $data : []);
610+
}
611+
558612
/**
559613
* Fetch an item from GET data.
560614
*

tests/system/HTTP/IncomingRequestTest.php

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use CodeIgniter\Exceptions\InvalidArgumentException;
2020
use CodeIgniter\HTTP\Exceptions\HTTPException;
2121
use CodeIgniter\HTTP\Files\UploadedFile;
22+
use CodeIgniter\Input\InputData;
2223
use CodeIgniter\Superglobals;
2324
use CodeIgniter\Test\CIUnitTestCase;
2425
use Config\App;
@@ -86,6 +87,35 @@ public function testCanGrabPostVars(): void
8687
$this->assertNull($this->request->getPost('TESTY'));
8788
}
8889

90+
public function testGetQueryInputReadsQueryData(): void
91+
{
92+
service('superglobals')->setGet('page', '3');
93+
service('superglobals')->setGet('filters', ['active' => 'true']);
94+
service('superglobals')->setPost('page', '10');
95+
96+
$request = $this->createRequest();
97+
$input = $request->getQueryInput();
98+
99+
$this->assertInstanceOf(InputData::class, $input);
100+
$this->assertSame(3, $input->integer('page'));
101+
$this->assertTrue($input->boolean('filters.active'));
102+
$this->assertSame(1, $input->integer('missing', 1));
103+
}
104+
105+
public function testGetPostInputReadsPostData(): void
106+
{
107+
service('superglobals')->setGet('remember', '0');
108+
service('superglobals')->setPost('remember', '1');
109+
service('superglobals')->setPost('tags', ['php', 'ci4']);
110+
111+
$request = $this->createRequest();
112+
$input = $request->getPostInput();
113+
114+
$this->assertInstanceOf(InputData::class, $input);
115+
$this->assertTrue($input->boolean('remember'));
116+
$this->assertSame(['php', 'ci4'], $input->array('tags'));
117+
}
118+
89119
public function testCanGrabPostBeforeGet(): void
90120
{
91121
service('superglobals')->setPost('TEST', '5');
@@ -572,6 +602,104 @@ public function testCanGrabGetRawInput(): void
572602
$this->assertSame($expected, $request->getRawInput());
573603
}
574604

605+
public function testGetPayloadInputReadsJsonBody(): void
606+
{
607+
$json = json_encode([
608+
'page' => '4',
609+
'filters' => ['active' => 'true'],
610+
'nullable' => null,
611+
]);
612+
613+
$request = $this->createRequest(new App(), $json);
614+
$request->setHeader('Content-Type', 'application/json');
615+
616+
$input = $request->getPayloadInput();
617+
618+
$this->assertInstanceOf(InputData::class, $input);
619+
$this->assertSame(4, $input->integer('page'));
620+
$this->assertTrue($input->boolean('filters.active'));
621+
$this->assertTrue($input->has('nullable'));
622+
}
623+
624+
#[DataProvider('provideGetPayloadInputReadsRawBodyForWriteRequests')]
625+
public function testGetPayloadInputReadsRawBodyForWriteRequests(string $method): void
626+
{
627+
$request = $this->createRequest(new App(), 'title=Hello&published=1')
628+
->withMethod($method);
629+
630+
$input = $request->getPayloadInput();
631+
632+
$this->assertSame('Hello', $input->string('title'));
633+
$this->assertTrue($input->boolean('published'));
634+
}
635+
636+
/**
637+
* @return iterable<string, array{string}>
638+
*/
639+
public static function provideGetPayloadInputReadsRawBodyForWriteRequests(): iterable
640+
{
641+
yield 'PUT' => ['PUT'];
642+
643+
yield 'PATCH' => ['PATCH'];
644+
645+
yield 'DELETE' => ['DELETE'];
646+
}
647+
648+
public function testGetPayloadInputReadsPostBodyForPostRequests(): void
649+
{
650+
service('superglobals')->setGet('title', 'Query title');
651+
service('superglobals')->setPost('title', 'Post title');
652+
653+
$request = $this->createRequest()->withMethod('POST');
654+
$input = $request->getPayloadInput();
655+
656+
$this->assertSame('Post title', $input->string('title'));
657+
}
658+
659+
public function testGetPayloadInputDoesNotReadQueryDataForGetRequests(): void
660+
{
661+
service('superglobals')->setGet('page', '2');
662+
663+
$request = $this->createRequest()->withMethod('GET');
664+
$input = $request->getPayloadInput();
665+
666+
$this->assertFalse($input->has('page'));
667+
$this->assertSame(1, $input->integer('page', 1));
668+
}
669+
670+
public function testGetPayloadInputReturnsEmptyInputForEmptyJsonBody(): void
671+
{
672+
$request = $this->createRequest(new App());
673+
$request->setHeader('Content-Type', 'application/json');
674+
675+
$input = $request->getPayloadInput();
676+
677+
$this->assertInstanceOf(InputData::class, $input);
678+
$this->assertFalse($input->has('name'));
679+
}
680+
681+
public function testGetPayloadInputRejectsScalarJsonBody(): void
682+
{
683+
$this->expectException(HTTPException::class);
684+
$this->expectExceptionMessage('The provided JSON format is not supported.');
685+
686+
$request = $this->createRequest(new App(), '"hello"');
687+
$request->setHeader('Content-Type', 'application/json');
688+
689+
$request->getPayloadInput();
690+
}
691+
692+
public function testGetPayloadInputKeepsInvalidJsonError(): void
693+
{
694+
$this->expectException(HTTPException::class);
695+
$this->expectExceptionMessage('Failed to parse JSON string. Error: Syntax error');
696+
697+
$request = $this->createRequest(new App(), 'Invalid JSON string');
698+
$request->setHeader('Content-Type', 'application/json');
699+
700+
$request->getPayloadInput();
701+
}
702+
575703
/**
576704
* @param string $rawstring
577705
* @param mixed $var

user_guide_src/source/changelogs/v4.8.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ HTTP
261261

262262
- 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`.
263263
- Added :ref:`Form Requests <form-requests>` - a new ``FormRequest`` base class that encapsulates validation rules, custom error messages, and authorization logic for a single HTTP request.
264+
- Added ``IncomingRequest::getQueryInput()``, ``getPostInput()``, and ``getPayloadInput()`` to read source-specific request data through ``InputData``.
264265
- Added ``SSEResponse`` class for streaming Server-Sent Events (SSE) over HTTP. See :ref:`server-sent-events`.
265266
- ``Response`` and its child classes no longer require ``Config\App`` passed to their constructors.
266267
Consequently, ``CURLRequest``'s ``$config`` parameter is unused and will be removed in a future release.

user_guide_src/source/incoming/incomingrequest.rst

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,32 @@ The ``getVar()`` method will pull from ``$_REQUEST``, so will return any data fr
161161
.. note:: If the incoming request has a ``Content-Type`` header set to ``application/json``,
162162
the ``getVar()`` method returns the JSON data instead of ``$_REQUEST`` data.
163163

164+
.. _incomingrequest-typed-source-input:
165+
166+
Typed Source Input
167+
==================
168+
169+
.. versionadded:: 4.8.0
170+
171+
``getQueryInput()``, ``getPostInput()``, and ``getPayloadInput()`` return
172+
request data as a ``CodeIgniter\Input\InputData`` object. Use these methods
173+
when you want source-explicit access with typed fallback helpers:
174+
175+
.. literalinclude:: incomingrequest/046.php
176+
:lines: 2-
177+
178+
``getQueryInput()`` reads query-string parameters. ``getPostInput()`` reads
179+
POST body parameters. ``getPayloadInput()`` reads the request body payload:
180+
JSON requests use the decoded JSON body, ``PUT``, ``PATCH``, and ``DELETE``
181+
requests use ``getRawInput()`` when they are not multipart requests, and
182+
ordinary form requests use POST body parameters.
183+
For non-JSON ``GET`` and ``HEAD`` requests, use ``getQueryInput()``;
184+
``getPayloadInput()`` returns an empty input object.
185+
186+
These methods do not validate input. They are fallback-friendly helpers for
187+
reading raw request data. Use Validation or :ref:`form-requests` when input
188+
must satisfy application rules before it is consumed.
189+
164190
.. _incomingrequest-getting-json-data:
165191

166192
Getting JSON Data
@@ -406,6 +432,11 @@ The methods provided by the parent classes that are available are:
406432

407433
.. literalinclude:: incomingrequest/045.php
408434

435+
.. php:method:: getQueryInput()
436+
437+
:returns: Query-string parameters as a typed input object.
438+
:rtype: CodeIgniter\\Input\\InputData
439+
409440
.. php:method:: getPost([$index = null[, $filter = null[, $flags = null]]])
410441
411442
: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:
418449

419450
This method is identical to ``getGet()``, only it fetches POST data.
420451

452+
.. php:method:: getPostInput()
453+
454+
:returns: POST body parameters as a typed input object.
455+
:rtype: CodeIgniter\\Input\\InputData
456+
457+
.. php:method:: getPayloadInput()
458+
459+
:returns: Request body payload parameters as a typed input object.
460+
:rtype: CodeIgniter\\Input\\InputData
461+
421462
.. php:method:: getPostGet([$index = null[, $filter = null[, $flags = null]]])
422463
423464
: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:
519560
.. note:: Prior to v4.4.0, this was the safest method to determine the
520561
"current URI", since ``IncomingRequest::$uri`` might not be aware of
521562
the complete App configuration for base URLs.
522-
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
$page = $request->getQueryInput()->integer('page', 1);
4+
$remember = $request->getPostInput()->boolean('remember', false);
5+
$name = $request->getPayloadInput()->string('name');

0 commit comments

Comments
 (0)