Skip to content

Commit 32e7fb5

Browse files
author
Bernd Hoehl
committed
adding support for optional and referenced parameters in BUILT-IN functions
1 parent c043bf2 commit 32e7fb5

File tree

2 files changed

+156
-38
lines changed

2 files changed

+156
-38
lines changed

src/AspectMock/Intercept/FunctionInjector.php

Lines changed: 105 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,54 +3,123 @@
33

44
class FunctionInjector
55
{
6-
protected $template = <<<EOF
6+
protected $template = <<<EOF
77
<?php
88
namespace {{ns}};
99
if (!function_exists('{{ns}}\{{func}}')) {
10-
function {{func}}() {
11-
if ((\$__am_res = __amock_before_func('{{ns}}','{{func}}', func_get_args())) !== __AM_CONTINUE__) {
12-
return \$__am_res;
10+
function {{func}}() {
11+
if ((\$__am_res = __amock_before_func('{{ns}}','{{func}}', func_get_args())) !== __AM_CONTINUE__) {
12+
return \$__am_res;
13+
}
14+
return call_user_func_array('{{func}}', func_get_args());
1315
}
14-
return call_user_func_array('{{func}}', func_get_args());
1516
}
17+
EOF;
18+
19+
protected $templateByRefOptional = <<<EOF
20+
<?php
21+
namespace {{ns}};
22+
if (!function_exists('{{ns}}\{{func}}')) {
23+
function {{func}}({{arguments}}) {
24+
\$args = [];
25+
switch(count(func_get_args())) {
26+
{{code}} }
27+
if ((\$__am_res = __amock_before_func('{{ns}}','{{func}}', \$args)) !== __AM_CONTINUE__) {
28+
return \$__am_res;
29+
}
30+
return call_user_func_array('{{func}}', \$args);
31+
}
1632
}
1733
EOF;
18-
protected $namespace;
19-
protected $function;
20-
protected $fileName;
2134

22-
function __construct($namespace, $function)
23-
{
24-
$this->namespace = $namespace;
25-
$this->function = $function;
26-
$this->place('ns', $this->namespace);
27-
$this->place('func', $this->function);
35+
protected $namespace;
2836

29-
}
37+
protected $function;
38+
protected $fileName;
3039

31-
public function save()
32-
{
33-
$this->fileName = tempnam(sys_get_temp_dir(), $this->function);
34-
file_put_contents($this->fileName, $this->template);
35-
}
40+
function __construct($namespace, $function)
41+
{
42+
$this->namespace = $namespace;
43+
$this->function = $function;
44+
$this->placeOptionalAndReferenceFunction($namespace, $function);
45+
$this->place('ns', $this->namespace);
46+
$this->place('func', $this->function);
47+
}
3648

37-
public function inject()
38-
{
39-
require_once $this->fileName;
40-
}
49+
public function getParameterDeclaration(\ReflectionParameter $parameter)
50+
{
51+
$text = (string)$parameter;
52+
if (preg_match('@Parameter\s#[0-9]+\s\[\s<(required|optional)>(.*)(\sor NULL)(.*)\s\]@', $text, $match)) {
53+
$text = $match(2).$match[4];
54+
} elseif (preg_match('@Parameter\s#[0-9]+\s\[\s<(required|optional)>\s(.*)\s\]@', $text, $match)) {
55+
$text = $match[2];
56+
} else {
57+
throw new \Exception('reflection api changed. adjust code.');
58+
}
59+
if ($parameter->isOptional()) {
60+
$text .= "=NULL";
61+
}
62+
return $text;
63+
}
4164

42-
public function getFileName()
43-
{
44-
return $this->fileName;
45-
}
65+
public function placeOptionalAndReferenceFunction($namespace, $function)
66+
{
67+
$reflect = new \ReflectionFunction($function);
68+
$parameters = [];
69+
$args = '';
70+
$byRef = false;
71+
$optionals = false;
72+
$names = [];
73+
foreach ($reflect->getParameters() as $parameter) {
74+
$name = '$'.$parameter->getName();
75+
$newname = '$p'.$parameter->getPosition();
76+
$declaration = str_replace($name, $newname, $this->getParameterDeclaration($parameter));
77+
$name = $newname;
78+
if (!$optionals && $parameter->isOptional()) {
79+
$optionals = true;
80+
}
81+
if ($parameter->isPassedByReference()) {
82+
$name = '&'.$name;
83+
$byRef = true;
84+
}
85+
$names[] = $name;
86+
$parameters[$newname] = $declaration;
87+
}
88+
if ($optionals || $byRef) {
89+
$this->template = $this->templateByRefOptional;
90+
$this->place('arguments', join(', ', $parameters));
91+
$code = '';
92+
for ($i = count($parameters); $i > 0; $i--) {
93+
$code .= " case {$i}: \$args = [" . join(', ', $names) . "]; break;\n";
94+
array_pop($names);
95+
}
96+
$this->place('code', $code);
97+
}
98+
}
4699

47-
public function getPHP()
48-
{
49-
return $this->template;
50-
}
100+
public function save()
101+
{
102+
$this->fileName = tempnam(sys_get_temp_dir(), $this->function);
103+
file_put_contents($this->fileName, $this->template);
104+
}
51105

52-
protected function place($var, $value)
53-
{
54-
$this->template = str_replace("{{{$var}}}", $value, $this->template);
55-
}
106+
public function inject()
107+
{
108+
require_once $this->fileName;
109+
}
110+
111+
public function getFileName()
112+
{
113+
return $this->fileName;
114+
}
115+
116+
public function getPHP()
117+
{
118+
return $this->template;
119+
}
120+
121+
protected function place($var, $value)
122+
{
123+
$this->template = str_replace("{{{$var}}}", $value, $this->template);
124+
}
56125
}

tests/unit/FunctionInjectorTest.php

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,60 @@ class FunctionInjectorTest extends \Codeception\TestCase\Test
1111
* @var FunctionInjector
1212
*/
1313
protected $funcInjector;
14+
/**
15+
* @var FunctionInjector
16+
*/
17+
protected $funcOptionalParameterInjector;
18+
/**
19+
* @var FunctionInjector
20+
*/
21+
protected $funcReferencedParameterInjector;
1422

1523
public function _before()
1624
{
1725
$this->funcInjector = new FunctionInjector('demo', 'strlen');
26+
$this->funcOptionalParameterInjector = new FunctionInjector('demo', 'explode');
27+
$this->funcReferencedParameterInjector = new FunctionInjector('demo', 'preg_match');
1828
test::clean();
1929
}
2030

2131
public function testTemplate()
2232
{
2333
$php = $this->funcInjector->getPHP();
24-
verify($php)->contains("function strlen()");
25-
verify($php)->contains("return call_user_func_array('strlen', func_get_args());");
34+
if ((int)ini_get('mbstring.func_overload') & 2 !== 0) {
35+
// in this case strlen is overloaded by mb_strlen with optional parameter charset
36+
verify($php)->contains("function strlen(\$p0, \$p1=NULL)");
37+
verify($php)->contains("case 2: \$args = [\$p0, \$p1]; break;");
38+
verify($php)->contains("case 1: \$args = [\$p0]; break;");
39+
verify($php)->contains("return call_user_func_array('strlen', \$args);");
40+
} else {
41+
verify($php)->contains("function strlen()");
42+
verify($php)->contains("return call_user_func_array('strlen', func_get_args());");
43+
}
2644
}
2745

46+
public function testOptionalParameterTemplate()
47+
{
48+
$php = $this->funcOptionalParameterInjector->getPHP();
49+
verify($php)->contains("function explode(\$p0, \$p1, \$p2=NULL)");
50+
verify($php)->contains("case 3: \$args = [\$p0, \$p1, \$p2]; break;");
51+
verify($php)->contains("case 2: \$args = [\$p0, \$p1]; break;");
52+
verify($php)->contains("case 1: \$args = [\$p0]; break;");
53+
verify($php)->contains("return call_user_func_array('explode', \$args);");
54+
}
55+
56+
public function testReferencedParameterTemplate()
57+
{
58+
$php = $this->funcReferencedParameterInjector->getPHP();
59+
verify($php)->contains("function preg_match(\$p0, \$p1, &\$p2=NULL, \$p3=NULL, \$p4=NULL)");
60+
verify($php)->contains("case 5: \$args = [\$p0, \$p1, &\$p2, \$p3, \$p4]; break;");
61+
verify($php)->contains("case 4: \$args = [\$p0, \$p1, &\$p2, \$p3]; break;");
62+
verify($php)->contains("case 3: \$args = [\$p0, \$p1, &\$p2]; break;");
63+
verify($php)->contains("case 2: \$args = [\$p0, \$p1]; break;");
64+
verify($php)->contains("case 1: \$args = [\$p0]; break;");
65+
verify($php)->contains("return call_user_func_array('preg_match', \$args);");
66+
}
67+
2868
public function testSave()
2969
{
3070
$this->funcInjector->save();
@@ -82,4 +122,13 @@ public function testFailedVerification()
82122
$func->verifyNeverInvoked('strlen');
83123
}
84124

125+
public function testReferencedParameter()
126+
{
127+
$func = test::func('\demo', 'preg_match', 10);
128+
expect(preg_match('@[0-9]+@', '1234', $match))->equals(10);
129+
test::clean();
130+
expect(preg_match('@[0-9]+@', '1234#', $match))->equals(1);
131+
expect($match[0])->equals('1234');
132+
}
133+
85134
}

0 commit comments

Comments
 (0)