Skip to content

Commit 474afbd

Browse files
committed
Add CallMethod Filter
1 parent 93121f9 commit 474afbd

File tree

5 files changed

+323
-2
lines changed

5 files changed

+323
-2
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"require": {
1717
"php": "^8.1.0",
1818
"devizzent/cebe-php-openapi": "^1.1.2",
19-
"atto/codegen-tools": "^0.1",
19+
"atto/codegen-tools": "^0.1.1",
2020
"psr/http-message": "^1.0 || ^2.0",
2121
"psr/log": "^2.0 || ^3.0",
2222
"symfony/console": "^6.2 || ^7.0",

docs/filters.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ interface Filter
1616
}
1717
```
1818

19+
If you need a custom Filter, implement this in your own class.
20+
21+
The `filter` method SHOULD NOT throw on failure.
22+
It SHOULD provide an invalid result with an error message.
23+
This ensures the processor can fail gracefully and provide all errors in one go.
24+
25+
It MAY throw exceptions in other methods (such as invalid arguments to `__construct`).
26+
1927
## Methods
2028

2129
### Filter
@@ -44,6 +52,47 @@ This method can also be called implicitly by typecasting your filter as string.
4452

4553
Create objects from external data.
4654

55+
### CallMethod
56+
57+
This filter exists to call bespoke methods required by a class, when [FromArray](#fromarray) and [WithNamedArguments](#withnamedarguments) are not sufficiently flexible.
58+
59+
If you are calling methods on an external class, as a way of reusing logic; create a reusable [Filter](#interface) instead.
60+
61+
```php
62+
new CallMethod($className, $methodName)
63+
```
64+
65+
| Parameter | Type |
66+
|-------------|--------|
67+
| $className | string |
68+
| $methodName | string |
69+
70+
**Example**
71+
72+
Your class may have a constructor with variadic number of arguments.
73+
Currently, no filter exists to handle this.
74+
75+
```php
76+
$classWithMethod = new class () {
77+
public function __construct(string ...$tags) {
78+
// do some stuff...
79+
}
80+
81+
/** @param list<string> $list */
82+
public static function fromList(array $list): self
83+
{
84+
return new self(...$list);
85+
}
86+
};
87+
88+
$callMethod = new CallMethod($class::class, 'fromList');
89+
90+
$result = $callMethod->filter(['foo', 'bar', 'baz'])
91+
92+
echo $result->value;
93+
echo $result->isValid() ? 'Result was valid' : 'Result was invalid';
94+
```
95+
4796
### FromArray
4897

4998
construct new data object from an array. $className must correspond to a class with a method named 'fromArray'

src/Exception/InvalidFilterArguments.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66

77
class InvalidFilterArguments extends \RuntimeException
88
{
9-
public const EMPTY_STRING_DELIMITER = 0;
9+
public const METHOD_NOT_CALLABLE = 0;
10+
public const EMPTY_STRING_DELIMITER = 1;
11+
12+
public static function methodNotCallable(string $class, string $method): self
13+
{
14+
return new self("$class::$method must be callable", self::METHOD_NOT_CALLABLE);
15+
}
1016

1117
public static function emptyStringDelimiter(): self
1218
{
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Membrane\Filter\CreateObject;
6+
7+
use Membrane\Exception\InvalidFilterArguments;
8+
use Membrane\Filter;
9+
use Membrane\Result\Message;
10+
use Membrane\Result\MessageSet;
11+
use Membrane\Result\Result;
12+
13+
final class CallMethod implements Filter
14+
{
15+
/**
16+
* @param class-string $class
17+
*/
18+
public function __construct(
19+
private readonly string $class,
20+
private readonly string $method
21+
) {
22+
if (!is_callable([$this->class, $this->method])) {
23+
throw InvalidFilterArguments::methodNotCallable(
24+
$this->class,
25+
$this->method,
26+
);
27+
}
28+
}
29+
30+
public function __toString(): string
31+
{
32+
return "Call $this->class::$this->method with array value as arguments";
33+
}
34+
35+
public function __toPHP(): string
36+
{
37+
return sprintf(
38+
'new %s(\'%s\', \'%s\')',
39+
self::class,
40+
$this->class,
41+
$this->method,
42+
);
43+
}
44+
45+
public function filter(mixed $value): Result
46+
{
47+
48+
if (!is_array($value)) {
49+
$message = new Message(
50+
'CallMethod requires arrays of arguments, %s given',
51+
[gettype($value)],
52+
);
53+
return Result::invalid($value, new MessageSet(null, $message));
54+
}
55+
56+
try {
57+
$result = call_user_func(
58+
sprintf('%s::%s', $this->class, $this->method),
59+
...$value,
60+
);
61+
} catch (\Throwable $e) {
62+
return Result::invalid(
63+
$value,
64+
new MessageSet(null, new Message($e->getMessage(), [])),
65+
);
66+
}
67+
68+
return Result::noResult($result);
69+
}
70+
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Filter\CreateObject;
6+
7+
use Membrane\Exception\InvalidFilterArguments;
8+
use Membrane\Filter;
9+
use Membrane\Result\Message;
10+
use Membrane\Result\MessageSet;
11+
use Membrane\Result\Result;
12+
use Membrane\Tests\MembraneTestCase;
13+
use PHPUnit\Framework\Attributes\CoversClass;
14+
use PHPUnit\Framework\Attributes\DataProvider;
15+
use PHPUnit\Framework\Attributes\Test;
16+
use PHPUnit\Framework\Attributes\UsesClass;
17+
18+
#[UsesClass(Result::class)]
19+
#[UsesClass(MessageSet::class)]
20+
#[UsesClass(Message::class)]
21+
#[CoversClass(Filter\CreateObject\CallMethod::class)]
22+
class CallMethodTest extends MembraneTestCase
23+
{
24+
#[Test]
25+
public function itExpectsClassToExist(): void
26+
{
27+
$class = 'n/a';
28+
$method = 'n/a';
29+
30+
self::expectExceptionObject(
31+
InvalidFilterArguments::methodNotCallable($class, $method),
32+
);
33+
34+
new Filter\CreateObject\CallMethod($class, $method);
35+
}
36+
37+
#[Test]
38+
public function itExpectsMethodToExist(): void
39+
{
40+
$class = new class () {};
41+
$method = 'n/a';
42+
43+
self::expectExceptionObject(
44+
InvalidFilterArguments::methodNotCallable($class::class, $method),
45+
);
46+
47+
new Filter\CreateObject\CallMethod($class::class, $method);
48+
}
49+
50+
#[Test]
51+
public function itExpectsMethodToBePublic(): void
52+
{
53+
$class = new class() {private static function foo() {}};
54+
$method = 'foo';
55+
56+
self::expectExceptionObject(
57+
InvalidFilterArguments::methodNotCallable($class::class, $method),
58+
);
59+
60+
new Filter\CreateObject\CallMethod($class::class, $method);
61+
}
62+
63+
#[Test]
64+
public function itExpectsMethodToBeStatic(): void
65+
{
66+
$class = new class() {public function foo() {}};
67+
$method = 'foo';
68+
69+
self::expectExceptionObject(
70+
InvalidFilterArguments::methodNotCallable($class::class, $method),
71+
);
72+
73+
new Filter\CreateObject\CallMethod($class::class, $method);
74+
}
75+
76+
#[Test]
77+
public function itIsStringable(): void
78+
{
79+
$class = new class() {public static function foo() {}};
80+
$method = 'foo';
81+
82+
$sut = new Filter\CreateObject\CallMethod($class::class, $method);
83+
84+
self::assertSame(
85+
sprintf('Call %s::%s with array value as arguments', $class::class, $method),
86+
$sut->__toString(),
87+
);
88+
}
89+
90+
#[Test]
91+
public function itIsPhpStringable(): void
92+
{
93+
$class = new class() {public static function foo() {}};
94+
$method = 'foo';
95+
96+
$sut = new Filter\CreateObject\CallMethod($class::class, $method);
97+
98+
self::assertSame(
99+
sprintf(
100+
'new %s(\'%s\', \'%s\')',
101+
$sut::class,
102+
$class::class,
103+
$method,
104+
),
105+
$sut->__toPHP(),
106+
);
107+
}
108+
109+
#[Test]
110+
#[DataProvider('provideValuesToFilter')]
111+
public function itFiltersValue(
112+
Result $expected,
113+
string $class,
114+
string $method,
115+
mixed $value,
116+
): void {
117+
$sut = new Filter\CreateObject\CallMethod($class, $method);
118+
119+
self::assertResultEquals($expected, $sut->filter($value));
120+
}
121+
122+
/**
123+
* @return \Generator<array{
124+
* 0: Result,
125+
* 1: class-string,
126+
* 2: string,
127+
* 3: mixed,
128+
* }>
129+
*/
130+
public static function provideValuesToFilter(): \Generator
131+
{
132+
yield 'it expects an array' => (function () {
133+
$class = new class () {public static function foo() {}};
134+
return [
135+
Result::invalid(
136+
'Howdy, planet!',
137+
new MessageSet(
138+
null,
139+
new Message('CallMethod requires arrays of arguments, %s given', ['string']),
140+
)
141+
),
142+
$class::class,
143+
'foo',
144+
'Howdy, planet!',
145+
];
146+
})();
147+
148+
yield 'noop' => (function () {
149+
$class = new class () {public static function foo() {}};
150+
return [
151+
Result::noResult(null),
152+
$class::class,
153+
'foo',
154+
[],
155+
];
156+
})();
157+
158+
yield 'returns 1' => (function () {
159+
$class = new class () {public static function foo() {return 1;}};
160+
return [
161+
Result::noResult(1),
162+
$class::class,
163+
'foo',
164+
['Hello, world!'],
165+
];
166+
})();
167+
168+
yield 'capitalizes string' => (function () {
169+
$class = new class () {
170+
public static function shout(string $greeting) {
171+
return strtoupper($greeting);
172+
}
173+
};
174+
return [
175+
Result::noResult('HOWDY, PLANET!'),
176+
$class::class,
177+
'shout',
178+
['Howdy, planet!'],
179+
];
180+
})();
181+
182+
yield 'sum numbers' => (function () {
183+
$class = new class () {
184+
public static function sum(...$numbers) {
185+
return array_sum($numbers);
186+
}
187+
};
188+
return [
189+
Result::noResult(6),
190+
$class::class,
191+
'sum',
192+
[1, 2, 3],
193+
];
194+
})();
195+
}
196+
}

0 commit comments

Comments
 (0)