Skip to content

Commit 4de13c5

Browse files
committed
Add util class Instance
1 parent fd812b7 commit 4de13c5

2 files changed

Lines changed: 380 additions & 0 deletions

File tree

src/Utils/Instance.php

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
<?php
2+
/**
3+
* CROSS PHPunit Utils
4+
*
5+
* @filesource
6+
* @copyright 2019 Cross Solution <https://www.cross-solution.de>
7+
* @license MIT
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace Cross\TestUtils\Utils;
13+
14+
use Cross\TestUtils\Exception\InvalidUsageException;
15+
16+
/**
17+
* Creates object instances.
18+
*
19+
* @author Mathias Gelhausen <gelhausen@cross-solution.de>
20+
*/
21+
final class Instance
22+
{
23+
24+
/**
25+
* Create a \ReflectionClass instance for an object or FQCN.
26+
*
27+
* This method is mainly used by various traits, to
28+
* allow creating reflection instances from an array specification
29+
* The sole purpose of this method is:
30+
* If an array is passed, the target object or FQCN is taken from
31+
* * the element with the key 'class' or
32+
* * the first element in the array
33+
*
34+
* @param string|array|object $fqcnOrObject
35+
*
36+
* @return \ReflectionClass
37+
*/
38+
public static function reflection($fqcnOrObject): \ReflectionClass
39+
{
40+
if (is_array($fqcnOrObject)) {
41+
$fqcnOrObject = $fqcnOrObject['class'] ?? reset($fqcnOrObject);
42+
}
43+
44+
return new \ReflectionClass($fqcnOrObject);
45+
}
46+
47+
/**
48+
* Creates an object.
49+
*
50+
* if __$fqcn__ is a string and starts with "!", a \ReflectionClass
51+
* object is returned.
52+
*
53+
* if __$fqcn__ is an array, the first element is used as FQCN and
54+
* all other elements are used as constructor arguments - other
55+
* arguments passed in are ignored.
56+
*
57+
* @param string|array $fqcn
58+
* @param mixed ...$arguments
59+
*
60+
* @return object
61+
*/
62+
public static function create($fqcn, ...$arguments): object
63+
{
64+
if (is_array($fqcn)) {
65+
$arguments = array_slice($fqcn, 1);
66+
$fqcn = reset($fqcn);
67+
}
68+
69+
if (!is_string($fqcn)) {
70+
throw InvalidUsageException::fromClass(
71+
__CLASS__,
72+
'Expected a string as FQCN, but received %s',
73+
gettype($fqcn)
74+
);
75+
}
76+
77+
if ('!' == $fqcn{0}) {
78+
return self::reflection(substr($fqcn, 1));
79+
}
80+
81+
return new $fqcn(...$arguments);
82+
}
83+
84+
/**
85+
* Creates an object.
86+
*
87+
* For each entry in __$arguments__ the following operations are made:
88+
*
89+
* * if the key is a string
90+
* * if the value is a callable, it is called and the returned value
91+
* used as argument value.
92+
* * if the value is not a callable, the key is used as method name
93+
* to call on the value - which should be an object or FQCN
94+
*
95+
*
96+
* * if the key is numeric
97+
* * if value is a string starting with '@', the remaining string will be used:
98+
* * as callable: the returned value is used as argument value
99+
* * as method name to call on the __$context__ object to get the
100+
* argument value - if __$context__ is not null
101+
* * if the value is an array that has the key '@', the value of that key is treated
102+
* * as callable: the returned value is used as argument value
103+
* * as method name to call on the __$context__ object to get the
104+
* argument value - if __$context__ is not null
105+
*
106+
* __NOTE__: Also private or protected method on the __$context__ can be used as
107+
* method name, because reflection is used as last possibility.
108+
*
109+
* If none of the above leads to calling a callback, the value is taken as argument value
110+
* as is.
111+
*
112+
* You may pass the FQCN and the arguments as array to __$fqcn__
113+
* In that case - or if no arguments are needed - you may pass the context object
114+
* to __$arguments__
115+
*
116+
* @param string|array $fqcn
117+
* @param array|object $arguments
118+
* @param object|null $context
119+
*
120+
* @return object
121+
*/
122+
public static function withMappedArguments($fqcn, $arguments, ?object $context = null): object
123+
{
124+
if (is_object($arguments)) {
125+
$context = $arguments;
126+
$arguments = [];
127+
}
128+
129+
if (is_array($fqcn)) {
130+
$arguments = array_slice($fqcn, 1);
131+
$fqcn = reset($fqcn);
132+
}
133+
134+
$f = function ($value, $key) use ($context) {
135+
/** @var \ReflectionClass $reflection */
136+
static $reflection;
137+
138+
switch (true) {
139+
// 'method' => object|FQCN|callable
140+
case is_string($key):
141+
$callback = is_callable($value) ? $value : [$value, $key];
142+
break;
143+
144+
// '@function'
145+
case is_string($value) && '@' == $value{0}:
146+
$callback = ltrim($value, '@');
147+
break;
148+
149+
// ['@' => callable]
150+
case is_array($value) && isset($value['@']):
151+
$callback = $value['@'];
152+
break;
153+
154+
default:
155+
return $value;
156+
}
157+
158+
if (is_callable($callback)) {
159+
return $callback();
160+
}
161+
162+
if (!$context) {
163+
return $value;
164+
}
165+
166+
$contextCallback = [$context, $callback];
167+
168+
if (is_callable($contextCallback)) {
169+
return $contextCallback();
170+
}
171+
172+
if (!is_string($callback)) {
173+
return $value;
174+
}
175+
176+
if (!$reflection) {
177+
$reflection = new \ReflectionClass($context);
178+
}
179+
180+
if ($reflection->hasMethod($callback)) {
181+
$method = $reflection->getMethod($callback);
182+
$method->setAccessible(true);
183+
184+
return $method->invoke($context);
185+
}
186+
187+
return $value;
188+
};
189+
190+
$arguments = array_map($f, $arguments, array_keys($arguments));
191+
192+
return self::create($fqcn, ...$arguments);
193+
}
194+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
<?php
2+
/**
3+
* CROSS PHPunit Utils
4+
*
5+
* @filesource
6+
* @copyright 2019 Cross Solution <https://www.cross-solution.de>
7+
* @license MIT
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace Cross\TestUtilsTest\Utils;
13+
14+
use Cross\TestUtils\Exception\InvalidUsageException;
15+
16+
use Cross\TestUtils\Utils\Instance;
17+
18+
/**
19+
* Tests for \Cross\TestUtils\Utils\Instance
20+
*
21+
* @covers \Cross\TestUtils\Utils\Instance
22+
* @author Mathias Gelhausen <gelhausen@cross-solution.de>
23+
*
24+
* @group Cross.TestUtils
25+
* @group Cross.TestUtils.Utils
26+
* @group Cross.TestUtils.Utils.Instance
27+
*/
28+
class InstanceTest extends \PHPUnit_Framework_TestCase
29+
{
30+
31+
public function createsReflectionData()
32+
{
33+
return [
34+
'fromString' => [\stdClass::class],
35+
'fromObject' => [new \stdClass()],
36+
'fromArrayNumeric' => [[\stdClass::class, 'elem1', 'elem2']],
37+
'fromArray' => [['elem', 'class' => \stdClass::class]],
38+
];
39+
}
40+
41+
/**
42+
* @dataProvider createsReflectionData
43+
*
44+
* @param mixed $spec
45+
* @return void
46+
*/
47+
public function testCreatesReflection($spec)
48+
{
49+
$actual = Instance::reflection($spec);
50+
51+
static::assertInstanceOf(\ReflectionClass::class, $actual);
52+
}
53+
54+
public function createsObjectsData()
55+
{
56+
$object = new class
57+
{
58+
public $args;
59+
60+
public function __construct(...$args)
61+
{
62+
$this->args = $args;
63+
}
64+
};
65+
$fqcn = get_class($object);
66+
67+
return [
68+
[$fqcn, [], $fqcn],
69+
[$fqcn, ['arg1'], $fqcn, 'arg1'],
70+
[$fqcn, ['arg1'], [$fqcn, 'arg1'], 'arg2'],
71+
[\ReflectionClass::class, false, "!$fqcn"]
72+
];
73+
}
74+
75+
/**
76+
* @dataProvider createsObjectsData
77+
* @param string $fqcn
78+
* @param array|false $expect
79+
* @param array ...$args
80+
* @return void
81+
*/
82+
public function testCreatesObjects($fqcn, $expect, ...$args)
83+
{
84+
$actual = Instance::create(...$args);
85+
86+
static::assertInstanceOf($fqcn, $actual);
87+
if (false !== $expect) {
88+
static::assertEquals($expect, $actual->args);
89+
}
90+
}
91+
92+
public function testCreateThrowsException()
93+
{
94+
$this->expectException(InvalidUsageException::class);
95+
$this->expectExceptionMessage('string as FQCN');
96+
97+
Instance::create(1);
98+
}
99+
100+
public function mappedArgumentsData()
101+
{
102+
$object = new class
103+
{
104+
public $called;
105+
public $args;
106+
107+
public function __construct(...$args)
108+
{
109+
$this->args = $args;
110+
}
111+
};
112+
$context = new class
113+
{
114+
public $called;
115+
116+
public function __call($method, $args)
117+
{
118+
$this->called[] = $method;
119+
return '__mapped__';
120+
}
121+
};
122+
$context2 = new class
123+
{
124+
private function privateCallback()
125+
{
126+
return '__mapped__';
127+
}
128+
};
129+
130+
$fqcn = get_class($object);
131+
132+
return [
133+
// expectFqcn, expectArgs, expectCalled, fqcn, arguments, context
134+
[$fqcn, [], null, $fqcn, [], null],
135+
[$fqcn, ['arg1', 'arg2'], false, $fqcn, ['some' => 'arg1', 'arg2'], null],
136+
[$fqcn, ['@'], null, $fqcn, ['arg' => '@'], $context],
137+
[$fqcn, ['__mapped__', phpversion()], ['arg'], $fqcn, ['arg' => $context, 'rev' => 'phpversion'], $context],
138+
[$fqcn, [phpversion(), '__mapped__'], ['arg'], $fqcn, ['@phpversion', '@arg'], $context],
139+
[
140+
$fqcn,
141+
[phpversion(), '__mapped__', '__mapped__'],
142+
['arg', 'arg2'],
143+
$fqcn,
144+
[['@' => 'phpversion'], ['@' => 'arg'], ['@' => [$context, 'arg2']]],
145+
$context
146+
],
147+
[$fqcn, ['@nonExistentMethod'], false, $fqcn, ['@nonExistentMethod'], $context2],
148+
[$fqcn, ['__mapped__'], false, $fqcn, ['@privateCallback'], $context2],
149+
[$fqcn, ['__mapped__'], ['arg'], [$fqcn, '@arg'], ['ignored'], $context],
150+
[$fqcn, ['__mapped__'], ['arg'], [$fqcn, '@arg'], $context, null],
151+
];
152+
}
153+
154+
/**
155+
* @dataProvider mappedArgumentsData
156+
* @param string $expectFqcn
157+
* @param array|false|null $expectArgs
158+
* @param array|false|null $expectCalled
159+
* @param string|array $fqcn
160+
* @param array|object $arguments
161+
* @param object|null $context
162+
* @return void
163+
*/
164+
public function testCreatesObjectsWithMappedArguments(
165+
$expectFqcn,
166+
$expectArgs,
167+
$expectCalled,
168+
$fqcn,
169+
$arguments,
170+
$context
171+
) {
172+
if ($context) {
173+
$context->called = null;
174+
}
175+
176+
$actual = Instance::withMappedArguments($fqcn, $arguments, $context);
177+
178+
static::assertInstanceOf($expectFqcn, $actual);
179+
if (false !== $expectArgs) {
180+
static::assertEquals($expectArgs, $actual->args);
181+
}
182+
if (false !== $expectCalled && $context) {
183+
static::assertEquals($expectCalled, $context->called);
184+
}
185+
}
186+
}

0 commit comments

Comments
 (0)