Skip to content

Commit 3108ebe

Browse files
committed
PresenterComponentReflection::convertType() support for all PHP typehints
1 parent a9e3c52 commit 3108ebe

File tree

5 files changed

+175
-24
lines changed

5 files changed

+175
-24
lines changed

src/Application/UI/Presenter.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,8 +1036,8 @@ public static function argsToParams($class, $method, & $args, $supplemental = []
10361036
}
10371037

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

src/Application/UI/PresenterComponentReflection.php

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,15 @@ public static function combineArgs(\ReflectionFunctionAbstract $method, $args)
120120
$i = 0;
121121
foreach ($method->getParameters() as $param) {
122122
$name = $param->getName();
123-
if (isset($args[$name])) { // NULLs are ignored
124-
$res[$i++] = $args[$name];
125-
$type = $param->isArray() ? 'array' : ($param->isDefaultValueAvailable() ? gettype($param->getDefaultValue()) : 'NULL');
126-
if (!self::convertType($res[$i - 1], $type)) {
123+
if (!isset($args[$name]) && $param->isDefaultValueAvailable()) {
124+
$res[$i++] = $param->getDefaultValue();
125+
} else {
126+
$res[$i++] = isset($args[$name]) ? $args[$name] : NULL;
127+
list($type, $isClass) = self::getParameterType($param);
128+
if (!self::convertType($res[$i - 1], $type, $isClass)) {
127129
$mName = $method instanceof \ReflectionMethod ? $method->getDeclaringClass()->getName() . '::' . $method->getName() : $method->getName();
128130
throw new BadRequestException("Invalid value for parameter '$name' in method $mName(), expected " . ($type === 'NULL' ? 'scalar' : $type) . ".");
129131
}
130-
} else {
131-
$res[$i++] = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : ($param->isArray() ? [] : NULL);
132132
}
133133
}
134134
return $res;
@@ -141,21 +141,27 @@ public static function combineArgs(\ReflectionFunctionAbstract $method, $args)
141141
* @param string
142142
* @return bool
143143
*/
144-
public static function convertType(& $val, $type)
144+
public static function convertType(& $val, $type, $isClass = FALSE)
145145
{
146-
if ($val === NULL) {
147-
settype($val, $type);
146+
if ($isClass) {
147+
return $val instanceof $type;
148+
149+
} elseif ($type === 'callable') {
150+
return FALSE;
148151

149-
} elseif (is_object($val)) {
150-
return $type === 'NULL';
152+
} elseif ($type === 'NULL') { // means 'not array'
153+
return !is_array($val);
154+
155+
} elseif ($val === NULL) {
156+
settype($val, $type); // to scalar or array
151157

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

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

158-
} elseif ($type !== 'NULL') {
164+
} else {
159165
$old = $tmp = ($val === FALSE ? '0' : (string) $val);
160166
settype($tmp, $type);
161167
if ($old !== ($tmp === FALSE ? '0' : (string) $tmp)) {
@@ -184,4 +190,27 @@ public static function parseAnnotation(\Reflector $ref, $name)
184190
return $res;
185191
}
186192

193+
194+
/**
195+
* @return [string, bool]
196+
*/
197+
public static function getParameterType(\ReflectionParameter $param)
198+
{
199+
$def = gettype($param->isDefaultValueAvailable() ? $param->getDefaultValue() : NULL);
200+
if (PHP_VERSION_ID >= 70000) {
201+
return [(string) $param->getType() ?: $def, $param->hasType() && !$param->getType()->isBuiltin()];
202+
} elseif ($param->isArray() || $param->isCallable()) {
203+
return [$param->isArray() ? 'array' : 'callable', FALSE];
204+
} else {
205+
try {
206+
return ($ref = $param->getClass()) ? [$ref->getName(), TRUE] : [$def, FALSE];
207+
} catch (\ReflectionException $e) {
208+
if (preg_match('#Class (.+) does not exist#', $e->getMessage(), $m)) {
209+
return [$m[1], TRUE];
210+
}
211+
throw $e;
212+
}
213+
}
214+
}
215+
187216
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
/**
4+
* Test: Nette\Application\UI\Presenter::link()
5+
* @phpVersion 7
6+
*/
7+
8+
use Nette\Http;
9+
use Nette\Application;
10+
use Tester\Assert;
11+
12+
13+
require __DIR__ . '/../bootstrap.php';
14+
15+
16+
class TestPresenter extends Application\UI\Presenter
17+
{
18+
/** @persistent */
19+
public $var1 = 10;
20+
21+
22+
protected function createTemplate($class = NULL)
23+
{
24+
}
25+
26+
27+
protected function startup()
28+
{
29+
parent::startup();
30+
$this->invalidLinkMode = self::INVALID_LINK_TEXTUAL;
31+
32+
Assert::same('/index.php?action=default&do=hint&presenter=Test', $this->link('hint!', ['var1' => $this->var1]));
33+
Assert::same('/index.php?var1=20&action=default&do=hint&presenter=Test', $this->link('hint!', ['var1' => $this->var1 * 2]));
34+
Assert::same('/index.php?y=2&action=default&do=hint&presenter=Test', $this->link('hint!', 1, 2));
35+
Assert::same('/index.php?y=2&bool=1&str=1&action=default&do=hint&presenter=Test', $this->link('hint!', '1', '2', TRUE, TRUE));
36+
Assert::same('/index.php?y=2&str=0&action=default&do=hint&presenter=Test', $this->link('hint!', '1', '2', FALSE, FALSE));
37+
Assert::same('/index.php?action=default&do=hint&presenter=Test', $this->link('hint!', [1]));
38+
Assert::same("#error: Invalid value for parameter 'x' in method TestPresenter::handlehint(), expected int.", $this->link('hint!', [1], (object) [1]));
39+
Assert::same('/index.php?y=2&action=default&do=hint&presenter=Test', $this->link('hint!', [1, 'y' => 2]));
40+
Assert::same('/index.php?y=2&action=default&do=hint&presenter=Test', $this->link('hint!', ['x' => 1, 'y' => 2, 'var1' => $this->var1]));
41+
Assert::same('#error: Signal must be non-empty string.', $this->link('!'));
42+
Assert::same('/index.php?action=default&presenter=Test', $this->link('this', ['var1' => $this->var1]));
43+
Assert::same('/index.php?action=default&presenter=Test', $this->link('this!', ['var1' => $this->var1]));
44+
Assert::same('/index.php?sort%5By%5D%5Basc%5D=1&action=default&presenter=Test', $this->link('this', ['sort' => ['y' => ['asc' => TRUE]]]));
45+
46+
// Presenter & signal link type checking
47+
Assert::same("#error: Invalid value for parameter 'x' in method TestPresenter::handlehint(), expected int.", $this->link('hint!', 'x'));
48+
Assert::same("#error: Invalid value for parameter 'bool' in method TestPresenter::handlehint(), expected bool.", $this->link('hint!', 1, 2, 3));
49+
Assert::same("#error: Invalid value for parameter 'x' in method TestPresenter::handlehint(), expected int.", $this->link('hint!', [[]]));
50+
Assert::same('/index.php?action=default&do=hint&presenter=Test', $this->link('hint!'));
51+
Assert::same("#error: Invalid value for parameter 'x' in method TestPresenter::handlehint(), expected int.", $this->link('hint!', [new stdClass]));
52+
}
53+
54+
55+
public function handleHint(int $x = 1, int $y, bool $bool, string $str)
56+
{
57+
}
58+
59+
}
60+
61+
62+
class MockPresenterFactory extends Nette\Object implements Nette\Application\IPresenterFactory
63+
{
64+
function getPresenterClass(& $name)
65+
{
66+
return str_replace(':', 'Module\\', $name) . 'Presenter';
67+
}
68+
69+
function createPresenter($name)
70+
{}
71+
}
72+
73+
74+
$url = new Http\UrlScript('http://localhost/index.php');
75+
$url->setScriptPath('/index.php');
76+
77+
$presenter = new TestPresenter;
78+
$presenter->injectPrimary(
79+
NULL,
80+
new MockPresenterFactory,
81+
new Application\Routers\SimpleRouter,
82+
new Http\Request($url),
83+
new Http\Response
84+
);
85+
86+
$presenter->invalidLinkMode = TestPresenter::INVALID_LINK_WARNING;
87+
$presenter->autoCanonicalize = FALSE;
88+
89+
$request = new Application\Request('Test', Http\Request::GET, []);
90+
$presenter->run($request);

tests/Application/Presenter.link().phpt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,13 @@ class TestPresenter extends Application\UI\Presenter
120120
Assert::same('/index.php?action=default&do=buy&presenter=Test', $this->link('buy!'));
121121
Assert::same("#error: Invalid value for parameter 'x' in method TestPresenter::handlebuy(), expected integer.", $this->link('buy!', [new stdClass]));
122122

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

132132
// Component link

tests/Application/PresenterComponentReflection.convertType.phpt

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,12 @@ use Tester\Assert;
1111
require __DIR__ . '/../bootstrap.php';
1212

1313

14-
// [$type] null scalar array object* callable*
14+
// [$type] null scalar array object callable
1515
// [$val] ----------------------------------------------------------
16-
// null (not used) pass cast cast error error
17-
// scalar pass cast/deny deny error error
16+
// null pass cast cast deny deny
17+
// scalar pass cast/deny deny deny deny
1818
// array deny deny pass deny deny
19-
// object pass deny error pass/error pass/error
20-
//
21-
// error = E_RECOVERABLE_ERROR * = only as native typehint
19+
// object pass deny deny pass/deny deny
2220

2321

2422
function testIt($type, $val, $res = NULL)
@@ -116,3 +114,37 @@ testIt('array', 0);
116114
testIt('array', 1);
117115
testIt('array', 1.0);
118116
testIt('array', 1.2);
117+
118+
testIt('callable', NULL);
119+
testIt('callable', []);
120+
testIt('callable', $obj);
121+
testIt('callable', function () {});
122+
testIt('callable', '');
123+
testIt('callable', 'trim');
124+
testIt('callable', '1');
125+
testIt('callable', '1.0');
126+
testIt('callable', '1.1');
127+
testIt('callable', '1a');
128+
testIt('callable', TRUE);
129+
testIt('callable', FALSE);
130+
testIt('callable', 0);
131+
testIt('callable', 1);
132+
testIt('callable', 1.0);
133+
testIt('callable', 1.2);
134+
135+
$var = NULL;
136+
Assert::false(PresenterComponentReflection::convertType($var, 'stdClass', TRUE));
137+
$var = [];
138+
Assert::false(PresenterComponentReflection::convertType($var, 'stdClass', TRUE));
139+
Assert::true(PresenterComponentReflection::convertType($obj, 'stdClass', TRUE));
140+
$var = function () {};
141+
Assert::false(PresenterComponentReflection::convertType($var, 'stdClass', TRUE));
142+
Assert::true(PresenterComponentReflection::convertType($var, 'Closure', TRUE));
143+
$var = '';
144+
Assert::false(PresenterComponentReflection::convertType($var, 'stdClass', TRUE));
145+
$var = '1a';
146+
Assert::false(PresenterComponentReflection::convertType($var, 'stdClass', TRUE));
147+
$var = TRUE;
148+
Assert::false(PresenterComponentReflection::convertType($var, 'stdClass', TRUE));
149+
$var = 0;
150+
Assert::false(PresenterComponentReflection::convertType($var, 'stdClass', TRUE));

0 commit comments

Comments
 (0)