Skip to content

Commit

Permalink
PresenterComponentReflection::convertType() support for all PHP typeh…
Browse files Browse the repository at this point in the history
…ints
  • Loading branch information
dg committed Oct 2, 2015
1 parent a9e3c52 commit 5ef83ea
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 26 deletions.
4 changes: 2 additions & 2 deletions src/Application/UI/Presenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -1036,8 +1036,8 @@ public static function argsToParams($class, $method, & $args, $supplemental = []
}

$def = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : NULL;
$type = $param->isArray() ? 'array' : gettype($def);
if (!PresenterComponentReflection::convertType($args[$name], $type)) {
list($type, $isClass) = PresenterComponentReflection::getParameterType($param);
if (!PresenterComponentReflection::convertType($args[$name], $type, $isClass)) {
throw new InvalidLinkException("Invalid value for parameter '$name' in method $class::$method(), expected " . ($type === 'NULL' ? 'scalar' : $type) . ".");
}

Expand Down
53 changes: 41 additions & 12 deletions src/Application/UI/PresenterComponentReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,15 @@ public static function combineArgs(\ReflectionFunctionAbstract $method, $args)
$i = 0;
foreach ($method->getParameters() as $param) {
$name = $param->getName();
if (isset($args[$name])) { // NULLs are ignored
$res[$i++] = $args[$name];
$type = $param->isArray() ? 'array' : ($param->isDefaultValueAvailable() ? gettype($param->getDefaultValue()) : 'NULL');
if (!self::convertType($res[$i - 1], $type)) {
if (!isset($args[$name]) && $param->isDefaultValueAvailable()) {
$res[$i++] = $param->getDefaultValue();
} else {
$res[$i++] = isset($args[$name]) ? $args[$name] : NULL;
list($type, $isClass) = self::getParameterType($param);
if (!self::convertType($res[$i - 1], $type, $isClass)) {
$mName = $method instanceof \ReflectionMethod ? $method->getDeclaringClass()->getName() . '::' . $method->getName() : $method->getName();
throw new BadRequestException("Invalid value for parameter '$name' in method $mName(), expected " . ($type === 'NULL' ? 'scalar' : $type) . ".");
}
} else {
$res[$i++] = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : ($param->isArray() ? [] : NULL);
}
}
return $res;
Expand All @@ -141,21 +141,27 @@ public static function combineArgs(\ReflectionFunctionAbstract $method, $args)
* @param string
* @return bool
*/
public static function convertType(& $val, $type)
public static function convertType(& $val, $type, $isClass = FALSE)
{
if ($val === NULL) {
settype($val, $type);
if ($isClass) {
return $val instanceof $type;

} elseif ($type === 'callable') {
return FALSE;

} elseif (is_object($val)) {
return $type === 'NULL';
} elseif ($type === 'NULL') { // means 'not array'
return !is_array($val);

} elseif ($val === NULL) {
settype($val, $type); // to scalar or array

} elseif ($type === 'array') {
return is_array($val);

} elseif (!is_scalar($val)) { // array, resource, etc.
return FALSE;

} elseif ($type !== 'NULL') {
} else {
$old = $tmp = ($val === FALSE ? '0' : (string) $val);
settype($tmp, $type);
if ($old !== ($tmp === FALSE ? '0' : (string) $tmp)) {
Expand Down Expand Up @@ -184,4 +190,27 @@ public static function parseAnnotation(\Reflector $ref, $name)
return $res;
}


/**
* @return [string, bool]
*/
public static function getParameterType(\ReflectionParameter $param)
{
$def = gettype($param->isDefaultValueAvailable() ? $param->getDefaultValue() : NULL);
if (PHP_VERSION_ID >= 70000) {
return [(string) $param->getType() ?: $def, $param->hasType() && !$param->getType()->isBuiltin()];
} elseif ($param->isArray() || $param->isCallable()) {
return [$param->isArray() ? 'array' : 'callable', FALSE];
} else {
try {
return ($ref = $param->getClass()) ? [$ref->getName(), TRUE] : [$def, FALSE];
} catch (\ReflectionException $e) {
if (preg_match('#Class (.+) does not exist#', $e->getMessage(), $m)) {
return [$m[1], TRUE];
}
throw $e;
}
}
}

}
90 changes: 90 additions & 0 deletions tests/Application/Presenter.link().php7.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

/**
* Test: Nette\Application\UI\Presenter::link()
* @phpVersion 7
*/

use Nette\Http;
use Nette\Application;
use Tester\Assert;


require __DIR__ . '/../bootstrap.php';


class TestPresenter extends Application\UI\Presenter
{
/** @persistent */
public $var1 = 10;


protected function createTemplate($class = NULL)
{
}


protected function startup()
{
parent::startup();
$this->invalidLinkMode = self::INVALID_LINK_TEXTUAL;

Assert::same('/index.php?action=default&do=hint&presenter=Test', $this->link('hint!', ['var1' => $this->var1]));
Assert::same('/index.php?var1=20&action=default&do=hint&presenter=Test', $this->link('hint!', ['var1' => $this->var1 * 2]));
Assert::same('/index.php?y=2&action=default&do=hint&presenter=Test', $this->link('hint!', 1, 2));
Assert::same('/index.php?y=2&bool=1&str=1&action=default&do=hint&presenter=Test', $this->link('hint!', '1', '2', TRUE, TRUE));
Assert::same('/index.php?y=2&str=0&action=default&do=hint&presenter=Test', $this->link('hint!', '1', '2', FALSE, FALSE));
Assert::same('/index.php?action=default&do=hint&presenter=Test', $this->link('hint!', [1]));
Assert::same("#error: Invalid value for parameter 'x' in method TestPresenter::handlehint(), expected int.", $this->link('hint!', [1], (object) [1]));
Assert::same('/index.php?y=2&action=default&do=hint&presenter=Test', $this->link('hint!', [1, 'y' => 2]));
Assert::same('/index.php?y=2&action=default&do=hint&presenter=Test', $this->link('hint!', ['x' => 1, 'y' => 2, 'var1' => $this->var1]));
Assert::same('#error: Signal must be non-empty string.', $this->link('!'));
Assert::same('/index.php?action=default&presenter=Test', $this->link('this', ['var1' => $this->var1]));
Assert::same('/index.php?action=default&presenter=Test', $this->link('this!', ['var1' => $this->var1]));
Assert::same('/index.php?sort%5By%5D%5Basc%5D=1&action=default&presenter=Test', $this->link('this', ['sort' => ['y' => ['asc' => TRUE]]]));

// Presenter & signal link type checking
Assert::same("#error: Invalid value for parameter 'x' in method TestPresenter::handlehint(), expected int.", $this->link('hint!', 'x'));
Assert::same("#error: Invalid value for parameter 'bool' in method TestPresenter::handlehint(), expected bool.", $this->link('hint!', 1, 2, 3));
Assert::same("#error: Invalid value for parameter 'x' in method TestPresenter::handlehint(), expected int.", $this->link('hint!', [[]]));
Assert::same('/index.php?action=default&do=hint&presenter=Test', $this->link('hint!'));
Assert::same("#error: Invalid value for parameter 'x' in method TestPresenter::handlehint(), expected int.", $this->link('hint!', [new stdClass]));
}


public function handleHint(int $x = 1, int $y, bool $bool, string $str)
{
}

}


class MockPresenterFactory extends Nette\Object implements Nette\Application\IPresenterFactory
{
function getPresenterClass(& $name)
{
return str_replace(':', 'Module\\', $name) . 'Presenter';
}

function createPresenter($name)
{}
}


$url = new Http\UrlScript('http://localhost/index.php');
$url->setScriptPath('/index.php');

$presenter = new TestPresenter;
$presenter->injectPrimary(
NULL,
new MockPresenterFactory,
new Application\Routers\SimpleRouter,
new Http\Request($url),
new Http\Response
);

$presenter->invalidLinkMode = TestPresenter::INVALID_LINK_WARNING;
$presenter->autoCanonicalize = FALSE;

$request = new Application\Request('Test', Http\Request::GET, []);
$presenter->run($request);
8 changes: 4 additions & 4 deletions tests/Application/Presenter.link().phpt
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,13 @@ class TestPresenter extends Application\UI\Presenter
Assert::same('/index.php?action=default&do=buy&presenter=Test', $this->link('buy!'));
Assert::same("#error: Invalid value for parameter 'x' in method TestPresenter::handlebuy(), expected integer.", $this->link('buy!', [new stdClass]));

Assert::same('/index.php?a=x&action=default&do=obj&presenter=Test', $this->link('obj!', ['x']));
Assert::same("#error: Invalid value for parameter 'a' in method TestPresenter::handleobj(), expected stdClass.", $this->link('obj!', ['x']));
Assert::same('/index.php?action=default&do=obj&presenter=Test', $this->link('obj!', [new stdClass]));
Assert::same('/index.php?action=default&do=obj&presenter=Test', $this->link('obj!', [new Exception]));
Assert::same("#error: Invalid value for parameter 'a' in method TestPresenter::handleobj(), expected stdClass.", $this->link('obj!', [new Exception]));
Assert::same('/index.php?action=default&do=obj&presenter=Test', $this->link('obj!', [NULL]));
Assert::same('/index.php?b=x&action=default&do=obj&presenter=Test', $this->link('obj!', ['b' => 'x']));
Assert::same("#error: Invalid value for parameter 'b' in method TestPresenter::handleobj(), expected stdClass.", $this->link('obj!', ['b' => 'x']));
Assert::same('/index.php?action=default&do=obj&presenter=Test', $this->link('obj!', ['b' => new stdClass]));
Assert::same('/index.php?action=default&do=obj&presenter=Test', $this->link('obj!', ['b' => new Exception]));
Assert::same("#error: Invalid value for parameter 'b' in method TestPresenter::handleobj(), expected stdClass.", $this->link('obj!', ['b' => new Exception]));
Assert::same('/index.php?action=default&do=obj&presenter=Test', $this->link('obj!', ['b' => NULL]));

// Component link
Expand Down
51 changes: 43 additions & 8 deletions tests/Application/PresenterComponentReflection.convertType.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,22 @@ use Tester\Assert;
require __DIR__ . '/../bootstrap.php';


// [$type] null scalar array object* callable*
// [$type] null scalar array object callable
// [$val] ----------------------------------------------------------
// null (not used) pass cast cast error error
// scalar pass cast/deny deny error error
// null pass cast cast deny deny
// scalar pass cast/deny deny deny deny
// array deny deny pass deny deny
// object pass deny error pass/error pass/error
//
// error = E_RECOVERABLE_ERROR * = only as native typehint
// object pass deny deny pass/deny deny


function testIt($type, $val, $res = NULL)
{
$isClass = strtolower($type) !== $type;
if (func_num_args() === 3) {
Assert::true(PresenterComponentReflection::convertType($val, $type));
Assert::true(PresenterComponentReflection::convertType($val, $type, $isClass));
} else {
$res = $val;
Assert::false(PresenterComponentReflection::convertType($val, $type));
Assert::false(PresenterComponentReflection::convertType($val, $type, $isClass));
}
Assert::same($res, $val);
}
Expand Down Expand Up @@ -116,3 +115,39 @@ testIt('array', 0);
testIt('array', 1);
testIt('array', 1.0);
testIt('array', 1.2);

testIt('callable', NULL);
testIt('callable', []);
testIt('callable', $obj);
testIt('callable', function () {});
testIt('callable', '');
testIt('callable', 'trim');
testIt('callable', '1');
testIt('callable', '1.0');
testIt('callable', '1.1');
testIt('callable', '1a');
testIt('callable', TRUE);
testIt('callable', FALSE);
testIt('callable', 0);
testIt('callable', 1);
testIt('callable', 1.0);
testIt('callable', 1.2);

testIt('stdClass', NULL);
testIt('stdClass', []);
testIt('stdClass', $obj, $obj);
testIt('stdClass', function () {});
testIt('stdClass', '');
testIt('stdClass', 'a');
testIt('stdClass', '1');
testIt('stdClass', '1.0');
testIt('stdClass', '1.1');
testIt('stdClass', '1a');
testIt('stdClass', TRUE);
testIt('stdClass', FALSE);
testIt('stdClass', 0);
testIt('stdClass', 1);
testIt('stdClass', 1.0);
testIt('stdClass', 1.2);

testIt('Closure', $var = function () {}, $var);

0 comments on commit 5ef83ea

Please sign in to comment.