diff --git a/system/HTTP/Parameters/InputParameters.php b/system/HTTP/Parameters/InputParameters.php new file mode 100644 index 000000000000..8aa050807588 --- /dev/null +++ b/system/HTTP/Parameters/InputParameters.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP\Parameters; + +use CodeIgniter\Exceptions\InvalidArgumentException; +use CodeIgniter\Exceptions\RuntimeException; + +/** + * @template TKey of string + * @template TValue of scalar|array<(int|string), mixed> + * + * @extends Parameters + * + * @see \CodeIgniter\HTTP\Parameters\InputParametersTest + */ +class InputParameters extends Parameters +{ + public function override(array $parameters = []): void + { + $this->parameters = []; + + foreach ($parameters as $key => $value) { + $this->set($key, $value); + } + } + + public function get(string $key, mixed $default = null): mixed + { + if ($default !== null && ! is_scalar($default)) { + throw new InvalidArgumentException(sprintf('The default value for the InputParameters must be a scalar type, "%s" given.', gettype($default))); + } + + // TODO: We need to check that the default value is set. Let's check the unique string + $tempDefault = bin2hex(random_bytes(8)); + + $value = parent::get($key, $tempDefault); + + if ($value !== null && $value !== $tempDefault && ! is_scalar($value)) { + throw new RuntimeException(sprintf('The value of the key "%s" InputParameters does not contain a scalar value, "%s" given.', $key, gettype($value))); + } + + return $value === $tempDefault ? $default : $value; + } + + public function set(string $key, mixed $value): void + { + if (! is_scalar($value) && ! is_array($value)) { + throw new InvalidArgumentException(sprintf('The value for the InputParameters must be a scalar type, "%s" given.', gettype($value))); + } + + $this->parameters[$key] = $value; + } +} diff --git a/system/HTTP/Parameters/Parameters.php b/system/HTTP/Parameters/Parameters.php new file mode 100644 index 000000000000..20f9b7d66c41 --- /dev/null +++ b/system/HTTP/Parameters/Parameters.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP\Parameters; + +use ArrayIterator; +use CodeIgniter\Exceptions\RuntimeException; + +/** + * @template TKey of string + * @template TValue + * + * @implements ParametersInterface + * + * @see \CodeIgniter\HTTP\Parameters\ParametersTest + */ +class Parameters implements ParametersInterface +{ + /** + * @param array $parameters + */ + public function __construct( + protected array $parameters = [], + ) { + } + + public function override(array $parameters = []): void + { + $this->parameters = $parameters; + } + + public function has(string $key): bool + { + return array_key_exists($key, $this->parameters); + } + + public function get(string $key, mixed $default = null): mixed + { + return array_key_exists($key, $this->parameters) ? $this->parameters[$key] : $default; + } + + public function set(string $key, mixed $value): void + { + $this->parameters[$key] = $value; + } + + public function remove(string $key): void + { + unset($this->parameters[$key]); + } + + public function all(?string $key = null): array + { + if ($key === null) { + return $this->parameters; + } + + if (! isset($this->parameters[$key]) || ! is_array($this->parameters[$key])) { + throw new RuntimeException(sprintf('The key "%s" value for Parameters is not an array or was not found.', $key)); + } + + return $this->parameters[$key]; + } + + public function keys(): array + { + return array_keys($this->parameters); + } + + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->parameters); + } + + public function count(): int + { + return count($this->parameters); + } +} diff --git a/system/HTTP/Parameters/ParametersInterface.php b/system/HTTP/Parameters/ParametersInterface.php new file mode 100644 index 000000000000..6873e78f7849 --- /dev/null +++ b/system/HTTP/Parameters/ParametersInterface.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP\Parameters; + +use Countable; +use IteratorAggregate; + +/** + * @template TKey of string + * @template TValue + * + * @extends IteratorAggregate + */ +interface ParametersInterface extends IteratorAggregate, Countable +{ + /** + * @param array $parameters + */ + public function __construct(array $parameters = []); + + /** + * @param array $parameters + */ + public function override(array $parameters = []): void; + + /** + * @param TKey $key + */ + public function has(string $key): bool; + + /** + * @param TKey $key + * @param TValue $default + * + * @return TValue|null + */ + public function get(string $key, mixed $default = null): mixed; + + /** + * @param TKey $key + * @param TValue $value + */ + public function set(string $key, mixed $value): void; + + /** + * @param TKey $key + * + * @return array + */ + public function all(?string $key = null): array; + + /** + * @return list + */ + public function keys(): array; +} diff --git a/system/HTTP/Parameters/ServerParameters.php b/system/HTTP/Parameters/ServerParameters.php new file mode 100644 index 000000000000..c6e87d49ae4e --- /dev/null +++ b/system/HTTP/Parameters/ServerParameters.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP\Parameters; + +/** + * @template TKey of string + * @template TValue + * + * @extends Parameters + */ +class ServerParameters extends Parameters +{ +} diff --git a/tests/system/HTTP/Parameters/InputParametersTest.php b/tests/system/HTTP/Parameters/InputParametersTest.php new file mode 100644 index 000000000000..414f8a7dd4e7 --- /dev/null +++ b/tests/system/HTTP/Parameters/InputParametersTest.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP\Parameters; + +use CodeIgniter\Exceptions\InvalidArgumentException; +use CodeIgniter\Test\CIUnitTestCase; +use PHPUnit\Framework\Attributes\Group; +use stdClass; + +/** + * @internal + */ +#[Group('Others')] +final class InputParametersTest extends CIUnitTestCase +{ + /** + * @var array + */ + private array $original = [ + 'title' => '', + 'toolbar' => '1', + 'path' => 'public/index.php', + 'current_time' => 1741522635.661474, + 'debug' => true, + 'pages' => 15, + 'filters' => [ + 0 => 'name', + 1 => 'sum', + ], + 'sort' => [ + 'date' => 'ASC', + 'age' => 'DESC', + ], + ]; + + public function testGetNonScalarValues(): void + { + $parameters = new InputParameters($this->original); + + $this->assertNull($parameters->get('undefined_or_null', null)); + + $this->expectException(InvalidArgumentException::class); + + $parameters->get('undefined_throw', new stdClass()); // @phpstan-ignore argument.type + } + + public function testAttemptSetNullValues(): void + { + $parameters = new InputParameters($this->original); + + $this->expectException(InvalidArgumentException::class); + + $parameters->set('nullable', null); // @phpstan-ignore argument.type + } + + public function testAttemptSetNonScalarValues(): void + { + $parameters = new InputParameters($this->original); + + $this->expectException(InvalidArgumentException::class); + + $parameters->set('nullable', new stdClass()); // @phpstan-ignore argument.type + } + + public function testUpdateAndSetNewValues(): void + { + /** + * @var array + */ + $expected = [ + 'title' => 'CodeIgniter', + 'toolbar' => '0', + 'path' => '', + 'current_time' => 1741522888.661434, + 'debug' => false, + 'pages' => 10, + 'filters' => [ + 0 => 'sum', + 1 => 'name', + ], + 'sort' => [ + 'age' => 'ASC', + 'date' => 'DESC', + ], + 'slug' => 'Ben-i-need-help', + ]; + + $parameters = new InputParameters($this->original); + + foreach (array_keys($expected) as $key) { + $parameters->set($key, $expected[$key]); + } + + $this->assertSame($expected, $parameters->all()); + } +} diff --git a/tests/system/HTTP/Parameters/ParametersTest.php b/tests/system/HTTP/Parameters/ParametersTest.php new file mode 100644 index 000000000000..fb824145de0e --- /dev/null +++ b/tests/system/HTTP/Parameters/ParametersTest.php @@ -0,0 +1,193 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP\Parameters; + +use CodeIgniter\Exceptions\RuntimeException; +use CodeIgniter\Test\CIUnitTestCase; +use PHPUnit\Framework\Attributes\Group; +use stdClass; + +/** + * @internal + */ +#[Group('Others')] +final class ParametersTest extends CIUnitTestCase +{ + /** + * @var array + */ + private array $original = []; + + protected function setUp(): void + { + $this->original = [ + 'DOCUMENT_ROOT' => '', + 'DISPLAY' => '1', + 'SCRIPT_NAME' => 'vendor/bin/phpunit', + 'REQUEST_TIME_FLOAT' => 1741522635.661474, + 'IS_HTTPS' => true, + 'PHP_NULLABLE_VAR' => null, + 'OBJECT' => new stdClass(), + 'argc' => 3, + 'argv' => [ + 0 => 'vendor/bin/phpunit', + 1 => './tests/ParametersTest.php', + 2 => '--no-coverage', + ], + ]; + } + + public function testCreateParametersAndCompareIdentity(): void + { + $parameters = new Parameters($this->original); + + $this->assertSame($this->original, $parameters->all()); + $this->assertSame($this->original, iterator_to_array($parameters->getIterator())); + } + + public function testCreateEmptyParameters(): void + { + $parameters = new Parameters(); + + $this->assertSame([], $parameters->all()); + $this->assertSame([], $parameters->keys()); + } + + public function testGetValues(): void + { + $parameters = new Parameters($this->original); + + foreach ($parameters->keys() as $key) { + $this->assertSame($this->original[$key], $parameters->get($key)); + } + } + + public function testUpdateAndSetNewValues(): void + { + /** + * @var array + */ + $expected = [ + 'DOCUMENT_ROOT' => '/www', + 'DISPLAY' => '0', + 'SCRIPT_NAME' => '', + 'REQUEST_TIME_FLOAT' => 1741522600.661400, + 'IS_HTTPS' => false, + 'PHP_NULLABLE_VAR' => null, + 'OBJECT' => new stdClass(), + 'argc' => 2, + 'argv' => [ + 0 => 'bin/phpunit', + 1 => './ParametersTest.php', + ], + 'XDEBUG' => 'enabled', + ]; + + $parameters = new Parameters($this->original); + + foreach (array_keys($expected) as $key) { + $parameters->set($key, $expected[$key]); + } + + $this->assertSame($expected, $parameters->all()); + } + + public function testOverrideParameters(): void + { + /** + * @var array + */ + $expected = [ + 'XDEBUG' => 'enabled', + 'DOCUMENT_ROOT' => '/www', + 'DISPLAY' => '0', + 'SCRIPT_NAME' => '', + 'REQUEST_TIME_FLOAT' => 1741522600.661400, + 'IS_HTTPS' => false, + 'PHP_NULLABLE_VAR' => null, + 'OBJECT' => new stdClass(), + 'argc' => 2, + 'argv' => [ + 0 => 'bin/phpunit', + 1 => './ParametersTest.php', + ], + ]; + + $parameters = new Parameters($this->original); + + $parameters->override($expected); + + $this->assertSame($expected, $parameters->all()); + } + + public function testGetUndefinedParametersWithDefaultValueReturn(): void + { + $parameters = new Parameters([]); + + $this->assertNull($parameters->get('undefined')); + $this->assertInstanceOf(stdClass::class, $parameters->get('undefined', new stdClass())); + $this->assertSame('', $parameters->get('undefined', '')); + $this->assertSame(1000, $parameters->get('undefined', 1000)); + $this->assertSame(['name' => 'Ivan'], $parameters->get('undefined', ['name' => 'Ivan'])); + $this->assertEqualsWithDelta(10.00, $parameters->get('undefined', 10.00), PHP_FLOAT_EPSILON); + } + + public function testRemoveKeys(): void + { + $parameters = new Parameters($this->original); + + $parameters->remove('argc'); + $parameters->remove('argv'); + + unset($this->original['argc'], $this->original['argv']); + + $this->assertSame($this->original, $parameters->all()); + } + + public function testCount(): void + { + $parameters = new Parameters($this->original); + + $this->assertCount(count($this->original), $parameters); + + $parameters->remove('DOCUMENT_ROOT'); + $parameters->remove('DISPLAY'); + + $this->assertCount(count($this->original) - 2, $parameters); + } + + public function testGetAll(): void + { + $parameters = new Parameters($this->original); + + $this->assertSame($this->original, $parameters->all()); + $this->assertSame( + [ + 'vendor/bin/phpunit', + './tests/ParametersTest.php', + '--no-coverage', + ], + $parameters->all('argv'), + ); + } + + public function testAttemptGetAllNonIterableValues(): void + { + $parameters = new Parameters($this->original); + + $this->expectException(RuntimeException::class); + + $parameters->all('argc'); + } +}