Skip to content

Commit 827bd17

Browse files
- Simplified static and non-static method invocation in CallHandlers.
- Implemented NamedConstructor CallHandler.
1 parent c3eef6c commit 827bd17

8 files changed

Lines changed: 195 additions & 42 deletions

File tree

src/Magic/AbstractCallHandler.php

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,9 @@
1717

1818
abstract class AbstractCallHandler implements CallHandler
1919
{
20-
public function executeStatic(
21-
string $className,
22-
string $methodName,
23-
array $arguments,
24-
ClassMetadata $classMetadata
25-
)
20+
public function requiresObjectContext() : bool
2621
{
27-
throw new \Error("Calling non-static method when not in object context.");
22+
return true;
2823
}
2924

3025
protected function checkForMethod(

src/Magic/CallHandler.php

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ interface CallHandler
2626
*/
2727
public function canHandle(string $methodName, ClassMetadata $classMetadata, array $options) : bool;
2828

29+
/**
30+
* Does the CallHandler require an object context (or implements a static method).
31+
*/
32+
public function requiresObjectContext() : bool;
33+
2934
/**
3035
* Executes a method on a given object.
3136
*
@@ -37,14 +42,10 @@ public function canHandle(string $methodName, ClassMetadata $classMetadata, arra
3742
* - Assert for the correct return type.
3843
* - Return the result.
3944
*
45+
* NOTE: If you implement a CallHandler for static methods, remove the `object` type hint in the concrete
46+
* implementation.
47+
*
4048
* @return mixed
4149
*/
4250
public function execute(object $object, string $methodName, array $arguments, ClassMetadata $classMetadata);
43-
44-
/**
45-
* Executes a static method on a given class (name).
46-
*
47-
* Cf. self::execute()
48-
*/
49-
public function executeStatic(string $className, string $methodName, array $arguments, ClassMetadata $classMetadata);
5051
}

src/Magic/Dispatcher.php

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ private static function instance() : self
4343
}
4444

