Skip to content

Commit

Permalink
Merge pull request #89 from utopia-php/feat-handle-null-param
Browse files Browse the repository at this point in the history
feat: handle null param
  • Loading branch information
TorstenDittmann authored Mar 1, 2023
2 parents 9b2e1fc + ba66e5a commit d4f36ae
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 140 deletions.
197 changes: 99 additions & 98 deletions composer.lock

Large diffs are not rendered by default.

46 changes: 29 additions & 17 deletions src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -645,18 +645,30 @@ protected function getArguments(Hook $hook, array $values, array $requestParams)
{
$arguments = [];
foreach ($hook->getParams() as $key => $param) { // Get value from route or request object
$arg = (isset($requestParams[$key])) ? $requestParams[$key] : $param['default'];
$value = (isset($values[$key])) ? $values[$key] : $arg;
$existsInRequest = \array_key_exists($key, $requestParams);
$existsInValues = \array_key_exists($key, $values);
$paramExists = $existsInRequest || $existsInValues;

$arg = $existsInRequest ? $requestParams[$key] : $param['default'];
$value = $existsInValues ? $values[$key] : $arg;

if ($hook instanceof Route) {
if ($hook->getIsAlias() && isset($hook->getAliasParams($hook->getAliasPath())[$key])) {
$value = $hook->getAliasParams($hook->getAliasPath())[$key];
$paramExists = true;
}
}

$value = ($value === '' || is_null($value)) ? $param['default'] : $value;
if (!$param['skipValidation']) {
if (!$paramExists && !$param['optional']) {
throw new Exception('Param "' . $key . '" is not optional.', 400);
}

if ($paramExists) {
$this->validate($key, $param, $value);
}
}

$this->validate($key, $param, $value);
$hook->setParamValue($key, $value);
$arguments[$param['order']] = $value;
}
Expand Down Expand Up @@ -798,22 +810,22 @@ public function run(Request $request, Response $response): static
*/
protected function validate(string $key, array $param, mixed $value): void
{
if ('' !== $value && ! is_null($value)) {
$validator = $param['validator']; // checking whether the class exists
if ($param['optional'] && \is_null($value)) {
return;
}

if (\is_callable($validator)) {
$validator = \call_user_func_array($validator, $this->getResources($param['injections']));
}
$validator = $param['validator']; // checking whether the class exists

if (! $validator instanceof Validator) { // is the validator object an instance of the Validator class
throw new Exception('Validator object is not an instance of the Validator class', 500);
}
if (\is_callable($validator)) {
$validator = \call_user_func_array($validator, $this->getResources($param['injections']));
}

if (! $validator->isValid($value)) {
throw new Exception('Invalid '.$key.': '.$validator->getDescription(), 400);
}
} elseif (! $param['optional']) {
throw new Exception('Param "'.$key.'" is not optional.', 400);
if (!$validator instanceof Validator) { // is the validator object an instance of the Validator class
throw new Exception('Validator object is not an instance of the Validator class', 500);
}

if (!$validator->isValid($value)) {
throw new Exception('Invalid '.$key.': '.$validator->getDescription(), 400);
}
}

Expand Down
4 changes: 3 additions & 1 deletion src/Hook.php
Original file line number Diff line number Diff line change
Expand Up @@ -196,16 +196,18 @@ public function inject(string $injection): static
* @param string $description
* @param bool $optional
* @param array $injections
* @param bool $skipValidation
* @return static
*/
public function param(string $key, mixed $default, Validator|callable $validator, string $description = '', bool $optional = false, array $injections = []): static
public function param(string $key, mixed $default, Validator|callable $validator, string $description = '', bool $optional = false, array $injections = [], bool $skipValidation = false): static
{
$this->params[$key] = [
'default' => $default,
'validator' => $validator,
'description' => $description,
'optional' => $optional,
'injections' => $injections,
'skipValidation' => $skipValidation,
'value' => null,
'order' => count($this->params) + count($this->injections),
];
Expand Down
65 changes: 65 additions & 0 deletions src/Validator/Nullable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace Utopia\Validator;

use Utopia\Validator;

class Nullable extends Validator
{
public function __construct(protected Validator $validator)
{
}

/**
* Get Description
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return $this->validator->getDescription() . ' or null';
}

/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}

/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return $this->validator->getType();
}

/**
* Is valid
*
* Validation will pass when $value is text with valid length.
*
* @param mixed $value
* @return bool
*/
public function isValid(mixed $value): bool
{
if (\is_null($value)) {
return true;
}

return $this->validator->isValid($value);
}
}
31 changes: 25 additions & 6 deletions src/Validator/Text.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@ class Text extends Validator
/**
* @var int
*/
protected int $length = 0;
protected int $length;

/**
* @var int
*/
protected int $min;

/**
* @var string[]
*/
protected array $allowList = [];
protected array $allowList;

/**
* Text constructor.
Expand All @@ -34,11 +39,13 @@ class Text extends Validator
* Optionally, provide allowList characters array $allowList to only allow specific character.
*
* @param int $length
* @param int $min
* @param string[] $allowList
*/
public function __construct(int $length, array $allowList = [])
public function __construct(int $length, int $min = 1, array $allowList = [])
{
$this->length = $length;
$this->min = $min;
$this->allowList = $allowList;
}

Expand All @@ -53,8 +60,16 @@ public function getDescription(): string
{
$message = 'Value must be a valid string';

if ($this->length) {
$message .= ' and no longer than '.$this->length.' chars';
if ($this->min === $this->length) {
$message .= ' and exactly '.$this->length.' chars';
} else {
if ($this->min) {
$message .= ' and at least '.$this->min.' chars';
}

if ($this->length) {
$message .= ' and no longer than '.$this->length.' chars';
}
}

if ($this->allowList) {
Expand Down Expand Up @@ -98,7 +113,11 @@ public function getType(): string
*/
public function isValid(mixed $value): bool
{
if (! \is_string($value)) {
if (!\is_string($value)) {
return false;
}

if (\mb_strlen($value) < $this->min) {
return false;
}

Expand Down
28 changes: 14 additions & 14 deletions tests/AppTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ public function testCanGetResources(): void

$route
->inject('rand')
->param('x', 'x-def', new Text(200), 'x param', false)
->param('y', 'y-def', new Text(200), 'y param', false)
->param('x', 'x-def', new Text(200), 'x param', true)
->param('y', 'y-def', new Text(200), 'y param', true)
->action(function ($x, $y, $rand) {
echo $x.'-'.$y.'-'.$rand;
});
Expand Down Expand Up @@ -144,8 +144,8 @@ public function testCanExecuteRoute(): void

$route
->alias('/path1', ['x' => 'x-def-1', 'y' => 'y-def-1'])
->param('x', 'x-def', new Text(200), 'x param', false)
->param('y', 'y-def', new Text(200), 'y param', false)
->param('x', 'x-def', new Text(200), 'x param', true)
->param('y', 'y-def', new Text(200), 'y param', true)
->action(function ($x, $y) {
echo $x.'-'.$y;
});
Expand All @@ -171,21 +171,21 @@ public function testCanExecuteRoute(): void
$route = new Route('GET', '/path');

$route
->param('x', 'x-def', new Text(200), 'x param', false)
->param('y', 'y-def', new Text(200), 'y param', false)
->param('x', 'x-def', new Text(200), 'x param', true)
->param('y', 'y-def', new Text(200), 'y param', true)
->inject('rand')
->param('z', 'z-def', function ($rand) {
echo $rand.'-';

return new Text(200);
}, 'z param', false, ['rand'])
}, 'z param', true, ['rand'])
->action(function ($x, $y, $z, $rand) {
echo $x.'-', $y;
});

\ob_start();
$request = new UtopiaRequestTest();
$request::_setParams(['x' => 'param-x', 'y' => 'param-y']);
$request::_setParams(['x' => 'param-x', 'y' => 'param-y', 'z' => 'param-z']);
$this->app->execute($route, $request);
$result = \ob_get_contents();
\ob_end_clean();
Expand All @@ -197,8 +197,8 @@ public function testCanExecuteRoute(): void
$route = new Route('GET', '/path');

$route
->param('x', 'x-def', new Text(1), 'x param', false)
->param('y', 'y-def', new Text(1), 'y param', false)
->param('x', 'x-def', new Text(1, min: 0), 'x param', false)
->param('y', 'y-def', new Text(1, min: 0), 'y param', false)
->action(function ($x, $y) {
echo $x.'-', $y;
});
Expand Down Expand Up @@ -311,7 +311,7 @@ public function testCanAddAndExecuteHooks()
// Default Params
$route = new Route('GET', '/path');
$route
->param('x', 'x-def', new Text(200), 'x param', false)
->param('x', 'x-def', new Text(200), 'x param', true)
->action(function ($x) {
echo $x;
});
Expand All @@ -326,7 +326,7 @@ public function testCanAddAndExecuteHooks()
// Default Params
$route = new Route('GET', '/path');
$route
->param('x', 'x-def', new Text(200), 'x param', false)
->param('x', 'x-def', new Text(200), 'x param', true)
->hook(false)
->action(function ($x) {
echo $x;
Expand Down Expand Up @@ -365,7 +365,7 @@ public function testCanHookThrowExceptions()
// param not provided for init
$route = new Route('GET', '/path');
$route
->param('x', 'x-def', new Text(200), 'x param', false)
->param('x', 'x-def', new Text(200), 'x param', true)
->action(function ($x) {
echo $x;
});
Expand Down Expand Up @@ -591,4 +591,4 @@ public function testWildcardRoute(): void

$this->assertEquals('HELLO', $result);
}
}
}
17 changes: 13 additions & 4 deletions tests/Validator/TextTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,21 @@ public function testCanValidateBoundaries(): void
$this->assertTrue($validator->isValid('hell'));
$this->assertTrue($validator->isValid('hello'));
$this->assertFalse($validator->isValid('hellow'));
$this->assertFalse($validator->isValid(''));

$validator = new Text(5, 3);
$this->assertTrue($validator->isValid('hel'));
$this->assertTrue($validator->isValid('hell'));
$this->assertTrue($validator->isValid('hello'));
$this->assertFalse($validator->isValid('hellow'));
$this->assertFalse($validator->isValid('he'));
$this->assertFalse($validator->isValid('h'));
}

public function testCanValidateTextWithAllowList(): void
{
// Test lowercase alphabet
$validator = new Text(100, Text::ALPHABET_LOWER);
$validator = new Text(100, allowList: Text::ALPHABET_LOWER);
$this->assertFalse($validator->isArray());
$this->assertTrue($validator->isValid('qwertzuiopasdfghjklyxcvbnm'));
$this->assertTrue($validator->isValid('hello'));
Expand All @@ -42,7 +51,7 @@ public function testCanValidateTextWithAllowList(): void
$this->assertFalse($validator->isValid('hello123'));

// Test uppercase alphabet
$validator = new Text(100, Text::ALPHABET_UPPER);
$validator = new Text(100, allowList: Text::ALPHABET_UPPER);
$this->assertFalse($validator->isArray());
$this->assertTrue($validator->isValid('QWERTZUIOPASDFGHJKLYXCVBNM'));
$this->assertTrue($validator->isValid('HELLO'));
Expand All @@ -53,15 +62,15 @@ public function testCanValidateTextWithAllowList(): void
$this->assertFalse($validator->isValid('HELLO123'));

// Test numbers
$validator = new Text(100, Text::NUMBERS);
$validator = new Text(100, allowList: Text::NUMBERS);
$this->assertFalse($validator->isArray());
$this->assertTrue($validator->isValid('1234567890'));
$this->assertTrue($validator->isValid('123'));
$this->assertFalse($validator->isValid('123 456'));
$this->assertFalse($validator->isValid('hello123'));

// Test combination of allowLists
$validator = new Text(100, [
$validator = new Text(100, allowList: [
...Text::ALPHABET_LOWER,
...Text::ALPHABET_UPPER,
...Text::NUMBERS,
Expand Down

0 comments on commit d4f36ae

Please sign in to comment.