diff --git a/composer.json b/composer.json index fce9a97c..a780f6c3 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ "require": { "php": "^8.1.0", "devizzent/cebe-php-openapi": "^1.1.2", - "atto/codegen-tools": "^0.1", + "atto/codegen-tools": "^0.1.1", "psr/http-message": "^1.0 || ^2.0", "psr/log": "^2.0 || ^3.0", "symfony/console": "^6.2 || ^7.0", diff --git a/docs/filters.md b/docs/filters.md index 0b1ff1c0..284a8c44 100644 --- a/docs/filters.md +++ b/docs/filters.md @@ -16,6 +16,14 @@ interface Filter } ``` +If you need a custom Filter, implement this in your own class. + +The `filter` method SHOULD NOT throw on failure. +It SHOULD provide an invalid result with an error message. +This ensures the processor can fail gracefully and provide all errors in one go. + +It MAY throw exceptions in other methods (such as invalid arguments to `__construct`). + ## Methods ### Filter @@ -44,6 +52,47 @@ This method can also be called implicitly by typecasting your filter as string. Create objects from external data. +### CallMethod + +This filter exists to call bespoke methods required by a class, when [FromArray](#fromarray) and [WithNamedArguments](#withnamedarguments) are not sufficiently flexible. + +If you are calling methods on an external class, as a way of reusing logic; create a reusable [Filter](#interface) instead. + +```php +new CallMethod($className, $methodName) +``` + +| Parameter | Type | +|-------------|--------| +| $className | string | +| $methodName | string | + +**Example** + +Your class may have a constructor with variadic number of arguments. +Currently, no filter exists to handle this. + +```php +$classWithMethod = new class () { + public function __construct(string ...$tags) { + // do some stuff... + } + + /** @param list $list */ + public static function fromList(array $list): self + { + return new self(...$list); + } +}; + +$callMethod = new CallMethod($class::class, 'fromList'); + +$result = $callMethod->filter(['foo', 'bar', 'baz']) + +echo $result->value; +echo $result->isValid() ? 'Result was valid' : 'Result was invalid'; +``` + ### FromArray construct new data object from an array. $className must correspond to a class with a method named 'fromArray' diff --git a/src/Exception/InvalidFilterArguments.php b/src/Exception/InvalidFilterArguments.php index 54db45fa..9eefc879 100644 --- a/src/Exception/InvalidFilterArguments.php +++ b/src/Exception/InvalidFilterArguments.php @@ -6,7 +6,13 @@ class InvalidFilterArguments extends \RuntimeException { - public const EMPTY_STRING_DELIMITER = 0; + public const METHOD_NOT_CALLABLE = 0; + public const EMPTY_STRING_DELIMITER = 1; + + public static function methodNotCallable(string $class, string $method): self + { + return new self("$class::$method must be callable", self::METHOD_NOT_CALLABLE); + } public static function emptyStringDelimiter(): self { diff --git a/src/Filter/CreateObject/CallMethod.php b/src/Filter/CreateObject/CallMethod.php new file mode 100644 index 00000000..421b063f --- /dev/null +++ b/src/Filter/CreateObject/CallMethod.php @@ -0,0 +1,72 @@ +callable = $callable; + } + + public function __toString(): string + { + return sprintf( + 'Call %s with array value as arguments', + implode('::', $this->callable), + ); + } + + public function __toPHP(): string + { + return sprintf( + 'new %s(\'%s\', \'%s\')', + self::class, + $this->callable[0], + $this->callable[1], + ); + } + + public function filter(mixed $value): Result + { + + if (!is_array($value)) { + $message = new Message( + 'CallMethod requires arrays of arguments, %s given', + [gettype($value)], + ); + return Result::invalid($value, new MessageSet(null, $message)); + } + + try { + $result = call_user_func($this->callable, ...$value); + } catch (\Throwable $e) { + return Result::invalid( + $value, + new MessageSet(null, new Message($e->getMessage(), [])), + ); + } + + return Result::noResult($result); + } +} diff --git a/tests/Filter/CreateObject/CallMethodTest.php b/tests/Filter/CreateObject/CallMethodTest.php new file mode 100644 index 00000000..7109ad37 --- /dev/null +++ b/tests/Filter/CreateObject/CallMethodTest.php @@ -0,0 +1,196 @@ +__toString(), + ); + } + + #[Test] + public function itIsPhpStringable(): void + { + $class = new class() {public static function foo() {}}; + $method = 'foo'; + + $sut = new Filter\CreateObject\CallMethod($class::class, $method); + + self::assertSame( + sprintf( + 'new %s(\'%s\', \'%s\')', + $sut::class, + $class::class, + $method, + ), + $sut->__toPHP(), + ); + } + + #[Test] + #[DataProvider('provideValuesToFilter')] + public function itFiltersValue( + Result $expected, + string $class, + string $method, + mixed $value, + ): void { + $sut = new Filter\CreateObject\CallMethod($class, $method); + + self::assertResultEquals($expected, $sut->filter($value)); + } + + /** + * @return \Generator + */ + public static function provideValuesToFilter(): \Generator + { + yield 'it expects an array' => (function () { + $class = new class () {public static function foo() {}}; + return [ + Result::invalid( + 'Howdy, planet!', + new MessageSet( + null, + new Message('CallMethod requires arrays of arguments, %s given', ['string']), + ) + ), + $class::class, + 'foo', + 'Howdy, planet!', + ]; + })(); + + yield 'noop' => (function () { + $class = new class () {public static function foo() {}}; + return [ + Result::noResult(null), + $class::class, + 'foo', + [], + ]; + })(); + + yield 'returns 1' => (function () { + $class = new class () {public static function foo() {return 1;}}; + return [ + Result::noResult(1), + $class::class, + 'foo', + ['Hello, world!'], + ]; + })(); + + yield 'capitalizes string' => (function () { + $class = new class () { + public static function shout(string $greeting) { + return strtoupper($greeting); + } + }; + return [ + Result::noResult('HOWDY, PLANET!'), + $class::class, + 'shout', + ['Howdy, planet!'], + ]; + })(); + + yield 'sum numbers' => (function () { + $class = new class () { + public static function sum(...$numbers) { + return array_sum($numbers); + } + }; + return [ + Result::noResult(6), + $class::class, + 'sum', + [1, 2, 3], + ]; + })(); + } +}