4545
/**
46+
* @param array $prioritizedCallHandlerClassNames
47+
* Values of that array can be
48+
* - a string $className, or
49+
* - an array [string $className, array $options]
50+
*
4651
* @return mixed
4752
*/
4853
public static function invoke(
@@ -99,18 +104,28 @@ private static function doInvocation(
99104
if ($callHandler->canHandle($methodName, $classMetadata, $options)) {
100105
$instance->assertGivenParametersMatchMethodSignature($methodName, $arguments, $classMetadata);
101106

102-
if (! $isStatic) {
103-
$return = $callHandler->execute($objectOrClassName, $methodName, $arguments, $classMetadata);
104-
} else {
105-
$return = $callHandler->executeStatic($objectOrClassName, $methodName, $arguments, $classMetadata);
107+
if (
108+
$isStatic &&
109+
$callHandler->requiresObjectContext()
110+
) {
111+
throw new \Error("Calling a non-static method when not in object context.");
106112
}
107113

114+
$return = $callHandler->execute($objectOrClassName, $methodName, $arguments, $classMetadata);
115+
108116
$instance->assertCorrectReturnType($objectOrClassName, $methodName, $return, $classMetadata);
109117

110118
return $return;
111119
}
112120
}
113121

122+
if (
123+
! $isStatic &&
124+
method_exists($className, '__callStatic')
125+
) {
126+
return $className::$methodName(... $arguments);
127+
}
128+
114129
throw new \Error(
115130
sprintf(
116131
'Call to undefined method %s::%s()',

src/Magic/NamedConstructor.php

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,19 @@ public function canHandle(string $methodName, ClassMetadata $classMetadata, arra
2323
return true;
2424
}
2525

26-
public function execute(object $object, string $methodName, array $arguments, ClassMetadata $classMetadata)
26+
public function requiresObjectContext() : bool
2727
{
28-
return $this->executeStatic(
29-
get_class($object),
30-
$methodName,
31-
$arguments,
32-
$classMetadata
33-
);
28+
return false;
3429
}
3530

36-
public function executeStatic(string $className, string $methodName, array $arguments, ClassMetadata $classMetadata)
31+
/**
32+
* @param $objectOrClassName
33+
* NOTE: Intentionally the object type was left out for $objectOrClassName here.
34+
*/
35+
public function execute($objectOrClassName, string $methodName, array $arguments, ClassMetadata $classMetadata)
3736
{
37+
$className = is_string($objectOrClassName) ? $objectOrClassName : get_class($objectOrClassName);
38+
3839
$newObject = Reflection::classByName($className)
3940
->newInstanceWithoutConstructor();
4041

tests/PhpUnit/Magic/AbstractCallHandlerTest.php

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -115,20 +115,4 @@ public function it_returns_the_corresponding_property_name_to_a_method_name(
115115
// then the result is as expected
116116
$this->assertSame($expectedPropertyResult, $result);
117117
}
118-
119-
/**
120-
* @test
121-
* @covers ::executeStatic()
122-
*/
123-
public function it_throws_an_exception_when_executing_a_call_handler_statically_that_requires_object_context()
124-
{
125-
// given a mocked AbstractCallHandler, and ClassMetadata of some object as provided in setUp()
126-
127-
// when calling a CallHandler, that requires an object context, statically
128-
// then an exception is thrown
129-
$this->expectException(\Error::class);
130-
$this->expectExceptionMessage("Calling non-static method when not in object context.");
131-
132-
$this->callHandler->executeStatic('SomeClassName', 'someMethodName', [], $this->classMetadata);
133-
}
134118
}

tests/PhpUnit/Magic/DispatcherTest.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use ScaleUpStack\EasyObject\Magic\NamedConstructor;
1717
use ScaleUpStack\EasyObject\Magic\VirtualGetter;
1818
use ScaleUpStack\EasyObject\Tests\Resources\Magic\ClassForDispatcherTesting;
19+
use ScaleUpStack\EasyObject\Tests\Resources\Magic\ClassForNamedConstructorTesting;
1920
use ScaleUpStack\EasyObject\Tests\Resources\TestCase;
2021
use ScaleUpStack\Reflection\Reflection;
2122

@@ -90,6 +91,23 @@ public function it_invokes_a_virtual_static_method_on_some_object_via_some_call_
9091
);
9192
}
9293

94+
/**
95+
* @test
96+
* @covers ::doInvocation()
97+
*/
98+
public function it_can_execute_the_static_method_non_statically_on_objects()
99+
{
100+
// given an object with a __call() method that does not handle the static method itself but has a __callStatic()
101+
// method that can handle it
102+
$object = ClassForNamedConstructorTesting::create('some string', 42);
103+
104+
// when calling the factory method non-statically
105+
$newObject = $object->create('some other string', 11);
106+
107+
// then the invocation is redirected to __callStatic(), and the result is as expected from the CallHandler
108+
$this->assertInstanceOf(ClassForNamedConstructorTesting::class, $newObject);
109+
}
110+
93111
/**
94112
* @test
95113
* @covers ::assertGivenParametersMatchMethodSignature()
@@ -123,6 +141,30 @@ public function it_throws_an_exception_when_the_number_of_parameters_is_invalid(
123141
);
124142
}
125143

144+
/**
145+
* @test
146+
* @covers ::doInvocation()
147+
* @covers \ScaleUpStack\EasyObject\Magic\AbstractCallHandler::requiresObjectContext()
148+
*/
149+
public function it_throws_an_exception_when_executing_a_call_handler_statically_that_requires_object_context()
150+
{
151+
// given a mocked AbstractCallHandler, and ClassMetadata of some object as provided in setUp()
152+
153+
// when calling a CallHandler, that requires an object context, statically
154+
// then an exception is thrown
155+
$this->expectException(\Error::class);
156+
$this->expectExceptionMessage("Calling a non-static method when not in object context.");
157+
158+
Dispatcher::invokeStatically(
159+
ClassForDispatcherTesting::class,
160+
'getSomeProperty',
161+
[],
162+
[
163+
VirtualGetter::class
164+
]
165+
);
166+
}
167+
126168
/**
127169
* @test
128170
* @covers ::assertCorrectReturnType()
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types = 1);
2+
3+
/**
4+
* This file is part of ScaleUpStack/EasyObject
5+
*
6+
* For the full copyright and license information, please view the README.md and LICENSE.md files that were distributed
7+
* with this source code.
8+
*
9+
* @copyright 2019 - present ScaleUpVentures GmbH, https://www.scaleupventures.com
10+
* @link https://github.com/scaleupstack/easy-object
11+
*/
12+
13+
namespace ScaleUpStack\EasyObject\Tests\PhpUnit\Magic;
14+
15+
use ScaleUpStack\EasyObject\Magic\NamedConstructor;
16+
use ScaleUpStack\EasyObject\Tests\Resources\Magic\ClassForNamedConstructorTesting;
17+
use ScaleUpStack\EasyObject\Tests\Resources\TestCase;
18+
use ScaleUpStack\Reflection\Reflection;
19+
20+
/**
21+
* @coversDefaultClass \ScaleUpStack\EasyObject\Magic\NamedConstructor
22+
*/
23+
final class NamedConstructorTest extends TestCase
24+
{
25+
/**
26+
* @test
27+
* @covers ::canHandle()
28+
* @covers ::requiresObjectContext()
29+
* @covers ::execute()
30+
*/
31+
public function it_handles_and_executes_virtual_named_constructors()
32+
{
33+
// given a class name with a named constructor name, and required arguments
34+
$className = ClassForNamedConstructorTesting::class;
35+
$methodName = 'create';
36+
$stringValue = 'someString';
37+
$intValue = 42;
38+
39+
// when calling the virtual named constructor
40+
$result = $className::$methodName($stringValue, $intValue);
41+
42+
// then an instance of that class with the correct property values is returned
43+
$this->assertInstanceOf(ClassForNamedConstructorTesting::class, $result);
44+
$this->assertSame(
45+
$stringValue,
46+
Reflection::getPropertyValue($result, 'firstProperty')
47+
);
48+
$this->assertSame(
49+
$intValue,
50+
Reflection::getPropertyValue($result, 'secondProperty')
51+
);
52+
}
53+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php declare(strict_types = 1);
2+
3+
/**
4+
* This file is part of ScaleUpStack/EasyObject
5+
*
6+
* For the full copyright and license information, please view the README.md and LICENSE.md files that were distributed
7+
* with this source code.
8+
*
9+
* @copyright 2019 - present ScaleUpVentures GmbH, https://www.scaleupventures.com
10+
* @link https://github.com/scaleupstack/easy-object
11+
*/
12+
13+
namespace ScaleUpStack\EasyObject\Tests\Resources\Magic;
14+
use ScaleUpStack\EasyObject\Magic\Dispatcher;
15+
use ScaleUpStack\EasyObject\Magic\NamedConstructor;
16+
17+
/**
18+
* @method static self create(string $firstProperty, int $secondProperty)
19+
*/
20+
final class ClassForNamedConstructorTesting
21+
{
22+
private $firstProperty;
23+
24+
private $secondProperty;
25+
26+
/**
27+
* NOTES:
28+
*
29+
* - This method is required for automatic fall-back to __callStatic()
30+
*
31+
* - For test coverage, a call to Dispatcher is required. In practice, a simpler implementation would be sufficient
32+
* when no CallHandlers are handled by __call():
33+
*
34+
* `return self::__callStatic($methodName, $arguments);`
35+
*/
36+
public function __call($methodName, $arguments)
37+
{
38+
return Dispatcher::invoke(
39+
$this,
40+
$methodName,
41+
$arguments,
42+
[]
43+
);
44+
}
45+
46+
public static function __callStatic($methodName, $arguments)
47+
{
48+
return Dispatcher::invokeStatically(
49+
self::class,
50+
$methodName,
51+
$arguments,
52+
[
53+
[
54+
NamedConstructor::class,
55+
[
56+
'methodName' => 'create',
57+
],
58+
]
59+
]
60+
);
61+
}
62+
}

0 commit comments

Comments
 (0)